fix(orders): review-save route review fixes (auth, 404, customer, fixture)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
3d8d39af2a
commit
cd33e425fc
2 changed files with 76 additions and 17 deletions
|
|
@ -3,11 +3,11 @@ import { NextRequest } from "next/server";
|
|||
|
||||
const updateOrderReviewFields = vi.fn();
|
||||
const updateOrderItemsPrices = vi.fn();
|
||||
const collisionError: Error & { collidingOrderId?: string; attemptedNumber?: string } =
|
||||
new Error("collision");
|
||||
collisionError.name = "OrderNumberCollisionError";
|
||||
collisionError.collidingOrderId = "order-collide";
|
||||
collisionError.attemptedNumber = "ORD-2026-07-014";
|
||||
const assertAgentAuth = vi.fn();
|
||||
|
||||
vi.mock("@/lib/server/agent-auth", () => ({
|
||||
assertAgentAuth: (...args: unknown[]) => assertAgentAuth(...args),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/server/orders-service", () => ({
|
||||
updateOrderReviewFields: (...args: unknown[]) =>
|
||||
|
|
@ -27,6 +27,7 @@ vi.mock("@/lib/server/orders-service", () => ({
|
|||
}));
|
||||
|
||||
import { POST } from "./route";
|
||||
import { OrderNumberCollisionError } from "@/lib/server/orders-service";
|
||||
|
||||
function postJSON(body: unknown) {
|
||||
return new NextRequest(
|
||||
|
|
@ -42,6 +43,29 @@ describe("POST /api/v1/orders/[id]/review-save", () => {
|
|||
beforeEach(() => {
|
||||
updateOrderReviewFields.mockReset();
|
||||
updateOrderItemsPrices.mockReset();
|
||||
assertAgentAuth.mockReset();
|
||||
});
|
||||
|
||||
it("calls assertAgentAuth before processing", async () => {
|
||||
updateOrderReviewFields.mockResolvedValueOnce({
|
||||
id: "order-1",
|
||||
order_number: "ORD-2026-07-001",
|
||||
customer: "cust-1",
|
||||
});
|
||||
await POST(postJSON({}), { params: Promise.resolve({ id: "order-1" }) });
|
||||
expect(assertAgentAuth).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("returns 401 when assertAgentAuth throws", async () => {
|
||||
const authError = Object.assign(new Error("Unauthorized"), { status: 401 });
|
||||
assertAgentAuth.mockImplementationOnce(() => {
|
||||
throw authError;
|
||||
});
|
||||
const res = await POST(postJSON({ order_number: "X" }), {
|
||||
params: Promise.resolve({ id: "order-1" }),
|
||||
});
|
||||
expect(res.status).toBe(401);
|
||||
expect(updateOrderReviewFields).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("400s when body is not JSON", async () => {
|
||||
|
|
@ -95,8 +119,29 @@ describe("POST /api/v1/orders/[id]/review-save", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("calls updateOrderReviewFields with customer when supplied", async () => {
|
||||
updateOrderReviewFields.mockResolvedValueOnce({
|
||||
id: "order-1",
|
||||
order_number: "ORD-2026-07-001",
|
||||
customer: "cust-X",
|
||||
});
|
||||
updateOrderItemsPrices.mockResolvedValueOnce({ updated: 0, failed: [] });
|
||||
|
||||
const res = await POST(
|
||||
postJSON({ customer: "cust-X" }),
|
||||
{ params: Promise.resolve({ id: "order-1" }) },
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(updateOrderReviewFields).toHaveBeenCalledWith("order-1", {
|
||||
customer: "cust-X",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns 409 on OrderNumberCollisionError with collidingOrderId", async () => {
|
||||
updateOrderReviewFields.mockRejectedValueOnce(collisionError);
|
||||
updateOrderReviewFields.mockRejectedValueOnce(
|
||||
new OrderNumberCollisionError("order-collide", "ORD-2026-07-014"),
|
||||
);
|
||||
|
||||
const res = await POST(
|
||||
postJSON({ order_number: "ORD-2026-07-014" }),
|
||||
|
|
@ -110,6 +155,15 @@ describe("POST /api/v1/orders/[id]/review-save", () => {
|
|||
expect(body.attemptedNumber).toBe("ORD-2026-07-014");
|
||||
});
|
||||
|
||||
it("returns 404 when PB throws ClientResponseError(404)", async () => {
|
||||
const err = Object.assign(new Error("not found"), { status: 404 });
|
||||
updateOrderReviewFields.mockRejectedValueOnce(err);
|
||||
const res = await POST(postJSON({}), {
|
||||
params: Promise.resolve({ id: "order-x" }),
|
||||
});
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
it("calls updateOrderItemsPrices when items are supplied", async () => {
|
||||
updateOrderReviewFields.mockResolvedValueOnce({
|
||||
id: "order-1",
|
||||
|
|
@ -151,4 +205,4 @@ describe("POST /api/v1/orders/[id]/review-save", () => {
|
|||
const body = await res.json();
|
||||
expect(body.error).toMatch(/boom/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { assertAgentAuth } from "@/lib/server/agent-auth";
|
||||
import {
|
||||
OrderNumberCollisionError,
|
||||
updateOrderItemsPrices,
|
||||
|
|
@ -30,6 +31,7 @@ import {
|
|||
* Responses:
|
||||
* 200 — saved. Body: { order: Order, items: { updated, failed } }
|
||||
* 400 — body is missing or not JSON
|
||||
* 401 — unauthorized (no/invalid bearer token)
|
||||
* 404 — order not found (PB throws; caught and mapped)
|
||||
* 409 — order_number collision. Body: { error, collidingOrderId, attemptedNumber }
|
||||
* 500 — anything else
|
||||
|
|
@ -42,9 +44,18 @@ export async function POST(
|
|||
|
||||
let body: unknown;
|
||||
try {
|
||||
assertAgentAuth(req);
|
||||
body = await req.json();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Body must be JSON" }, { status: 400 });
|
||||
} catch (err) {
|
||||
// Auth errors propagate with `status: 401` — surface that to the
|
||||
// client. Anything else from `req.json()` is a malformed body.
|
||||
const status =
|
||||
(err as Error & { status?: number }).status === 401 ? 401 : 400;
|
||||
const message =
|
||||
status === 401
|
||||
? (err instanceof Error ? err.message : "Unauthorized")
|
||||
: "Body must be JSON";
|
||||
return NextResponse.json({ error: message }, { status });
|
||||
}
|
||||
if (body === null || typeof body !== "object") {
|
||||
return NextResponse.json({ error: "Body must be an object" }, { status: 400 });
|
||||
|
|
@ -80,13 +91,7 @@ export async function POST(
|
|||
|
||||
return NextResponse.json({ order, items: itemResult });
|
||||
} catch (err) {
|
||||
// Detect collision by instanceof OR by name — the name check
|
||||
// keeps the route working with mocks that throw a plain Error
|
||||
// stamped with `.name = "OrderNumberCollisionError"`.
|
||||
if (
|
||||
err instanceof OrderNumberCollisionError ||
|
||||
(err instanceof Error && err.name === "OrderNumberCollisionError")
|
||||
) {
|
||||
if (err instanceof OrderNumberCollisionError) {
|
||||
const collidingOrderId =
|
||||
(err as Error & { collidingOrderId?: string }).collidingOrderId ?? "";
|
||||
const attemptedNumber =
|
||||
|
|
@ -105,4 +110,4 @@ export async function POST(
|
|||
(err as Error & { status?: number }).status === 404 ? 404 : 500;
|
||||
return NextResponse.json({ error: message }, { status });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue