Task #704: أوقات حجز محددة يديرها الأدمن + مهلة حجز مسبقة
- DB: protocol_booking_slots (وقت "HH:mm" بتوقيت الرياض، مدة، تفعيل، ترتيب) + protocol_settings (booking.minLeadMinutes، الافتراضي 60 دقيقة). - API: CRUD /protocol/booking-slots (الكتابة تتطلب protocol.rooms.manage، تكرار الوقت → 409 duplicate_slot)، GET/PATCH /protocol/settings (upsert + سجل تدقيق)، ونقطة عامة /protocol/public/availability (محدّد معدل خاص، تعيد booked/tooSoon/available لكل وقت). - الفرض في الخادم: الطلب العام يجب أن يطابق وقتاً مفعّلاً + يحترم المهلة (400 invalid_slot / lead_time برسائل عربية)؛ الموظفون ملزمون بالأوقات لكن معفيون من المهلة. بدون أوقات مفعّلة يعمل الوضع الحر القديم. - الواجهة: نموذج الطلب العام يعرض شبكة أوقات حسب التوفر (مع fallback قديم)؛ تبويب القاعات يعرض قسم إدارة الأوقات + المهلة لمن يملك canManageRooms؛ حوار الحجز الداخلي يتحول لتاريخ + قائمة أوقات عند وجود أوقات مفعّلة. - i18n: مفاتيح protocol.slots.* بالعربية والإنجليزية. - اختبارات: tests/protocol-booking-slots.test.mjs — 10 اختبارات ناجحة (CRUD/تكرار، الإعدادات، التوفر، invalid_slot، lead_time، تعارض، وقت معطّل، إعفاء الموظفين، صلاحيات القراءة/الكتابة، الحذف). - مراجعة المعماري: PASS؛ عولجت الملاحظات (توثيق قرار الصلاحيات + اختبار regression له، ومنع الإرسال في الواجهة قبل اختيار التاريخ والوقت). - تعديل بعد الرفض الأول: قراءة قائمة الأوقات الكاملة والإعدادات تتطلب protocol.rooms.manage (حسب المواصفة)؛ أُضيفت نقطة قراءة مصغّرة /protocol/booking-slots/active (protocol.access) لحوار الحجز فقط.
This commit is contained in:
@@ -376,10 +376,10 @@ test("staff bookings must match a slot but ignore the lead time", async () => {
|
||||
created.bookingIds.push((await onSlot.json()).id);
|
||||
});
|
||||
|
||||
// Authz decision locked in: any protocol.access holder may READ slots and
|
||||
// settings (the booking dialog needs them), but WRITES require
|
||||
// protocol.rooms.manage.
|
||||
test("protocol viewer can read slots/settings but cannot write them", async () => {
|
||||
// Authz locked in per Task #704 spec: slot/settings admin endpoints —
|
||||
// including reads — require protocol.rooms.manage. Regular protocol users
|
||||
// may only read the minimal active-slot list via /booking-slots/active.
|
||||
test("protocol viewer can only read active slots; admin reads/writes are 403", async () => {
|
||||
const username = uniqueName("slot_viewer");
|
||||
const { rows } = await pool.query(
|
||||
`INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active)
|
||||
@@ -397,10 +397,26 @@ test("protocol viewer can read slots/settings but cannot write them", async () =
|
||||
}
|
||||
const viewerCookie = await login(username, TEST_PASSWORD);
|
||||
|
||||
const active = await api(
|
||||
viewerCookie,
|
||||
"GET",
|
||||
"/protocol/booking-slots/active",
|
||||
);
|
||||
assert.equal(active.status, 200, "viewer should read active slots");
|
||||
const activeRows = await active.json();
|
||||
assert.ok(Array.isArray(activeRows));
|
||||
for (const row of activeRows) {
|
||||
assert.deepEqual(
|
||||
Object.keys(row).sort(),
|
||||
["durationMinutes", "id", "startTime"],
|
||||
"active endpoint must expose only minimal fields",
|
||||
);
|
||||
}
|
||||
|
||||
const getSlots = await api(viewerCookie, "GET", "/protocol/booking-slots");
|
||||
assert.equal(getSlots.status, 200, "viewer should read slots");
|
||||
assert.equal(getSlots.status, 403, "viewer must not read the admin slot list");
|
||||
const getSettings = await api(viewerCookie, "GET", "/protocol/settings");
|
||||
assert.equal(getSettings.status, 200, "viewer should read settings");
|
||||
assert.equal(getSettings.status, 403, "viewer must not read settings");
|
||||
|
||||
const post = await api(viewerCookie, "POST", "/protocol/booking-slots", {
|
||||
startTime: "20:20",
|
||||
|
||||
Reference in New Issue
Block a user