xpeditis2.0/apps/backend/test/integration/redis-cache.adapter.spec.ts
2025-10-27 20:54:01 +01:00

269 lines
7.8 KiB
TypeScript

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<typeof RedisMock>;
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<string, any> = {
REDIS_HOST: 'localhost',
REDIS_PORT: 6379,
REDIS_PASSWORD: '',
REDIS_DB: 0,
};
return config[key];
}),
},
},
],
}).compile();
adapter = module.get<RedisCacheAdapter>(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<string>(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<typeof value>(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<string>(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<typeof complexObject>('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<typeof array>('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<typeof value>(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<any>('bookings:2025:01:booking-1');
const jan2 = await adapter.get<any>('bookings:2025:01:booking-2');
const feb3 = await adapter.get<any>('bookings:2025:02:booking-3');
expect(jan1?.id).toBe('booking-1');
expect(jan2?.id).toBe('booking-2');
expect(feb3?.id).toBe('booking-3');
});
});
});