قرار المالك 2026-07-27: الباك إند المعتمد صار باك إند سيرو PHP، ويُنقل إلى هذا المستودع كنسخة جديدة باسم «انطلق» على api.intaleqapp.com. backend/ → backend-archive/ (305 ملفاً، إعادة تسمية بلا تعديل محتوى) + README-ARCHIVE.md يوضّح سبب الأرشفة وسبب عدم الحذف: scripts/e2e-test.mjs هو أدق توثيق سلوكي لدورة الرحلة كاملة، ويبقى مرجعاً عند بناء الوحدات الناقصة في باك إند PHP (المحفظتان · الاستحقاقات · محرّك التسعير · الوحدات). مسارات backend/ في docs/*.md لم تُعدَّل عمداً — سجلّ تاريخي. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
100 lines
4.1 KiB
JavaScript
100 lines
4.1 KiB
JavaScript
// اختبار تحمّل: يشغّل دورة رحلة كاملة (طلب→قبول→حالات→دفع) بالتوازي ويقيس القدرة.
|
|
// يستخدم 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); });
|