test(core-consent): add missing test coverage and fix diff allowlist

Add symbols.test.ts to cover CONSENT_SYMBOLS constant.
Add recording-consent.test.ts to core-testing (follows the pattern
of all other recording doubles which each have a sibling test file).
Refactor payload-consent.test.ts to extract a makeConsent() helper,
reducing clone groups from 7 to 2.
Add new test to cover grantedAt=null branch in deserializeEntry (withdraw
before any grant → persisted as null → loaded as undefined).
Extend coverage/diff.mjs allowlist for packages/core-testing/ since that
tooling package does not install @vitest/coverage-v8 and therefore produces
no lcov data.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-19 12:48:50 +00:00
parent 7dd46b68b2
commit 500a163e90
6 changed files with 205 additions and 128 deletions

View File

@@ -0,0 +1,14 @@
import { describe, it, expect } from "vitest";
import { CONSENT_SYMBOLS } from "@/di/symbols";
describe("CONSENT_SYMBOLS", () => {
it("IConsentFactory is a unique Symbol", () => {
expect(typeof CONSENT_SYMBOLS.IConsentFactory).toBe("symbol");
});
it("IConsentFactory uses Symbol.for (global registry)", () => {
expect(CONSENT_SYMBOLS.IConsentFactory).toBe(
Symbol.for("core-consent:IConsentFactory"),
);
});
});

View File

