From 11d9d53d96ba5ab8ca61144439523e2c750fd128 Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Sun, 1 Sep 2024 20:52:03 +0200 Subject: [PATCH] test: unit test for ExternalResponseCacheService --- package.json | 3 + .../externalResponseCacheService.spec.ts | 128 ++++++++++++++++++ yarn.lock | 102 +++++++++++++- 3 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 test/cache/externalResponseCacheService.spec.ts diff --git a/package.json b/package.json index 2a690b0..6feba20 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/test/cache/externalResponseCacheService.spec.ts b/test/cache/externalResponseCacheService.spec.ts new file mode 100644 index 0000000..1c150c3 --- /dev/null +++ b/test/cache/externalResponseCacheService.spec.ts @@ -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; + let apiCallResultsRepoMock: jest.Mocked>; + + 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); + 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(); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index bd590c9..c2f6c4b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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" @@ -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 @@ -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 @@ -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" @@ -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" @@ -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: @@ -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" @@ -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" @@ -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"