Files
0pt/tests/p0/source-registry.test.ts
T

186 lines
6.7 KiB
TypeScript

/**
* P0 Source Registry (Master-DoD §10): register/query/describe/health +
* rights/license/purpose metadata, freshness, provenance — including the §50
* cross-tenant attack case and the fail-closed purpose gate.
*/
import { describe, it, expect } from "vitest";
import { SourceRegistryService } from "@/src/services/source-registry-service";
import { SiaxError } from "@/src/platform/errors";
function svc() {
return new SourceRegistryService();
}
async function registerBase(registry: SourceRegistryService, overrides: Record<string, unknown> = {}) {
return registry.registerSource({
source_id: "src_bolagsverket",
tenant_id: "t_a",
project_id: "p_1",
source_type: "api",
name: "Bolagsverket API",
access: "api",
created_by: "actor_1",
rights: {
license_id: "official-public-data",
allowed_purposes: ["company_enrichment", "compliance"],
pii_allowed: false,
redistribution: "customer_deliverable",
verified_at: "2026-08-31",
verified_by: "actor_1",
},
provenance: {
method: "official_api",
authority: "Bolagsverket",
cadence: "daily",
freshness_sla_hours: 48,
},
...overrides,
});
}
describe("SourceRegistryService — register + describe", () => {
it("source.register stores base + rights + provenance and source.describe returns the full descriptor", async () => {
const registry = svc();
const descriptor = await registerBase(registry);
expect(descriptor.source.source_id).toBe("src_bolagsverket");
expect(descriptor.rights?.license_id).toBe("official-public-data");
expect(descriptor.rights?.allowed_purposes).toContain("company_enrichment");
expect(descriptor.provenance?.method).toBe("official_api");
expect(descriptor.provenance?.authority).toBe("Bolagsverket");
expect(descriptor.health.status).toBe("unknown");
expect(descriptor.freshness.fresh).toBe(false); // SLA declared, no success recorded yet
});
it("describe on a foreign tenant's source → NOT_FOUND (no existence oracle, §50)", async () => {
const registry = svc();
await registerBase(registry);
try {
await registry.describeSource("src_bolagsverket", "t_b");
expect.unreachable("must throw");
} catch (err) {
expect(err).toBeInstanceOf(SiaxError);
expect((err as SiaxError).code).toBe("NOT_FOUND");
}
});
});
describe("SourceRegistryService — purpose gate (fail closed)", () => {
it("allows a purpose on the allowlist", async () => {
const registry = svc();
await registerBase(registry);
const rights = await registry.assertPurposeAllowed("src_bolagsverket", "t_a", "company_enrichment");
expect(rights.license_id).toBe("official-public-data");
});
it("denies a purpose outside the allowlist", async () => {
const registry = svc();
await registerBase(registry);
await expect(registry.assertPurposeAllowed("src_bolagsverket", "t_a", "marketing")).rejects.toMatchObject(
{
code: "AUTHORIZATION_DENIED",
},
);
});
it("denies EVERY purpose when no rights record exists (fail closed)", async () => {
const registry = svc();
await registry.registerSource({
source_id: "src_norights",
tenant_id: "t_a",
project_id: "p_1",
source_type: "web",
name: "No rights yet",
});
await expect(registry.assertPurposeAllowed("src_norights", "t_a", "anything")).rejects.toMatchObject({
code: "AUTHORIZATION_DENIED",
});
});
});
describe("SourceRegistryService — health + freshness", () => {
it("records health checks, tracks consecutive failures, and an 'up' check drives freshness", async () => {
const registry = svc();
await registerBase(registry);
let health = await registry.recordHealthCheck({
source_id: "src_bolagsverket",
tenant_id: "t_a",
status: "down",
checked_at: new Date().toISOString(),
});
expect(health.status).toBe("down");
expect(health.consecutive_failures).toBe(1);
health = await registry.recordHealthCheck({
source_id: "src_bolagsverket",
tenant_id: "t_a",
status: "up",
checked_at: new Date().toISOString(),
latency_ms: 120,
});
expect(health.status).toBe("up");
expect(health.consecutive_failures).toBe(0);
const descriptor = await registry.describeSource("src_bolagsverket", "t_a");
expect(descriptor.freshness.fresh).toBe(true);
expect(descriptor.freshness.age_hours).toBeLessThan(1);
});
it("freshness goes stale when the last success exceeds the SLA", async () => {
const registry = svc();
await registerBase(registry);
const old = new Date(Date.now() - 72 * 3_600_000).toISOString(); // 72h > 48h SLA
await registry.recordSuccess("src_bolagsverket", "t_a", old);
const freshness = await registry.getFreshness("src_bolagsverket", "t_a");
expect(freshness.fresh).toBe(false);
expect(freshness.age_hours).toBeGreaterThan(48);
});
});
describe("SourceRegistryService — source.query with purpose/rights filtering", () => {
it("filters by purpose and redistribution; sources without rights never match a purpose filter", async () => {
const registry = svc();
await registerBase(registry);
await registry.registerSource({
source_id: "src_internal_only",
tenant_id: "t_a",
project_id: "p_1",
source_type: "file",
name: "Internal drop",
rights: {
license_id: "internal",
allowed_purposes: ["compliance"],
pii_allowed: false,
redistribution: "internal",
verified_at: "2026-08-31",
verified_by: "actor_1",
},
});
await registry.registerSource({
source_id: "src_unassessed",
tenant_id: "t_a",
project_id: "p_1",
source_type: "web",
name: "Unassessed",
});
const enrichment = await registry.querySources({ tenant_id: "t_a", purpose: "company_enrichment" });
expect(enrichment.map((s) => s.source_id)).toEqual(["src_bolagsverket"]);
const compliance = await registry.querySources({ tenant_id: "t_a", purpose: "compliance" });
expect(compliance.map((s) => s.source_id).sort()).toEqual(["src_bolagsverket", "src_internal_only"]);
const internalOnly = await registry.querySources({ tenant_id: "t_a", redistribution: "internal" });
expect(internalOnly.map((s) => s.source_id)).toEqual(["src_internal_only"]);
// Tenant isolation on query.
const foreign = await registry.querySources({ tenant_id: "t_b", purpose: "compliance" });
expect(foreign).toEqual([]);
});
it("rejects a query without tenant_id", async () => {
const registry = svc();
await expect(registry.querySources({ tenant_id: "" })).rejects.toMatchObject({ code: "INVALID_REQUEST" });
});
});