- Created comprehensive test suite for cache.ts covering: - set() and get() operations with various data types - TTL expiration logic (default, custom, and edge cases) - has() method for checking key existence - delete() method for removing individual keys - clear() method for removing all keys - Expiration boundary testing - Different TTL values for different items - TTL reset when items are overwritten - Fixed vitest.config.ts to include src/utils/** tests - Fixed tsconfig.json to include src/utils/** files - Used vi.useFakeTimers() for deterministic time-based testing - All tests properly isolated with beforeEach/afterEach hooks Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
283 lines
8.4 KiB
TypeScript
283 lines
8.4 KiB
TypeScript
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
import { cache } from './cache';
|
|
|
|
describe('Cache', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
cache.clear(); // Clear cache before each test
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
cache.clear(); // Clean up after each test
|
|
});
|
|
|
|
describe('set and get', () => {
|
|
it('should set and retrieve a value', () => {
|
|
cache.set('key', 'value');
|
|
expect(cache.get('key')).toBe('value');
|
|
});
|
|
|
|
it('should set multiple values with different keys', () => {
|
|
cache.set('key1', 'value1');
|
|
cache.set('key2', 'value2');
|
|
cache.set('key3', 'value3');
|
|
|
|
expect(cache.get('key1')).toBe('value1');
|
|
expect(cache.get('key2')).toBe('value2');
|
|
expect(cache.get('key3')).toBe('value3');
|
|
});
|
|
|
|
it('should overwrite existing value for same key', () => {
|
|
cache.set('key', 'value1');
|
|
expect(cache.get('key')).toBe('value1');
|
|
|
|
cache.set('key', 'value2');
|
|
expect(cache.get('key')).toBe('value2');
|
|
});
|
|
|
|
it('should return null for non-existing key', () => {
|
|
expect(cache.get('nonexistent')).toBeNull();
|
|
});
|
|
|
|
it('should store different types of values', () => {
|
|
cache.set('string', 'text');
|
|
cache.set('number', 42);
|
|
cache.set('boolean', true);
|
|
cache.set('object', { name: 'test' });
|
|
cache.set('array', [1, 2, 3]);
|
|
cache.set('null', null);
|
|
|
|
expect(cache.get('string')).toBe('text');
|
|
expect(cache.get('number')).toBe(42);
|
|
expect(cache.get('boolean')).toBe(true);
|
|
expect(cache.get('object')).toEqual({ name: 'test' });
|
|
expect(cache.get('array')).toEqual([1, 2, 3]);
|
|
expect(cache.get('null')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('TTL expiration', () => {
|
|
it('should use default TTL of 5 minutes', () => {
|
|
cache.set('test', 'value');
|
|
|
|
// Should be available before TTL expires
|
|
expect(cache.get('test')).toBe('value');
|
|
|
|
// Advance time by 4 minutes 59 seconds
|
|
vi.advanceTimersByTime(4 * 60 * 1000 + 59 * 1000);
|
|
expect(cache.get('test')).toBe('value');
|
|
|
|
// Advance time by 2 more seconds (total 5 minutes 1 second)
|
|
vi.advanceTimersByTime(2 * 1000);
|
|
expect(cache.get('test')).toBeNull();
|
|
});
|
|
|
|
it('should accept custom TTL per item', () => {
|
|
cache.set('short', 'value', 1000); // 1 second TTL
|
|
cache.set('long', 'value', 10000); // 10 seconds TTL
|
|
|
|
// Both should be available initially
|
|
expect(cache.get('short')).toBe('value');
|
|
expect(cache.get('long')).toBe('value');
|
|
|
|
// After 1.5 seconds, short should expire but long should remain
|
|
vi.advanceTimersByTime(1500);
|
|
expect(cache.get('short')).toBeNull();
|
|
expect(cache.get('long')).toBe('value');
|
|
|
|
// After 9 more seconds (total 10.5), long should expire
|
|
vi.advanceTimersByTime(9000);
|
|
expect(cache.get('long')).toBeNull();
|
|
});
|
|
|
|
it('should return null for expired item', () => {
|
|
cache.set('key', 'value', 1000);
|
|
|
|
// Should be available initially
|
|
expect(cache.get('key')).toBe('value');
|
|
|
|
// Should be null after expiration
|
|
vi.advanceTimersByTime(1001);
|
|
expect(cache.get('key')).toBeNull();
|
|
});
|
|
|
|
it('should delete expired item from storage', () => {
|
|
cache.set('key', 'value', 1000);
|
|
|
|
// Item should exist
|
|
expect(cache.get('key')).toBe('value');
|
|
|
|
// After expiration, get should delete it
|
|
vi.advanceTimersByTime(1001);
|
|
expect(cache.get('key')).toBeNull();
|
|
|
|
// Subsequent get should also return null (item was deleted)
|
|
expect(cache.get('key')).toBeNull();
|
|
});
|
|
|
|
it('should return value at exact TTL boundary', () => {
|
|
cache.set('key', 'value', 1000);
|
|
|
|
// At exactly TTL time, should still be valid
|
|
vi.advanceTimersByTime(1000);
|
|
expect(cache.get('key')).toBe('value');
|
|
|
|
// One millisecond later, should expire
|
|
vi.advanceTimersByTime(1);
|
|
expect(cache.get('key')).toBeNull();
|
|
});
|
|
|
|
it('should respect different TTLs for different items', () => {
|
|
cache.set('item1', 'value1', 1000);
|
|
cache.set('item2', 'value2', 2000);
|
|
cache.set('item3', 'value3', 3000);
|
|
|
|
// All items should be available initially
|
|
expect(cache.get('item1')).toBe('value1');
|
|
expect(cache.get('item2')).toBe('value2');
|
|
expect(cache.get('item3')).toBe('value3');
|
|
|
|
// After 1.5 seconds, item1 should expire
|
|
vi.advanceTimersByTime(1500);
|
|
expect(cache.get('item1')).toBeNull();
|
|
expect(cache.get('item2')).toBe('value2');
|
|
expect(cache.get('item3')).toBe('value3');
|
|
|
|
// After 1 more second (total 2.5), item2 should expire
|
|
vi.advanceTimersByTime(1000);
|
|
expect(cache.get('item1')).toBeNull();
|
|
expect(cache.get('item2')).toBeNull();
|
|
expect(cache.get('item3')).toBe('value3');
|
|
|
|
// After 1 more second (total 3.5), item3 should expire
|
|
vi.advanceTimersByTime(1000);
|
|
expect(cache.get('item1')).toBeNull();
|
|
expect(cache.get('item2')).toBeNull();
|
|
expect(cache.get('item3')).toBeNull();
|
|
});
|
|
|
|
it('should reset TTL when item is overwritten', () => {
|
|
cache.set('key', 'value1', 1000);
|
|
|
|
// Advance time by 500ms
|
|
vi.advanceTimersByTime(500);
|
|
|
|
// Overwrite with new value and new TTL
|
|
cache.set('key', 'value2', 2000);
|
|
|
|
// After 1500ms more (total 2000ms from first set), should still be available
|
|
// because TTL was reset
|
|
vi.advanceTimersByTime(1500);
|
|
expect(cache.get('key')).toBe('value2');
|
|
|
|
// After 600ms more (2100ms from second set), should expire
|
|
vi.advanceTimersByTime(600);
|
|
expect(cache.get('key')).toBeNull();
|
|
});
|
|
|
|
it('should handle zero TTL', () => {
|
|
cache.set('key', 'value', 0);
|
|
|
|
// Should expire immediately
|
|
vi.advanceTimersByTime(1);
|
|
expect(cache.get('key')).toBeNull();
|
|
});
|
|
|
|
it('should handle very long TTL', () => {
|
|
const oneDayInMs = 24 * 60 * 60 * 1000;
|
|
cache.set('key', 'value', oneDayInMs);
|
|
|
|
// Should be available after 23 hours
|
|
vi.advanceTimersByTime(23 * 60 * 60 * 1000);
|
|
expect(cache.get('key')).toBe('value');
|
|
|
|
// Should still be available at exactly 24 hours
|
|
vi.advanceTimersByTime(60 * 60 * 1000);
|
|
expect(cache.get('key')).toBe('value');
|
|
|
|
// Should expire after 24 hours + 1ms
|
|
vi.advanceTimersByTime(1);
|
|
expect(cache.get('key')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('has', () => {
|
|
it('should return true for existing key', () => {
|
|
cache.set('key', 'value');
|
|
expect(cache.has('key')).toBe(true);
|
|
});
|
|
|
|
it('should return false for non-existing key', () => {
|
|
expect(cache.has('nonexistent')).toBe(false);
|
|
});
|
|
|
|
it('should return false for expired key', () => {
|
|
cache.set('key', 'value', 1000);
|
|
|
|
expect(cache.has('key')).toBe(true);
|
|
|
|
vi.advanceTimersByTime(1001);
|
|
expect(cache.has('key')).toBe(false);
|
|
});
|
|
|
|
it('should return false for null values', () => {
|
|
cache.set('key', null);
|
|
expect(cache.has('key')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('delete', () => {
|
|
it('should delete existing key', () => {
|
|
cache.set('key', 'value');
|
|
expect(cache.get('key')).toBe('value');
|
|
|
|
cache.delete('key');
|
|
expect(cache.get('key')).toBeNull();
|
|
});
|
|
|
|
it('should handle deleting non-existing key', () => {
|
|
expect(() => cache.delete('nonexistent')).not.toThrow();
|
|
expect(cache.get('nonexistent')).toBeNull();
|
|
});
|
|
|
|
it('should only delete specified key', () => {
|
|
cache.set('key1', 'value1');
|
|
cache.set('key2', 'value2');
|
|
cache.set('key3', 'value3');
|
|
|
|
cache.delete('key2');
|
|
|
|
expect(cache.get('key1')).toBe('value1');
|
|
expect(cache.get('key2')).toBeNull();
|
|
expect(cache.get('key3')).toBe('value3');
|
|
});
|
|
});
|
|
|
|
describe('clear', () => {
|
|
it('should clear all items from cache', () => {
|
|
cache.set('key1', 'value1');
|
|
cache.set('key2', 'value2');
|
|
cache.set('key3', 'value3');
|
|
|
|
cache.clear();
|
|
|
|
expect(cache.get('key1')).toBeNull();
|
|
expect(cache.get('key2')).toBeNull();
|
|
expect(cache.get('key3')).toBeNull();
|
|
});
|
|
|
|
it('should handle clearing empty cache', () => {
|
|
expect(() => cache.clear()).not.toThrow();
|
|
});
|
|
|
|
it('should allow setting items after clear', () => {
|
|
cache.set('key', 'value1');
|
|
cache.clear();
|
|
cache.set('key', 'value2');
|
|
|
|
expect(cache.get('key')).toBe('value2');
|
|
});
|
|
});
|
|
});
|