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); }); }); });