Files
0pt/tests/api/import-dispatch-policy.test.ts
admin 1c87b82e70
CI (SIAX Cloud) / integration (push) Skipped
CI (SIAX Cloud) / sonar (push) Skipped
masterplan-lock / check (push) Successful in 6s
masterplan-lock / check (pull_request) Successful in 7s
CI (SIAX Cloud) / security (push) Failing after 27s
Security Scan (SIAX Cloud) / security (push) Failing after 1m27s
CI (SIAX Cloud) / quality (push) Successful in 1m28s
CI (SIAX Cloud) / quality (pull_request) Successful in 2m29s
CI (SIAX Cloud) / integration (pull_request) Successful in 1m15s
CI (SIAX Cloud) / security (pull_request) Failing after 16s
CI (SIAX Cloud) / sonar (pull_request) Skipped
Security Scan (SIAX Cloud) / security (pull_request) Failing after 17s
feat(import): ImportDispatchPolicy (DES1GN ADR D3 §31) — ett objekt, en sanning
- lib/import-dispatch-policy.ts: maxInlineRows=500 (inline-tak, oförändrat
  kontrakt), maxRows=5000 (N0D-dispatch-signal), maxBytes=1_000_000 (bytes,
  UTF-8), format-allowlist ['csv']; ren evaluateImportDispatch() med
  deterministisk precedens format > bytes > rows
- route: taken härstammar från policyn; bytes-koll (ersätter chars); rows >
  maxRows -> 413 n0d_dispatch_required med hint {capability_id:
  0pt.import_execute, job_type: 0pt.import_execute_job, status: UNVERIFIED}
  (descriptor-gate stängd: aldrig tyst trunkering, aldrig tyst dispatch)
- tester: 9 nya (verdict per kombination inkl. gränsvärden 500/501/5000/5001,
  bytes-gräns, allowlist, precedens; route-koppling 200/413/413) — 21/21 gröna
  i sviten; full N0D-worker-integration markeras UNVERIFIED
2026-09-30 21:48:16 +02:00

106 lines
5.1 KiB
TypeScript
Raw Permalink 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");
});
});