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