Files
tripz-llc/backend/src/modules/trips/trip-state.service.spec.ts
T
Hamza-AyedandClaude Opus 4.8 da37dffa44 fix: أخطاء «الصفر الصادق» + عزل حالة ioredis-mock في الاختبارات
أول تشغيل فعلي للاختبارات على السيرفر كشف خطأين حقيقيين في الكود، لا في
الاختبارات:

- 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>
2026-07-17 02:56:50 +03:00

115 lines
3.9 KiB
TypeScript

import RedisMock from 'ioredis-mock';
import { TripStateService } from './trip-state.service';
import { Trip } from './entities/trip.entity';
const TENANT = 't1';
function makeTrip(overrides: Partial<Trip> = {}): Trip {
return {
id: 'trip-1',
tenant_id: TENANT,
rider_id: 'rider-1',
driver_id: null,
service_class: 'economy',
is_round_trip: false,
city: 'amman',
origin_lat: 31.95,
origin_lng: 35.91,
dest_lat: 31.98,
dest_lng: 35.87,
status: 'searching',
distance_km: 4.2,
duration_min: 11,
quoted_fare: 3.75,
currency: 'JOD',
payment_method: 'cash',
assigned_at: null,
...overrides,
} as Trip;
}
describe('TripStateService', () => {
let redis: any;
let state: TripStateService;
// ioredis-mock يشارك مخزناً واحداً بين النسخ — flushall يعزل كل اختبار.
beforeEach(async () => {
redis = new RedisMock({ keyPrefix: 'tripz:' });
await redis.flushall();
state = new TripStateService(redis);
});
it('يحفظ الحالة ويقرأها بأنواعها الصحيحة', async () => {
await state.save(makeTrip(), 'driver-user-1');
const s = await state.get(TENANT, 'trip-1');
expect(s).not.toBeNull();
expect(s!.status).toBe('searching');
expect(s!.driver_user_id).toBe('driver-user-1');
expect(s!.origin_lat).toBeCloseTo(31.95);
expect(s!.quoted_fare).toBe(3.75);
expect(s!.assigned_at).toBeNull();
});
it('يرجع null لرحلة غير موجودة (فيرجع المُنادي للقاعدة)', async () => {
expect(await state.get(TENANT, 'ghost')).toBeNull();
});
describe('claim — القبول الذرّي', () => {
it('أول سائق يفوز والثاني يخسر', async () => {
await state.save(makeTrip());
const at = new Date();
expect(await state.claim(TENANT, 'trip-1', 'd1', 'u1', at)).toBe('won');
expect(await state.claim(TENANT, 'trip-1', 'd2', 'u2', at)).toBe('lost');
const s = await state.get(TENANT, 'trip-1');
expect(s!.status).toBe('assigned');
expect(s!.driver_id).toBe('d1');
expect(s!.driver_user_id).toBe('u1');
expect(s!.assigned_at?.toISOString()).toBe(at.toISOString());
});
it('فائز واحد فقط عند تزامن عدة سائقين', async () => {
await state.save(makeTrip());
const at = new Date();
const results = await Promise.all(
Array.from({ length: 20 }, (_, i) =>
state.claim(TENANT, 'trip-1', `d${i}`, `u${i}`, at),
),
);
expect(results.filter((r) => r === 'won')).toHaveLength(1);
expect(results.filter((r) => r === 'lost')).toHaveLength(19);
});
it('يرجع unknown عند غياب المفتاح ليحسم الأمر على القاعدة', async () => {
expect(await state.claim(TENANT, 'gone', 'd1', 'u1', new Date())).toBe('unknown');
});
});
it('setStatus لا يُنشئ مفتاحاً لرحلة غير جارية', async () => {
await state.setStatus(TENANT, 'ghost', 'completed');
expect(await state.get(TENANT, 'ghost')).toBeNull();
});
it('clear يمسح الحالة والعروض معاً', async () => {
await state.save(makeTrip());
await state.addOffers(TENANT, 'trip-1', ['u1', 'u2']);
await state.clear(TENANT, 'trip-1');
expect(await state.get(TENANT, 'trip-1')).toBeNull();
expect(await state.takeLosingOffers(TENANT, 'trip-1', 'u1')).toEqual([]);
});
it('takeLosingOffers يستثني الفائز ويستهلك المجموعة مرة واحدة', async () => {
await state.addOffers(TENANT, 'trip-1', ['u1', 'u2', 'u3']);
const losers = await state.takeLosingOffers(TENANT, 'trip-1', 'u2');
expect(losers.sort()).toEqual(['u1', 'u3']);
// الاستدعاء الثاني فارغ — لا إشعارات مكرّرة لبقية السائقين
expect(await state.takeLosingOffers(TENANT, 'trip-1', 'u2')).toEqual([]);
});
});