import { test, before, after } from "node:test"; import assert from "node:assert/strict"; import pg from "pg"; const API_BASE = process.env.TEST_API_BASE ?? "http://localhost:8080"; const DATABASE_URL = process.env.DATABASE_URL; if (!DATABASE_URL) throw new Error("DATABASE_URL must be set to run these tests"); const TEST_PASSWORD = "TestPass123!"; const TEST_PASSWORD_HASH = "$2b$10$Bs636ukPMyz01nKrsi.5m.JlDXSN22AVCvn8cgPWWDbo5yJRQX2vu"; const pool = new pg.Pool({ connectionString: DATABASE_URL }); const createdUserIds = []; const createdOrderIds = []; async function createUser(prefix, roleName) { const username = `${prefix}_${Date.now().toString(36)}_${Math.random() .toString(36) .slice(2, 8)}`; const { rows } = await pool.query( `INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active) VALUES ($1, $2, $3, 'SO Test', 'en', true) RETURNING id`, [username, `${username}@example.com`, TEST_PASSWORD_HASH], ); const id = rows[0].id; createdUserIds.push(id); if (roleName) { await pool.query( `INSERT INTO user_roles (user_id, role_id) SELECT $1, id FROM roles WHERE name = $2`, [id, roleName], ); } return { id, username }; } async function login(username) { const res = await fetch(`${API_BASE}/api/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password: TEST_PASSWORD }), }); assert.equal(res.status, 200); const sc = res.headers.get("set-cookie"); return sc.split(",").map((c) => c.split(";")[0].trim()).find((c) => c.startsWith("connect.sid="), ); } async function call(cookie, method, path, body) { return fetch(`${API_BASE}/api${path}`, { method, headers: { "Content-Type": "application/json", Cookie: cookie }, body: body ? JSON.stringify(body) : undefined, }); } let serviceId; before(async () => { const { rows } = await pool.query( `SELECT id FROM services WHERE is_available = true ORDER BY id LIMIT 1`, ); if (rows.length === 0) throw new Error("No available service to test with"); serviceId = rows[0].id; }); after(async () => { if (createdOrderIds.length > 0) { await pool.query(`DELETE FROM service_orders WHERE id = ANY($1::int[])`, [ createdOrderIds, ]); } if (createdUserIds.length > 0) { await pool.query(`DELETE FROM service_orders WHERE user_id = ANY($1::int[]) OR assigned_to = ANY($1::int[])`, [createdUserIds]); await pool.query(`DELETE FROM notifications WHERE user_id = ANY($1::int[])`, [createdUserIds]); await pool.query(`DELETE FROM user_roles WHERE user_id = ANY($1::int[])`, [createdUserIds]); await pool.query(`DELETE FROM users WHERE id = ANY($1::int[])`, [createdUserIds]); } await pool.end(); }); test("client can place + list own orders; service summary excludes descriptions", async () => { const requester = await createUser("req", "user"); const cookie = await login(requester.username); let res = await call(cookie, "POST", "/orders", { serviceId, notes: "extra hot", }); assert.equal(res.status, 201); const order = await res.json(); createdOrderIds.push(order.id); assert.equal(order.status, "pending"); assert.equal(order.userId, requester.id); assert.ok(order.service); assert.ok(order.service.nameAr); assert.ok(order.service.nameEn); assert.equal(order.service.descriptionAr, undefined); assert.equal(order.service.descriptionEn, undefined); res = await call(cookie, "GET", "/orders/my"); assert.equal(res.status, 200); const list = await res.json(); const found = list.find((o) => o.id === order.id); assert.ok(found); assert.equal(found.service.descriptionAr, undefined); }); test("non-receiver gets 403 from /incoming and /confirm-receipt", async () => { const owner = await createUser("own", "user"); const cookieOwner = await login(owner.username); let res = await call(cookieOwner, "POST", "/orders", { serviceId }); const order = await res.json(); createdOrderIds.push(order.id); // Non-receiver listing res = await call(cookieOwner, "GET", "/orders/incoming"); assert.equal(res.status, 403); // Non-receiver tries to claim res = await call(cookieOwner, "PATCH", `/orders/${order.id}/confirm-receipt`); assert.equal(res.status, 403); }); test("user with no roles is 403 on /incoming (middleware has no auto-bypass)", async () => { const noRole = await createUser("noro", null); const cookie = await login(noRole.username); const res = await call(cookie, "GET", "/orders/incoming"); assert.equal(res.status, 403); }); test("two parallel confirm-receipt: one wins (200), other 409 already_claimed", async () => { const owner = await createUser("own2", "user"); const r1 = await createUser("r1", "order_receiver"); const r2 = await createUser("r2", "order_receiver"); const co = await login(owner.username); const c1 = await login(r1.username); const c2 = await login(r2.username); let res = await call(co, "POST", "/orders", { serviceId }); const order = await res.json(); createdOrderIds.push(order.id); const [a, b] = await Promise.all([ call(c1, "PATCH", `/orders/${order.id}/confirm-receipt`), call(c2, "PATCH", `/orders/${order.id}/confirm-receipt`), ]); const codes = [a.status, b.status].sort(); assert.deepEqual(codes, [200, 409]); const losing = a.status === 409 ? a : b; const body = await losing.json(); assert.equal(body.error, "already_claimed"); }); test("status transitions matrix: receiver flows + owner/admin cancel rules", async () => { const owner = await createUser("own3", "user"); const recv = await createUser("recv3", "order_receiver"); const stranger = await createUser("strg3", "user"); const adminUser = await createUser("admn3", "admin"); const co = await login(owner.username); const cr = await login(recv.username); const cs = await login(stranger.username); const ca = await login(adminUser.username); // Order 1: receiver flow pending → received → preparing → completed let res = await call(co, "POST", "/orders", { serviceId }); let order = await res.json(); createdOrderIds.push(order.id); // Owner CAN cancel while pending res = await call(co, "POST", "/orders", { serviceId }); const cancelOrder = await res.json(); createdOrderIds.push(cancelOrder.id); res = await call(co, "PATCH", `/orders/${cancelOrder.id}/status`, { status: "cancelled", }); assert.equal(res.status, 200); assert.equal((await res.json()).status, "cancelled"); // Stranger cannot move status to preparing res = await call(cs, "PATCH", `/orders/${order.id}/status`, { status: "preparing", }); assert.equal(res.status, 403); // Receiver claims first res = await call(cr, "PATCH", `/orders/${order.id}/confirm-receipt`); assert.equal(res.status, 200); // Receiver moves to preparing (allowed from received) res = await call(cr, "PATCH", `/orders/${order.id}/status`, { status: "preparing", }); assert.equal(res.status, 200); assert.equal((await res.json()).status, "preparing"); // Owner cannot cancel once preparing (only pending/received) res = await call(co, "PATCH", `/orders/${order.id}/status`, { status: "cancelled", }); assert.equal(res.status, 403); // Admin can cancel anytime // (use a NEW preparing order) res = await call(co, "POST", "/orders", { serviceId }); const o2 = await res.json(); createdOrderIds.push(o2.id); res = await call(cr, "PATCH", `/orders/${o2.id}/confirm-receipt`); assert.equal(res.status, 200); res = await call(cr, "PATCH", `/orders/${o2.id}/status`, { status: "preparing" }); assert.equal(res.status, 200); res = await call(ca, "PATCH", `/orders/${o2.id}/status`, { status: "cancelled" }); assert.equal(res.status, 200); // Assigned receiver CAN cancel their own assigned order while preparing res = await call(co, "POST", "/orders", { serviceId }); const o3 = await res.json(); createdOrderIds.push(o3.id); res = await call(cr, "PATCH", `/orders/${o3.id}/confirm-receipt`); assert.equal(res.status, 200); res = await call(cr, "PATCH", `/orders/${o3.id}/status`, { status: "preparing" }); assert.equal(res.status, 200); res = await call(cr, "PATCH", `/orders/${o3.id}/status`, { status: "cancelled" }); assert.equal(res.status, 200); assert.equal((await res.json()).status, "cancelled"); // A different receiver (not the assignee) cannot cancel res = await call(co, "POST", "/orders", { serviceId }); const o4 = await res.json(); createdOrderIds.push(o4.id); res = await call(cr, "PATCH", `/orders/${o4.id}/confirm-receipt`); assert.equal(res.status, 200); const otherRecv = await createUser("recv4", "order_receiver"); const cor = await login(otherRecv.username); res = await call(cor, "PATCH", `/orders/${o4.id}/status`, { status: "cancelled" }); assert.equal(res.status, 403); // Receiver completes original order res = await call(cr, "PATCH", `/orders/${order.id}/status`, { status: "completed", }); assert.equal(res.status, 200); assert.equal((await res.json()).status, "completed"); // Cannot transition completed → anything (non-admin); now 409 order_unavailable per Task #80 res = await call(cr, "PATCH", `/orders/${order.id}/status`, { status: "preparing", }); assert.equal(res.status, 409); // Admin CAN cancel a completed order res = await call(ca, "PATCH", `/orders/${order.id}/status`, { status: "cancelled", }); assert.equal(res.status, 200); assert.equal((await res.json()).status, "cancelled"); }); test("requester gets a distinct notification when the receiver cancels a claimed order", async () => { const owner = await createUser("ownN", "user"); const recv = await createUser("recvN", "order_receiver"); const co = await login(owner.username); const cr = await login(recv.username); let res = await call(co, "POST", "/orders", { serviceId }); assert.equal(res.status, 201); const order = await res.json(); createdOrderIds.push(order.id); // Receiver claims, then cancels res = await call(cr, "PATCH", `/orders/${order.id}/confirm-receipt`); assert.equal(res.status, 200); res = await call(cr, "PATCH", `/orders/${order.id}/status`, { status: "cancelled", }); assert.equal(res.status, 200); // Owner should have a cancellation notification with the receiver-cancel body const { rows } = await pool.query( `SELECT title_ar, title_en, body_ar, body_en FROM notifications WHERE user_id = $1 AND related_type = 'order' AND related_id = $2 ORDER BY id DESC`, [owner.id, order.id], ); const cancelNotif = rows.find( (n) => n.title_en === "Your order was cancelled", ); assert.ok(cancelNotif, "expected a cancellation notification for the owner"); assert.match( cancelNotif.body_en, /claimed but later cancelled by the receiver/, ); assert.match(cancelNotif.body_ar, /استلم طلبك ولكن تم إلغاؤه لاحقاً/); }); test("DELETE /orders/:id — owner can delete completed/cancelled, admin can delete any, others 403", async () => { const owner = await createUser("delOwn", "user"); const recv = await createUser("delRecv", "order_receiver"); const stranger = await createUser("delStr", "user"); const adminUser = await createUser("delAdm", "admin"); const co = await login(owner.username); const cr = await login(recv.username); const cs = await login(stranger.username); const ca = await login(adminUser.username); // Helper to create an order owned by `co` const placeOrder = async () => { const r = await call(co, "POST", "/orders", { serviceId }); assert.equal(r.status, 201); const o = await r.json(); createdOrderIds.push(o.id); return o; }; // 1) Pending order: owner cannot delete (not terminal) const o1 = await placeOrder(); let res = await call(co, "DELETE", `/orders/${o1.id}`); assert.equal(res.status, 403); // 2) Stranger cannot delete a terminal order they don't own const o2 = await placeOrder(); res = await call(co, "PATCH", `/orders/${o2.id}/status`, { status: "cancelled" }); assert.equal(res.status, 200); res = await call(cs, "DELETE", `/orders/${o2.id}`); assert.equal(res.status, 403); // 3) Owner CAN delete a cancelled order they own res = await call(co, "DELETE", `/orders/${o2.id}`); assert.equal(res.status, 204); // Confirm it's gone from /orders/my res = await call(co, "GET", "/orders/my"); const list1 = await res.json(); assert.ok(!list1.find((o) => o.id === o2.id)); // 3b) Deleting the same order again returns 404 (idempotent semantics) res = await call(co, "DELETE", `/orders/${o2.id}`); assert.equal(res.status, 404); // 4) Owner CAN delete a completed order const o3 = await placeOrder(); res = await call(cr, "PATCH", `/orders/${o3.id}/confirm-receipt`); assert.equal(res.status, 200); res = await call(cr, "PATCH", `/orders/${o3.id}/status`, { status: "preparing" }); assert.equal(res.status, 200); res = await call(cr, "PATCH", `/orders/${o3.id}/status`, { status: "completed" }); assert.equal(res.status, 200); res = await call(co, "DELETE", `/orders/${o3.id}`); assert.equal(res.status, 204); // 5) Admin CAN delete a non-terminal (pending) order const o4 = await placeOrder(); res = await call(ca, "DELETE", `/orders/${o4.id}`); assert.equal(res.status, 204); // 6) 404 for unknown id res = await call(co, "DELETE", `/orders/9999999`); assert.equal(res.status, 404); // 7) Unauthenticated → 401 res = await fetch(`${API_BASE}/api/orders/1`, { method: "DELETE" }); assert.equal(res.status, 401); }); test("owner can restore (undo) a freshly cancelled order to its prior status; expired window blocks restore", async () => { const owner = await createUser("undoOwn", "user"); const recv = await createUser("undoRecv", "order_receiver"); const stranger = await createUser("undoStr", "user"); const co = await login(owner.username); const cr = await login(recv.username); const cs = await login(stranger.username); // Case A: cancel from pending → restore to pending let res = await call(co, "POST", "/orders", { serviceId }); assert.equal(res.status, 201); const oA = await res.json(); createdOrderIds.push(oA.id); res = await call(co, "PATCH", `/orders/${oA.id}/status`, { status: "cancelled" }); assert.equal(res.status, 200); // Stranger cannot restore res = await call(cs, "PATCH", `/orders/${oA.id}/status`, { status: "pending" }); assert.equal(res.status, 403); // Owner restores within window res = await call(co, "PATCH", `/orders/${oA.id}/status`, { status: "pending" }); assert.equal(res.status, 200); assert.equal((await res.json()).status, "pending"); // Case B: cancel from received → restore to received res = await call(co, "POST", "/orders", { serviceId }); const oB = await res.json(); createdOrderIds.push(oB.id); res = await call(cr, "PATCH", `/orders/${oB.id}/confirm-receipt`); assert.equal(res.status, 200); res = await call(co, "PATCH", `/orders/${oB.id}/status`, { status: "cancelled" }); assert.equal(res.status, 200); // Restoring to "pending" should fail because assignedTo is set res = await call(co, "PATCH", `/orders/${oB.id}/status`, { status: "pending" }); assert.equal(res.status, 400); // Correct restore is "received" res = await call(co, "PATCH", `/orders/${oB.id}/status`, { status: "received" }); assert.equal(res.status, 200); assert.equal((await res.json()).status, "received"); // Case C: undo window expired → restore is rejected res = await call(co, "POST", "/orders", { serviceId }); const oC = await res.json(); createdOrderIds.push(oC.id); res = await call(co, "PATCH", `/orders/${oC.id}/status`, { status: "cancelled" }); assert.equal(res.status, 200); // Backdate updatedAt past the 15s server window. await pool.query( `UPDATE service_orders SET updated_at = NOW() - INTERVAL '60 seconds' WHERE id = $1`, [oC.id], ); res = await call(co, "PATCH", `/orders/${oC.id}/status`, { status: "pending" }); assert.equal(res.status, 400); const body = await res.json(); assert.match(body.error, /undo_window_expired/); }); test("confirm-receipt returns 409 order_unavailable when the order is cancelled or completed", async () => { const owner = await createUser("ownU", "user"); const recv1 = await createUser("recvU1", "order_receiver"); const recv2 = await createUser("recvU2", "order_receiver"); const co = await login(owner.username); const cr1 = await login(recv1.username); const cr2 = await login(recv2.username); // Case A: order cancelled before any claim → second receiver sees order_unavailable let res = await call(co, "POST", "/orders", { serviceId }); assert.equal(res.status, 201); const oCancelled = await res.json(); createdOrderIds.push(oCancelled.id); res = await call(co, "PATCH", `/orders/${oCancelled.id}/status`, { status: "cancelled", }); assert.equal(res.status, 200); res = await call(cr1, "PATCH", `/orders/${oCancelled.id}/confirm-receipt`); assert.equal(res.status, 409); let body = await res.json(); assert.equal( body.error, "order_unavailable", "cancelled order should report order_unavailable, not already_claimed", ); // Case B: order taken all the way to completed → another receiver sees order_unavailable res = await call(co, "POST", "/orders", { serviceId }); assert.equal(res.status, 201); const oCompleted = await res.json(); createdOrderIds.push(oCompleted.id); res = await call(cr1, "PATCH", `/orders/${oCompleted.id}/confirm-receipt`); assert.equal(res.status, 200); res = await call(cr1, "PATCH", `/orders/${oCompleted.id}/status`, { status: "preparing", }); assert.equal(res.status, 200); res = await call(cr1, "PATCH", `/orders/${oCompleted.id}/status`, { status: "completed", }); assert.equal(res.status, 200); res = await call(cr2, "PATCH", `/orders/${oCompleted.id}/confirm-receipt`); assert.equal(res.status, 409); body = await res.json(); assert.equal( body.error, "order_unavailable", "completed order should report order_unavailable, not already_claimed", ); }); test("PATCH /orders/:id/status returns 409 order_unavailable when targeting a terminal order", async () => { const owner = await createUser("ownT", "user"); const recv = await createUser("recvT", "order_receiver"); const co = await login(owner.username); const cr = await login(recv.username); // Build a completed order (assignee = recv) let res = await call(co, "POST", "/orders", { serviceId }); assert.equal(res.status, 201); const oDone = await res.json(); createdOrderIds.push(oDone.id); res = await call(cr, "PATCH", `/orders/${oDone.id}/confirm-receipt`); assert.equal(res.status, 200); res = await call(cr, "PATCH", `/orders/${oDone.id}/status`, { status: "preparing", }); assert.equal(res.status, 200); res = await call(cr, "PATCH", `/orders/${oDone.id}/status`, { status: "completed", }); assert.equal(res.status, 200); // Assignee tries preparing on a completed order → 409 order_unavailable res = await call(cr, "PATCH", `/orders/${oDone.id}/status`, { status: "preparing", }); assert.equal(res.status, 409); let body = await res.json(); assert.equal(body.error, "order_unavailable"); // Assignee tries completed (again) on a completed order → 409 order_unavailable res = await call(cr, "PATCH", `/orders/${oDone.id}/status`, { status: "completed", }); assert.equal(res.status, 409); body = await res.json(); assert.equal(body.error, "order_unavailable"); // Owner tries to cancel a completed order → 409 order_unavailable (non-admin) res = await call(co, "PATCH", `/orders/${oDone.id}/status`, { status: "cancelled", }); assert.equal(res.status, 409); body = await res.json(); assert.equal(body.error, "order_unavailable"); // Build a cancelled order (no assignee) res = await call(co, "POST", "/orders", { serviceId }); assert.equal(res.status, 201); const oCxl = await res.json(); createdOrderIds.push(oCxl.id); res = await call(co, "PATCH", `/orders/${oCxl.id}/status`, { status: "cancelled", }); assert.equal(res.status, 200); // Owner tries to cancel an already-cancelled order → 409 order_unavailable (non-admin) res = await call(co, "PATCH", `/orders/${oCxl.id}/status`, { status: "cancelled", }); assert.equal(res.status, 409); body = await res.json(); assert.equal(body.error, "order_unavailable"); }); test("unauthenticated cannot place order", async () => { const res = await fetch(`${API_BASE}/api/orders`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ serviceId }), }); assert.equal(res.status, 401); });