import RedisMock from 'ioredis-mock'; import { CacheService, CacheKeys } from './cache.service'; describe('CacheService', () => { let redis: any; let cache: CacheService; // ioredis-mock يشارك مخزناً واحداً بين النسخ ذات الإعدادات نفسها — // بلا flushall تتسرّب مفاتيح اختبار إلى الذي يليه. beforeEach(async () => { redis = new RedisMock({ keyPrefix: 'tripz:' }); await redis.flushall(); cache = new CacheService(redis); }); it('يخزّن ويقرأ قيمة مركّبة', async () => { await cache.set('k', { a: 1, b: ['x'] }, 60); expect(await cache.get<{ a: number; b: string[] }>('k')).toEqual({ a: 1, b: ['x'] }); }); it('يرجع null لمفتاح غائب', async () => { expect(await cache.get('nope')).toBeNull(); }); it('wrap: يحمّل من المصدر مرة واحدة ثم يخدم من الكاش', async () => { const loader = jest.fn().mockResolvedValue({ id: 't1' }); expect(await cache.wrap('k', 60, loader)).toEqual({ id: 't1' }); expect(await cache.wrap('k', 60, loader)).toEqual({ id: 't1' }); expect(loader).toHaveBeenCalledTimes(1); }); it('wrap: لا يخزّن null — الغياب المؤقّت لا يُثبَّت', async () => { const loader = jest.fn().mockResolvedValue(null); expect(await cache.wrap('k', 60, loader)).toBeNull(); expect(await cache.wrap('k', 60, loader)).toBeNull(); expect(loader).toHaveBeenCalledTimes(2); }); it('del يُبطل المفتاح فيعود المصدر', async () => { const loader = jest.fn().mockResolvedValue('v1'); await cache.wrap('k', 60, loader); await cache.del('k'); await cache.wrap('k', 60, loader); expect(loader).toHaveBeenCalledTimes(2); }); it('mget يقرأ عدة مفاتيح ويعطي null للغائب', async () => { await cache.set('a', 1, 60); await cache.set('c', 3, 60); expect(await cache.mget(['a', 'b', 'c'])).toEqual([1, null, 3]); }); it('فشل Redis لا يُسقط الطلب — يرجع للمصدر', async () => { const broken = { get: jest.fn().mockRejectedValue(new Error('redis down')), set: jest.fn().mockRejectedValue(new Error('redis down')), } as any; const c = new CacheService(broken); const loader = jest.fn().mockResolvedValue('from-db'); expect(await c.wrap('k', 60, loader)).toBe('from-db'); expect(loader).toHaveBeenCalledTimes(1); }); it('المفاتيح معزولة بالمستأجر', () => { expect(CacheKeys.userLang('t1', 'u1')).not.toBe(CacheKeys.userLang('t2', 'u1')); expect(CacheKeys.tariff('t1', 'amman', 'economy')).toBe('tariff:t1:amman:economy'); }); });