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:
co-authored by
Claude Opus 4.8
parent
7e06b1241f
commit
2045896b95
@@ -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} طلباً).`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 \
|
||||||
|
|||||||
Reference in New Issue
Block a user