Add test coverage for the order-unavailable error responses (Task #86)

Task #80 introduced a new 409 `order_unavailable` error code returned by:
  - PATCH /api/orders/:id/confirm-receipt
  - PATCH /api/orders/:id/status
…to distinguish "already_claimed" (lost a race) from "no longer claimable"
(target order is cancelled or completed). The integration test suite did
not cover these paths.

Changes
-------
artifacts/api-server/tests/service-orders.test.mjs
  + Added test "confirm-receipt returns 409 order_unavailable when the order
    is cancelled or completed":
      - Owner cancels a pending order → another receiver's confirm-receipt
        returns 409 with body.error === "order_unavailable" (not
        "already_claimed").
      - Receiver claims, prepares, completes an order → another receiver's
        confirm-receipt returns 409 "order_unavailable".

  + Added test "PATCH /orders/:id/status returns 409 order_unavailable when
    targeting a terminal order":
      - Completed order: assignee → preparing  → 409 "order_unavailable"
      - Completed order: assignee → completed  → 409 "order_unavailable"
      - Completed order: owner    → cancelled  → 409 "order_unavailable"
      - Cancelled order: owner    → cancelled  → 409 "order_unavailable"

Verification
------------
All 11 service-orders integration tests pass (including both new tests):
  pnpm --filter @workspace/api-server test → 11 passed, 0 failed.

No production code changes; tests only.
This commit is contained in:
Riyadh
2026-04-27 12:11:57 +00:00
parent 694239f7b8
commit 9fac768d2a
@@ -423,6 +423,128 @@ test("owner can restore (undo) a freshly cancelled order to its prior status; ex
assert.match(body.error, /undo_window_expired/); 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 () => { test("unauthenticated cannot place order", async () => {
const res = await fetch(`${API_BASE}/api/orders`, { const res = await fetch(`${API_BASE}/api/orders`, {
method: "POST", method: "POST",