import { vi } from 'vitest'; import { mockDeep, mockReset, type DeepMockProxy } from 'vitest-mock-extended'; import type { Redis } from 'ioredis'; /** * Mock Redis instance for testing * Uses in-memory Map to simulate Redis operations */ export class MockRedis { private store = new Map(); get(key: string): Promise { const item = this.store.get(key); if (!item) return Promise.resolve(null); // Check expiry if (item.expiry && Date.now() > item.expiry) { this.store.delete(key); return Promise.resolve(null); } return Promise.resolve(item.value); } set(key: string, value: string): Promise<'OK'> { this.store.set(key, { value }); return Promise.resolve('OK'); } setex(key: string, seconds: number, value: string): Promise<'OK'> { const expiry = Date.now() + seconds * 1000; this.store.set(key, { value, expiry }); return Promise.resolve('OK'); } del(...keys: string[]): Promise { let count = 0; for (const key of keys) { if (this.store.delete(key)) count++; } return Promise.resolve(count); } incr(key: string): Promise { const item = this.store.get(key); const current = item ? parseInt(item.value, 10) : 0; const newValue = current + 1; this.store.set(key, { value: String(newValue) }); return Promise.resolve(newValue); } expire(key: string, seconds: number): Promise { const item = this.store.get(key); if (!item) return Promise.resolve(0); const expiry = Date.now() + seconds * 1000; this.store.set(key, { ...item, expiry }); return Promise.resolve(1); } exists(...keys: string[]): Promise { let count = 0; for (const key of keys) { if (this.store.has(key)) count++; } return Promise.resolve(count); } keys(pattern: string): Promise { const regex = new RegExp(pattern.replace(/\*/g, '.*')); const matchingKeys = Array.from(this.store.keys()).filter((key) => regex.test(key)); return Promise.resolve(matchingKeys); } flushall(): Promise<'OK'> { this.store.clear(); return Promise.resolve('OK'); } flushdb(): Promise<'OK'> { this.store.clear(); return Promise.resolve('OK'); } /** * Clear all stored data (for test cleanup) */ clear(): void { this.store.clear(); } /** * Get all keys (for debugging) */ getAllKeys(): string[] { return Array.from(this.store.keys()); } /** * Get store size (for assertions) */ size(): number { return this.store.size; } } /** * Create a mock Redis instance using vitest-mock-extended * This provides full type safety and spy functionality */ export function createMockRedis(): DeepMockProxy { const mock = mockDeep(); // Default implementations mock.get.mockResolvedValue(null); mock.set.mockResolvedValue('OK'); mock.setex.mockResolvedValue('OK'); mock.del.mockResolvedValue(1); mock.incr.mockResolvedValue(1); mock.expire.mockResolvedValue(1); return mock; } /** * Create a functional mock Redis with in-memory storage * Use this when you need Redis behavior (caching, expiry, etc.) */ export function createFunctionalMockRedis(): MockRedis { return new MockRedis(); } /** * Reset a mock Redis instance */ export function resetMockRedis(redis: DeepMockProxy): void { mockReset(redis); // Restore default implementations redis.get.mockResolvedValue(null); redis.set.mockResolvedValue('OK'); redis.setex.mockResolvedValue('OK'); redis.del.mockResolvedValue(1); redis.incr.mockResolvedValue(1); redis.expire.mockResolvedValue(1); }