fix(test): expect fixed to be rejected + flush tariff cache before testing

الدورة السابقة أعلنت الفشل والنظام سليم — العطل كان في السكربت:

1. **`fixed` تُرفض بـ400 وهذا هو المطلوب** (أُخرجت من الزرع عمداً)، لكن
   السكربت كان يعدّها فئة متوقَّعة فأسقط التغطية إلى 72/81. صارت في
   `REJECTED_CLASSES`: الرفض يُتحقَّق منه صراحةً، وغيابه هو الفشل.

2. **429 على `send-otp`** (10 لكل 5 دقائق) عند تشغيلتين متتاليتين — يُنتظر
   ويُعاد بدل الإبلاغ عن فشل ليس في النظام.

3. **إبطال كاش التعرفة صار خطوة في الدورة.** `getActive` تكاشي 900 ثانية،
   و**Redis حاوية منفصلة لا تتأثر بإعادة تشغيل الـapi** — وتطبيق
   `tariffs.sql` مباشرةً على القاعدة يتجاوز التطبيق فلا يُبطل شيئاً. النتيجة
   أن النظام خدم أسعاراً قديمة عبر دورتَي نشر كاملتين وهو يعلن النجاح.
   المسح الآن مبنيّ في السكربت لا متروك للتذكّر.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-19 02:09:05 +03:00
