-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: price in memory caching #31
Changes from all commits
b290492
48b22a0
f307c36
ee9118a
0dd3f0a
8e0015d
74c7ec2
ce0ad2f
545c857
e01801c
bbb3668
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,16 @@ | ||
import { createMock } from "@golevelup/ts-jest"; | ||
import { Cache, CACHE_MANAGER } from "@nestjs/cache-manager"; | ||
import { Logger } from "@nestjs/common"; | ||
import { Test, TestingModule } from "@nestjs/testing"; | ||
import { AxiosError, AxiosInstance } from "axios"; | ||
import { AxiosInstance } from "axios"; | ||
import MockAdapter from "axios-mock-adapter"; | ||
import { WINSTON_MODULE_PROVIDER } from "nest-winston"; | ||
|
||
import { ApiNotAvailable, RateLimitExceeded } from "@zkchainhub/pricing/exceptions"; | ||
import { TokenPrices } from "@zkchainhub/pricing/types/tokenPrice.type"; | ||
import { BASE_CURRENCY } from "@zkchainhub/shared"; | ||
|
||
import { CoingeckoService } from "./coingecko.service"; | ||
import { CoingeckoService, DECIMALS_PRECISION } from "./coingecko.service"; | ||
|
||
export const mockLogger: Partial<Logger> = { | ||
log: jest.fn(), | ||
|
@@ -20,6 +23,7 @@ describe("CoingeckoService", () => { | |
let service: CoingeckoService; | ||
let axios: AxiosInstance; | ||
let mockAxios: MockAdapter; | ||
let cache: Cache; | ||
const apiKey = "COINGECKO_API_KEY"; | ||
const apiBaseUrl = "https://api.coingecko.com/api/v3/"; | ||
|
||
|
@@ -29,21 +33,31 @@ describe("CoingeckoService", () => { | |
CoingeckoService, | ||
{ | ||
provide: CoingeckoService, | ||
useFactory: (logger: Logger) => { | ||
return new CoingeckoService(apiKey, apiBaseUrl, logger); | ||
useFactory: (logger: Logger, cache: Cache) => { | ||
return new CoingeckoService(apiKey, apiBaseUrl, logger, cache); | ||
}, | ||
inject: [WINSTON_MODULE_PROVIDER], | ||
inject: [WINSTON_MODULE_PROVIDER, CACHE_MANAGER], | ||
}, | ||
{ | ||
provide: WINSTON_MODULE_PROVIDER, | ||
useValue: mockLogger, | ||
}, | ||
{ | ||
provide: CACHE_MANAGER, | ||
useValue: createMock<Cache>({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pretty neat solution the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. its from some guys that made utilities and helpers for NestJs actually jajaja |
||
store: createMock<Cache["store"]>({ | ||
mget: jest.fn(), | ||
mset: jest.fn(), | ||
}), | ||
}), | ||
}, | ||
], | ||
}).compile(); | ||
|
||
service = module.get<CoingeckoService>(CoingeckoService); | ||
axios = service["axios"]; | ||
mockAxios = new MockAdapter(axios); | ||
cache = module.get<Cache>(CACHE_MANAGER); | ||
}); | ||
|
||
afterEach(() => { | ||
|
@@ -67,71 +81,70 @@ describe("CoingeckoService", () => { | |
}); | ||
|
||
describe("getTokenPrices", () => { | ||
it("return token prices", async () => { | ||
it("fetches all token prices from Coingecko", async () => { | ||
const tokenIds = ["token1", "token2"]; | ||
const currency = "usd"; | ||
const expectedResponse: TokenPrices = { | ||
token1: { usd: 1.23 }, | ||
token2: { usd: 4.56 }, | ||
}; | ||
|
||
jest.spyOn(cache.store, "mget").mockResolvedValueOnce([null, null]); | ||
jest.spyOn(axios, "get").mockResolvedValueOnce({ | ||
data: expectedResponse, | ||
}); | ||
|
||
const result = await service.getTokenPrices(tokenIds, { currency }); | ||
const result = await service.getTokenPrices(tokenIds); | ||
|
||
expect(result).toEqual({ | ||
token1: 1.23, | ||
token2: 4.56, | ||
}); | ||
expect(axios.get).toHaveBeenCalledWith(`simple/price`, { | ||
params: { | ||
vs_currencies: currency, | ||
vs_currencies: BASE_CURRENCY, | ||
ids: tokenIds.join(","), | ||
precision: service["DECIMALS_PRECISION"].toString(), | ||
precision: DECIMALS_PRECISION.toString(), | ||
}, | ||
}); | ||
expect(cache.store.mget).toHaveBeenCalledWith("token1", "token2"); | ||
expect(cache.store.mset).toHaveBeenCalledWith([ | ||
["token1", 1.23], | ||
["token2", 4.56], | ||
]); | ||
}); | ||
|
||
it("throw ApiNotAvailable when Coingecko returns a 500 family exception", async () => { | ||
const tokenIds = ["token1", "token2"]; | ||
const currency = "usd"; | ||
|
||
jest.spyOn(cache.store, "mget").mockResolvedValueOnce([null, null]); | ||
mockAxios.onGet().replyOnce(503, { | ||
data: {}, | ||
status: 503, | ||
statusText: "Service not available", | ||
}); | ||
|
||
await expect(service.getTokenPrices(tokenIds, { currency })).rejects.toThrow( | ||
await expect(service.getTokenPrices(tokenIds)).rejects.toThrow( | ||
new ApiNotAvailable("Coingecko"), | ||
); | ||
}); | ||
|
||
it("throw RateLimitExceeded when Coingecko returns 429 exception", async () => { | ||
const tokenIds = ["token1", "token2"]; | ||
const currency = "usd"; | ||
|
||
jest.spyOn(cache.store, "mget").mockResolvedValueOnce([null, null]); | ||
mockAxios.onGet().replyOnce(429, { | ||
data: {}, | ||
status: 429, | ||
statusText: "Too Many Requests", | ||
}); | ||
|
||
await expect(service.getTokenPrices(tokenIds, { currency })).rejects.toThrow( | ||
new RateLimitExceeded(), | ||
); | ||
await expect(service.getTokenPrices(tokenIds)).rejects.toThrow(new RateLimitExceeded()); | ||
}); | ||
|
||
it("throw an HttpException with the error message when an error occurs", async () => { | ||
const tokenIds = ["invalidTokenId", "token2"]; | ||
const currency = "usd"; | ||
|
||
jest.spyOn(axios, "get").mockRejectedValueOnce( | ||
new AxiosError("Invalid token ID", "400"), | ||
); | ||
|
||
jest.spyOn(cache.store, "mget").mockResolvedValueOnce([null, null]); | ||
mockAxios.onGet().replyOnce(400, { | ||
data: { | ||
message: "Invalid token ID", | ||
|
@@ -140,7 +153,7 @@ describe("CoingeckoService", () => { | |
statusText: "Bad Request", | ||
}); | ||
|
||
await expect(service.getTokenPrices(tokenIds, { currency })).rejects.toThrow(); | ||
await expect(service.getTokenPrices(tokenIds)).rejects.toThrow(); | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export const TOKEN_CACHE_TTL_IN_SEC = 60; | ||
export const BASE_CURRENCY = "usd"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sweet