@@ -19,34 +19,32 @@ function makePayloadMock(initialConsentState: unknown[] = []) {
return { getPayload, findByID, update, db };
}
async function makeConsent(opts?: {
initial?: unknown[];
auditLog?: RecordingAuditLog;
}) {
const mock = makePayloadMock(opts?.initial);
const auditLog = opts?.auditLog ?? new RecordingAuditLog();
const consent = new PayloadConsent(
"user_1",
{} as never,
auditLog,
mock.getPayload,
);
await consent.load();
return { consent, auditLog, ...mock };
}
describe("PayloadConsent.isGranted", () => {
it("returns false for an unknown category before any grant", async () => {
const { getPayload } = makePayloadMock();
const auditLog = new RecordingAuditLog();
const consent = new PayloadConsent(
"user_1",
{} as never,
auditLog,
getPayload,
);
await consent.load();
const { consent } = await makeConsent();
expect(consent.isGranted("analytics")).toBe(false);
});
it("returns true after grant and false after withdraw", async () => {
const { getPayload } = makePayloadMock();
const auditLog = new RecordingAuditLog();
const consent = new PayloadConsent(
"user_1",
{} as never,
auditLog,
getPayload,
);
await consent.load();
const { consent } = await makeConsent();
await consent.grant("analytics");
expect(consent.isGranted("analytics")).toBe(true);
await consent.withdraw("analytics");
expect(consent.isGranted("analytics")).toBe(false);
});
@@ -54,16 +52,7 @@ describe("PayloadConsent.isGranted", () => {
describe("PayloadConsent.grant", () => {
it("writes state to Payload via update", async () => {
const { getPayload, update } = makePayloadMock();
const auditLog = new RecordingAuditLog();
const consent = new PayloadConsent(
"user_1",
{} as never,
auditLog,
getPayload,
);
await consent.load();
const { consent, update } = await makeConsent();
await consent.grant("analytics");
expect(update).toHaveBeenCalledOnce();
@@ -84,16 +73,7 @@ describe("PayloadConsent.grant", () => {
});
it("emits a CONSENT_GRANT audit entry with correct shape", async () => {
const { getPayload } = makePayloadMock();
const auditLog = new RecordingAuditLog();
const consent = new PayloadConsent(
"user_1",
{} as never,
auditLog,
getPayload,
);
await consent.load();
const { consent, auditLog } = await makeConsent();
await consent.grant("functional");
expect(auditLog.recorded).toHaveLength(1);
@@ -107,16 +87,7 @@ describe("PayloadConsent.grant", () => {
});
it("preserves bannerVersion, policyVersion, method in state round-trip", async () => {
const mock = makePayloadMock();
const auditLog = new RecordingAuditLog();
const consent1 = new PayloadConsent(
"user_1",
{} as never,
auditLog,
mock.getPayload,
);
await consent1.load();
const { consent: consent1, getPayload } = await makeConsent();
await consent1.grant("marketing", {
bannerVersion: "v2",
policyVersion: "2026-01",
@@ -128,7 +99,7 @@ describe("PayloadConsent.grant", () => {
"user_1",
{} as never,
new RecordingAuditLog(),
mock.getPayload,
getPayload,
);
await consent2.load();
@@ -146,16 +117,7 @@ describe("PayloadConsent.grant", () => {
describe("PayloadConsent.withdraw", () => {
it("sets state to denied and persists to Payload", async () => {
const { getPayload, update } = makePayloadMock();
const auditLog = new RecordingAuditLog();
const consent = new PayloadConsent(
"user_1",
{} as never,
auditLog,
getPayload,
);
await consent.load();
const { consent, update } = await makeConsent();
await consent.grant("analytics");
update.mockClear();
await consent.withdraw("analytics");
@@ -175,16 +137,7 @@ describe("PayloadConsent.withdraw", () => {
});
it("emits a CONSENT_WITHDRAW audit entry", async () => {
const { getPayload } = makePayloadMock();
const auditLog = new RecordingAuditLog();
const consent = new PayloadConsent(
"user_1",
{} as never,
auditLog,
getPayload,
);
await consent.load();
const { consent, auditLog } = await makeConsent();
await consent.grant("analytics");
auditLog.reset();
await consent.withdraw("analytics");
@@ -197,16 +150,7 @@ describe("PayloadConsent.withdraw", () => {
describe("PayloadConsent.getCategories", () => {
it("returns all categories after multiple grants", async () => {
const { getPayload } = makePayloadMock();
const auditLog = new RecordingAuditLog();
const consent = new PayloadConsent(
"user_1",
{} as never,
auditLog,
getPayload,
);
await consent.load();
const { consent } = await makeConsent();
await consent.grant("necessary");
await consent.grant("analytics");
await consent.grant("marketing");
@@ -218,16 +162,7 @@ describe("PayloadConsent.getCategories", () => {
});
it("reflects withdrawn state in category list", async () => {
const { getPayload } = makePayloadMock();
const auditLog = new RecordingAuditLog();
const consent = new PayloadConsent(
"user_1",
{} as never,
auditLog,
getPayload,
);
await consent.load();
const { consent } = await makeConsent();
await consent.grant("analytics");
await consent.withdraw("analytics");
@@ -250,15 +185,7 @@ describe("PayloadConsent.load", () => {
method: "banner-accept",
},
];
const { getPayload } = makePayloadMock(existingState);
const auditLog = new RecordingAuditLog();
const consent = new PayloadConsent(
"user_1",
{} as never,
auditLog,
getPayload,
);
await consent.load();
const { consent } = await makeConsent({ initial: existingState });
expect(consent.isGranted("functional")).toBe(true);
const cats = consent.getCategories();
@@ -306,18 +233,29 @@ describe("PayloadConsent.load — deserializeEntry branches", () => {
withdrawnAt: new Date("2026-01-15T00:00:00.000Z").toISOString(),
},
];
const { getPayload } = makePayloadMock(existingState);
const consent = new PayloadConsent(
"user_1",
{} as never,
new RecordingAuditLog(),
getPayload,
);
await consent.load();
const { consent } = await makeConsent({ initial: existingState });
expect(consent.isGranted("analytics")).toBe(false);
const cats = consent.getCategories();
expect(cats[0]!.withdrawnAt).toBeInstanceOf(Date);
expect(cats[0]!.grantedAt).toBeInstanceOf(Date);
});
it("handles an entry with null grantedAt (withdraw before any grant)", async () => {
const { consent: consent1, getPayload } = await makeConsent();
// Withdraw without a prior grant — grantedAt stays undefined → persisted as null
await consent1.withdraw("analytics");
const consent2 = new PayloadConsent(
"user_1",
{} as never,
new RecordingAuditLog(),
getPayload,
);
await consent2.load();
const cats = consent2.getCategories();
expect(cats[0]!.state).toBe("denied");
expect(cats[0]!.grantedAt).toBeUndefined();
expect(cats[0]!.withdrawnAt).toBeInstanceOf(Date);
});
});

View File

@@ -0,0 +1,105 @@
import { describe, it, expect } from "vitest";
import { RecordingConsent } from "./recording-consent";
describe("RecordingConsent.isGranted", () => {
it("returns false for unknown category before any grant", () => {
const consent = new RecordingConsent();
expect(consent.isGranted("analytics")).toBe(false);
});
it("returns true after grant and false after withdraw", async () => {
const consent = new RecordingConsent();
await consent.grant("analytics");
expect(consent.isGranted("analytics")).toBe(true);
await consent.withdraw("analytics");
expect(consent.isGranted("analytics")).toBe(false);
});
});
describe("RecordingConsent.grant", () => {
it("records the grant with category and meta", async () => {
const consent = new RecordingConsent();
await consent.grant("marketing", {
bannerVersion: "v2",
policyVersion: "2026-01",
method: "banner-accept",
});
expect(consent.grants).toHaveLength(1);
expect(consent.grants[0]!.category).toBe("marketing");
expect(consent.grants[0]!.meta?.bannerVersion).toBe("v2");
expect(consent.grants[0]!.meta?.policyVersion).toBe("2026-01");
expect(consent.grants[0]!.meta?.method).toBe("banner-accept");
});
it("records grant without meta when none provided", async () => {
const consent = new RecordingConsent();
await consent.grant("necessary");
expect(consent.grants[0]!.meta).toBeUndefined();
});
it("preserves withdrawnAt on regrant", async () => {
const consent = new RecordingConsent();
await consent.grant("analytics");
await consent.withdraw("analytics");
const before = consent
.getCategories()
.find((c) => c.category === "analytics");
const withdrawnAt = before?.withdrawnAt;
await consent.grant("analytics");
const after = consent
.getCategories()
.find((c) => c.category === "analytics");
expect(after?.withdrawnAt).toEqual(withdrawnAt);
});
});
describe("RecordingConsent.withdraw", () => {
it("records the withdrawal", async () => {
const consent = new RecordingConsent();
await consent.grant("analytics");
await consent.withdraw("analytics");
expect(consent.withdrawals).toContain("analytics");
});
it("sets state to denied and records withdrawnAt", async () => {
const consent = new RecordingConsent();
await consent.grant("functional");
await consent.withdraw("functional");
const cats = consent.getCategories();
const entry = cats.find((c) => c.category === "functional");
expect(entry?.state).toBe("denied");
expect(entry?.withdrawnAt).toBeInstanceOf(Date);
});
});
describe("RecordingConsent.getCategories", () => {
it("returns all categories after grants", async () => {
const consent = new RecordingConsent();
await consent.grant("necessary");
await consent.grant("analytics");
const cats = consent.getCategories();
expect(cats).toHaveLength(2);
});
it("reflects withdrawn state", async () => {
const consent = new RecordingConsent();
await consent.grant("marketing");
await consent.withdraw("marketing");
const cats = consent.getCategories();
expect(cats[0]!.state).toBe("denied");
});
});
describe("RecordingConsent.reset", () => {
it("clears grants, withdrawals, and state", async () => {
const consent = new RecordingConsent();
await consent.grant("analytics");
await consent.withdraw("analytics");
consent.reset();
expect(consent.grants).toHaveLength(0);
expect(consent.withdrawals).toHaveLength(0);
expect(consent.getCategories()).toHaveLength(0);
expect(consent.isGranted("analytics")).toBe(false);
});
});