import { Test, TestingModule } from '@nestjs/testing'; import { Cache } from 'cache-manager'; import { createHash } from 'crypto'; import { CachedBaseService } from '../cached-base.service'; import { CacheConfiguration, InvalidationStrategy, TestMessage, TestCreateMessage, TestUpdateMessage, } from './mock-types'; import { dataResultMock, paginatedResultMock } from './mocks/paginated-result.mock'; import { paginatedQueryMock } from './mocks/paginated-query.mock'; import { IBaseService } from '../base.interface'; const mockLoggerService = { debug: jest.fn(), error: jest.fn(), log: jest.fn(), }; const mockCacheManager = { get: jest.fn(), set: jest.fn(), mdel: jest.fn(), stores: [ { iterator: jest.fn(async function* (namespace: string) { yield ['cachedbase:findOne:12345', '{"id":1, "name":"cached"}']; yield ['cachedbase:findAll:all', '{"data":[]}']; yield ['another-service:findAll:all', '{"data":[]}']; }), }, ], }; const mockService: IBaseService = { findOne: jest.fn(), findAll: jest.fn(), getMany: jest.fn(), create: jest.fn(), update: jest.fn(), remove: jest.fn(), count: jest.fn(), }; describe('CachedBaseService', () => { let cachedService: CachedBaseService; let service: jest.Mocked>; let cacheManager: jest.Mocked; const testId = 1; const testMessage: TestMessage = { id: testId, name: 'Test Entity' }; const testCreateDTO: TestCreateMessage = { name: 'Test Entity' }; const testUpdateDTO: TestUpdateMessage = { id: testId, name: 'Updated Entity' }; const cachedData = JSON.stringify(testMessage); const generateHash = (params: any) => createHash('md5').update(JSON.stringify(params)).digest('hex'); const cacheConfigOnChange: CacheConfiguration = { invalidationStrategy: InvalidationStrategy.OnChange }; const cacheConfigTTL: CacheConfiguration = { invalidationStrategy: InvalidationStrategy.TTL }; const setupModule = async (config: CacheConfiguration) => { const module: TestingModule = await Test.createTestingModule({ providers: [ { provide: CachedBaseService, useFactory: () => new CachedBaseService( mockLoggerService as any, mockService as unknown as jest.Mocked>, mockCacheManager as unknown as jest.Mocked, config, ), }, ], }).compile(); cachedService = module.get>( CachedBaseService, ); service = mockService as unknown as jest.Mocked>; cacheManager = mockCacheManager as unknown as jest.Mocked; jest.clearAllMocks(); }; describe('Read operations (findOne, findAll, getMany, count)', () => { beforeEach(async () => { await setupModule(cacheConfigTTL); }); it('should return cached data on cache hit (findOne)', async () => { // Arrange cacheManager.get.mockResolvedValue(cachedData); // Act const result = await cachedService.findOne(testId); // Assert expect(cacheManager.get).toHaveBeenCalledWith( `cachedbase:findOne:${generateHash({ id: testId })}`, ); expect(service.findOne).not.toHaveBeenCalled(); expect(cacheManager.set).not.toHaveBeenCalled(); expect(result).toEqual(testMessage); }); it('should fetch data from service and cache it on cache miss (findOne)', async () => { // Arrange cacheManager.get.mockResolvedValue(undefined); service.findOne.mockResolvedValue(testMessage); // Act const result = await cachedService.findOne(testId); // Assert expect(cacheManager.get).toHaveBeenCalledTimes(1); expect(service.findOne).toHaveBeenCalledWith(testId); expect(cacheManager.set).toHaveBeenCalledWith( `cachedbase:findOne:${generateHash({ id: testId })}`, cachedData, ); expect(result).toEqual(testMessage); }); it('should return cached data on cache hit (getMany)', async () => { // Arrange cacheManager.get.mockResolvedValue(cachedData); // Act const result = await cachedService.getMany(paginatedQueryMock); // Assert expect(cacheManager.get).toHaveBeenCalledWith( `cachedbase:getMany:${generateHash(paginatedQueryMock)}`, ); expect(service.getMany).not.toHaveBeenCalled(); expect(cacheManager.set).not.toHaveBeenCalled(); expect(result).toEqual(testMessage); }); it('should fetch data from service and cache it on cache miss (getMany)', async () => { // Arrange cacheManager.get.mockResolvedValue(undefined); service.getMany.mockResolvedValue(paginatedResultMock as any); // Act const result = await cachedService.getMany(paginatedQueryMock); // Assert expect(cacheManager.get).toHaveBeenCalledTimes(1); expect(service.getMany).toHaveBeenCalledWith(paginatedQueryMock); expect(cacheManager.set).toHaveBeenCalledWith( `cachedbase:getMany:${generateHash(paginatedQueryMock)}`, JSON.stringify(paginatedResultMock), ); expect(result).toEqual(paginatedResultMock); }); it('should return cached data on cache hit (findAll)', async () => { // Arrange cacheManager.get.mockResolvedValue(JSON.stringify({ data: dataResultMock })); // Act const result = await cachedService.findAll(); // Assert expect(cacheManager.get).toHaveBeenCalledWith( `cachedbase:findAll:all`, ); expect(service.findAll).not.toHaveBeenCalled(); expect(cacheManager.set).not.toHaveBeenCalled(); expect(result).toEqual({ data: dataResultMock }); }); it('should fetch data from service and cache it on cache miss (findAll)', async () => { // Arrange cacheManager.get.mockResolvedValue(undefined); service.findAll.mockResolvedValue({ data: dataResultMock }); // Act const result = await cachedService.findAll(); // Assert expect(cacheManager.get).toHaveBeenCalledTimes(1); expect(service.findAll).toHaveBeenCalledWith(); expect(cacheManager.set).toHaveBeenCalledWith( `cachedbase:findAll:all`, JSON.stringify({ data: dataResultMock }), ); expect(result).toEqual({ data: dataResultMock }); }); it('should fetch data from service when an error occurred retrieving data from cache', async () => { // Arrange cacheManager.get.mockRejectedValue(new Error('Cache not reachable')); service.findAll.mockResolvedValue({ data: dataResultMock }); // Act const result = await cachedService.findAll(); // Assert expect(cacheManager.get).toHaveBeenCalledTimes(1); expect(service.findAll).toHaveBeenCalledWith(); expect(cacheManager.set).not.toHaveBeenCalled(); expect(result).toEqual({ data: dataResultMock }); }); it('should handle getMany correctly (pagination and query hashing)', async () => { // Arrange const stringifiedResult = JSON.stringify(paginatedResultMock); cacheManager.get.mockResolvedValue(undefined); service.getMany.mockResolvedValue(paginatedResultMock as any); // Act await cachedService.getMany(paginatedQueryMock); // Assert const expectedKey = `cachedbase:getMany:${generateHash(paginatedQueryMock)}`; expect(service.getMany).toHaveBeenCalledWith(paginatedQueryMock); expect(cacheManager.set).toHaveBeenCalledWith(expectedKey, stringifiedResult); }); it('should return cached data on cache hit (count)', async () => { // Arrange cacheManager.get.mockResolvedValue(JSON.stringify({ count: 42 })); // Act const result = await cachedService.count(); // Assert expect(cacheManager.get).toHaveBeenCalledWith( `cachedbase:count:all`, ); expect(service.count).not.toHaveBeenCalled(); expect(cacheManager.set).not.toHaveBeenCalled(); expect(result).toEqual({ count: 42 }); }); it('should fetch data from service and cache it on cache miss (count)', async () => { // Arrange cacheManager.get.mockResolvedValue(undefined); service.count.mockResolvedValue({ count: 42 }); // Act const result = await cachedService.count(); // Assert expect(cacheManager.get).toHaveBeenCalledWith( `cachedbase:count:all`, ); expect(service.count).toHaveBeenCalledTimes(1); expect(cacheManager.set).toHaveBeenCalledWith( `cachedbase:count:all`, JSON.stringify({ count: 42 }), ); expect(result).toEqual({ count: 42 }); }); }); describe('Write operations (create, update, remove)', () => { it('should invalidate cache after create when strategy is OnChange', async () => { // Arrange await setupModule(cacheConfigOnChange); service.create.mockResolvedValue(testMessage); // Act const result = await cachedService.create(testCreateDTO); // Assert expect(service.create).toHaveBeenCalledWith(testCreateDTO); expect(cacheManager.mdel).toHaveBeenCalledWith([ 'cachedbase:findOne:12345', 'cachedbase:findAll:all', ]); expect(result).toEqual(testMessage); }); it('should invalidate cache after update when strategy is OnChange', async () => { // Arrange await setupModule(cacheConfigOnChange); service.update.mockResolvedValue(testUpdateDTO); // Act const result = await cachedService.update(testUpdateDTO); // Assert expect(service.update).toHaveBeenCalledWith(testUpdateDTO); expect(cacheManager.mdel).toHaveBeenCalledWith([ 'cachedbase:findOne:12345', 'cachedbase:findAll:all', ]); expect(result).toEqual(testUpdateDTO); }); it('should invalidate cache after remove when strategy is OnChange', async () => { await setupModule(cacheConfigOnChange); service.remove.mockResolvedValue(); await cachedService.remove(testId); expect(service.remove).toHaveBeenCalledWith(testId); expect(cacheManager.mdel).toHaveBeenCalledWith([ 'cachedbase:findOne:12345', 'cachedbase:findAll:all', ]); }); it('should not invalidate cache after create when strategy is TTL', async () => { // Arrange await setupModule(cacheConfigTTL); service.create.mockResolvedValue(testMessage); // Act const result = await cachedService.create(testCreateDTO); // Assert expect(service.create).toHaveBeenCalledWith(testCreateDTO); expect(cacheManager.mdel).not.toHaveBeenCalled(); expect(result).toEqual(testMessage); }); it('should not invalidate cache after update when strategy is TTL', async () => { // Arrange await setupModule(cacheConfigTTL); service.update.mockResolvedValue(testUpdateDTO); // Act const result = await cachedService.update(testUpdateDTO); // Assert expect(service.update).toHaveBeenCalledWith(testUpdateDTO); expect(cacheManager.mdel).not.toHaveBeenCalled(); expect(result).toEqual(testUpdateDTO); }); it('should not invalidate cache after remove when strategy is TTL', async () => { await setupModule(cacheConfigTTL); service.remove.mockResolvedValue(); await cachedService.remove(testId); expect(service.remove).toHaveBeenCalledWith(testId); expect(cacheManager.mdel).not.toHaveBeenCalled(); }); }); it('should return data in case of error while invalidating cache when strategy is OnChange', async () => { // Arrange await setupModule(cacheConfigOnChange); service.create.mockResolvedValue(testMessage); cacheManager.mdel.mockRejectedValue(new Error('Cannot delete data from cache')); // Act const result = await cachedService.create(testCreateDTO); // Assert expect(service.create).toHaveBeenCalledWith(testCreateDTO); expect(cacheManager.mdel).toHaveBeenCalledWith([ 'cachedbase:findOne:12345', 'cachedbase:findAll:all', ]); expect(result).toEqual(testMessage); }); });