526 lines
13 KiB
JavaScript
526 lines
13 KiB
JavaScript
import assert from "node:assert/strict";
|
|
|
|
import { createConsentEngine } from "../../core/consent.js";
|
|
import {
|
|
isStoragePayload
|
|
} from "../../core/storage.js";
|
|
import {
|
|
isValidCategoriesObject
|
|
} from "../../core/categories.js";
|
|
|
|
let jar = new Map();
|
|
let blockWrites = false;
|
|
|
|
globalThis.location = { protocol: "http:" };
|
|
globalThis.document = {};
|
|
|
|
Object.defineProperty(globalThis.document, "cookie", {
|
|
configurable: true,
|
|
|
|
get() {
|
|
return [...jar.entries()]
|
|
.map(([key, value]) => `${key}=${value}`)
|
|
.join("; ");
|
|
},
|
|
|
|
set(value) {
|
|
if (blockWrites) {
|
|
return;
|
|
}
|
|
|
|
const parts = value.split(";").map((item) => item.trim());
|
|
const [name, raw = ""] = parts[0].split("=");
|
|
|
|
const maxAge = parts.find((item) => /^Max-Age=/i.test(item));
|
|
|
|
if (maxAge && maxAge.toLowerCase() === "max-age=0") {
|
|
jar.delete(name);
|
|
return;
|
|
}
|
|
|
|
jar.set(name, raw);
|
|
}
|
|
});
|
|
|
|
function clearEnvironment() {
|
|
jar.clear();
|
|
blockWrites = false;
|
|
}
|
|
|
|
function make(options = {}) {
|
|
return createConsentEngine({
|
|
policyVersion: "2026-01",
|
|
jurisdiction: "global",
|
|
storage: {
|
|
name: "cc_reliability",
|
|
duration: 180,
|
|
path: "/",
|
|
sameSite: "Lax"
|
|
},
|
|
...options
|
|
});
|
|
}
|
|
|
|
let passed = 0;
|
|
|
|
function test(name, fn) {
|
|
clearEnvironment();
|
|
|
|
try {
|
|
fn();
|
|
passed += 1;
|
|
console.log(`PASS ${name}`);
|
|
} catch (error) {
|
|
console.error(`FAIL ${name}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Input contract */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
test("engine accepts omitted configuration and applies defaults", () => {
|
|
const consent = createConsentEngine();
|
|
|
|
consent.initialize();
|
|
|
|
const state = consent.getState();
|
|
|
|
assert.equal(state.hasDecision, false);
|
|
assert.equal(state.categories.necessary, "granted");
|
|
assert.equal(state.categories.preferences, "unknown");
|
|
assert.equal(state.categories.analytics, "unknown");
|
|
assert.equal(state.categories.marketing, "unknown");
|
|
});
|
|
|
|
test("rejects invalid consent category value", () => {
|
|
const consent = make();
|
|
|
|
consent.initialize();
|
|
|
|
assert.throws(
|
|
() => consent.setConsent({
|
|
analytics: "invalid"
|
|
}),
|
|
TypeError
|
|
);
|
|
});
|
|
|
|
test("rejects denial of necessary category", () => {
|
|
const consent = make();
|
|
|
|
consent.initialize();
|
|
|
|
assert.throws(
|
|
() => consent.setConsent({
|
|
necessary: "denied"
|
|
}),
|
|
TypeError
|
|
);
|
|
});
|
|
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* State isolation */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
test("separate engines do not share state", () => {
|
|
const first = make();
|
|
const second = make();
|
|
|
|
first.initialize();
|
|
second.initialize();
|
|
|
|
first.acceptAll();
|
|
|
|
assert.equal(first.getState().hasDecision, true);
|
|
assert.equal(second.getState().hasDecision, false);
|
|
});
|
|
|
|
test("returned state mutation cannot alter internal categories", () => {
|
|
const consent = make();
|
|
|
|
consent.initialize();
|
|
|
|
const state = consent.getState();
|
|
|
|
state.categories.analytics = "granted";
|
|
state.categories.marketing = "granted";
|
|
|
|
const current = consent.getState();
|
|
|
|
assert.equal(current.categories.analytics, "unknown");
|
|
assert.equal(current.categories.marketing, "unknown");
|
|
});
|
|
|
|
test("returned state mutation cannot alter jurisdiction", () => {
|
|
const consent = make({
|
|
jurisdiction: "us-ma"
|
|
});
|
|
|
|
consent.initialize();
|
|
|
|
const state = consent.getState();
|
|
|
|
state.jurisdiction = "eu";
|
|
|
|
assert.equal(
|
|
consent.getState().jurisdiction,
|
|
"us-ma"
|
|
);
|
|
});
|
|
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Consent transitions */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
test("partial consent does not become a complete decision", () => {
|
|
const consent = make();
|
|
|
|
consent.initialize();
|
|
|
|
const state = consent.setConsent({
|
|
analytics: "granted"
|
|
});
|
|
|
|
assert.equal(state.categories.analytics, "granted");
|
|
assert.equal(state.categories.preferences, "unknown");
|
|
assert.equal(state.categories.marketing, "unknown");
|
|
assert.equal(state.hasDecision, false);
|
|
});
|
|
|
|
test("complete selective consent creates a decision", () => {
|
|
const consent = make();
|
|
|
|
consent.initialize();
|
|
|
|
const state = consent.setConsent({
|
|
preferences: "denied",
|
|
analytics: "granted",
|
|
marketing: "denied"
|
|
});
|
|
|
|
assert.equal(state.hasDecision, true);
|
|
assert.equal(state.categories.necessary, "granted");
|
|
});
|
|
|
|
test("acceptAll is idempotent", () => {
|
|
const consent = make();
|
|
|
|
consent.initialize();
|
|
|
|
let changed = 0;
|
|
let accepted = 0;
|
|
|
|
consent.on("consent:changed", () => changed++);
|
|
consent.on("consent:accepted", () => accepted++);
|
|
|
|
const first = consent.acceptAll();
|
|
const second = consent.acceptAll();
|
|
|
|
assert.equal(first.hasDecision, true);
|
|
assert.equal(second.hasDecision, true);
|
|
assert.equal(changed, 1);
|
|
assert.equal(accepted, 1);
|
|
assert.equal(second.timestamp, first.timestamp);
|
|
});
|
|
|
|
test("rejectAll is idempotent", () => {
|
|
const consent = make();
|
|
|
|
consent.initialize();
|
|
|
|
let changed = 0;
|
|
let rejected = 0;
|
|
|
|
consent.on("consent:changed", () => changed++);
|
|
consent.on("consent:rejected", () => rejected++);
|
|
|
|
const first = consent.rejectAll();
|
|
const second = consent.rejectAll();
|
|
|
|
assert.equal(first.hasDecision, true);
|
|
assert.equal(second.hasDecision, true);
|
|
assert.equal(changed, 1);
|
|
assert.equal(rejected, 1);
|
|
});
|
|
|
|
test("revoke after acceptance creates the Core-defined denied state", () => {
|
|
const consent = make();
|
|
|
|
consent.initialize();
|
|
consent.acceptAll();
|
|
|
|
let revoked = 0;
|
|
|
|
consent.on("consent:revoked", () => revoked++);
|
|
|
|
const first = consent.revoke();
|
|
const second = consent.revoke();
|
|
|
|
assert.equal(first.categories.necessary, "granted");
|
|
assert.equal(first.categories.preferences, "denied");
|
|
assert.equal(first.categories.analytics, "denied");
|
|
assert.equal(first.categories.marketing, "denied");
|
|
|
|
assert.equal(second.categories.analytics, "denied");
|
|
assert.equal(revoked, 1);
|
|
});
|
|
|
|
test("reset returns the engine to undecided state", () => {
|
|
const consent = make();
|
|
|
|
consent.initialize();
|
|
consent.acceptAll();
|
|
|
|
const state = consent.reset();
|
|
|
|
assert.equal(state.hasDecision, false);
|
|
assert.equal(state.categories.necessary, "granted");
|
|
assert.equal(state.categories.preferences, "unknown");
|
|
assert.equal(state.categories.analytics, "unknown");
|
|
assert.equal(state.categories.marketing, "unknown");
|
|
});
|
|
|
|
test("reset is idempotent", () => {
|
|
const consent = make();
|
|
|
|
consent.initialize();
|
|
|
|
let reset = 0;
|
|
|
|
consent.on("consent:reset", () => reset++);
|
|
|
|
const first = consent.reset();
|
|
const second = consent.reset();
|
|
|
|
assert.equal(first.hasDecision, false);
|
|
assert.equal(second.hasDecision, false);
|
|
assert.equal(reset, 2);
|
|
});
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* GPC */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
test("GPC never grants optional consent", () => {
|
|
const consent = make();
|
|
|
|
consent.initialize();
|
|
|
|
const state = consent.setGpc(true);
|
|
|
|
assert.equal(state.gpc, true);
|
|
assert.equal(state.hasDecision, false);
|
|
assert.equal(state.categories.preferences, "unknown");
|
|
assert.equal(state.categories.analytics, "unknown");
|
|
assert.equal(state.categories.marketing, "unknown");
|
|
});
|
|
|
|
test("GPC can coexist with explicit consent without changing categories", () => {
|
|
const consent = make();
|
|
|
|
consent.initialize();
|
|
|
|
consent.acceptAll();
|
|
|
|
const state = consent.setGpc(true);
|
|
|
|
assert.equal(state.gpc, true);
|
|
assert.equal(state.categories.analytics, "granted");
|
|
assert.equal(state.categories.marketing, "granted");
|
|
});
|
|
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Persistence */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
test("accepted consent survives a fresh engine instance", () => {
|
|
const first = make();
|
|
|
|
first.initialize();
|
|
first.acceptAll();
|
|
|
|
const second = make();
|
|
|
|
second.initialize();
|
|
|
|
const state = second.getState();
|
|
|
|
assert.equal(state.source, "storage");
|
|
assert.equal(state.hasDecision, true);
|
|
assert.equal(state.categories.analytics, "granted");
|
|
});
|
|
|
|
test("rejected consent survives a fresh engine instance", () => {
|
|
const first = make();
|
|
|
|
first.initialize();
|
|
first.rejectAll();
|
|
|
|
const second = make();
|
|
|
|
second.initialize();
|
|
|
|
assert.equal(
|
|
second.getState().categories.analytics,
|
|
"denied"
|
|
);
|
|
});
|
|
|
|
test("policy mismatch invalidates persisted consent", () => {
|
|
const first = make();
|
|
|
|
first.initialize();
|
|
first.acceptAll();
|
|
|
|
const second = make({
|
|
policyVersion: "2027-01"
|
|
});
|
|
|
|
second.initialize();
|
|
|
|
assert.equal(second.isComplete(), false);
|
|
assert.equal(document.cookie, "");
|
|
});
|
|
|
|
test("invalid persisted payload is ignored and removed", () => {
|
|
const bad = {
|
|
engineVersion: "invalid",
|
|
policyVersion: "2026-01",
|
|
timestamp: Date.now(),
|
|
categories: {
|
|
necessary: "granted",
|
|
preferences: "banana",
|
|
analytics: "granted",
|
|
marketing: "denied"
|
|
},
|
|
jurisdiction: "global",
|
|
gpc: false
|
|
};
|
|
|
|
document.cookie =
|
|
`cc_reliability=${encodeURIComponent(JSON.stringify(bad))}; Path=/; SameSite=Lax`;
|
|
|
|
const consent = make();
|
|
|
|
consent.initialize();
|
|
|
|
assert.equal(consent.isComplete(), false);
|
|
assert.equal(document.cookie, "");
|
|
});
|
|
test("storage removal failure does not falsely report reset", () => {
|
|
const consent = make();
|
|
|
|
consent.initialize();
|
|
consent.acceptAll();
|
|
|
|
blockWrites = true;
|
|
|
|
let resetEvents = 0;
|
|
let storageErrors = 0;
|
|
|
|
consent.on("consent:reset", () => resetEvents++);
|
|
consent.on("storage:error", () => storageErrors++);
|
|
|
|
consent.reset();
|
|
|
|
assert.equal(resetEvents, 0);
|
|
assert.equal(storageErrors, 1);
|
|
assert.equal(consent.getState().hasDecision, true);
|
|
});
|
|
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Data contracts */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
test("valid categories object satisfies its contract", () => {
|
|
assert.equal(
|
|
isValidCategoriesObject({
|
|
necessary: "granted",
|
|
preferences: "unknown",
|
|
analytics: "granted",
|
|
marketing: "denied"
|
|
}),
|
|
true
|
|
);
|
|
});
|
|
|
|
test("invalid categories object fails its contract", () => {
|
|
assert.equal(
|
|
isValidCategoriesObject({
|
|
necessary: "granted",
|
|
preferences: "invalid",
|
|
analytics: "granted",
|
|
marketing: "denied"
|
|
}),
|
|
false
|
|
);
|
|
});
|
|
|
|
test("valid stored envelope satisfies its contract", () => {
|
|
assert.equal(
|
|
isStoragePayload({
|
|
engineVersion: "1.0.1",
|
|
policyVersion: "2026-01",
|
|
timestamp: Date.now(),
|
|
categories: {
|
|
necessary: "granted",
|
|
preferences: "unknown",
|
|
analytics: "granted",
|
|
marketing: "denied"
|
|
},
|
|
jurisdiction: "global",
|
|
gpc: false
|
|
}),
|
|
true
|
|
);
|
|
});
|
|
|
|
test("empty stored envelope fails its contract", () => {
|
|
assert.equal(
|
|
isStoragePayload({}),
|
|
false
|
|
);
|
|
});
|
|
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Lifecycle */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
test("initialize is idempotent", () => {
|
|
const consent = make();
|
|
|
|
let initialized = 0;
|
|
|
|
consent.on("initialized", () => initialized++);
|
|
|
|
consent.initialize();
|
|
consent.initialize();
|
|
consent.initialize();
|
|
|
|
assert.equal(initialized, 1);
|
|
});
|
|
|
|
test("initialize without stored consent starts undecided", () => {
|
|
const consent = make();
|
|
|
|
consent.initialize();
|
|
|
|
const state = consent.getState();
|
|
|
|
assert.equal(state.hasDecision, false);
|
|
assert.equal(state.source, "default");
|
|
assert.equal(state.categories.necessary, "granted");
|
|
assert.equal(state.categories.analytics, "unknown");
|
|
});
|
|
|
|
|
|
console.log(`\n${passed} reliability tests passed.`);
|