import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { cache } from '../../utils/cache'; // We need to import the Cache class itself to create test instances // Since it's not exported, we'll use dynamic import and manipulation type CacheItem = { value: T; timestamp: number; ttl: number; lastAccessed: number; }; class TestCache { private storage: Map>; private readonly defaultTTL: number; private readonly maxSize?: number; constructor(defaultTTL = 5 * 60 * 1000, maxSize?: number) { this.storage = new Map(); this.defaultTTL = defaultTTL; this.maxSize = maxSize; } set(key: string, value: T, ttl = this.defaultTTL): void { const now = Date.now(); if (this.maxSize && this.storage.size >= this.maxSize && !this.storage.has(key)) { this.evictLRU(); } this.storage.set(key, { value, timestamp: now, ttl, lastAccessed: now }); } private evictLRU(): void { let lruKey: string | null = null; let lruTime = Infinity; for (const [key, item] of this.storage.entries()) { if (item.lastAccessed < lruTime) { lruTime = item.lastAccessed; lruKey = key; } } if (lruKey) { this.storage.delete(lruKey); } } get(key: string): T | null { const item = this.storage.get(key); if (!item) return null; if (Date.now() > item.timestamp + item.ttl) { this.storage.delete(key); return null; } item.lastAccessed = Date.now(); return item.value; } has(key: string): boolean { return this.get(key) !== null; } delete(key: string): void { this.storage.delete(key); } clear(): void { this.storage.clear(); } // Helper method for testing size(): number { return this.storage.size; } } describe('Cache', () => { let testCache: TestCache; beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.restoreAllMocks(); vi.useRealTimers(); }); describe('Basic Operations', () => { beforeEach(() => { testCache = new TestCache(); }); it('should store and retrieve values', () => { testCache.set('key1', 'value1'); expect(testCache.get('key1')).toBe('value1'); }); it('should return null for non-existent keys', () => { expect(testCache.get('nonexistent')).toBeNull(); }); it('should delete values', () => { testCache.set('key1', 'value1'); testCache.delete('key1'); expect(testCache.get('key1')).toBeNull(); }); it('should clear all values', () => { testCache.set('key1', 'value1'); testCache.set('key2', 'value2'); testCache.clear(); expect(testCache.get('key1')).toBeNull(); expect(testCache.get('key2')).toBeNull(); }); it('should check if key exists with has()', () => { testCache.set('key1', 'value1'); expect(testCache.has('key1')).toBe(true); expect(testCache.has('nonexistent')).toBe(false); }); it('should handle different data types', () => { testCache.set('string', 'text'); testCache.set('number', 42); testCache.set('object', { foo: 'bar' }); testCache.set('array', [1, 2, 3]); testCache.set('boolean', true); expect(testCache.get('string')).toBe('text'); expect(testCache.get('number')).toBe(42); expect(testCache.get('object')).toEqual({ foo: 'bar' }); expect(testCache.get('array')).toEqual([1, 2, 3]); expect(testCache.get('boolean')).toBe(true); }); }); describe('TTL Expiration', () => { beforeEach(() => { testCache = new TestCache(1000); // 1 second default TTL }); it('should expire items after TTL', () => { testCache.set('key1', 'value1'); expect(testCache.get('key1')).toBe('value1'); vi.advanceTimersByTime(1001); expect(testCache.get('key1')).toBeNull(); }); it('should use custom TTL per item', () => { testCache.set('key1', 'value1', 500); testCache.set('key2', 'value2', 2000); vi.advanceTimersByTime(501); expect(testCache.get('key1')).toBeNull(); expect(testCache.get('key2')).toBe('value2'); vi.advanceTimersByTime(1500); expect(testCache.get('key2')).toBeNull(); }); it('should use default TTL when not specified', () => { testCache.set('key1', 'value1'); vi.advanceTimersByTime(999); expect(testCache.get('key1')).toBe('value1'); vi.advanceTimersByTime(2); expect(testCache.get('key1')).toBeNull(); }); it('should remove expired items from storage', () => { testCache.set('key1', 'value1', 1000); expect(testCache.size()).toBe(1); vi.advanceTimersByTime(1001); testCache.get('key1'); // Triggers cleanup expect(testCache.size()).toBe(0); }); it('should return false for has() on expired items', () => { testCache.set('key1', 'value1', 1000); expect(testCache.has('key1')).toBe(true); vi.advanceTimersByTime(1001); expect(testCache.has('key1')).toBe(false); }); }); describe('Size Limit and LRU Eviction', () => { beforeEach(() => { testCache = new TestCache(60000, 3); // 60s TTL, max 3 items }); it('should enforce size limit', () => { testCache.set('key1', 'value1'); testCache.set('key2', 'value2'); testCache.set('key3', 'value3'); expect(testCache.size()).toBe(3); testCache.set('key4', 'value4'); expect(testCache.size()).toBe(3); }); it('should evict least recently used item when adding new item', () => { testCache.set('key1', 'value1'); vi.advanceTimersByTime(10); testCache.set('key2', 'value2'); vi.advanceTimersByTime(10); testCache.set('key3', 'value3'); vi.advanceTimersByTime(10); // key1 is oldest, should be evicted when adding key4 testCache.set('key4', 'value4'); expect(testCache.get('key1')).toBeNull(); expect(testCache.get('key2')).toBe('value2'); expect(testCache.get('key3')).toBe('value3'); expect(testCache.get('key4')).toBe('value4'); }); it('should update access time on get and affect LRU eviction', () => { testCache.set('key1', 'value1'); vi.advanceTimersByTime(10); testCache.set('key2', 'value2'); vi.advanceTimersByTime(10); testCache.set('key3', 'value3'); vi.advanceTimersByTime(10); // Access key1, making it recently used testCache.get('key1'); vi.advanceTimersByTime(10); // Now key2 is least recently used, should be evicted testCache.set('key4', 'value4'); expect(testCache.get('key1')).toBe('value1'); expect(testCache.get('key2')).toBeNull(); expect(testCache.get('key3')).toBe('value3'); expect(testCache.get('key4')).toBe('value4'); }); it('should not evict when updating existing key', () => { testCache.set('key1', 'value1'); testCache.set('key2', 'value2'); testCache.set('key3', 'value3'); // Update key1, should not trigger eviction testCache.set('key1', 'updated1'); expect(testCache.size()).toBe(3); expect(testCache.get('key1')).toBe('updated1'); expect(testCache.get('key2')).toBe('value2'); expect(testCache.get('key3')).toBe('value3'); }); it('should handle multiple evictions correctly', () => { testCache.set('key1', 'value1'); vi.advanceTimersByTime(10); testCache.set('key2', 'value2'); vi.advanceTimersByTime(10); testCache.set('key3', 'value3'); vi.advanceTimersByTime(10); // Add key4, evicts key1 testCache.set('key4', 'value4'); vi.advanceTimersByTime(10); // Add key5, evicts key2 testCache.set('key5', 'value5'); expect(testCache.get('key1')).toBeNull(); expect(testCache.get('key2')).toBeNull(); expect(testCache.get('key3')).toBe('value3'); expect(testCache.get('key4')).toBe('value4'); expect(testCache.get('key5')).toBe('value5'); }); }); describe('Edge Cases', () => { it('should handle maxSize of 1', () => { testCache = new TestCache(60000, 1); testCache.set('key1', 'value1'); expect(testCache.get('key1')).toBe('value1'); testCache.set('key2', 'value2'); expect(testCache.get('key1')).toBeNull(); expect(testCache.get('key2')).toBe('value2'); }); it('should handle maxSize of 0 (treated as no limit)', () => { testCache = new TestCache(60000, 0); testCache.set('key1', 'value1'); // maxSize of 0 is falsy, so it's treated as no limit expect(testCache.size()).toBe(1); expect(testCache.get('key1')).toBe('value1'); }); it('should handle empty cache operations', () => { testCache = new TestCache(); expect(testCache.get('key1')).toBeNull(); expect(testCache.has('key1')).toBe(false); testCache.delete('key1'); // Should not throw testCache.clear(); // Should not throw }); it('should handle deleting non-existent keys', () => { testCache = new TestCache(); testCache.set('key1', 'value1'); testCache.delete('nonexistent'); expect(testCache.get('key1')).toBe('value1'); }); it('should handle clearing empty cache', () => { testCache = new TestCache(); testCache.clear(); expect(testCache.size()).toBe(0); }); }); describe('Backward Compatibility', () => { it('should work as unlimited cache without maxSize', () => { testCache = new TestCache(); // Add many items without size limit for (let i = 0; i < 100; i++) { testCache.set(`key${i}`, `value${i}`); } expect(testCache.size()).toBe(100); // All items should still be accessible expect(testCache.get('key0')).toBe('value0'); expect(testCache.get('key50')).toBe('value50'); expect(testCache.get('key99')).toBe('value99'); }); it('should use default TTL of 5 minutes when not specified', () => { testCache = new TestCache(); testCache.set('key1', 'value1'); // Should still be available after 4 minutes vi.advanceTimersByTime(4 * 60 * 1000); expect(testCache.get('key1')).toBe('value1'); // Should expire after 5 minutes vi.advanceTimersByTime(61 * 1000); expect(testCache.get('key1')).toBeNull(); }); it('should maintain original behavior for set/get/delete/clear', () => { testCache = new TestCache(); testCache.set('key1', 'value1'); expect(testCache.get('key1')).toBe('value1'); testCache.set('key2', 'value2'); testCache.delete('key1'); expect(testCache.get('key1')).toBeNull(); expect(testCache.get('key2')).toBe('value2'); testCache.clear(); expect(testCache.get('key2')).toBeNull(); }); }); describe('Complex LRU Scenarios', () => { beforeEach(() => { testCache = new TestCache(60000, 3); }); it('should evict based on access time, not insertion time', () => { testCache.set('key1', 'value1'); vi.advanceTimersByTime(10); testCache.set('key2', 'value2'); vi.advanceTimersByTime(10); testCache.set('key3', 'value3'); vi.advanceTimersByTime(10); // Access key1 and key2, making key3 the least recently used testCache.get('key1'); vi.advanceTimersByTime(5); testCache.get('key2'); vi.advanceTimersByTime(5); // key3 should be evicted testCache.set('key4', 'value4'); expect(testCache.get('key1')).toBe('value1'); expect(testCache.get('key2')).toBe('value2'); expect(testCache.get('key3')).toBeNull(); expect(testCache.get('key4')).toBe('value4'); }); it('should handle repeated access correctly', () => { testCache.set('key1', 'value1'); vi.advanceTimersByTime(10); testCache.set('key2', 'value2'); vi.advanceTimersByTime(10); testCache.set('key3', 'value3'); vi.advanceTimersByTime(10); // Keep accessing key1 for (let i = 0; i < 5; i++) { testCache.get('key1'); vi.advanceTimersByTime(5); } // key2 should be evicted (oldest access) testCache.set('key4', 'value4'); expect(testCache.get('key1')).toBe('value1'); expect(testCache.get('key2')).toBeNull(); expect(testCache.get('key3')).toBe('value3'); expect(testCache.get('key4')).toBe('value4'); }); it('should handle mixed operations correctly', () => { testCache.set('key1', 'value1'); vi.advanceTimersByTime(10); testCache.set('key2', 'value2'); vi.advanceTimersByTime(10); testCache.set('key3', 'value3'); vi.advanceTimersByTime(10); // Update key1 (should update access time) testCache.set('key1', 'updated1'); vi.advanceTimersByTime(10); // Delete key2 testCache.delete('key2'); // Add key4 - should not trigger eviction (only 2 items now) testCache.set('key4', 'value4'); expect(testCache.size()).toBe(3); // Add key5 - should evict key3 (least recently used) vi.advanceTimersByTime(10); testCache.set('key5', 'value5'); expect(testCache.get('key1')).toBe('updated1'); expect(testCache.get('key2')).toBeNull(); expect(testCache.get('key3')).toBeNull(); expect(testCache.get('key4')).toBe('value4'); expect(testCache.get('key5')).toBe('value5'); }); }); describe('Global cache instance', () => { it('should export a global cache instance', () => { expect(cache).toBeDefined(); expect(typeof cache.get).toBe('function'); expect(typeof cache.set).toBe('function'); expect(typeof cache.delete).toBe('function'); expect(typeof cache.clear).toBe('function'); expect(typeof cache.has).toBe('function'); }); }); });