Skip to content

Commit 4cfac4a

Browse files
authored
Extract CryptoCompare API (#353)
The CryptoCompare API interactions have been extracted from the CurrencyRateController and moved into a separate module.
1 parent 720e1d5 commit 4cfac4a

File tree

4 files changed

+221
-114
lines changed

4 files changed

+221
-114
lines changed

src/apis/crypto-compare.test.ts

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import * as nock from 'nock';
2+
3+
import { fetchExchangeRate } from './crypto-compare';
4+
5+
const cryptoCompareHost = 'https://min-api.cryptocompare.com';
6+
7+
describe('CryptoCompare', () => {
8+
beforeAll(() => {
9+
nock.disableNetConnect();
10+
});
11+
12+
afterAll(() => {
13+
nock.enableNetConnect();
14+
});
15+
16+
afterEach(() => {
17+
nock.cleanAll();
18+
});
19+
20+
it('should return CAD conversion rate', async () => {
21+
nock(cryptoCompareHost)
22+
.get('/data/price?fsym=ETH&tsyms=CAD')
23+
.reply(200, { CAD: 2000.42 });
24+
25+
const { conversionRate } = await fetchExchangeRate('CAD', 'ETH');
26+
27+
expect(conversionRate).toEqual(2000.42);
28+
});
29+
30+
it('should return conversion date', async () => {
31+
nock(cryptoCompareHost)
32+
.get('/data/price?fsym=ETH&tsyms=CAD')
33+
.reply(200, { CAD: 2000.42 });
34+
35+
const before = Date.now() / 1000;
36+
const { conversionDate } = await fetchExchangeRate('CAD', 'ETH');
37+
const after = Date.now() / 1000;
38+
39+
expect(conversionDate).toBeGreaterThanOrEqual(before);
40+
expect(conversionDate).toBeLessThanOrEqual(after);
41+
});
42+
43+
it('should return CAD conversion rate given lower-cased currency', async () => {
44+
nock(cryptoCompareHost)
45+
.get('/data/price?fsym=ETH&tsyms=CAD')
46+
.reply(200, { CAD: 2000.42 });
47+
48+
const { conversionRate } = await fetchExchangeRate('cad', 'ETH');
49+
50+
expect(conversionRate).toEqual(2000.42);
51+
});
52+
53+
it('should return CAD conversion rate given lower-cased native currency', async () => {
54+
nock(cryptoCompareHost)
55+
.get('/data/price?fsym=ETH&tsyms=CAD')
56+
.reply(200, { CAD: 2000.42 });
57+
58+
const { conversionRate } = await fetchExchangeRate('CAD', 'eth');
59+
60+
expect(conversionRate).toEqual(2000.42);
61+
});
62+
63+
it('should not return USD conversion rate when fetching just CAD conversion rate', async () => {
64+
nock(cryptoCompareHost)
65+
.get('/data/price?fsym=ETH&tsyms=CAD')
66+
.reply(200, { CAD: 1000.42 });
67+
68+
const { usdConversionRate } = await fetchExchangeRate('CAD', 'ETH');
69+
70+
expect(usdConversionRate).toBeFalsy();
71+
});
72+
73+
it('should return USD conversion rate for USD even when includeUSD is disabled', async () => {
74+
nock(cryptoCompareHost)
75+
.get('/data/price?fsym=ETH&tsyms=USD')
76+
.reply(200, { USD: 1000.42 });
77+
78+
const { conversionRate, usdConversionRate } = await fetchExchangeRate('USD', 'ETH', false);
79+
80+
expect(conversionRate).toEqual(1000.42);
81+
expect(usdConversionRate).toEqual(1000.42);
82+
});
83+
84+
it('should return USD conversion rate for USD when includeUSD is enabled', async () => {
85+
nock(cryptoCompareHost)
86+
.get('/data/price?fsym=ETH&tsyms=USD')
87+
.reply(200, { USD: 1000.42 });
88+
89+
const { conversionRate, usdConversionRate } = await fetchExchangeRate('USD', 'ETH', true);
90+
91+
expect(conversionRate).toEqual(1000.42);
92+
expect(usdConversionRate).toEqual(1000.42);
93+
});
94+
95+
it('should return CAD and USD conversion rate', async () => {
96+
nock(cryptoCompareHost)
97+
.get('/data/price?fsym=ETH&tsyms=CAD,USD')
98+
.reply(200, { CAD: 2000.42, USD: 1000.42 });
99+
100+
const { conversionRate, usdConversionRate } = await fetchExchangeRate('CAD', 'ETH', true);
101+
102+
expect(conversionRate).toEqual(2000.42);
103+
expect(usdConversionRate).toEqual(1000.42);
104+
});
105+
106+
it('should throw if fetch throws', async () => {
107+
nock(cryptoCompareHost)
108+
.get('/data/price?fsym=ETH&tsyms=CAD')
109+
.replyWithError('Example network error');
110+
111+
await expect(fetchExchangeRate('CAD', 'ETH')).rejects.toThrow('Example network error');
112+
});
113+
114+
it('should throw if fetch returns unsuccessful response', async () => {
115+
nock(cryptoCompareHost)
116+
.get('/data/price?fsym=ETH&tsyms=CAD')
117+
.reply(500);
118+
119+
await expect(fetchExchangeRate('CAD', 'ETH')).rejects.toThrow(`Fetch failed with status '500' for request '${cryptoCompareHost}/data/price?fsym=ETH&tsyms=CAD'`);
120+
});
121+
122+
it('should throw if conversion rate is invalid', async () => {
123+
nock(cryptoCompareHost)
124+
.get('/data/price?fsym=ETH&tsyms=CAD')
125+
.reply(200, { CAD: 'invalid' });
126+
127+
await expect(fetchExchangeRate('CAD', 'ETH')).rejects.toThrow('Invalid response for CAD: invalid');
128+
});
129+
130+
it('should throw if USD conversion rate is invalid', async () => {
131+
nock(cryptoCompareHost)
132+
.get('/data/price?fsym=ETH&tsyms=CAD,USD')
133+
.reply(200, { CAD: 2000.47, USD: 'invalid' });
134+
135+
await expect(fetchExchangeRate('CAD', 'ETH', true)).rejects.toThrow('Invalid response for usdConversionRate: invalid');
136+
});
137+
});

src/apis/crypto-compare.ts

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { handleFetch } from '../util';
2+
3+
function getPricingURL(currentCurrency: string, nativeCurrency: string, includeUSDRate?: boolean) {
4+
return (
5+
`https://min-api.cryptocompare.com/data/price?fsym=` +
6+
`${nativeCurrency.toUpperCase()}&tsyms=${currentCurrency.toUpperCase()}` +
7+
`${includeUSDRate && currentCurrency.toUpperCase() !== 'USD' ? ',USD' : ''}`
8+
);
9+
}
10+
11+
/**
12+
* Fetches the exchange rate for a given currency
13+
*
14+
* @param currency - ISO 4217 currency code
15+
* @param nativeCurrency - Symbol for base asset
16+
* @param includeUSDRate - Whether to add the USD rate to the fetch
17+
* @returns - Promise resolving to exchange rate for given currency
18+
*/
19+
export async function fetchExchangeRate(currency: string, nativeCurrency: string, includeUSDRate?: boolean): Promise<{ conversionDate: number; conversionRate: number; usdConversionRate: number }> {
20+
const json = await handleFetch(getPricingURL(currency, nativeCurrency, includeUSDRate));
21+
const conversionRate = Number(json[currency.toUpperCase()]);
22+
const usdConversionRate = Number(json.USD);
23+
if (!Number.isFinite(conversionRate)) {
24+
throw new Error(`Invalid response for ${currency.toUpperCase()}: ${json[currency.toUpperCase()]}`);
25+
}
26+
if (includeUSDRate && !Number.isFinite(usdConversionRate)) {
27+
throw new Error(`Invalid response for usdConversionRate: ${json.USD}`);
28+
}
29+
30+
return {
31+
conversionDate: Date.now() / 1000,
32+
conversionRate,
33+
usdConversionRate,
34+
};
35+
}
+43-76
Original file line numberDiff line numberDiff line change
@@ -1,142 +1,109 @@
11
import 'isomorphic-fetch';
22
import { stub } from 'sinon';
3-
import * as nock from 'nock';
43
import CurrencyRateController from './CurrencyRateController';
54

65
describe('CurrencyRateController', () => {
7-
beforeEach(() => {
8-
nock(/.+/u)
9-
.get(/XYZ,USD/u)
10-
.reply(200, { XYZ: 123, USD: 456 })
11-
.get(/DEF,USD/u)
12-
.reply(200, { DEF: 123 })
13-
.get(/.+/u)
14-
.reply(200, { USD: 1337 })
15-
.persist();
16-
});
17-
18-
afterEach(() => {
19-
nock.cleanAll();
20-
});
21-
226
it('should set default state', () => {
23-
const controller = new CurrencyRateController();
7+
const fetchExchangeRateStub = stub();
8+
const controller = new CurrencyRateController({}, {}, fetchExchangeRateStub);
249
expect(controller.state).toEqual({
2510
conversionDate: 0,
2611
conversionRate: 0,
2712
currentCurrency: 'usd',
2813
nativeCurrency: 'ETH',
2914
usdConversionRate: 0,
3015
});
16+
17+
controller.disabled = true;
3118
});
3219

3320
it('should initialize with the default config', () => {
34-
const controller = new CurrencyRateController();
21+
const fetchExchangeRateStub = stub();
22+
const controller = new CurrencyRateController({}, {}, fetchExchangeRateStub);
3523
expect(controller.config).toEqual({
3624
currentCurrency: 'usd',
3725
disabled: false,
3826
interval: 180000,
3927
nativeCurrency: 'ETH',
4028
includeUSDRate: false,
4129
});
30+
31+
controller.disabled = true;
4232
});
4333

4434
it('should initialize with the currency in state', () => {
35+
const fetchExchangeRateStub = stub();
4536
const existingState = { currentCurrency: 'rep' };
46-
const controller = new CurrencyRateController({}, existingState);
37+
const controller = new CurrencyRateController({}, existingState, fetchExchangeRateStub);
4738
expect(controller.config).toEqual({
4839
currentCurrency: 'rep',
4940
disabled: false,
5041
interval: 180000,
5142
nativeCurrency: 'ETH',
5243
includeUSDRate: false,
5344
});
45+
46+
controller.disabled = true;
5447
});
5548

56-
it('should poll and update rate in the right interval', () => {
57-
return new Promise((resolve) => {
58-
const controller = new CurrencyRateController({ interval: 100 });
59-
const mock = stub(controller, 'fetchExchangeRate');
60-
setTimeout(() => {
61-
expect(mock.called).toBe(true);
62-
expect(mock.calledTwice).toBe(false);
63-
}, 1);
64-
setTimeout(() => {
65-
expect(mock.calledTwice).toBe(true);
66-
mock.restore();
67-
resolve();
68-
}, 150);
69-
});
49+
it('should poll and update rate in the right interval', async () => {
50+
const fetchExchangeRateStub = stub();
51+
const controller = new CurrencyRateController({ interval: 100 }, {}, fetchExchangeRateStub);
52+
53+
await new Promise((resolve) => setTimeout(() => resolve(), 1));
54+
expect(fetchExchangeRateStub.called).toBe(true);
55+
expect(fetchExchangeRateStub.calledTwice).toBe(false);
56+
await new Promise((resolve) => setTimeout(() => resolve(), 150));
57+
expect(fetchExchangeRateStub.calledTwice).toBe(true);
58+
59+
controller.disabled = true;
7060
});
7161

7262
it('should not update rates if disabled', async () => {
73-
const controller = new CurrencyRateController({
74-
interval: 10,
75-
});
76-
controller.fetchExchangeRate = stub().resolves({});
63+
const fetchExchangeRateStub = stub().resolves({});
64+
const controller = new CurrencyRateController({ interval: 10 }, {}, fetchExchangeRateStub);
7765
controller.disabled = true;
66+
7867
await controller.updateExchangeRate();
79-
expect((controller.fetchExchangeRate as any).called).toBe(false);
68+
expect(fetchExchangeRateStub.called).toBe(false);
8069
});
8170

8271
it('should clear previous interval', () => {
72+
const fetchExchangeRateStub = stub();
8373
const mock = stub(global, 'clearTimeout');
84-
const controller = new CurrencyRateController({ interval: 1337 });
74+
const controller = new CurrencyRateController({ interval: 1337 }, {}, fetchExchangeRateStub);
8575
return new Promise((resolve) => {
8676
setTimeout(() => {
8777
controller.poll(1338);
8878
expect(mock.called).toBe(true);
8979
mock.restore();
80+
81+
controller.disabled = true;
9082
resolve();
9183
}, 100);
9284
});
9385
});
9486

9587
it('should update currency', async () => {
96-
const controller = new CurrencyRateController({ interval: 10 });
88+
const fetchExchangeRateStub = stub().resolves({ conversionRate: 10 });
89+
const controller = new CurrencyRateController({ interval: 10 }, {}, fetchExchangeRateStub);
9790
expect(controller.state.conversionRate).toEqual(0);
9891
await controller.updateExchangeRate();
99-
expect(controller.state.conversionRate).toBeGreaterThan(0);
100-
});
101-
102-
it('should add usd rate to state when includeUSDRate is configured true', async () => {
103-
const controller = new CurrencyRateController({ includeUSDRate: true, currentCurrency: 'xyz' });
104-
expect(controller.state.usdConversionRate).toEqual(0);
105-
await controller.updateExchangeRate();
106-
expect(controller.state.usdConversionRate).toEqual(456);
107-
});
92+
expect(controller.state.conversionRate).toEqual(10);
10893

109-
it('should add usd rate to state fetches when configured', async () => {
110-
const controller = new CurrencyRateController({ includeUSDRate: true });
111-
const result = await controller.fetchExchangeRate('xyz', 'FOO', true);
112-
expect(result.usdConversionRate).toEqual(456);
113-
expect(result.conversionRate).toEqual(123);
94+
controller.disabled = true;
11495
});
11596

116-
it('should throw correctly when configured to return usd but receives an invalid response for currentCurrency rate', async () => {
117-
const controller = new CurrencyRateController({ includeUSDRate: true });
118-
await expect(controller.fetchExchangeRate('abc', 'FOO', true)).rejects.toThrow(
119-
'Invalid response for ABC: undefined',
97+
it('should add usd rate to state when includeUSDRate is configured true', async () => {
98+
const fetchExchangeRateStub = stub().resolves({});
99+
const controller = new CurrencyRateController(
100+
{ includeUSDRate: true, currentCurrency: 'xyz' },
101+
{},
102+
fetchExchangeRateStub,
120103
);
121-
});
122104

123-
it('should throw correctly when configured to return usd but receives an invalid response for usdConversionRate', async () => {
124-
const controller = new CurrencyRateController({ includeUSDRate: true });
125-
await expect(controller.fetchExchangeRate('def', 'FOO', true)).rejects.toThrow(
126-
'Invalid response for usdConversionRate: undefined',
127-
);
128-
});
129-
130-
describe('#fetchExchangeRate', () => {
131-
it('should handle a valid symbol in the API response', async () => {
132-
const controller = new CurrencyRateController({ nativeCurrency: 'usd' });
133-
const response = await controller.fetchExchangeRate('usd');
134-
expect(response.conversionRate).toEqual(1337);
135-
});
105+
await controller.updateExchangeRate();
136106

137-
it('should handle a missing symbol in the API response', async () => {
138-
const controller = new CurrencyRateController({ nativeCurrency: 'usd' });
139-
await expect(controller.fetchExchangeRate('foo')).rejects.toThrow('Invalid response for FOO: undefined');
140-
});
107+
expect(fetchExchangeRateStub.alwaysCalledWithExactly('xyz', 'ETH', true)).toBe(true);
141108
});
142109
});

0 commit comments

Comments
 (0)