Files
0pt/tests/api/import-dispatch-policy.test.ts
T

106 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* DES1GN ADR D3 (§31): ImportDispatchPolicy — ren policy-funktion, testad per
* kombination (format × bytes × rows, gränsvärden inkluderade), plus route-
* kopplingen: rows > policy.maxRows -> 413 n0d_dispatch_required med hint
* (descriptor-gate stängd), inline-taket oförändrat.
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { NextRequest } from "next/server";
import { IMPORT_DISPATCH_POLICY, evaluateImportDispatch } from "@/lib/import-dispatch-policy";
describe("evaluateImportDispatch — verdict per kombination", () => {
it("gränsvärde maxInlineRows: exakt 500 rader -> inline", () => {
expect(evaluateImportDispatch({ format: "csv", bytes: 100, rows: IMPORT_DISPATCH_POLICY.maxInlineRows })).toEqual({ verdict: "inline" });
});
it("maxInlineRows < rows <= maxRows -> row_cap_exceeded (inline-taket, oförändrat)", () => {
expect(evaluateImportDispatch({ format: "csv", bytes: 100, rows: IMPORT_DISPATCH_POLICY.maxInlineRows + 1 })).toEqual({
verdict: "row_cap_exceeded",
rows: 501,
maxInlineRows: 500,
});
expect(evaluateImportDispatch({ format: "csv", bytes: 100, rows: IMPORT_DISPATCH_POLICY.maxRows }).verdict).toBe("row_cap_exceeded");
});
it("rows > maxRows -> dispatch_n0d (D3-signalen)", () => {
expect(evaluateImportDispatch({ format: "csv", bytes: 100, rows: IMPORT_DISPATCH_POLICY.maxRows + 1 })).toEqual({
verdict: "dispatch_n0d",
rows: 5001,
maxRows: 5000,
});
});
it("bytes > maxBytes -> byte_cap_exceeded, oavsett rows", () => {
expect(evaluateImportDispatch({ format: "csv", bytes: IMPORT_DISPATCH_POLICY.maxBytes + 1, rows: 1 }).verdict).toBe("byte_cap_exceeded");
expect(evaluateImportDispatch({ format: "csv", bytes: IMPORT_DISPATCH_POLICY.maxBytes, rows: 1 }).verdict).toBe("inline");
});
it("format utanför allowlist -> unsupported_format", () => {
expect(evaluateImportDispatch({ format: "xlsx", bytes: 10, rows: 1 })).toEqual({ verdict: "unsupported_format", format: "xlsx" });
});
it("precedens: format slår bytes, bytes slår rows", () => {
expect(evaluateImportDispatch({ format: "json", bytes: 9e9, rows: 9e9 }).verdict).toBe("unsupported_format");
expect(evaluateImportDispatch({ format: "csv", bytes: 9e9, rows: 9e9 }).verdict).toBe("byte_cap_exceeded");
});
});
describe("route-koppling: POST /api/orders/import respekterar policyn", () => {
const { requireOrderOwnerMock, createOrderMock, ingestEventMock, mockOwner } = vi.hoisted(() => ({
requireOrderOwnerMock: vi.fn(),
createOrderMock: vi.fn(),
ingestEventMock: vi.fn().mockResolvedValue({}),
mockOwner: { owner: { tenantId: "tenant-test", subject: "buyer-test" } },
}));
const _unused = mockOwner;
vi.mock("@/src/auth/require-order-owner", () => ({
requireOrderOwner: (...args: unknown[]) => requireOrderOwnerMock(...args),
}));
vi.mock("@/lib/orders-store", () => ({ createOrder: (...args: unknown[]) => createOrderMock(...args) }));
vi.mock("@/src/clients/aud0-client", () => ({ createAud0Client: () => ({ ingestEvent: ingestEventMock }) }));
let POST: typeof import("@/app/api/orders/import/route").POST;
let MAX_IMPORT_ROWS: number;
beforeEach(async () => {
vi.clearAllMocks();
requireOrderOwnerMock.mockResolvedValue(mockOwner);
({ POST, MAX_IMPORT_ROWS } = await import("@/app/api/orders/import/route"));
});
function postReq(csv: string): NextRequest {
return new NextRequest(new URL("/api/orders/import", "http://localhost"), {
method: "POST",
body: JSON.stringify({ mode: "validate", csv }),
headers: { "content-type": "application/json" },
});
}
const line = "custom,icp,Bolag,10,Engångs,SE";
it("exakt maxInlineRows rader -> 200 (inline-beteende oförändrat)", async () => {
const csv = ["flow,service_type,type,volume,frequency,markets", ...Array.from({ length: MAX_IMPORT_ROWS }, () => line)].join("\n");
const res = await POST(postReq(csv));
expect(res.status).toBe(200);
});
it("maxRows < rows -> 413 n0d_dispatch_required med UNVERIFIED-hint, ingen skrivning", async () => {
const csv = ["flow,service_type,type,volume,frequency,markets", ...Array.from({ length: IMPORT_DISPATCH_POLICY.maxRows + 1 }, () => line)].join("\n");
const res = await POST(postReq(csv));
expect(res.status).toBe(413);
const body = (await res.json()) as { error: { code: string; hint: { capability_id: string; job_type: string; status: string } } };
expect(body.error.code).toBe("n0d_dispatch_required");
expect(body.error.hint).toEqual({ capability_id: "0pt.import_execute", job_type: "0pt.import_execute_job", status: "UNVERIFIED" });
expect(createOrderMock).not.toHaveBeenCalled();
expect(ingestEventMock).not.toHaveBeenCalled();
});
it("CSV > maxBytes -> 413 csv_too_large", async () => {
const csv = "flow,service_type,type,volume,frequency,markets\n" + "x".repeat(IMPORT_DISPATCH_POLICY.maxBytes);
const res = await POST(postReq(csv));
expect(res.status).toBe(413);
const body = (await res.json()) as { error: { code: string } };
expect(body.error.code).toBe("csv_too_large");
});
});