co-authored by Claude Opus 4.8
parent 7e06b1241f
commit 2045896b95
2 changed files with 48 additions and 5 deletions
+30 -3
View File
@@ -33,10 +33,16 @@ if (!PLATFORM_SECRET) {
process.exit(1); process.exit(1);
} }
// الفئات المتوقَّع أن تُسعَّر. **`fixed` ليست منها**: أُخرجت من الزرع عمداً
// (سعر ثابت بلا جدول خطوط = رقم واحد لكل الوجهات)، فالمتوقَّع منها رفض 400
// لا سعر — ويُتحقَّق من ذلك صراحةً في `REJECTED_CLASSES`.
const SERVICE_CLASSES = [ const SERVICE_CLASSES = [
'saver', 'delivery', 'economy', 'fixed', 'electric', 'lady', 'comfort', 'van', 'vip', 'saver', 'delivery', 'economy', 'electric', 'lady', 'comfort', 'vip', 'van',
]; ];
/** فئات يجب أن يرفضها الخادم — الرفض هنا نجاحٌ لا فشل. */
const REJECTED_CLASSES = ['fixed'];
/** /**
* رحلات نموذجية داخل الزرقاء/عمّان (إحداثيات قابلة لإعادة الاستعمال في * رحلات نموذجية داخل الزرقاء/عمّان (إحداثيات قابلة لإعادة الاستعمال في
* أي بلد — الفارق بين البلدان هو التعرفة لا الجغرافيا؛ المسافة والزمن * أي بلد — الفارق بين البلدان هو التعرفة لا الجغرافيا؛ المسافة والزمن
@@ -194,7 +200,14 @@ async function loginRider(tenantSlug, tenantId, countryPack, seed) {
// `send-otp` يخزّن الرمز في Redis **قبل** محاولة الإرسال، فحتى لو فشل // `send-otp` يخزّن الرمز في Redis **قبل** محاولة الإرسال، فحتى لو فشل
// الإرسال لرقم وهمي (503) يكون الرمز محفوظاً — وهو ما نقرأه. // الإرسال لرقم وهمي (503) يكون الرمز محفوظاً — وهو ما نقرأه.
const sent = await api('POST', '/auth/send-otp', { tenant: tenantSlug, body: { phone } }); let sent = await api('POST', '/auth/send-otp', { tenant: tenantSlug, body: { phone } });
// 429: `send-otp` محدود المعدّل (10 لكل 5 دقائق). تشغيلتان متتاليتان
// تصطدمان به، فننتظر بدل أن نُبلغ عن فشل ليس في النظام.
for (let wait = 0; sent.status === 429 && wait < 3; wait++) {
console.log(` ⏳ حدّ المعدّل على send-otp — انتظار 20 ثانية…`);
await new Promise((r) => setTimeout(r, 20000));
sent = await api('POST', '/auth/send-otp', { tenant: tenantSlug, body: { phone } });
}
if (sent.body?.dev_code) { if (sent.body?.dev_code) {
// وضع تطويري مفعّل (بيئة غير إنتاجية) — الرمز في الاستجابة مباشرةً. // وضع تطويري مفعّل (بيئة غير إنتاجية) — الرمز في الاستجابة مباشرةً.
return (await verify(tenantSlug, phone, sent.body.dev_code))?.access_token; return (await verify(tenantSlug, phone, sent.body.dev_code))?.access_token;
@@ -260,6 +273,7 @@ async function main() {
} }
const rows = []; const rows = [];
let rejectionsOk = true;
for (const t of TENANTS) { for (const t of TENANTS) {
console.log(`\n--- ${t.slug} (${t.countryPack} / ${t.currency}) ---`); console.log(`\n--- ${t.slug} (${t.countryPack} / ${t.currency}) ---`);
@@ -275,6 +289,19 @@ async function main() {
continue; continue;
} }
// الفئات المسحوبة يجب أن تُرفض — تسريب الـ2.5 دينار كان هنا.
for (const serviceClass of REJECTED_CLASSES) {
const res = await quoteFor(t.slug, token, serviceClass, RIDES[2]); // الأطول
if (res.status === 400) {
console.log(` ✅ ${serviceClass} مرفوضة كما يجب (400)`);
} else {
console.error(
` ❌ ${serviceClass} لم تُرفض: ${res.status} — رحلة ${RIDES[2].label} بسعر ثابت تسريب.`,
);
rejectionsOk = false;
}
}
for (const ride of RIDES) { for (const ride of RIDES) {
for (const serviceClass of SERVICE_CLASSES) { for (const serviceClass of SERVICE_CLASSES) {
const res = await quoteFor(t.slug, token, serviceClass, ride); const res = await quoteFor(t.slug, token, serviceClass, ride);
@@ -357,7 +384,7 @@ async function main() {
const comparable = rows.filter((r) => ['economy', 'van'].includes(r.serviceClass)).length; const comparable = rows.filter((r) => ['economy', 'van'].includes(r.serviceClass)).length;
if (comparable === 0) console.error('⚠️ لا بيانات كافية للمقارنة — التحقّق أعلاه لم يقع فعلياً.'); if (comparable === 0) console.error('⚠️ لا بيانات كافية للمقارنة — التحقّق أعلاه لم يقع فعلياً.');
const bad = noQuote.length > 0 || !orderOk || rows.length < expected; const bad = noQuote.length > 0 || !orderOk || rows.length < expected || !rejectionsOk;
process.exitCode = bad ? 1 : 0; process.exitCode = bad ? 1 : 0;
console.log(bad ? '\n❌ فشل الاختبار.' : `\n✅ الاختبار ناجح (${rows.length} طلباً).`); console.log(bad ? '\n❌ فشل الاختبار.' : `\n✅ الاختبار ناجح (${rows.length} طلباً).`);
} }
+18 -2
View File
@@ -97,8 +97,24 @@ if [[ "$DEPLOY_ONLY" == "1" ]]; then
hr; echo "✅ النشر سليم (بلا اختبار)."; exit 0 hr; echo "✅ النشر سليم (بلا اختبار)."; exit 0
fi fi
# ── 4) اختبار التسعير ──────────────────────────────────────────────── # ── 4) إبطال كاش التعرفة ─────────────────────────────────────────────
hr; echo "4) اختبار التسعير" # `getActive` تكاشي في Redis 900 ثانية، و**Redis حاوية منفصلة لا تتأثر
# بإعادة تشغيل الـapi**. تطبيق `tariffs.sql` مباشرةً على القاعدة يتجاوز
# التطبيق فلا يُبطل شيئاً — فبقي النظام يخدم أسعاراً قديمة عبر دورتَي نشر
# كاملتين وهو يعلن النجاح. المسح هنا يجعل الطزاجة مضمونة بالبناء لا
# بالتذكّر.
hr; echo "4) إبطال كاش التعرفة"
KEYS=$(docker compose exec -T redis redis-cli -n "${REDIS_DB:-3}" --scan --pattern 'tripz:tariff:*' 2>/dev/null | tr -d '\r')
if [[ -n "$KEYS" ]]; then
COUNT=$(printf '%s\n' "$KEYS" | grep -c . || true)
printf '%s\n' "$KEYS" | xargs -r docker compose exec -T redis redis-cli -n "${REDIS_DB:-3}" DEL >/dev/null
echo " ✅ مُسح $COUNT مفتاح تعرفة"
else
echo " ℹ️ لا مفاتيح تعرفة مكاشية"
fi
# ── 5) اختبار التسعير ────────────────────────────────────────────────
hr; echo "5) اختبار التسعير"
SECRET=$(grep -E '^PLATFORM_SECRET=' "$ENV_FILE" | cut -d= -f2- | tr -d '"'"'"'\r ') SECRET=$(grep -E '^PLATFORM_SECRET=' "$ENV_FILE" | cut -d= -f2- | tr -d '"'"'"'\r ')
docker run --rm --network tripz-net \ docker run --rm --network tripz-net \
-e BASE=http://tripz-api:4010/api \ -e BASE=http://tripz-api:4010/api \