CI (SIAX Cloud) / integration (push) Has been skipped
masterplan-lock / check (push) Successful in 4s
CI (SIAX Cloud) / security (push) Successful in 14s
masterplan-lock / check (pull_request) Successful in 4s
CI (SIAX Cloud) / security (pull_request) Successful in 14s
Security Scan (SIAX Cloud) / security (push) Successful in 30s
CI (SIAX Cloud) / quality (push) Successful in 35s
CI (SIAX Cloud) / integration (pull_request) Successful in 25s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 54s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m31s
56 lines
2.2 KiB
TypeScript
56 lines
2.2 KiB
TypeScript
import { beforeEach, expect, it, vi } from "vitest";
|
|
import { NextRequest, NextResponse } from "next/server";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
auth: vi.fn(),
|
|
getOrder: vi.fn(),
|
|
setStatusIfCurrent: vi.fn(),
|
|
}));
|
|
vi.mock("@/src/auth/require-order-owner", () => ({ requireOrderOwner: mocks.auth }));
|
|
vi.mock("@/lib/orders-store", () => ({
|
|
getOwnedOrder: mocks.getOrder,
|
|
setOwnedStatusIfCurrent: mocks.setStatusIfCurrent,
|
|
}));
|
|
|
|
import { PATCH } from "@/app/api/orders/[id]/route";
|
|
|
|
beforeEach(() => {
|
|
mocks.auth.mockReset().mockResolvedValue({ owner: { tenantId: "verified-tenant", subject: "buyer-1" } });
|
|
mocks.getOrder.mockReset().mockResolvedValue({ order_id: "order-1", status: "draft" });
|
|
mocks.setStatusIfCurrent.mockReset().mockResolvedValue({ order_id: "order-1", status: "accepted" });
|
|
});
|
|
|
|
function request(status: string) {
|
|
return new NextRequest("https://0pt.app/api/orders/order-1", {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ status }),
|
|
});
|
|
}
|
|
|
|
const ctx = { params: Promise.resolve({ id: "order-1" }) };
|
|
|
|
it("requires verified auth before changing an order", async () => {
|
|
mocks.auth.mockResolvedValue({ response: NextResponse.json({ error: "unauthenticated" }, { status: 401 }) });
|
|
expect((await PATCH(request("accepted"), ctx)).status).toBe(401);
|
|
expect(mocks.getOrder).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("rejects client writes to payment-confirmed states", async () => {
|
|
expect((await PATCH(request("in_production"), ctx)).status).toBe(403);
|
|
expect((await PATCH(request("delivered"), ctx)).status).toBe(403);
|
|
expect(mocks.setStatusIfCurrent).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("cannot jump from draft directly to accepted", async () => {
|
|
expect((await PATCH(request("accepted"), ctx)).status).toBe(409);
|
|
expect(mocks.setStatusIfCurrent).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("uses an atomic compare-and-set transition after review", async () => {
|
|
mocks.getOrder.mockResolvedValue({ order_id: "order-1", status: "review" });
|
|
expect((await PATCH(request("accepted"), ctx)).status).toBe(200);
|
|
expect(mocks.getOrder).toHaveBeenCalledWith("order-1", "verified-tenant");
|
|
expect(mocks.setStatusIfCurrent).toHaveBeenCalledWith("order-1", "verified-tenant", "review", "accepted");
|
|
});
|