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