asset-swapper: Mockable axios for QuoteRequestor (#2549)

* Mockable axios for QuoteRequestor

* Move RFQT Mocker to src

* move MockedRfqtFirmQuoteResponse into types file

* fix import
This commit is contained in:
Steve Klebanoff 2020-04-10 20:09:56 -07:00 committed by GitHub
parent 0cb5e4553b
commit 84adbcb683
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 161 additions and 3 deletions

View File

@ -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"
},

View File

@ -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';

View File

@ -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;
}

View File

@ -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;
}
}),

View File

@ -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<void>,
) => {
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();
}
},
};

View File

@ -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());
});
});
});
});

View File

@ -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"