Files
0pt/tests/api/order-status-transitions.test.ts
T
admin a1557b2a7d
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
feat(payments): bind orders to verified tenant and buyer
2026-09-15 19:53:58 +02:00

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");
});