269 lines
7.8 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|