Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
"devDependencies": {
"@fluffy-spoon/substitute": "1.208.0",
"@golevelup/ts-jest": "0.4.0",
"@nestjs/core": "10.2.8",
"@nestjs/testing": "10.2.8",
"@nestjs/typeorm": "10.0.2",
"@parcel/core": "2.10.1",
"@parcel/packager-ts": "2.10.1",
Expand All @@ -48,6 +50,7 @@
"lint-staged": "14.0.1",
"parcel": "2.10.1",
"prettier": "3.0.3",
"rxjs": "7.8.1",
"ts-jest": "29.1.1",
"ts-loader": "9.4.4",
"ts-md5": "1.3.1",
Expand Down
128 changes: 128 additions & 0 deletions test/cache/externalResponseCacheService.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { ExternalResponseCacheService } from '../../src/cache/externalResponseCacheService';
import { ApiCallResults } from '../../src/cache/apiCallResults.entity';
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import AsyncRedis from 'async-redis';
import { Repository } from 'typeorm';

jest.mock('async-redis');
jest.mock('ethers');

describe('ExternalResponseCacheService', () => {
let service: ExternalResponseCacheService;
let cacheMock: jest.Mocked<AsyncRedis>;
let apiCallResultsRepoMock: jest.Mocked<Repository<ApiCallResults>>;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ExternalResponseCacheService,
{
provide: 'CACHE_ETH',
useValue: {
get: jest.fn(),
set: jest.fn(),
},
},
{
provide: getRepositoryToken(ApiCallResults),
useValue: {
findOneBy: jest.fn(),
upsert: jest.fn(),
},
},
],
}).compile();

service = module.get<ExternalResponseCacheService>(ExternalResponseCacheService);
cacheMock = module.get('CACHE_ETH');
apiCallResultsRepoMock = module.get(getRepositoryToken(ApiCallResults));
});

afterEach(() => {
jest.clearAllMocks();
});

describe('getRestClient', () => {
it('should return the cache instance', () => {
const cacheClient = service.getRestClient();
expect(cacheClient).toBe(cacheMock);
});
});

describe('cacheOrPerform', () => {
const mockChainId = '1';
const mockCacheKey = 'test_key';
const mockTTL = 60;
const mockEntity = { data: 'test_data' };
const mockOnCacheMiss = jest.fn().mockResolvedValue(mockEntity);
const mockDbEntity = { key: mockCacheKey, value: mockEntity } as ApiCallResults;

it('should return cached data from Redis if available', async () => {
cacheMock.get.mockResolvedValueOnce(JSON.stringify(mockEntity));

const result = await service.cacheOrPerform(mockChainId, mockCacheKey, mockTTL, mockOnCacheMiss);
expect(result).toEqual(mockEntity);
expect(cacheMock.get).toHaveBeenCalledWith(mockCacheKey);
expect(apiCallResultsRepoMock.findOneBy).not.toHaveBeenCalled();
expect(mockOnCacheMiss).not.toHaveBeenCalled();
});

it('should return cached data from the database if Redis cache is empty', async () => {
cacheMock.get.mockResolvedValueOnce(null);
apiCallResultsRepoMock.findOneBy.mockResolvedValueOnce(mockDbEntity);

const result = await service.cacheOrPerform(mockChainId, mockCacheKey, mockTTL, mockOnCacheMiss);
expect(result).toEqual(mockEntity);
expect(cacheMock.get).toHaveBeenCalledWith(mockCacheKey);
expect(apiCallResultsRepoMock.findOneBy).toHaveBeenCalledWith({ key: mockCacheKey });
expect(mockOnCacheMiss).not.toHaveBeenCalled();
});

it('should call onCacheMiss and cache the result if no cache is found', async () => {
cacheMock.get.mockResolvedValueOnce(null);
apiCallResultsRepoMock.findOneBy.mockResolvedValueOnce(null);

const result = await service.cacheOrPerform(mockChainId, mockCacheKey, mockTTL, mockOnCacheMiss);
expect(result).toEqual(mockEntity);
expect(mockOnCacheMiss).toHaveBeenCalled();
expect(cacheMock.set).toHaveBeenCalledWith(mockCacheKey, JSON.stringify(mockEntity), 'EX', mockTTL);
});

it('should not cache the result if the entity is 0', async () => {
mockOnCacheMiss.mockResolvedValueOnce(0);

const result = await service.cacheOrPerform(mockChainId, mockCacheKey, mockTTL, mockOnCacheMiss);
expect(result).toEqual(0);
expect(cacheMock.set).not.toHaveBeenCalled();
});

it('should not cache the result if the entity is null or empty', async () => {
mockOnCacheMiss.mockResolvedValueOnce(null);

const result = await service.cacheOrPerform(mockChainId, mockCacheKey, mockTTL, mockOnCacheMiss);
expect(result).toBeNull();
expect(cacheMock.set).not.toHaveBeenCalled();

mockOnCacheMiss.mockResolvedValueOnce('');

const resultEmpty = await service.cacheOrPerform(mockChainId, mockCacheKey, mockTTL, mockOnCacheMiss);
expect(resultEmpty).toBeNull();
expect(cacheMock.set).not.toHaveBeenCalled();
});

it('should save to the database if the TTL is PERM_CACHE_DURATION', async () => {
const permTTL = ExternalResponseCacheService.PERM_CACHE_DURATION;
cacheMock.get.mockResolvedValueOnce(null);
apiCallResultsRepoMock.findOneBy.mockResolvedValueOnce(null);

const result = await service.cacheOrPerform(mockChainId, mockCacheKey, permTTL, mockOnCacheMiss);
expect(result).toEqual(mockEntity);
expect(apiCallResultsRepoMock.upsert).toHaveBeenCalledWith(
expect.objectContaining({ key: mockCacheKey, value: mockEntity }),
['key'],
);
expect(cacheMock.set).not.toHaveBeenCalled();
});
});
});
102 changes: 101 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1513,6 +1513,53 @@ __metadata:
languageName: node
linkType: hard

