429 lines
12 KiB
TypeScript
429 lines
12 KiB
TypeScript
import { beforeEach, describe, expect, it, mock } from 'bun:test';
|
|
import { CacheAdapter, NamespacedCache } from '../src/namespaced-cache';
|
|
import type { CacheProvider, ICache } from '../src/types';
|
|
|
|
describe('NamespacedCache', () => {
|
|
let mockCache: CacheProvider;
|
|
let namespacedCache: NamespacedCache;
|
|
|
|
beforeEach(() => {
|
|
// Create mock base cache
|
|
mockCache = {
|
|
get: mock(async () => null),
|
|
set: mock(async () => null),
|
|
del: mock(async () => {}),
|
|
exists: mock(async () => false),
|
|
clear: mock(async () => {}),
|
|
keys: mock(async () => []),
|
|
getStats: mock(() => ({
|
|
hits: 100,
|
|
misses: 20,
|
|
errors: 5,
|
|
hitRate: 0.83,
|
|
total: 120,
|
|
uptime: 3600,
|
|
})),
|
|
health: mock(async () => true),
|
|
waitForReady: mock(async () => {}),
|
|
isReady: mock(() => true),
|
|
};
|
|
|
|
// Create namespaced cache
|
|
namespacedCache = new NamespacedCache(mockCache, 'test-namespace');
|
|
});
|
|
|
|
describe('constructor', () => {
|
|
it('should set namespace and prefix correctly', () => {
|
|
expect(namespacedCache.getNamespace()).toBe('test-namespace');
|
|
expect(namespacedCache.getFullPrefix()).toBe('test-namespace:');
|
|
});
|
|
|
|
it('should handle empty namespace', () => {
|
|
const emptyNamespace = new NamespacedCache(mockCache, '');
|
|
expect(emptyNamespace.getNamespace()).toBe('');
|
|
expect(emptyNamespace.getFullPrefix()).toBe(':');
|
|
});
|
|
});
|
|
|
|
describe('get', () => {
|
|
it('should prefix key when getting', async () => {
|
|
const testData = { value: 'test' };
|
|
(mockCache.get as any).mockResolvedValue(testData);
|
|
|
|
const result = await namespacedCache.get('mykey');
|
|
|
|
expect(mockCache.get).toHaveBeenCalledWith('test-namespace:mykey');
|
|
expect(result).toEqual(testData);
|
|
});
|
|
|
|
it('should handle null values', async () => {
|
|
(mockCache.get as any).mockResolvedValue(null);
|
|
|
|
const result = await namespacedCache.get('nonexistent');
|
|
|
|
expect(mockCache.get).toHaveBeenCalledWith('test-namespace:nonexistent');
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('set', () => {
|
|
it('should prefix key when setting with ttl number', async () => {
|
|
const value = { data: 'test' };
|
|
const ttl = 3600;
|
|
|
|
await namespacedCache.set('mykey', value, ttl);
|
|
|
|
expect(mockCache.set).toHaveBeenCalledWith('test-namespace:mykey', value, ttl);
|
|
});
|
|
|
|
it('should prefix key when setting with options object', async () => {
|
|
const value = 'test-value';
|
|
const options = { ttl: 7200 };
|
|
|
|
await namespacedCache.set('mykey', value, options);
|
|
|
|
expect(mockCache.set).toHaveBeenCalledWith('test-namespace:mykey', value, options);
|
|
});
|
|
|
|
it('should handle set without TTL', async () => {
|
|
const value = [1, 2, 3];
|
|
|
|
await namespacedCache.set('mykey', value);
|
|
|
|
expect(mockCache.set).toHaveBeenCalledWith('test-namespace:mykey', value, undefined);
|
|
});
|
|
});
|
|
|
|
describe('del', () => {
|
|
it('should prefix key when deleting', async () => {
|
|
await namespacedCache.del('mykey');
|
|
|
|
expect(mockCache.del).toHaveBeenCalledWith('test-namespace:mykey');
|
|
});
|
|
|
|
it('should handle multiple deletes', async () => {
|
|
await namespacedCache.del('key1');
|
|
await namespacedCache.del('key2');
|
|
|
|
expect(mockCache.del).toHaveBeenCalledTimes(2);
|
|
expect(mockCache.del).toHaveBeenCalledWith('test-namespace:key1');
|
|
expect(mockCache.del).toHaveBeenCalledWith('test-namespace:key2');
|
|
});
|
|
});
|
|
|
|
describe('exists', () => {
|
|
it('should prefix key when checking existence', async () => {
|
|
(mockCache.exists as any).mockResolvedValue(true);
|
|
|
|
const result = await namespacedCache.exists('mykey');
|
|
|
|
expect(mockCache.exists).toHaveBeenCalledWith('test-namespace:mykey');
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should return false for non-existent keys', async () => {
|
|
(mockCache.exists as any).mockResolvedValue(false);
|
|
|
|
const result = await namespacedCache.exists('nonexistent');
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('keys', () => {
|
|
it('should prefix pattern and strip prefix from results', async () => {
|
|
(mockCache.keys as any).mockResolvedValue([
|
|
'test-namespace:key1',
|
|
'test-namespace:key2',
|
|
'test-namespace:key3',
|
|
]);
|
|
|
|
const keys = await namespacedCache.keys('*');
|
|
|
|
expect(mockCache.keys).toHaveBeenCalledWith('test-namespace:*');
|
|
expect(keys).toEqual(['key1', 'key2', 'key3']);
|
|
});
|
|
|
|
it('should handle specific patterns', async () => {
|
|
(mockCache.keys as any).mockResolvedValue([
|
|
'test-namespace:user:123',
|
|
'test-namespace:user:456',
|
|
]);
|
|
|
|
const keys = await namespacedCache.keys('user:*');
|
|
|
|
expect(mockCache.keys).toHaveBeenCalledWith('test-namespace:user:*');
|
|
expect(keys).toEqual(['user:123', 'user:456']);
|
|
});
|
|
|
|
it('should filter out keys from other namespaces', async () => {
|
|
(mockCache.keys as any).mockResolvedValue([
|
|
'test-namespace:key1',
|
|
'other-namespace:key2',
|
|
'test-namespace:key3',
|
|
]);
|
|
|
|
const keys = await namespacedCache.keys('*');
|
|
|
|
expect(keys).toEqual(['key1', 'key3']);
|
|
});
|
|
|
|
it('should handle empty results', async () => {
|
|
(mockCache.keys as any).mockResolvedValue([]);
|
|
|
|
const keys = await namespacedCache.keys('nonexistent*');
|
|
|
|
expect(keys).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('clear', () => {
|
|
it('should clear only namespaced keys', async () => {
|
|
(mockCache.keys as any).mockResolvedValue([
|
|
'test-namespace:key1',
|
|
'test-namespace:key2',
|
|
'test-namespace:key3',
|
|
]);
|
|
|
|
await namespacedCache.clear();
|
|
|
|
expect(mockCache.keys).toHaveBeenCalledWith('test-namespace:*');
|
|
expect(mockCache.del).toHaveBeenCalledTimes(3);
|
|
expect(mockCache.del).toHaveBeenCalledWith('key1');
|
|
expect(mockCache.del).toHaveBeenCalledWith('key2');
|
|
expect(mockCache.del).toHaveBeenCalledWith('key3');
|
|
});
|
|
|
|
it('should handle empty namespace', async () => {
|
|
(mockCache.keys as any).mockResolvedValue([]);
|
|
|
|
await namespacedCache.clear();
|
|
|
|
expect(mockCache.keys).toHaveBeenCalledWith('test-namespace:*');
|
|
expect(mockCache.del).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('delegated methods', () => {
|
|
it('should delegate getStats', () => {
|
|
const stats = namespacedCache.getStats();
|
|
|
|
expect(mockCache.getStats).toHaveBeenCalled();
|
|
expect(stats).toEqual({
|
|
hits: 100,
|
|
misses: 20,
|
|
errors: 5,
|
|
hitRate: 0.83,
|
|
total: 120,
|
|
uptime: 3600,
|
|
});
|
|
});
|
|
|
|
it('should delegate health', async () => {
|
|
const health = await namespacedCache.health();
|
|
|
|
expect(mockCache.health).toHaveBeenCalled();
|
|
expect(health).toBe(true);
|
|
});
|
|
|
|
it('should delegate waitForReady', async () => {
|
|
await namespacedCache.waitForReady(5000);
|
|
|
|
expect(mockCache.waitForReady).toHaveBeenCalledWith(5000);
|
|
});
|
|
|
|
it('should delegate isReady', () => {
|
|
const ready = namespacedCache.isReady();
|
|
|
|
expect(mockCache.isReady).toHaveBeenCalled();
|
|
expect(ready).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('edge cases', () => {
|
|
it('should handle special characters in namespace', () => {
|
|
const specialNamespace = new NamespacedCache(mockCache, 'test:namespace:with:colons');
|
|
expect(specialNamespace.getFullPrefix()).toBe('test:namespace:with:colons:');
|
|
});
|
|
|
|
it('should handle very long keys', async () => {
|
|
const longKey = 'a'.repeat(1000);
|
|
await namespacedCache.get(longKey);
|
|
|
|
expect(mockCache.get).toHaveBeenCalledWith(`test-namespace:${longKey}`);
|
|
});
|
|
|
|
it('should handle errors from underlying cache', async () => {
|
|
const error = new Error('Cache error');
|
|
(mockCache.get as any).mockRejectedValue(error);
|
|
|
|
await expect(namespacedCache.get('key')).rejects.toThrow('Cache error');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('CacheAdapter', () => {
|
|
let mockICache: ICache;
|
|
let adapter: CacheAdapter;
|
|
|
|
beforeEach(() => {
|
|
mockICache = {
|
|
get: mock(async () => null),
|
|
set: mock(async () => {}),
|
|
del: mock(async () => {}),
|
|
exists: mock(async () => false),
|
|
clear: mock(async () => {}),
|
|
keys: mock(async () => []),
|
|
ping: mock(async () => true),
|
|
isConnected: mock(() => true),
|
|
has: mock(async () => false),
|
|
ttl: mock(async () => -1),
|
|
type: 'memory' as const,
|
|
};
|
|
|
|
adapter = new CacheAdapter(mockICache);
|
|
});
|
|
|
|
describe('get', () => {
|
|
it('should delegate to ICache.get', async () => {
|
|
const data = { value: 'test' };
|
|
(mockICache.get as any).mockResolvedValue(data);
|
|
|
|
const result = await adapter.get('key');
|
|
|
|
expect(mockICache.get).toHaveBeenCalledWith('key');
|
|
expect(result).toEqual(data);
|
|
});
|
|
});
|
|
|
|
describe('set', () => {
|
|
it('should handle TTL as number', async () => {
|
|
await adapter.set('key', 'value', 3600);
|
|
|
|
expect(mockICache.set).toHaveBeenCalledWith('key', 'value', 3600);
|
|
});
|
|
|
|
it('should handle TTL as options object', async () => {
|
|
await adapter.set('key', 'value', { ttl: 7200 });
|
|
|
|
expect(mockICache.set).toHaveBeenCalledWith('key', 'value', 7200);
|
|
});
|
|
|
|
it('should handle no TTL', async () => {
|
|
await adapter.set('key', 'value');
|
|
|
|
expect(mockICache.set).toHaveBeenCalledWith('key', 'value', undefined);
|
|
});
|
|
|
|
it('should always return null', async () => {
|
|
const result = await adapter.set('key', 'value');
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('del', () => {
|
|
it('should delegate to ICache.del', async () => {
|
|
await adapter.del('key');
|
|
|
|
expect(mockICache.del).toHaveBeenCalledWith('key');
|
|
});
|
|
});
|
|
|
|
describe('exists', () => {
|
|
it('should delegate to ICache.exists', async () => {
|
|
(mockICache.exists as any).mockResolvedValue(true);
|
|
|
|
const result = await adapter.exists('key');
|
|
|
|
expect(mockICache.exists).toHaveBeenCalledWith('key');
|
|
expect(result).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('clear', () => {
|
|
it('should delegate to ICache.clear', async () => {
|
|
await adapter.clear();
|
|
|
|
expect(mockICache.clear).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('keys', () => {
|
|
it('should delegate to ICache.keys', async () => {
|
|
const keys = ['key1', 'key2'];
|
|
(mockICache.keys as any).mockResolvedValue(keys);
|
|
|
|
const result = await adapter.keys('*');
|
|
|
|
expect(mockICache.keys).toHaveBeenCalledWith('*');
|
|
expect(result).toEqual(keys);
|
|
});
|
|
});
|
|
|
|
describe('getStats', () => {
|
|
it('should return default stats', () => {
|
|
const stats = adapter.getStats();
|
|
|
|
expect(stats).toEqual({
|
|
hits: 0,
|
|
misses: 0,
|
|
errors: 0,
|
|
hitRate: 0,
|
|
total: 0,
|
|
uptime: expect.any(Number),
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('health', () => {
|
|
it('should use ping for health check', async () => {
|
|
(mockICache.ping as any).mockResolvedValue(true);
|
|
|
|
const result = await adapter.health();
|
|
|
|
expect(mockICache.ping).toHaveBeenCalled();
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should handle ping failures', async () => {
|
|
(mockICache.ping as any).mockResolvedValue(false);
|
|
|
|
const result = await adapter.health();
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('waitForReady', () => {
|
|
it('should succeed if connected', async () => {
|
|
(mockICache.isConnected as any).mockReturnValue(true);
|
|
|
|
await expect(adapter.waitForReady()).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('should throw if not connected', async () => {
|
|
(mockICache.isConnected as any).mockReturnValue(false);
|
|
|
|
await expect(adapter.waitForReady()).rejects.toThrow('Cache not connected');
|
|
});
|
|
});
|
|
|
|
describe('isReady', () => {
|
|
it('should delegate to isConnected', () => {
|
|
(mockICache.isConnected as any).mockReturnValue(true);
|
|
|
|
const result = adapter.isReady();
|
|
|
|
expect(mockICache.isConnected).toHaveBeenCalled();
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should return false when not connected', () => {
|
|
(mockICache.isConnected as any).mockReturnValue(false);
|
|
|
|
const result = adapter.isReady();
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
});
|