From 84adbcb683be847ea8217658c6edacdf85c2ceea Mon Sep 17 00:00:00 2001 From: Steve Klebanoff Date: Fri, 10 Apr 2020 20:09:56 -0700 Subject: [PATCH] asset-swapper: Mockable axios for QuoteRequestor (#2549) * Mockable axios for QuoteRequestor * Move RFQT Mocker to src * move MockedRfqtFirmQuoteResponse into types file * fix import --- packages/asset-swapper/package.json | 1 + packages/asset-swapper/src/index.ts | 2 + packages/asset-swapper/src/types.ts | 13 +++ .../src/utils/quote_requestor.ts | 5 +- .../asset-swapper/src/utils/rfqt_mocker.ts | 37 +++++++++ .../test/quote_requestor_test.ts | 79 +++++++++++++++++++ yarn.lock | 27 +++++++ 7 files changed, 161 insertions(+), 3 deletions(-) create mode 100644 packages/asset-swapper/src/utils/rfqt_mocker.ts create mode 100644 packages/asset-swapper/test/quote_requestor_test.ts diff --git a/packages/asset-swapper/package.json b/packages/asset-swapper/package.json index 8db729fccf..2ad638cda4 100644 --- a/packages/asset-swapper/package.json +++ b/packages/asset-swapper/package.json @@ -55,6 +55,7 @@ "@0x/utils": "^5.4.1", "@0x/web3-wrapper": "^7.0.7", "axios": "^0.19.2", + "axios-mock-adapter": "^1.18.1", "heartbeats": "^5.0.1", "lodash": "^4.17.11" }, diff --git a/packages/asset-swapper/src/index.ts b/packages/asset-swapper/src/index.ts index 87de97ae35..bce3e2a540 100644 --- a/packages/asset-swapper/src/index.ts +++ b/packages/asset-swapper/src/index.ts @@ -44,6 +44,7 @@ export { MarketBuySwapQuote, MarketOperation, MarketSellSwapQuote, + MockedRfqtFirmQuoteResponse, RfqtFirmQuoteRequestOpts, SwapQuote, SwapQuoteConsumerBase, @@ -67,3 +68,4 @@ export { export { affiliateFeeUtils } from './utils/affiliate_fee_utils'; export { ProtocolFeeUtils } from './utils/protocol_fee_utils'; export { QuoteRequestor } from './utils/quote_requestor'; +export { rfqtMocker } from './utils/rfqt_mocker'; diff --git a/packages/asset-swapper/src/types.ts b/packages/asset-swapper/src/types.ts index b9b63532a7..a93e8ede40 100644 --- a/packages/asset-swapper/src/types.ts +++ b/packages/asset-swapper/src/types.ts @@ -277,3 +277,16 @@ export enum OrderPrunerPermittedFeeTypes { export interface RfqtFirmQuoteRequestOpts { makerEndpointMaxResponseTimeMs?: number; } + +/** + * Represents a mocked RFQT maker responses. + */ +export interface MockedRfqtFirmQuoteResponse { + endpoint: string; + requestApiKey: string; + requestParams: { + [key: string]: string | undefined; + }; + responseData: any; + responseCode: number; +} diff --git a/packages/asset-swapper/src/utils/quote_requestor.ts b/packages/asset-swapper/src/utils/quote_requestor.ts index ef5b9b6841..2f01de7a59 100644 --- a/packages/asset-swapper/src/utils/quote_requestor.ts +++ b/packages/asset-swapper/src/utils/quote_requestor.ts @@ -68,10 +68,9 @@ export class QuoteRequestor { }); } catch (err) { logUtils.warn( - `Failed to get RFQ-T quote from market maker endpoint ${rfqtMakerEndpoint} for API key ${takerApiKey} for taker address ${takerAddress}: ${JSON.stringify( - err, - )}`, + `Failed to get RFQ-T quote from market maker endpoint ${rfqtMakerEndpoint} for API key ${takerApiKey} for taker address ${takerAddress}`, ); + logUtils.warn(err); return undefined; } }), diff --git a/packages/asset-swapper/src/utils/rfqt_mocker.ts b/packages/asset-swapper/src/utils/rfqt_mocker.ts new file mode 100644 index 0000000000..dfb8426f67 --- /dev/null +++ b/packages/asset-swapper/src/utils/rfqt_mocker.ts @@ -0,0 +1,37 @@ +import axios from 'axios'; +import AxiosMockAdapter from 'axios-mock-adapter'; + +import { MockedRfqtFirmQuoteResponse } from '../types'; + +/** + * A helper utility for testing which mocks out + * requests to RFQ-t providers + */ +export const rfqtMocker = { + /** + * Stubs out responses from RFQ-T providers by mocking out + * HTTP calls via axios. Always restores the mock adapter + * after executing the `performFn`. + */ + withMockedRfqtFirmQuotes: async ( + mockedResponses: MockedRfqtFirmQuoteResponse[], + performFn: () => Promise, + ) => { + const mockedAxios = new AxiosMockAdapter(axios); + try { + // Mock out RFQT responses + for (const mockedResponse of mockedResponses) { + const { endpoint, requestApiKey, requestParams, responseData, responseCode } = mockedResponse; + const requestHeaders = { Accept: 'application/json, text/plain, */*', '0x-api-key': requestApiKey }; + mockedAxios + .onGet(`${endpoint}/quote`, { params: requestParams }, requestHeaders) + .replyOnce(responseCode, responseData); + } + + await performFn(); + } finally { + // Ensure we always restore axios afterwards + mockedAxios.restore(); + } + }, +}; diff --git a/packages/asset-swapper/test/quote_requestor_test.ts b/packages/asset-swapper/test/quote_requestor_test.ts new file mode 100644 index 0000000000..3cfcea9b48 --- /dev/null +++ b/packages/asset-swapper/test/quote_requestor_test.ts @@ -0,0 +1,79 @@ +import { tokenUtils } from '@0x/dev-utils'; +import { assetDataUtils } from '@0x/order-utils'; +import { StatusCodes } from '@0x/types'; +import { BigNumber } from '@0x/utils'; +import * as chai from 'chai'; +import 'mocha'; + +import { MarketOperation, MockedRfqtFirmQuoteResponse } from '../src/types'; +import { QuoteRequestor } from '../src/utils/quote_requestor'; +import { rfqtMocker } from '../src/utils/rfqt_mocker'; + +import { chaiSetup } from './utils/chai_setup'; +import { testOrderFactory } from './utils/test_order_factory'; + +chaiSetup.configure(); +const expect = chai.expect; + +describe('QuoteRequestor', async () => { + const [makerToken, takerToken] = tokenUtils.getDummyERC20TokenAddresses(); + const makerAssetData = assetDataUtils.encodeERC20AssetData(makerToken); + const takerAssetData = assetDataUtils.encodeERC20AssetData(takerToken); + + describe('requestRfqtFirmQuotesAsync', async () => { + it('should return successful RFQT requests', async () => { + const takerAddress = '0xd209925defc99488e3afff1174e48b4fa628302a'; + const takerApiKey = 'my-ko0l-api-key'; + + // Set up RFQT responses + // tslint:disable-next-line:array-type + const mockedRequests: MockedRfqtFirmQuoteResponse[] = []; + const expectedParams = { + sellToken: takerToken, + buyToken: makerToken, + sellAmount: '10000', + buyAmount: undefined, + takerAddress, + }; + // Successful response + const mockedOrder1 = testOrderFactory.generateTestSignedOrder({}); + mockedRequests.push({ + endpoint: 'https://1337.0.0.1', + requestApiKey: takerApiKey, + requestParams: expectedParams, + responseData: mockedOrder1, + responseCode: StatusCodes.Success, + }); + // Test out a bad response code, ensure it doesnt cause throw + mockedRequests.push({ + endpoint: 'https://420.0.0.1', + requestApiKey: takerApiKey, + requestParams: expectedParams, + responseData: { error: 'bad request' }, + responseCode: StatusCodes.InternalError, + }); + // Another Successful response + const mockedOrder3 = testOrderFactory.generateTestSignedOrder({}); + mockedRequests.push({ + endpoint: 'https://37.0.0.1', + requestApiKey: takerApiKey, + requestParams: expectedParams, + responseData: mockedOrder3, + responseCode: StatusCodes.Success, + }); + + return rfqtMocker.withMockedRfqtFirmQuotes(mockedRequests, async () => { + const qr = new QuoteRequestor(['https://1337.0.0.1', 'https://420.0.0.1', 'https://37.0.0.1']); + const resp = await qr.requestRfqtFirmQuotesAsync( + makerAssetData, + takerAssetData, + new BigNumber(10000), + MarketOperation.Sell, + takerApiKey, + takerAddress, + ); + expect(resp.sort()).to.eql([mockedOrder1, mockedOrder3].sort()); + }); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index ea9130eae9..2e29c3acc7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3069,6 +3069,13 @@ aws4@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" +axios-mock-adapter@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.18.1.tgz#a2ba2638ef513d954793f96bde3e26bd4a1b7940" + dependencies: + fast-deep-equal "^3.1.1" + is-buffer "^2.0.3" + axios@^0.18.0: version "0.18.0" resolved "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz#32d53e4851efdc0a11993b6cd000789d70c05102" @@ -3076,6 +3083,12 @@ axios@^0.18.0: follow-redirects "^1.3.0" is-buffer "^1.1.5" +axios@^0.19.2: + version "0.19.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" + dependencies: + follow-redirects "1.5.10" + babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" @@ -7353,6 +7366,10 @@ fast-deep-equal@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" +fast-deep-equal@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" + fast-diff@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" @@ -7659,6 +7676,12 @@ flush-write-stream@^1.0.0, flush-write-stream@^1.0.2: inherits "^2.0.1" readable-stream "^2.0.4" +follow-redirects@1.5.10: + version "1.5.10" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" + dependencies: + debug "=3.1.0" + follow-redirects@^1.3.0: version "1.5.8" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.8.tgz#1dbfe13e45ad969f813e86c00e5296f525c885a1" @@ -9133,6 +9156,10 @@ is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" +is-buffer@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" + is-buffer@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725"