Files
0pt/tests/p0/entity-resolution-http.test.ts

198 lines
6.1 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { NextRequest } from "next/server";
import { POST as candidatesPOST } from "@/app/api/0pt/internal/er/candidates/route";
const TOKEN = "test-er-internal-token";
let previousToken: string | undefined;
function request(body: unknown, token: string | undefined = TOKEN): NextRequest {
const headers: Record<string, string> = { "content-type": "application/json" };
if (token !== undefined) headers["x-internal-token"] = token;
return new NextRequest("http://localhost/api/0pt/internal/er/candidates", {
method: "POST",
headers,
body: JSON.stringify(body),
});
}
function baseBody(records: Array<{ record_id: string; fields: Record<string, unknown> }>) {
return {
tenant_id: "tenant_a",
project_id: "project_a",
records,
blocking_fields: ["city"],
compare_fields: [{ field: "name", weight: 1 }],
auto_match_threshold: 0.9,
review_threshold: 0.6,
};
}
beforeEach(() => {
previousToken = process.env.INTERNAL_TOKEN;
process.env.INTERNAL_TOKEN = TOKEN;
});
afterEach(() => {
if (previousToken === undefined) delete process.env.INTERNAL_TOKEN;
else process.env.INTERNAL_TOKEN = previousToken;
});
describe("POST /api/0pt/internal/er/candidates", () => {
it("fails closed without the internal service token", async () => {
// OBS: request(body, undefined) triggar default-parametern (skickar GILTIGT
// token) — skicka tom string för "utan token" (fail-closed → 401).
const res = await candidatesPOST(
request(
baseBody([
{ record_id: "a", fields: { name: "Acme AB", city: "Stockholm" } },
{ record_id: "b", fields: { name: "Acme AB", city: "Stockholm" } },
]),
"",
),
);
expect(res.status).toBe(401);
});
it("never emits a score-only fuzzy company match even above auto_match_threshold", async () => {
const res = await candidatesPOST(
request(
baseBody([
{ record_id: "a", fields: { name: "Acme Consulting AB", city: "Stockholm" } },
{ record_id: "b", fields: { name: "Acme Consulting A B", city: "Stockholm" } },
]),
),
);
expect(res.status).toBe(200);
const body = await res.json();
expect(body.pairs).toHaveLength(1);
expect(body.pairs[0].score).toBeGreaterThanOrEqual(0.9);
expect(body.pairs[0].decided_by).toBe("score");
expect(body.pairs[0].status).toBe("pending_review");
expect(body.review_queue.map((pair: { candidate_id: string }) => pair.candidate_id)).toEqual([
body.pairs[0].candidate_id,
]);
});
it("allows a same-country registration number to hard-link despite punctuation differences", async () => {
const res = await candidatesPOST(
request(
baseBody([
{
record_id: "a",
fields: {
name: "Acme Sweden AB",
city: "Stockholm",
registration_country: "SE",
org_nr: "556000-0001",
},
},
{
record_id: "b",
fields: {
name: "Acme Sverige",
city: "Stockholm",
registration_country: "se",
org_nr: "5560000001",
},
},
]),
),
);
expect(res.status).toBe(200);
const body = await res.json();
expect(body.pairs).toHaveLength(1);
expect(body.pairs[0].status).toBe("match");
expect(body.pairs[0].decided_by).toBe("0pt.company.stable-identifiers.v3");
expect(body.review_queue).toEqual([]);
});
it("does not treat the same national registration number in different countries as global identity", async () => {
const res = await candidatesPOST(
request(
baseBody([
{
record_id: "a",
fields: {
name: "Acme Consulting AB",
city: "Stockholm",
registration_country: "SE",
org_nr: "123456789",
},
},
{
record_id: "b",
fields: {
name: "Acme Consulting A B",
city: "Stockholm",
registration_country: "NO",
org_nr: "123456789",
},
},
]),
),
);
expect(res.status).toBe(200);
const body = await res.json();
expect(body.pairs).toHaveLength(1);
expect(body.pairs[0].decided_by).toBe("score");
expect(body.pairs[0].status).toBe("pending_review");
});
it("lets conflicting stable identity evidence dominate a matching global LEI", async () => {
const res = await candidatesPOST(
request(
baseBody([
{
record_id: "a",
fields: {
name: "Acme Sweden AB",
city: "Stockholm",
registration_country: "SE",
lei: "549300EXAMPLE123",
org_nr: "556000-0001",
},
},
{
record_id: "b",
fields: {
name: "Acme Sweden AB",
city: "Stockholm",
registration_country: "SE",
lei: "549300EXAMPLE123",
org_nr: "556000-9999",
},
},
]),
),
);
expect(res.status).toBe(200);
const body = await res.json();
expect(body.pairs).toHaveLength(1);
expect(body.pairs[0].status).toBe("non_match");
expect(body.pairs[0].decided_by).toBe("0pt.company.stable-identifiers.v3");
});
it("can hard-link a globally matching LEI when no scoped identifier contradicts it", async () => {
const res = await candidatesPOST(
request(
baseBody([
{
record_id: "a",
fields: { name: "Acme Holding", city: "Stockholm", lei: "549300-EXAMPLE-123" },
},
{
record_id: "b",
fields: { name: "ACME Holdings Group", city: "Stockholm", lei: "549300EXAMPLE123" },
},
]),
),
);
expect(res.status).toBe(200);
const body = await res.json();
expect(body.pairs).toHaveLength(1);
expect(body.pairs[0].status).toBe("match");
expect(body.pairs[0].decided_by).toBe("0pt.company.stable-identifiers.v3");
});
});