import { Test, TestingModule } from '@nestjs/testing'; import { ConfigService } from '@nestjs/config'; import RedisMock from 'ioredis-mock'; import { RedisCacheAdapter } from '../../src/infrastructure/cache/redis-cache.adapter'; describe('RedisCacheAdapter (Integration)', () => { let adapter: RedisCacheAdapter; let redisMock: InstanceType; beforeAll(async () => { // Create a mock Redis instance redisMock = new RedisMock(); const module: TestingModule = await Test.createTestingModule({ providers: [ RedisCacheAdapter, { provide: ConfigService, useValue: { get: jest.fn((key: string) => { const config: Record = { REDIS_HOST: 'localhost', REDIS_PORT: 6379, REDIS_PASSWORD: '', REDIS_DB: 0, }; return config[key]; }), }, }, ], }).compile(); adapter = module.get(RedisCacheAdapter); // Replace the real Redis client with the mock (adapter as any).client = redisMock; }); afterEach(async () => { // Clear all keys between tests await redisMock.flushall(); // Reset statistics adapter.resetStats(); }); afterAll(async () => { await adapter.onModuleDestroy(); }); describe('get and set operations', () => { it('should set and get a string value', async () => { const key = 'test-key'; const value = 'test-value'; await adapter.set(key, value); const result = await adapter.get(key); expect(result).toBe(value); }); it('should set and get an object value', async () => { const key = 'test-object'; const value = { name: 'Test', count: 42, active: true }; await adapter.set(key, value); const result = await adapter.get(key); expect(result).toEqual(value); }); it('should return null for non-existent key', async () => { const result = await adapter.get('non-existent-key'); expect(result).toBeNull(); }); it('should set value with TTL', async () => { const key = 'ttl-key'; const value = 'ttl-value'; const ttl = 60; // 60 seconds await adapter.set(key, value, ttl); const result = await adapter.get(key); expect(result).toBe(value); // Verify TTL was set const remainingTtl = await redisMock.ttl(key); expect(remainingTtl).toBeGreaterThan(0); expect(remainingTtl).toBeLessThanOrEqual(ttl); }); }); describe('delete operations', () => { it('should delete an existing key', async () => { const key = 'delete-key'; const value = 'delete-value'; await adapter.set(key, value); expect(await adapter.get(key)).toBe(value); await adapter.delete(key); expect(await adapter.get(key)).toBeNull(); }); it('should delete multiple keys', async () => { await adapter.set('key1', 'value1'); await adapter.set('key2', 'value2'); await adapter.set('key3', 'value3'); await adapter.deleteMany(['key1', 'key2']); expect(await adapter.get('key1')).toBeNull(); expect(await adapter.get('key2')).toBeNull(); expect(await adapter.get('key3')).toBe('value3'); }); it('should clear all keys', async () => { await adapter.set('key1', 'value1'); await adapter.set('key2', 'value2'); await adapter.set('key3', 'value3'); await adapter.clear(); expect(await adapter.get('key1')).toBeNull(); expect(await adapter.get('key2')).toBeNull(); expect(await adapter.get('key3')).toBeNull(); }); }); describe('statistics tracking', () => { it('should track cache hits and misses', async () => { // Initial stats const initialStats = await adapter.getStats(); expect(initialStats.hits).toBe(0); expect(initialStats.misses).toBe(0); // Set a value await adapter.set('stats-key', 'stats-value'); // Cache hit await adapter.get('stats-key'); let stats = await adapter.getStats(); expect(stats.hits).toBe(1); expect(stats.misses).toBe(0); // Cache miss await adapter.get('non-existent'); stats = await adapter.getStats(); expect(stats.hits).toBe(1); expect(stats.misses).toBe(1); // Another cache hit await adapter.get('stats-key'); stats = await adapter.getStats(); expect(stats.hits).toBe(2); expect(stats.misses).toBe(1); }); it('should calculate hit rate correctly', async () => { await adapter.set('key1', 'value1'); await adapter.set('key2', 'value2'); // 2 hits await adapter.get('key1'); await adapter.get('key2'); // 1 miss await adapter.get('non-existent'); const stats = await adapter.getStats(); expect(stats.hitRate).toBeCloseTo(66.67, 1); // 66.67% as percentage }); it('should report key count', async () => { await adapter.set('key1', 'value1'); await adapter.set('key2', 'value2'); await adapter.set('key3', 'value3'); const stats = await adapter.getStats(); expect(stats.keyCount).toBe(3); }); }); describe('error handling', () => { it('should handle JSON parse errors gracefully', async () => { // Manually set an invalid JSON value await redisMock.set('invalid-json', '{invalid-json}'); const result = await adapter.get('invalid-json'); expect(result).toBeNull(); }); it('should return null on Redis errors during get', async () => { // Mock a Redis error const getSpy = jest.spyOn(redisMock, 'get').mockRejectedValueOnce(new Error('Redis error')); const result = await adapter.get('error-key'); expect(result).toBeNull(); getSpy.mockRestore(); }); }); describe('complex data structures', () => { it('should handle nested objects', async () => { const complexObject = { user: { id: '123', name: 'John Doe', preferences: { theme: 'dark', notifications: true, }, }, metadata: { created: new Date('2025-01-01').toISOString(), tags: ['test', 'integration'], }, }; await adapter.set('complex-key', complexObject); const result = await adapter.get('complex-key'); expect(result).toEqual(complexObject); }); it('should handle arrays', async () => { const array = [ { id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }, { id: 3, name: 'Item 3' }, ]; await adapter.set('array-key', array); const result = await adapter.get('array-key'); expect(result).toEqual(array); }); }); describe('key patterns', () => { it('should work with namespace-prefixed keys', async () => { const namespace = 'rate-quotes'; const key = `${namespace}:NLRTM:CNSHA:2025-01-15`; const value = { price: 1500, carrier: 'MAERSK' }; await adapter.set(key, value); const result = await adapter.get(key); expect(result).toEqual(value); }); it('should handle colon-separated hierarchical keys', async () => { await adapter.set('bookings:2025:01:booking-1', { id: 'booking-1' }); await adapter.set('bookings:2025:01:booking-2', { id: 'booking-2' }); await adapter.set('bookings:2025:02:booking-3', { id: 'booking-3' }); const jan1 = await adapter.get('bookings:2025:01:booking-1'); const jan2 = await adapter.get('bookings:2025:01:booking-2'); const feb3 = await adapter.get('bookings:2025:02:booking-3'); expect(jan1?.id).toBe('booking-1'); expect(jan2?.id).toBe('booking-2'); expect(feb3?.id).toBe('booking-3'); }); }); });