"@nestjs/core@npm:10.2.8":
version: 10.2.8
resolution: "@nestjs/core@npm:10.2.8"
dependencies:
"@nuxtjs/opencollective": 0.3.2
fast-safe-stringify: 2.1.1
iterare: 1.2.1
path-to-regexp: 3.2.0
tslib: 2.6.2
uid: 2.0.2
peerDependencies:
"@nestjs/common": ^10.0.0
"@nestjs/microservices": ^10.0.0
"@nestjs/platform-express": ^10.0.0
"@nestjs/websockets": ^10.0.0
reflect-metadata: ^0.1.12
rxjs: ^7.1.0
peerDependenciesMeta:
"@nestjs/microservices":
optional: true
"@nestjs/platform-express":
optional: true
"@nestjs/websockets":
optional: true
checksum: bd75f4295da404fcb41effb66b781263b559b5ca0bc148d844d465f287a1f7402bdcc74293ec9df8ab2509c7deced08ed0b298acd29e9bc5c0bd630fe764d4d2
languageName: node
linkType: hard

"@nestjs/testing@npm:10.2.8":
version: 10.2.8
resolution: "@nestjs/testing@npm:10.2.8"
dependencies:
tslib: 2.6.2
peerDependencies:
"@nestjs/common": ^10.0.0
"@nestjs/core": ^10.0.0
"@nestjs/microservices": ^10.0.0
"@nestjs/platform-express": ^10.0.0
peerDependenciesMeta:
"@nestjs/microservices":
optional: true
"@nestjs/platform-express":
optional: true
checksum: 6aef6ace4b7fca1ce7ad84e0de9778eb5e1c58be036a3201242bf71c322692f85fe90986f5f863c3fd3ce31cd1e41fdfc8aa4c31d3b7e5a1c8bb67a0e88286ff
languageName: node
linkType: hard

"@nestjs/typeorm@npm:10.0.2":
version: 10.0.2
resolution: "@nestjs/typeorm@npm:10.0.2"
Expand Down Expand Up @@ -1625,6 +1672,19 @@ __metadata:
languageName: node
linkType: hard

"@nuxtjs/opencollective@npm:0.3.2":
version: 0.3.2
resolution: "@nuxtjs/opencollective@npm:0.3.2"
dependencies:
chalk: ^4.1.0
consola: ^2.15.0
node-fetch: ^2.6.1
bin:
opencollective: bin/opencollective.js
checksum: fd3737c12edf55b5c2279674664c3ed5e756410ea82e9cd324c3f0e032ed5ccd8df1959ec69ea97f2f1c9c33c884aae3d7a7108a73ea0faa90d74ea47cf364d4
languageName: node
linkType: hard

