-
-
Notifications
You must be signed in to change notification settings - Fork 203
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
The CryptoCompare API interactions have been extracted from the CurrencyRateController and moved into a separate module.
- Loading branch information
Showing
4 changed files
with
221 additions
and
114 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
import * as nock from 'nock'; | ||
|
||
import { fetchExchangeRate } from './crypto-compare'; | ||
|
||
const cryptoCompareHost = 'https://min-api.cryptocompare.com'; | ||
|
||
describe('CryptoCompare', () => { | ||
beforeAll(() => { | ||
nock.disableNetConnect(); | ||
}); | ||
|
||
afterAll(() => { | ||
nock.enableNetConnect(); | ||
}); | ||
|
||
afterEach(() => { | ||
nock.cleanAll(); | ||
}); | ||
|
||
it('should return CAD conversion rate', async () => { | ||
nock(cryptoCompareHost) | ||
.get('/data/price?fsym=ETH&tsyms=CAD') | ||
.reply(200, { CAD: 2000.42 }); | ||
|
||
const { conversionRate } = await fetchExchangeRate('CAD', 'ETH'); | ||
|
||
expect(conversionRate).toEqual(2000.42); | ||
}); | ||
|
||
it('should return conversion date', async () => { | ||
nock(cryptoCompareHost) | ||
.get('/data/price?fsym=ETH&tsyms=CAD') | ||
.reply(200, { CAD: 2000.42 }); | ||
|
||
const before = Date.now() / 1000; | ||
const { conversionDate } = await fetchExchangeRate('CAD', 'ETH'); | ||
const after = Date.now() / 1000; | ||
|
||
expect(conversionDate).toBeGreaterThanOrEqual(before); | ||
expect(conversionDate).toBeLessThanOrEqual(after); | ||
}); | ||
|
||
it('should return CAD conversion rate given lower-cased currency', async () => { | ||
nock(cryptoCompareHost) | ||
.get('/data/price?fsym=ETH&tsyms=CAD') | ||
.reply(200, { CAD: 2000.42 }); | ||
|
||
const { conversionRate } = await fetchExchangeRate('cad', 'ETH'); | ||
|
||
expect(conversionRate).toEqual(2000.42); | ||
}); | ||
|
||
it('should return CAD conversion rate given lower-cased native currency', async () => { | ||
nock(cryptoCompareHost) | ||
.get('/data/price?fsym=ETH&tsyms=CAD') | ||
.reply(200, { CAD: 2000.42 }); | ||
|
||
const { conversionRate } = await fetchExchangeRate('CAD', 'eth'); | ||
|
||
expect(conversionRate).toEqual(2000.42); | ||
}); | ||
|
||
it('should not return USD conversion rate when fetching just CAD conversion rate', async () => { | ||
nock(cryptoCompareHost) | ||
.get('/data/price?fsym=ETH&tsyms=CAD') | ||
.reply(200, { CAD: 1000.42 }); | ||
|
||
const { usdConversionRate } = await fetchExchangeRate('CAD', 'ETH'); | ||
|
||
expect(usdConversionRate).toBeFalsy(); | ||
}); | ||
|
||
it('should return USD conversion rate for USD even when includeUSD is disabled', async () => { | ||
nock(cryptoCompareHost) | ||
.get('/data/price?fsym=ETH&tsyms=USD') | ||
.reply(200, { USD: 1000.42 }); | ||
|
||
const { conversionRate, usdConversionRate } = await fetchExchangeRate('USD', 'ETH', false); | ||
|
||
expect(conversionRate).toEqual(1000.42); | ||
expect(usdConversionRate).toEqual(1000.42); | ||
}); | ||
|
||
it('should return USD conversion rate for USD when includeUSD is enabled', async () => { | ||
nock(cryptoCompareHost) | ||
.get('/data/price?fsym=ETH&tsyms=USD') | ||
.reply(200, { USD: 1000.42 }); | ||
|
||
const { conversionRate, usdConversionRate } = await fetchExchangeRate('USD', 'ETH', true); | ||
|
||
expect(conversionRate).toEqual(1000.42); | ||
expect(usdConversionRate).toEqual(1000.42); | ||
}); | ||
|
||
it('should return CAD and USD conversion rate', async () => { | ||
nock(cryptoCompareHost) | ||
.get('/data/price?fsym=ETH&tsyms=CAD,USD') | ||
.reply(200, { CAD: 2000.42, USD: 1000.42 }); | ||
|
||
const { conversionRate, usdConversionRate } = await fetchExchangeRate('CAD', 'ETH', true); | ||
|
||
expect(conversionRate).toEqual(2000.42); | ||
expect(usdConversionRate).toEqual(1000.42); | ||
}); | ||
|
||
it('should throw if fetch throws', async () => { | ||
nock(cryptoCompareHost) | ||
.get('/data/price?fsym=ETH&tsyms=CAD') | ||
.replyWithError('Example network error'); | ||
|
||
await expect(fetchExchangeRate('CAD', 'ETH')).rejects.toThrow('Example network error'); | ||
}); | ||
|
||
it('should throw if fetch returns unsuccessful response', async () => { | ||
nock(cryptoCompareHost) | ||
.get('/data/price?fsym=ETH&tsyms=CAD') | ||
.reply(500); | ||
|
||
await expect(fetchExchangeRate('CAD', 'ETH')).rejects.toThrow(`Fetch failed with status '500' for request '${cryptoCompareHost}/data/price?fsym=ETH&tsyms=CAD'`); | ||
}); | ||
|
||
it('should throw if conversion rate is invalid', async () => { | ||
nock(cryptoCompareHost) | ||
.get('/data/price?fsym=ETH&tsyms=CAD') | ||
.reply(200, { CAD: 'invalid' }); | ||
|
||
await expect(fetchExchangeRate('CAD', 'ETH')).rejects.toThrow('Invalid response for CAD: invalid'); | ||
}); | ||
|
||
it('should throw if USD conversion rate is invalid', async () => { | ||
nock(cryptoCompareHost) | ||
.get('/data/price?fsym=ETH&tsyms=CAD,USD') | ||
.reply(200, { CAD: 2000.47, USD: 'invalid' }); | ||
|
||
await expect(fetchExchangeRate('CAD', 'ETH', true)).rejects.toThrow('Invalid response for usdConversionRate: invalid'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { handleFetch } from '../util'; | ||
|
||
function getPricingURL(currentCurrency: string, nativeCurrency: string, includeUSDRate?: boolean) { | ||
return ( | ||
`https://min-api.cryptocompare.com/data/price?fsym=` + | ||
`${nativeCurrency.toUpperCase()}&tsyms=${currentCurrency.toUpperCase()}` + | ||
`${includeUSDRate && currentCurrency.toUpperCase() !== 'USD' ? ',USD' : ''}` | ||
); | ||
} | ||
|
||
/** | ||
* Fetches the exchange rate for a given currency | ||
* | ||
* @param currency - ISO 4217 currency code | ||
* @param nativeCurrency - Symbol for base asset | ||
* @param includeUSDRate - Whether to add the USD rate to the fetch | ||
* @returns - Promise resolving to exchange rate for given currency | ||
*/ | ||
export async function fetchExchangeRate(currency: string, nativeCurrency: string, includeUSDRate?: boolean): Promise<{ conversionDate: number; conversionRate: number; usdConversionRate: number }> { | ||
const json = await handleFetch(getPricingURL(currency, nativeCurrency, includeUSDRate)); | ||
const conversionRate = Number(json[currency.toUpperCase()]); | ||
const usdConversionRate = Number(json.USD); | ||
if (!Number.isFinite(conversionRate)) { | ||
throw new Error(`Invalid response for ${currency.toUpperCase()}: ${json[currency.toUpperCase()]}`); | ||
} | ||
if (includeUSDRate && !Number.isFinite(usdConversionRate)) { | ||
throw new Error(`Invalid response for usdConversionRate: ${json.USD}`); | ||
} | ||
|
||
return { | ||
conversionDate: Date.now() / 1000, | ||
conversionRate, | ||
usdConversionRate, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,142 +1,109 @@ | ||
import 'isomorphic-fetch'; | ||
import { stub } from 'sinon'; | ||
import * as nock from 'nock'; | ||
import CurrencyRateController from './CurrencyRateController'; | ||
|
||
describe('CurrencyRateController', () => { | ||
beforeEach(() => { | ||
nock(/.+/u) | ||
.get(/XYZ,USD/u) | ||
.reply(200, { XYZ: 123, USD: 456 }) | ||
.get(/DEF,USD/u) | ||
.reply(200, { DEF: 123 }) | ||
.get(/.+/u) | ||
.reply(200, { USD: 1337 }) | ||
.persist(); | ||
}); | ||
|
||
afterEach(() => { | ||
nock.cleanAll(); | ||
}); | ||
|
||
it('should set default state', () => { | ||
const controller = new CurrencyRateController(); | ||
const fetchExchangeRateStub = stub(); | ||
const controller = new CurrencyRateController({}, {}, fetchExchangeRateStub); | ||
expect(controller.state).toEqual({ | ||
conversionDate: 0, | ||
conversionRate: 0, | ||
currentCurrency: 'usd', | ||
nativeCurrency: 'ETH', | ||
usdConversionRate: 0, | ||
}); | ||
|
||
controller.disabled = true; | ||
}); | ||
|
||
it('should initialize with the default config', () => { | ||
const controller = new CurrencyRateController(); | ||
const fetchExchangeRateStub = stub(); | ||
const controller = new CurrencyRateController({}, {}, fetchExchangeRateStub); | ||
expect(controller.config).toEqual({ | ||
currentCurrency: 'usd', | ||
disabled: false, | ||
interval: 180000, | ||
nativeCurrency: 'ETH', | ||
includeUSDRate: false, | ||
}); | ||
|
||
controller.disabled = true; | ||
}); | ||
|
||
it('should initialize with the currency in state', () => { | ||
const fetchExchangeRateStub = stub(); | ||
const existingState = { currentCurrency: 'rep' }; | ||
const controller = new CurrencyRateController({}, existingState); | ||
const controller = new CurrencyRateController({}, existingState, fetchExchangeRateStub); | ||
expect(controller.config).toEqual({ | ||
currentCurrency: 'rep', | ||
disabled: false, | ||
interval: 180000, | ||
nativeCurrency: 'ETH', | ||
includeUSDRate: false, | ||
}); | ||
|
||
controller.disabled = true; | ||
}); | ||
|
||
it('should poll and update rate in the right interval', () => { | ||
return new Promise((resolve) => { | ||
const controller = new CurrencyRateController({ interval: 100 }); | ||
const mock = stub(controller, 'fetchExchangeRate'); | ||
setTimeout(() => { | ||
expect(mock.called).toBe(true); | ||
expect(mock.calledTwice).toBe(false); | ||
}, 1); | ||
setTimeout(() => { | ||
expect(mock.calledTwice).toBe(true); | ||
mock.restore(); | ||
resolve(); | ||
}, 150); | ||
}); | ||
it('should poll and update rate in the right interval', async () => { | ||
const fetchExchangeRateStub = stub(); | ||
const controller = new CurrencyRateController({ interval: 100 }, {}, fetchExchangeRateStub); | ||
|
||
await new Promise((resolve) => setTimeout(() => resolve(), 1)); | ||
expect(fetchExchangeRateStub.called).toBe(true); | ||
expect(fetchExchangeRateStub.calledTwice).toBe(false); | ||
await new Promise((resolve) => setTimeout(() => resolve(), 150)); | ||
expect(fetchExchangeRateStub.calledTwice).toBe(true); | ||
|
||
controller.disabled = true; | ||
}); | ||
|
||
it('should not update rates if disabled', async () => { | ||
const controller = new CurrencyRateController({ | ||
interval: 10, | ||
}); | ||
controller.fetchExchangeRate = stub().resolves({}); | ||
const fetchExchangeRateStub = stub().resolves({}); | ||
const controller = new CurrencyRateController({ interval: 10 }, {}, fetchExchangeRateStub); | ||
controller.disabled = true; | ||
|
||
await controller.updateExchangeRate(); | ||
expect((controller.fetchExchangeRate as any).called).toBe(false); | ||
expect(fetchExchangeRateStub.called).toBe(false); | ||
}); | ||
|
||
it('should clear previous interval', () => { | ||
const fetchExchangeRateStub = stub(); | ||
const mock = stub(global, 'clearTimeout'); | ||
const controller = new CurrencyRateController({ interval: 1337 }); | ||
const controller = new CurrencyRateController({ interval: 1337 }, {}, fetchExchangeRateStub); | ||
return new Promise((resolve) => { | ||
setTimeout(() => { | ||
controller.poll(1338); | ||
expect(mock.called).toBe(true); | ||
mock.restore(); | ||
|
||
controller.disabled = true; | ||
resolve(); | ||
}, 100); | ||
}); | ||
}); | ||
|
||
it('should update currency', async () => { | ||
const controller = new CurrencyRateController({ interval: 10 }); | ||
const fetchExchangeRateStub = stub().resolves({ conversionRate: 10 }); | ||
const controller = new CurrencyRateController({ interval: 10 }, {}, fetchExchangeRateStub); | ||
expect(controller.state.conversionRate).toEqual(0); | ||
await controller.updateExchangeRate(); | ||
expect(controller.state.conversionRate).toBeGreaterThan(0); | ||
}); | ||
|
||
it('should add usd rate to state when includeUSDRate is configured true', async () => { | ||
const controller = new CurrencyRateController({ includeUSDRate: true, currentCurrency: 'xyz' }); | ||
expect(controller.state.usdConversionRate).toEqual(0); | ||
await controller.updateExchangeRate(); | ||
expect(controller.state.usdConversionRate).toEqual(456); | ||
}); | ||
expect(controller.state.conversionRate).toEqual(10); | ||
|
||
it('should add usd rate to state fetches when configured', async () => { | ||
const controller = new CurrencyRateController({ includeUSDRate: true }); | ||
const result = await controller.fetchExchangeRate('xyz', 'FOO', true); | ||
expect(result.usdConversionRate).toEqual(456); | ||
expect(result.conversionRate).toEqual(123); | ||
controller.disabled = true; | ||
}); | ||
|
||
it('should throw correctly when configured to return usd but receives an invalid response for currentCurrency rate', async () => { | ||
const controller = new CurrencyRateController({ includeUSDRate: true }); | ||
await expect(controller.fetchExchangeRate('abc', 'FOO', true)).rejects.toThrow( | ||
'Invalid response for ABC: undefined', | ||
it('should add usd rate to state when includeUSDRate is configured true', async () => { | ||
const fetchExchangeRateStub = stub().resolves({}); | ||
const controller = new CurrencyRateController( | ||
{ includeUSDRate: true, currentCurrency: 'xyz' }, | ||
{}, | ||
fetchExchangeRateStub, | ||
); | ||
}); | ||
|
||
it('should throw correctly when configured to return usd but receives an invalid response for usdConversionRate', async () => { | ||
const controller = new CurrencyRateController({ includeUSDRate: true }); | ||
await expect(controller.fetchExchangeRate('def', 'FOO', true)).rejects.toThrow( | ||
'Invalid response for usdConversionRate: undefined', | ||
); | ||
}); | ||
|
||
describe('#fetchExchangeRate', () => { | ||
it('should handle a valid symbol in the API response', async () => { | ||
const controller = new CurrencyRateController({ nativeCurrency: 'usd' }); | ||
const response = await controller.fetchExchangeRate('usd'); | ||
expect(response.conversionRate).toEqual(1337); | ||
}); | ||
await controller.updateExchangeRate(); | ||
|
||
it('should handle a missing symbol in the API response', async () => { | ||
const controller = new CurrencyRateController({ nativeCurrency: 'usd' }); | ||
await expect(controller.fetchExchangeRate('foo')).rejects.toThrow('Invalid response for FOO: undefined'); | ||
}); | ||
expect(fetchExchangeRateStub.alwaysCalledWithExactly('xyz', 'ETH', true)).toBe(true); | ||
}); | ||
}); |
Oops, something went wrong.