// اختبار تحمّل: يشغّل دورة رحلة كاملة (طلب→قبول→حالات→دفع) بالتوازي ويقيس القدرة. // يستخدم fetch المدمج (Node 18+) — بلا تبعيات. يغطّي: تعرفة + خرائط + مطابقة GEO // + آلة الحالة + كتابة القاعدة + بث السوكت (خادمياً). // // التشغيل (حاوية node مؤقتة على شبكة tripz): // docker run --rm --network tripz-net -e BASE=http://tripz-api:4010/api \ // -v /home/tripz-llc/backend/scripts:/s node:22-alpine node /s/loadtest.mjs 300 30 // الوسائط: <إجمالي الرحلات> <التزامن> const BASE = process.env.BASE || 'http://localhost:4010/api'; const TENANT = process.env.TENANT || 'siro'; const TOTAL = parseInt(process.argv[2] || '200', 10); const CONC = parseInt(process.argv[3] || '20', 10); const CODE = '1234'; const ORIGIN = { lat: 31.9539, lng: 35.9106 }; const H = (t) => ({ 'Content-Type': 'application/json', ...(t ? { Authorization: `Bearer ${t}` } : {}) }); const jitter = (n) => n + (Math.random() - 0.5) * 0.01; async function api(method, path, token, body) { const res = await fetch(`${BASE}${path}`, { method, headers: { ...H(token), 'x-tenant-id': TENANT }, body: body ? JSON.stringify(body) : undefined, }); if (!res.ok) throw new Error(`${method} ${path} -> ${res.status} ${await res.text().catch(() => '')}`); return res.json().catch(() => ({})); } async function token(phone) { const r = await api('POST', '/auth/verify-otp', null, { phone, code: CODE }); return r.access_token; } async function setupDriver(i) { const t = await token(`0795${String(100000 + i).slice(-6)}`); const d = await api('POST', '/drivers/apply', t, { vehicle_make: 'LoadTest', service_class: 'economy' }); await api('PATCH', `/drivers/${d.id}/approve`, t, {}); await api('PATCH', '/drivers/status', t, { online: true }); await api('POST', '/drivers/location', t, { lat: jitter(ORIGIN.lat), lng: jitter(ORIGIN.lng) }); return t; } async function oneTrip(riderTok, driverTok) { const start = Date.now(); const { trip } = await api('POST', '/trips', riderTok, { origin: { lat: jitter(ORIGIN.lat), lng: jitter(ORIGIN.lng) }, destination: { lat: jitter(ORIGIN.lat + 0.03), lng: jitter(ORIGIN.lng + 0.03) }, service_class: 'economy', }); await api('POST', `/trips/${trip.id}/accept`, driverTok, {}); for (const s of ['driver_arriving', 'driver_arrived', 'in_progress', 'completed', 'paid']) { await api('PATCH', `/trips/${trip.id}/status`, driverTok, { status: s }); } return Date.now() - start; } async function main() { console.log(`Setup ${CONC} drivers+riders...`); const drivers = [], riders = []; for (let i = 0; i < CONC; i++) { drivers.push(await setupDriver(i)); riders.push(await token(`0796${String(100000 + i).slice(-6)}`)); } console.log(`Running ${TOTAL} trips @ concurrency ${CONC}...`); let done = 0, errors = 0; const lats = []; const t0 = Date.now(); await Promise.all( Array.from({ length: CONC }, (_, w) => (async () => { while (true) { const idx = done++; if (idx >= TOTAL) break; try { lats.push(await oneTrip(riders[w], drivers[w])); } catch (e) { errors++; if (errors <= 5) console.error('ERR:', e.message); } } })(), ), ); const secs = (Date.now() - t0) / 1000; lats.sort((a, b) => a - b); const pct = (p) => lats[Math.min(lats.length - 1, Math.floor((p / 100) * lats.length))] || 0; const ok = lats.length; console.log('\n===== النتائج ====='); console.log(`رحلات ناجحة: ${ok} / ${TOTAL} · أخطاء: ${errors}`); console.log(`الزمن: ${secs.toFixed(1)}s · الإنتاجية: ${(ok / secs).toFixed(1)} رحلة/ث (~${Math.round((ok / secs) * 86400).toLocaleString()} رحلة/يوم)`); console.log(`زمن الرحلة الكاملة (ms): p50=${pct(50)} · p95=${pct(95)} · max=${pct(100)}`); console.log('ملاحظة: كل رحلة = 7 نداءات API (طلب+قبول+5 حالات).'); } main().catch((e) => { console.error(e); process.exit(1); });