"@parcel/bundler-default@npm:2.10.1":
version: 2.10.1
resolution: "@parcel/bundler-default@npm:2.10.1"
Expand Down Expand Up @@ -3351,6 +3411,8 @@ __metadata:
"@fluffy-spoon/substitute": 1.208.0
"@golevelup/ts-jest": 0.4.0
"@nestjs/common": ^10.3.9
"@nestjs/core": 10.2.8
"@nestjs/testing": 10.2.8
"@nestjs/typeorm": 10.0.2
"@parcel/core": 2.10.1
"@parcel/packager-ts": 2.10.1
Expand Down Expand Up @@ -3385,6 +3447,7 @@ __metadata:
parcel: 2.10.1
prettier: 3.0.3
redis: 4.6.8
rxjs: 7.8.1
ts-jest: 29.1.1
ts-loader: 9.4.4
ts-md5: 1.3.1
Expand Down Expand Up @@ -4193,6 +4256,13 @@ __metadata:
languageName: node
linkType: hard

"consola@npm:^2.15.0":
version: 2.15.3
resolution: "consola@npm:2.15.3"
checksum: 8ef7a09b703ec67ac5c389a372a33b6dc97eda6c9876443a60d76a3076eea0259e7f67a4e54fd5a52f97df73690822d090cf8b7e102b5761348afef7c6d03e28
languageName: node
linkType: hard

"convert-source-map@npm:^2.0.0":
version: 2.0.0
resolution: "convert-source-map@npm:2.0.0"
Expand Down Expand Up @@ -5252,6 +5322,13 @@ __metadata:
languageName: node
linkType: hard

"fast-safe-stringify@npm:2.1.1":
version: 2.1.1
resolution: "fast-safe-stringify@npm:2.1.1"
checksum: a851cbddc451745662f8f00ddb622d6766f9bd97642dabfd9a405fb0d646d69fc0b9a1243cbf67f5f18a39f40f6fa821737651ff1bceeba06c9992ca2dc5bd3d
languageName: node
linkType: hard

"fast-stable-stringify@npm:^1.0.0":
version: 1.0.0
resolution: "fast-stable-stringify@npm:1.0.0"
Expand Down Expand Up @@ -7642,7 +7719,7 @@ __metadata:
languageName: node
linkType: hard

"node-fetch@npm:^2.6.7, node-fetch@npm:^2.7.0":
"node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7, node-fetch@npm:^2.7.0":
version: 2.7.0
resolution: "node-fetch@npm:2.7.0"
dependencies:
Expand Down Expand Up @@ -8085,6 +8162,13 @@ __metadata:
languageName: node
linkType: hard

"path-to-regexp@npm:3.2.0":
version: 3.2.0
resolution: "path-to-regexp@npm:3.2.0"
checksum: c3d35cda3b26d9e604d789b9a1764bb9845f53ca8009d5809356b4677a3c064b0f01117a05a5b4b77bafd5ae002a82592e3f3495e885c22961f8b1dab8bd6ae7
languageName: node
linkType: hard

"path-type@npm:^4.0.0":
version: 4.0.0
resolution: "path-type@npm:4.0.0"
Expand Down Expand Up @@ -8548,6 +8632,15 @@ __metadata:
languageName: node
linkType: hard

"rxjs@npm:7.8.1":
version: 7.8.1
resolution: "rxjs@npm:7.8.1"
dependencies:
tslib: ^2.1.0
checksum: de4b53db1063e618ec2eca0f7965d9137cabe98cf6be9272efe6c86b47c17b987383df8574861bcced18ebd590764125a901d5506082be84a8b8e364bf05f119
languageName: node
linkType: hard

"safe-array-concat@npm:^1.1.2":
version: 1.1.2
resolution: "safe-array-concat@npm:1.1.2"
Expand Down Expand Up @@ -9294,6 +9387,13 @@ __metadata:
languageName: node
linkType: hard

"tslib@npm:^2.1.0":
version: 2.7.0
resolution: "tslib@npm:2.7.0"
checksum: 1606d5c89f88d466889def78653f3aab0f88692e80bb2066d090ca6112ae250ec1cfa9dbfaab0d17b60da15a4186e8ec4d893801c67896b277c17374e36e1d28
languageName: node
linkType: hard

"tslib@npm:^2.5.0":
version: 2.6.3
resolution: "tslib@npm:2.6.3"
Expand Down