أول تشغيل فعلي للاختبارات على السيرفر كشف خطأين حقيقيين في الكود، لا في
الاختبارات:
- RatingAggregateService.get: البذر يكتب count='0' لهدف بلا تقييمات، و'0'
نصٌّ صادق في JS — ففحص !h.count لا يمسكه وكانت ترجع {avg:0,count:0} بدل
null. صارت مقارنة رقمية.
- DriverLocationService.readHash: نفس الفخ — خط عرض '0' إحداثي صالح لكنه
نصٌّ صادق؛ صار الفحص == null.
الاختبارات:
- ioredis-mock يشارك مخزناً واحداً بين النسخ رغم new RedisMock() — أُضيف
flushall في كل beforeEach (تسرّبت مفاتيح بين الاختبارات).
- ioredis-mock لا ينفّذ geoadd داخل multi() ولا redis.call، فلا يمكن اختبار
فهرس GEO به: صار MatchingService ضعفاً مزيّفاً في اختبارات المواقع،
والتحقق ينصبّ على منطق العتبات. سلوك GEO الحقيقي يُتحقَّق على السيرفر.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
72 lines
2.7 KiB
TypeScript
72 lines
2.7 KiB
TypeScript
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<number>(['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');
|
|
});
|
|
});
|