Initial reliable release of consent engine
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Cookie Consent Audit Lab</title>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; max-width: 900px; margin: 40px auto; padding: 0 20px; }
|
||||
button { padding: 10px 16px; cursor: pointer; }
|
||||
#status { margin: 20px 0; font-size: 1.2rem; font-weight: 700; }
|
||||
pre { background: #f5f5f5; padding: 16px; overflow: auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Cookie Consent Audit Lab</h1>
|
||||
<p>Browser audit for Core + Jurisdiction + Integration invariants.</p>
|
||||
<button id="run">Run audit</button>
|
||||
<div id="status">Not run</div>
|
||||
<pre id="log"></pre>
|
||||
|
||||
<script type="module">
|
||||
import { createIntegratedConsentEngine } from "../../integration/index.js";
|
||||
|
||||
const status = document.getElementById("status");
|
||||
const log = document.getElementById("log");
|
||||
const button = document.getElementById("run");
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
function clearConsentCookie() {
|
||||
document.cookie = "ucc_consent=; Max-Age=0; Path=/; SameSite=Lax";
|
||||
}
|
||||
|
||||
async function test(name, fn, output) {
|
||||
try {
|
||||
await fn();
|
||||
output.push(`PASS ${name}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
output.push(`FAIL ${name} — ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function runAudit() {
|
||||
clearConsentCookie();
|
||||
const output = [];
|
||||
let passed = 0;
|
||||
|
||||
if (await test("dynamic jurisdiction is reflected in Core state", () => {
|
||||
const consent = createIntegratedConsentEngine({
|
||||
jurisdiction: { mode: "fixed", jurisdiction: "global" }
|
||||
});
|
||||
consent.initialize();
|
||||
assert(consent.setJurisdiction("us-ma").jurisdiction === "us-ma", "jurisdiction mismatch");
|
||||
}, output)) passed++;
|
||||
|
||||
if (await test("GPC state is represented without granting optional consent", () => {
|
||||
const consent = createIntegratedConsentEngine({
|
||||
jurisdiction: { mode: "fixed", jurisdiction: "us-ma" }
|
||||
});
|
||||
consent.initialize();
|
||||
const state = consent.setGpc(true);
|
||||
assert(state.gpc === true, "GPC was not recorded");
|
||||
assert(state.hasDecision === false, "GPC unexpectedly created a decision");
|
||||
assert(state.categories.analytics === "unknown", "analytics was granted unexpectedly");
|
||||
}, output)) passed++;
|
||||
|
||||
if (await test("acceptance persists resolved jurisdiction", () => {
|
||||
clearConsentCookie();
|
||||
const consent = createIntegratedConsentEngine({
|
||||
jurisdiction: { mode: "fixed", jurisdiction: "us-ma" }
|
||||
});
|
||||
consent.initialize();
|
||||
const state = consent.acceptAll();
|
||||
assert(state.hasDecision === true, "decision was not saved");
|
||||
assert(state.jurisdiction === "us-ma", "wrong persisted jurisdiction");
|
||||
}, output)) passed++;
|
||||
|
||||
if (await test("reload recovers the persisted decision", () => {
|
||||
const consent = createIntegratedConsentEngine({
|
||||
jurisdiction: { mode: "fixed", jurisdiction: "us-ma" }
|
||||
});
|
||||
consent.initialize();
|
||||
const state = consent.getState();
|
||||
assert(state.hasDecision === true, "decision did not survive reinitialization");
|
||||
assert(state.jurisdiction === "us-ma", "jurisdiction did not survive reinitialization");
|
||||
}, output)) passed++;
|
||||
|
||||
if (await test("reset removes the persisted decision", () => {
|
||||
const consent = createIntegratedConsentEngine({
|
||||
jurisdiction: { mode: "fixed", jurisdiction: "us-ma" }
|
||||
});
|
||||
consent.initialize();
|
||||
const state = consent.reset();
|
||||
assert(state.hasDecision === false, "reset retained decision");
|
||||
assert(state.categories.analytics === "unknown", "analytics was not reset");
|
||||
}, output)) passed++;
|
||||
|
||||
if (await test("event sequence reports acceptance once", () => {
|
||||
clearConsentCookie();
|
||||
const consent = createIntegratedConsentEngine({
|
||||
jurisdiction: { mode: "fixed", jurisdiction: "us-ma" }
|
||||
});
|
||||
const events = [];
|
||||
consent.on("consent:changed", () => events.push("changed"));
|
||||
consent.on("consent:accepted", () => events.push("accepted"));
|
||||
consent.initialize();
|
||||
consent.acceptAll();
|
||||
assert(events.join(",") === "changed,accepted", "unexpected event sequence: " + events.join(","));
|
||||
}, output)) passed++;
|
||||
|
||||
if (await test("returned state cannot mutate Core internals", () => {
|
||||
const consent = createIntegratedConsentEngine({
|
||||
jurisdiction: { mode: "fixed", jurisdiction: "us-ma" }
|
||||
});
|
||||
consent.initialize();
|
||||
const state = consent.getState();
|
||||
state.categories.analytics = "granted";
|
||||
state.jurisdiction = "changed";
|
||||
const current = consent.getState();
|
||||
assert(current.categories.analytics === "unknown", "categories leaked");
|
||||
assert(current.jurisdiction === "us-ma", "jurisdiction leaked");
|
||||
}, output)) passed++;
|
||||
|
||||
log.textContent = output.join("\n");
|
||||
status.textContent = `${passed}/7 audit tests passed`;
|
||||
clearConsentCookie();
|
||||
}
|
||||
|
||||
button.addEventListener("click", runAudit);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,256 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createIntegratedConsentEngine } from "../../integration/index.js";
|
||||
|
||||
// This suite exercises real persistence (acceptAll/revoke/reset expected to
|
||||
// take effect), unlike tests/integration/integration-test.mjs which
|
||||
// deliberately asserts the no-op behavior of a bare Node environment.
|
||||
// A document.cookie mock is required here for the same reason it is
|
||||
// required in test-core.mjs.
|
||||
let jar = new Map();
|
||||
|
||||
globalThis.location = { protocol: "http:" };
|
||||
globalThis.document = {};
|
||||
Object.defineProperty(globalThis.document, "cookie", {
|
||||
configurable: true,
|
||||
get() {
|
||||
return [...jar.entries()].map(([k, v]) => `${k}=${v}`).join("; ");
|
||||
},
|
||||
set(value) {
|
||||
const parts = value.split(";").map((x) => x.trim());
|
||||
const [name, raw = ""] = parts[0].split("=");
|
||||
const maxAge = parts.find((x) => /^Max-Age=/i.test(x));
|
||||
if (maxAge && maxAge.toLowerCase() === "max-age=0") {
|
||||
jar.delete(name);
|
||||
return;
|
||||
}
|
||||
jar.set(name, raw);
|
||||
}
|
||||
});
|
||||
|
||||
let passed = 0;
|
||||
|
||||
function test(name, fn) {
|
||||
// Each test starts from a clean cookie jar so persisted consent from
|
||||
// one test cannot leak into the next (they share default jurisdiction
|
||||
// and policyVersion, so a stale cookie would otherwise be reloaded).
|
||||
jar.clear();
|
||||
|
||||
try {
|
||||
fn();
|
||||
console.log(`PASS ${name}`);
|
||||
passed += 1;
|
||||
} catch (error) {
|
||||
console.error(`FAIL ${name}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function createEngine(jurisdiction = "us-ma") {
|
||||
return createIntegratedConsentEngine({
|
||||
jurisdiction: {
|
||||
mode: "fixed",
|
||||
jurisdiction
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test("dynamic jurisdiction is reflected in Core state", () => {
|
||||
const consent = createEngine("global");
|
||||
consent.initialize();
|
||||
const state = consent.setJurisdiction("us-ma");
|
||||
|
||||
assert.equal(state.jurisdiction, "us-ma");
|
||||
});
|
||||
|
||||
test("GPC state is represented without granting optional consent", () => {
|
||||
const consent = createEngine();
|
||||
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("revoke produces the Core-defined denied state and is idempotent", () => {
|
||||
const consent = createEngine();
|
||||
consent.initialize();
|
||||
|
||||
consent.acceptAll();
|
||||
const accepted = consent.getState();
|
||||
assert.equal(accepted.hasDecision, true);
|
||||
|
||||
let revokedEvents = 0;
|
||||
consent.on("consent:revoked", () => revokedEvents++);
|
||||
|
||||
const revoked = consent.revoke();
|
||||
const secondRevoke = consent.revoke();
|
||||
|
||||
assert.equal(revoked.categories.necessary, "granted");
|
||||
assert.equal(revoked.categories.preferences, "denied");
|
||||
assert.equal(revoked.categories.analytics, "denied");
|
||||
assert.equal(revoked.categories.marketing, "denied");
|
||||
assert.equal(secondRevoke.categories.analytics, "denied");
|
||||
assert.equal(revokedEvents, 1);
|
||||
});
|
||||
|
||||
test("reset returns Core to an undecided state", () => {
|
||||
const consent = createEngine();
|
||||
consent.initialize();
|
||||
consent.acceptAll();
|
||||
|
||||
const reset = consent.reset();
|
||||
|
||||
assert.equal(reset.hasDecision, false);
|
||||
assert.equal(reset.categories.necessary, "granted");
|
||||
assert.equal(reset.categories.preferences, "unknown");
|
||||
assert.equal(reset.categories.analytics, "unknown");
|
||||
assert.equal(reset.categories.marketing, "unknown");
|
||||
});
|
||||
|
||||
test("policy version is part of the Core state", () => {
|
||||
const consent = createEngine();
|
||||
const config = consent.getConfig();
|
||||
|
||||
assert.equal(typeof config.policyVersion, "string");
|
||||
assert.ok(config.policyVersion.length > 0);
|
||||
});
|
||||
|
||||
test("persistence preserves the resolved jurisdiction after acceptance", () => {
|
||||
const consent = createEngine("us-ma");
|
||||
consent.initialize();
|
||||
|
||||
const saved = consent.acceptAll();
|
||||
|
||||
assert.equal(saved.hasDecision, true);
|
||||
assert.equal(saved.jurisdiction, "us-ma");
|
||||
});
|
||||
|
||||
test("event sequence reports the consent transition", () => {
|
||||
const consent = createEngine();
|
||||
const events = [];
|
||||
|
||||
consent.on("consent:changed", () => events.push("changed"));
|
||||
consent.on("consent:accepted", () => events.push("accepted"));
|
||||
|
||||
consent.initialize();
|
||||
consent.acceptAll();
|
||||
|
||||
assert.deepEqual(events, ["changed", "accepted"]);
|
||||
});
|
||||
|
||||
test("Core state remains isolated from returned object mutation", () => {
|
||||
const consent = createEngine();
|
||||
consent.initialize();
|
||||
|
||||
const state = consent.getState();
|
||||
state.categories.analytics = "granted";
|
||||
state.jurisdiction = "changed";
|
||||
|
||||
const current = consent.getState();
|
||||
|
||||
assert.equal(current.categories.analytics, "unknown");
|
||||
assert.equal(current.jurisdiction, "us-ma");
|
||||
});
|
||||
|
||||
|
||||
test("separate Core instances do not share in-memory state", () => {
|
||||
const first = createEngine("us-ma");
|
||||
const second = createEngine("eu");
|
||||
|
||||
first.initialize();
|
||||
second.initialize();
|
||||
|
||||
first.setConsent({ analytics: "granted" });
|
||||
|
||||
assert.equal(first.getState().categories.analytics, "granted");
|
||||
assert.equal(second.getState().categories.analytics, "unknown");
|
||||
assert.equal(first.getState().jurisdiction, "us-ma");
|
||||
assert.equal(second.getState().jurisdiction, "eu");
|
||||
});
|
||||
|
||||
test("partial consent remains undecided until all optional categories are resolved", () => {
|
||||
const consent = createEngine();
|
||||
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);
|
||||
assert.equal(consent.isComplete(), false);
|
||||
});
|
||||
|
||||
test("necessary remains granted through valid consent changes", () => {
|
||||
const consent = createEngine();
|
||||
consent.initialize();
|
||||
|
||||
const state = consent.setConsent({
|
||||
preferences: "denied",
|
||||
analytics: "granted",
|
||||
marketing: "denied"
|
||||
});
|
||||
|
||||
assert.equal(state.categories.necessary, "granted");
|
||||
assert.equal(state.categories.preferences, "denied");
|
||||
assert.equal(state.categories.analytics, "granted");
|
||||
assert.equal(state.categories.marketing, "denied");
|
||||
});
|
||||
|
||||
test("acceptAll is idempotent through the integration boundary", () => {
|
||||
const consent = createEngine();
|
||||
consent.initialize();
|
||||
|
||||
let changed = 0;
|
||||
let accepted = 0;
|
||||
|
||||
consent.on("consent:changed", () => changed++);
|
||||
consent.on("consent:accepted", () => accepted++);
|
||||
|
||||
consent.acceptAll();
|
||||
const first = consent.getState();
|
||||
consent.acceptAll();
|
||||
const second = consent.getState();
|
||||
|
||||
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("reset is idempotent through the integration boundary", () => {
|
||||
const consent = createEngine();
|
||||
consent.initialize();
|
||||
consent.acceptAll();
|
||||
|
||||
let reset = 0;
|
||||
consent.on("consent:reset", () => reset++);
|
||||
|
||||
consent.reset();
|
||||
const first = consent.getState();
|
||||
consent.reset();
|
||||
const second = consent.getState();
|
||||
|
||||
assert.equal(first.hasDecision, false);
|
||||
assert.equal(second.hasDecision, false);
|
||||
assert.equal(second.categories.analytics, "unknown");
|
||||
});
|
||||
|
||||
test("GPC never grants optional categories through the integration boundary", () => {
|
||||
const consent = createEngine();
|
||||
consent.initialize();
|
||||
|
||||
const state = consent.setGpc(true);
|
||||
|
||||
assert.equal(state.gpc, true);
|
||||
assert.equal(state.categories.preferences, "unknown");
|
||||
assert.equal(state.categories.analytics, "unknown");
|
||||
assert.equal(state.categories.marketing, "unknown");
|
||||
assert.equal(state.hasDecision, false);
|
||||
});
|
||||
|
||||
console.log(`${passed} audit tests passed.`);
|
||||
@@ -0,0 +1,173 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Cookie Consent Integration Test Lab</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
max-width: 900px;
|
||||
margin: 40px auto;
|
||||
padding: 0 20px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
button {
|
||||
padding: 10px 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#summary {
|
||||
font-weight: 700;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.pass { color: #176b2c; }
|
||||
.fail { color: #a11; }
|
||||
pre {
|
||||
background: #f5f5f5;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Cookie Consent Integration Test Lab</h1>
|
||||
<p>Browser regression suite for the Jurisdiction Engine + Consent Core boundary.</p>
|
||||
|
||||
<button id="run">Run tests</button>
|
||||
<div id="summary">Not run.</div>
|
||||
<pre id="output"></pre>
|
||||
|
||||
<script type="module">
|
||||
import {
|
||||
resolveConsentContext,
|
||||
createIntegratedConsentEngine
|
||||
} from "../../integration/index.js";
|
||||
|
||||
const output = document.getElementById("output");
|
||||
const summary = document.getElementById("summary");
|
||||
const runButton = document.getElementById("run");
|
||||
|
||||
function equal(actual, expected) {
|
||||
if (actual !== expected) {
|
||||
throw new Error(`Expected ${expected}, got ${actual}`);
|
||||
}
|
||||
}
|
||||
|
||||
function deepEqual(actual, expected) {
|
||||
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
||||
throw new Error("Values are not deeply equal.");
|
||||
}
|
||||
}
|
||||
|
||||
function throws(fn, pattern) {
|
||||
try {
|
||||
fn();
|
||||
} catch (error) {
|
||||
if (pattern.test(error.message)) return;
|
||||
throw error;
|
||||
}
|
||||
throw new Error("Expected function to throw.");
|
||||
}
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
output.textContent += `PASS: ${name}\n`;
|
||||
return true;
|
||||
} catch (error) {
|
||||
output.textContent += `FAIL: ${name}\n${error.stack || error}\n`;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
runButton.addEventListener("click", () => {
|
||||
output.textContent = "";
|
||||
let passed = 0;
|
||||
const total = 9;
|
||||
|
||||
if (test("fixed us-ma resolves into Core context", () => {
|
||||
const context = resolveConsentContext({
|
||||
mode: "fixed", jurisdiction: "us-ma"
|
||||
});
|
||||
equal(context.resolution.jurisdiction.id, "us-ma");
|
||||
equal(context.resolution.policy.id, "us-state-privacy");
|
||||
})) passed++;
|
||||
|
||||
if (test("fixed eu resolves into Core context", () => {
|
||||
const context = resolveConsentContext({
|
||||
mode: "fixed", jurisdiction: "eu"
|
||||
});
|
||||
equal(context.resolution.jurisdiction.id, "eu");
|
||||
equal(context.resolution.policy.id, "eu-privacy");
|
||||
})) passed++;
|
||||
|
||||
if (test("global resolves into global baseline", () => {
|
||||
const context = resolveConsentContext({ mode: "global" });
|
||||
equal(context.resolution.jurisdiction.id, "global");
|
||||
equal(context.resolution.policy.id, "global-baseline");
|
||||
})) passed++;
|
||||
|
||||
if (test("integration configures Core jurisdiction", () => {
|
||||
const consent = createIntegratedConsentEngine({
|
||||
jurisdiction: { mode: "fixed", jurisdiction: "us-ma" }
|
||||
});
|
||||
const state = consent.initialize();
|
||||
equal(state.jurisdiction, "us-ma");
|
||||
equal(state.hasDecision, false);
|
||||
})) passed++;
|
||||
|
||||
if (test("jurisdiction does not grant consent", () => {
|
||||
const consent = createIntegratedConsentEngine({
|
||||
jurisdiction: { mode: "fixed", jurisdiction: "us-ma" }
|
||||
});
|
||||
const state = consent.initialize();
|
||||
equal(state.categories.necessary, "granted");
|
||||
equal(state.categories.preferences, "unknown");
|
||||
equal(state.categories.analytics, "unknown");
|
||||
equal(state.categories.marketing, "unknown");
|
||||
equal(state.hasDecision, false);
|
||||
})) passed++;
|
||||
|
||||
if (test("resolved context is exposed as frozen data", () => {
|
||||
const consent = createIntegratedConsentEngine({
|
||||
jurisdiction: { mode: "fixed", jurisdiction: "eu" }
|
||||
});
|
||||
const context = consent.getJurisdictionContext();
|
||||
equal(context.resolution.jurisdiction.id, "eu");
|
||||
equal(Object.isFrozen(context), true);
|
||||
})) passed++;
|
||||
|
||||
if (test("Core persistence uses resolved jurisdiction", () => {
|
||||
const consent = createIntegratedConsentEngine({
|
||||
jurisdiction: { mode: "fixed", jurisdiction: "us-ma" }
|
||||
});
|
||||
consent.initialize();
|
||||
consent.acceptAll();
|
||||
equal(consent.getState().jurisdiction, "us-ma");
|
||||
equal(consent.getState().hasDecision, true);
|
||||
})) passed++;
|
||||
|
||||
if (test("integration is deterministic", () => {
|
||||
const input = {
|
||||
mode: "auto",
|
||||
request: { jurisdiction: "us-ma", source: "server" },
|
||||
visitor: { jurisdiction: "eu", source: "browser" }
|
||||
};
|
||||
deepEqual(
|
||||
resolveConsentContext(input),
|
||||
resolveConsentContext(input)
|
||||
);
|
||||
})) passed++;
|
||||
|
||||
if (test("invalid jurisdiction remains an Engine error", () => {
|
||||
throws(() => resolveConsentContext({
|
||||
mode: "fixed", jurisdiction: "not-a-jurisdiction"
|
||||
}), /Unknown jurisdiction/);
|
||||
})) passed++;
|
||||
|
||||
summary.textContent = `${passed}/${total} tests passed`;
|
||||
summary.className = passed === total ? "pass" : "fail";
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,136 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
resolveConsentContext,
|
||||
createIntegratedConsentEngine
|
||||
} from "../../integration/index.js";
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(`PASS ${name}`);
|
||||
} catch (error) {
|
||||
console.error(`FAIL ${name}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
test("fixed us-ma resolves into Core context", () => {
|
||||
const context = resolveConsentContext({
|
||||
mode: "fixed",
|
||||
jurisdiction: "us-ma"
|
||||
});
|
||||
|
||||
assert.equal(context.resolution.jurisdiction.id, "us-ma");
|
||||
assert.equal(context.resolution.policy.id, "us-state-privacy");
|
||||
});
|
||||
|
||||
test("fixed eu resolves into Core context", () => {
|
||||
const context = resolveConsentContext({
|
||||
mode: "fixed",
|
||||
jurisdiction: "eu"
|
||||
});
|
||||
|
||||
assert.equal(context.resolution.jurisdiction.id, "eu");
|
||||
assert.equal(context.resolution.policy.id, "eu-privacy");
|
||||
});
|
||||
|
||||
test("global resolves into global baseline", () => {
|
||||
const context = resolveConsentContext({ mode: "global" });
|
||||
|
||||
assert.equal(context.resolution.jurisdiction.id, "global");
|
||||
assert.equal(context.resolution.policy.id, "global-baseline");
|
||||
});
|
||||
|
||||
test("integration configures Core jurisdiction", () => {
|
||||
const consent = createIntegratedConsentEngine({
|
||||
jurisdiction: { mode: "fixed", jurisdiction: "us-ma" }
|
||||
});
|
||||
|
||||
const state = consent.initialize();
|
||||
|
||||
assert.equal(state.jurisdiction, "us-ma");
|
||||
assert.equal(state.hasDecision, false);
|
||||
});
|
||||
|
||||
test("jurisdiction does not grant consent", () => {
|
||||
const consent = createIntegratedConsentEngine({
|
||||
jurisdiction: { mode: "fixed", jurisdiction: "us-ma" }
|
||||
});
|
||||
|
||||
const state = consent.initialize();
|
||||
|
||||
assert.equal(state.categories.necessary, "granted");
|
||||
assert.equal(state.categories.preferences, "unknown");
|
||||
assert.equal(state.categories.analytics, "unknown");
|
||||
assert.equal(state.categories.marketing, "unknown");
|
||||
assert.equal(state.hasDecision, false);
|
||||
});
|
||||
|
||||
test("resolved context is exposed without mutable internals", () => {
|
||||
const consent = createIntegratedConsentEngine({
|
||||
jurisdiction: { mode: "fixed", jurisdiction: "eu" }
|
||||
});
|
||||
|
||||
const context = consent.getJurisdictionContext();
|
||||
|
||||
assert.equal(context.resolution.jurisdiction.id, "eu");
|
||||
assert.equal(Object.isFrozen(context), true);
|
||||
assert.equal(Object.isFrozen(context.resolution), true);
|
||||
});
|
||||
|
||||
test("Core configuration preserves resolved jurisdiction", () => {
|
||||
const consent = createIntegratedConsentEngine({
|
||||
jurisdiction: { mode: "fixed", jurisdiction: "us-ma" }
|
||||
});
|
||||
|
||||
const config = consent.getConfig();
|
||||
|
||||
assert.equal(config.jurisdiction, "us-ma");
|
||||
});
|
||||
|
||||
test("Node environment does not fake successful persistence", () => {
|
||||
const consent = createIntegratedConsentEngine({
|
||||
jurisdiction: { mode: "fixed", jurisdiction: "us-ma" }
|
||||
});
|
||||
|
||||
consent.initialize();
|
||||
const state = consent.acceptAll();
|
||||
|
||||
assert.equal(state.jurisdiction, "us-ma");
|
||||
assert.equal(state.hasDecision, false);
|
||||
assert.equal(state.categories.preferences, "unknown");
|
||||
assert.equal(state.categories.analytics, "unknown");
|
||||
assert.equal(state.categories.marketing, "unknown");
|
||||
});
|
||||
|
||||
test("integration remains deterministic", () => {
|
||||
const input = {
|
||||
mode: "auto",
|
||||
request: {
|
||||
jurisdiction: "us-ma",
|
||||
source: "server"
|
||||
},
|
||||
visitor: {
|
||||
jurisdiction: "eu",
|
||||
source: "browser"
|
||||
}
|
||||
};
|
||||
|
||||
const first = resolveConsentContext(input);
|
||||
const second = resolveConsentContext(input);
|
||||
|
||||
assert.deepEqual(first, second);
|
||||
});
|
||||
|
||||
test("invalid jurisdiction remains an Engine error", () => {
|
||||
assert.throws(
|
||||
() => resolveConsentContext({
|
||||
mode: "fixed",
|
||||
jurisdiction: "not-a-jurisdiction"
|
||||
}),
|
||||
/Unknown jurisdiction/
|
||||
);
|
||||
});
|
||||
|
||||
console.log("10 integration tests passed.");
|
||||
@@ -0,0 +1,84 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Jurisdiction Engine Test Lab</title>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; max-width: 900px; margin: 40px auto; padding: 0 20px; }
|
||||
button { padding: 10px 16px; cursor: pointer; }
|
||||
#summary { margin: 20px 0; font-weight: 700; }
|
||||
.pass { color: #176b2c; }
|
||||
.fail { color: #a51d2d; }
|
||||
pre { white-space: pre-wrap; background: #f5f5f5; padding: 16px; border-radius: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Jurisdiction Engine Test Lab</h1>
|
||||
<p>Browser regression suite for the pure Jurisdiction Engine.</p>
|
||||
<button id="run">Run tests</button>
|
||||
<div id="summary">Not run.</div>
|
||||
<pre id="log"></pre>
|
||||
|
||||
<script type="module">
|
||||
import {
|
||||
resolveJurisdiction,
|
||||
isValidResolution,
|
||||
getResolutionPriority,
|
||||
getSourceConfidence,
|
||||
hasJurisdiction,
|
||||
hasPolicy,
|
||||
JURISDICTION_CATALOG,
|
||||
POLICIES
|
||||
} from "../jurisdiction/index.js";
|
||||
|
||||
const tests = [
|
||||
["fixed us-ma", () => {
|
||||
const r = resolveJurisdiction({mode:"fixed", jurisdiction:"us-ma"});
|
||||
return r.status === "resolved" && r.jurisdiction.id === "us-ma" && r.policy.id === "us-state-privacy" && r.source === "site-config" && r.confidence === "explicit";
|
||||
}],
|
||||
["fixed eu", () => resolveJurisdiction({mode:"fixed", jurisdiction:"eu"}).policy.id === "eu-privacy"],
|
||||
["fixed global", () => resolveJurisdiction({mode:"fixed", jurisdiction:"global"}).policy.id === "global-baseline"],
|
||||
["global mode", () => resolveJurisdiction({mode:"global"}).jurisdiction.id === "global"],
|
||||
["auto server", () => resolveJurisdiction({mode:"auto", request:{jurisdiction:"us-ca", source:"server"}}).source === "server"],
|
||||
["auto application", () => resolveJurisdiction({mode:"auto", visitor:{jurisdiction:"us-ma", source:"application"}}).source === "application"],
|
||||
["auto browser", () => resolveJurisdiction({mode:"auto", visitor:{jurisdiction:"eu", source:"browser"}}).source === "browser"],
|
||||
["auto fallback", () => { const r=resolveJurisdiction({mode:"auto"}); return r.status === "fallback" && r.jurisdiction.id === "global"; }],
|
||||
["unknown signal fallback", () => resolveJurisdiction({mode:"auto", request:{jurisdiction:"xx-unknown", source:"server"}}).status === "fallback"],
|
||||
["server beats browser", () => resolveJurisdiction({mode:"auto", request:{jurisdiction:"us-ma",source:"server"}, visitor:{jurisdiction:"eu",source:"browser"}}).jurisdiction.id === "us-ma"],
|
||||
["site config beats server", () => resolveJurisdiction({mode:"auto", site:{jurisdiction:"us-ca",source:"site-config"}, request:{jurisdiction:"eu",source:"server"}}).jurisdiction.id === "us-ca"],
|
||||
["fixed requires jurisdiction", () => { try { resolveJurisdiction({mode:"fixed"}); return false; } catch { return true; } }],
|
||||
["fixed rejects unknown", () => { try { resolveJurisdiction({mode:"fixed",jurisdiction:"xx"}); return false; } catch { return true; } }],
|
||||
["invalid mode", () => { try { resolveJurisdiction({mode:"random"}); return false; } catch { return true; } }],
|
||||
["invalid input", () => { try { resolveJurisdiction(null); return false; } catch { return true; } }],
|
||||
["catalog policies exist", () => Object.values(JURISDICTION_CATALOG).every(e => hasPolicy(e.policy))],
|
||||
["result contract", () => [resolveJurisdiction({mode:"fixed",jurisdiction:"us-ma"}),resolveJurisdiction({mode:"global"}),resolveJurisdiction({mode:"auto"})].every(isValidResolution)],
|
||||
["source priority", () => getResolutionPriority("site-config") > getResolutionPriority("server") && getResolutionPriority("server") > getResolutionPriority("application") && getResolutionPriority("application") > getResolutionPriority("browser") && getResolutionPriority("browser") > getResolutionPriority("fallback")],
|
||||
["confidence mapping", () => getSourceConfidence("site-config") === "explicit" && getSourceConfidence("server") === "high" && getSourceConfidence("application") === "medium" && getSourceConfidence("browser") === "low" && getSourceConfidence("fallback") === "default"],
|
||||
["initial catalog", () => ["global","us","us-ma","us-ca","eu"].every(hasJurisdiction) && POLICIES["global-baseline"] !== undefined]
|
||||
];
|
||||
|
||||
const run = document.querySelector("#run");
|
||||
const summary = document.querySelector("#summary");
|
||||
const log = document.querySelector("#log");
|
||||
|
||||
run.addEventListener("click", () => {
|
||||
let passed = 0;
|
||||
const lines = [];
|
||||
for (const [name, fn] of tests) {
|
||||
try {
|
||||
const ok = Boolean(fn());
|
||||
if (!ok) throw new Error("Assertion returned false");
|
||||
passed++;
|
||||
lines.push(`PASS: ${name}`);
|
||||
} catch (error) {
|
||||
lines.push(`FAIL: ${name} — ${error.message}`);
|
||||
}
|
||||
}
|
||||
summary.textContent = `${passed}/${tests.length} tests passed`;
|
||||
summary.className = passed === tests.length ? "pass" : "fail";
|
||||
log.textContent = lines.join("\n");
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,196 @@
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
resolveJurisdiction,
|
||||
isValidResolution,
|
||||
getResolutionPriority,
|
||||
getSourceConfidence,
|
||||
hasJurisdiction,
|
||||
hasPolicy,
|
||||
JURISDICTION_CATALOG,
|
||||
POLICIES
|
||||
} from "../jurisdiction/index.js";
|
||||
|
||||
const tests = [];
|
||||
function test(name, fn) {
|
||||
tests.push({ name, fn });
|
||||
}
|
||||
|
||||
test("fixed us-ma resolves from site configuration", () => {
|
||||
const result = resolveJurisdiction({ mode: "fixed", jurisdiction: "us-ma" });
|
||||
assert.equal(result.status, "resolved");
|
||||
assert.equal(result.jurisdiction.id, "us-ma");
|
||||
assert.equal(result.policy.id, "us-state-privacy");
|
||||
assert.equal(result.source, "site-config");
|
||||
assert.equal(result.confidence, "explicit");
|
||||
});
|
||||
|
||||
test("fixed eu resolves correctly", () => {
|
||||
const result = resolveJurisdiction({ mode: "fixed", jurisdiction: "eu" });
|
||||
assert.equal(result.jurisdiction.id, "eu");
|
||||
assert.equal(result.policy.id, "eu-privacy");
|
||||
});
|
||||
|
||||
test("fixed global resolves global baseline", () => {
|
||||
const result = resolveJurisdiction({ mode: "fixed", jurisdiction: "global" });
|
||||
assert.equal(result.jurisdiction.id, "global");
|
||||
assert.equal(result.policy.id, "global-baseline");
|
||||
});
|
||||
|
||||
test("global mode deliberately selects global policy", () => {
|
||||
const result = resolveJurisdiction({ mode: "global" });
|
||||
assert.equal(result.jurisdiction.id, "global");
|
||||
assert.equal(result.source, "site-config");
|
||||
});
|
||||
|
||||
test("auto resolves server signal", () => {
|
||||
const result = resolveJurisdiction({
|
||||
mode: "auto",
|
||||
request: { jurisdiction: "us-ca", source: "server" }
|
||||
});
|
||||
assert.equal(result.jurisdiction.id, "us-ca");
|
||||
assert.equal(result.source, "server");
|
||||
assert.equal(result.confidence, "high");
|
||||
});
|
||||
|
||||
test("auto resolves application signal", () => {
|
||||
const result = resolveJurisdiction({
|
||||
mode: "auto",
|
||||
visitor: { jurisdiction: "us-ma", source: "application" }
|
||||
});
|
||||
assert.equal(result.jurisdiction.id, "us-ma");
|
||||
assert.equal(result.source, "application");
|
||||
});
|
||||
|
||||
test("auto resolves browser signal", () => {
|
||||
const result = resolveJurisdiction({
|
||||
mode: "auto",
|
||||
visitor: { jurisdiction: "eu", source: "browser" }
|
||||
});
|
||||
assert.equal(result.jurisdiction.id, "eu");
|
||||
assert.equal(result.source, "browser");
|
||||
assert.equal(result.confidence, "low");
|
||||
});
|
||||
|
||||
test("auto falls back conservatively when no signal exists", () => {
|
||||
const result = resolveJurisdiction({ mode: "auto" });
|
||||
assert.equal(result.status, "fallback");
|
||||
assert.equal(result.jurisdiction.id, "global");
|
||||
assert.equal(result.policy.id, "global-baseline");
|
||||
assert.equal(result.source, "fallback");
|
||||
assert.equal(result.confidence, "default");
|
||||
});
|
||||
|
||||
test("auto ignores unknown jurisdiction signals", () => {
|
||||
const result = resolveJurisdiction({
|
||||
mode: "auto",
|
||||
request: { jurisdiction: "xx-unknown", source: "server" }
|
||||
});
|
||||
assert.equal(result.status, "fallback");
|
||||
assert.equal(result.jurisdiction.id, "global");
|
||||
});
|
||||
|
||||
test("higher-trust server signal wins over browser signal", () => {
|
||||
const result = resolveJurisdiction({
|
||||
mode: "auto",
|
||||
request: { jurisdiction: "us-ma", source: "server" },
|
||||
visitor: { jurisdiction: "eu", source: "browser" }
|
||||
});
|
||||
assert.equal(result.jurisdiction.id, "us-ma");
|
||||
assert.equal(result.source, "server");
|
||||
});
|
||||
|
||||
test("explicit site configuration wins over lower-trust signals", () => {
|
||||
const result = resolveJurisdiction({
|
||||
mode: "auto",
|
||||
site: { jurisdiction: "us-ca", source: "site-config" },
|
||||
request: { jurisdiction: "eu", source: "server" }
|
||||
});
|
||||
assert.equal(result.jurisdiction.id, "us-ca");
|
||||
assert.equal(result.source, "site-config");
|
||||
});
|
||||
|
||||
test("fixed mode requires jurisdiction", () => {
|
||||
assert.throws(
|
||||
() => resolveJurisdiction({ mode: "fixed" }),
|
||||
/jurisdiction is required/i
|
||||
);
|
||||
});
|
||||
|
||||
test("fixed mode rejects unknown jurisdiction", () => {
|
||||
assert.throws(
|
||||
() => resolveJurisdiction({ mode: "fixed", jurisdiction: "xx" }),
|
||||
/unknown jurisdiction/i
|
||||
);
|
||||
});
|
||||
|
||||
test("invalid mode is rejected", () => {
|
||||
assert.throws(
|
||||
() => resolveJurisdiction({ mode: "random" }),
|
||||
/invalid jurisdiction mode/i
|
||||
);
|
||||
});
|
||||
|
||||
test("invalid input type is rejected", () => {
|
||||
assert.throws(
|
||||
() => resolveJurisdiction(null),
|
||||
/input must be an object/i
|
||||
);
|
||||
});
|
||||
|
||||
test("catalog entries reference existing policies", () => {
|
||||
for (const entry of Object.values(JURISDICTION_CATALOG)) {
|
||||
assert.equal(hasPolicy(entry.policy), true);
|
||||
}
|
||||
});
|
||||
|
||||
test("resolution result satisfies its contract", () => {
|
||||
const results = [
|
||||
resolveJurisdiction({ mode: "fixed", jurisdiction: "us-ma" }),
|
||||
resolveJurisdiction({ mode: "global" }),
|
||||
resolveJurisdiction({ mode: "auto" })
|
||||
];
|
||||
|
||||
for (const result of results) {
|
||||
assert.equal(isValidResolution(result), true);
|
||||
}
|
||||
});
|
||||
|
||||
test("source priority is deterministic", () => {
|
||||
assert.ok(getResolutionPriority("site-config") > getResolutionPriority("server"));
|
||||
assert.ok(getResolutionPriority("server") > getResolutionPriority("application"));
|
||||
assert.ok(getResolutionPriority("application") > getResolutionPriority("browser"));
|
||||
assert.ok(getResolutionPriority("browser") > getResolutionPriority("fallback"));
|
||||
});
|
||||
|
||||
test("source confidence mapping is deterministic", () => {
|
||||
assert.equal(getSourceConfidence("site-config"), "explicit");
|
||||
assert.equal(getSourceConfidence("server"), "high");
|
||||
assert.equal(getSourceConfidence("application"), "medium");
|
||||
assert.equal(getSourceConfidence("browser"), "low");
|
||||
assert.equal(getSourceConfidence("fallback"), "default");
|
||||
});
|
||||
|
||||
test("catalog contains the initial architectural jurisdictions", () => {
|
||||
for (const id of ["global", "us", "us-ma", "us-ca", "eu"]) {
|
||||
assert.equal(hasJurisdiction(id), true);
|
||||
}
|
||||
assert.equal(POLICIES["global-baseline"] !== undefined, true);
|
||||
});
|
||||
|
||||
let passed = 0;
|
||||
for (const { name, fn } of tests) {
|
||||
try {
|
||||
fn();
|
||||
passed += 1;
|
||||
console.log(`PASS: ${name}`);
|
||||
} catch (error) {
|
||||
console.error(`FAIL: ${name}`);
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n${passed}/${tests.length} tests passed`);
|
||||
if (passed !== tests.length) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
@@ -0,0 +1,525 @@
|
||||
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.0",
|
||||
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.`);
|
||||
@@ -0,0 +1,70 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Cookie Consent Core — Hardening Test Lab</title>
|
||||
<style>
|
||||
body{font-family:system-ui,sans-serif;margin:0;background:#f5f7fa;color:#1f2937}.wrap{max-width:1000px;margin:32px auto;padding:0 16px}.card{background:#fff;border:1px solid #dfe4ea;border-radius:12px;padding:20px;margin-bottom:18px}button{padding:9px 13px;border:1px solid #cbd2da;border-radius:7px;background:#fff;cursor:pointer}button.primary{background:#1f2937;color:#fff}.actions{display:flex;gap:8px;flex-wrap:wrap}.pass{color:#137333}.fail{color:#b42318}.neutral{color:#697386}.test{display:flex;justify-content:space-between;border-bottom:1px solid #eee;padding:10px 0;gap:16px}.test:last-child{border:0}pre{background:#111827;color:#e5e7eb;padding:16px;border-radius:8px;overflow:auto}code{font-family:ui-monospace,monospace}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header><h1>Cookie Consent Core — Hardening Test Lab</h1><p>Regression and robustness tests for Core v1.0.1.</p></header>
|
||||
<section class="card"><h2>Controls</h2><div class="actions"><button id="run" class="primary">Run tests</button><button id="accept">Accept All</button><button id="reject">Reject All</button><button id="revoke">Revoke</button><button id="reset">Reset</button><button id="refresh">Refresh</button></div></section>
|
||||
<section class="card"><h2>State</h2><pre id="state">—</pre></section>
|
||||
<section class="card"><h2>Tests</h2><div id="tests"></div><p id="summary" class="neutral">Not run.</p></section>
|
||||
<section class="card"><h2>Cookie</h2><pre id="cookie">—</pre></section>
|
||||
<section class="card"><h2>Event log</h2><pre id="log">—</pre></section>
|
||||
</div>
|
||||
<script type="module">
|
||||
import { createConsentEngine, ENGINE_VERSION } from "../core/consent.js";
|
||||
import { isStoragePayload } from "../core/storage.js";
|
||||
import { isValidCategoriesObject } from "../core/categories.js";
|
||||
|
||||
const config={policyVersion:"2026-01",jurisdiction:"global",storage:{name:"cc_consent",duration:180,path:"/",sameSite:"Lax"}};
|
||||
const engine=createConsentEngine(config);
|
||||
const log=[];
|
||||
const events=["initialized","consent:loaded","consent:accepted","consent:rejected","consent:changed","consent:revoked","consent:expired","consent:reset","storage:error"];
|
||||
for(const name of events) engine.on(name,payload=>{log.push({time:new Date().toLocaleTimeString(),event:name,payload});render();});
|
||||
|
||||
function render(){document.querySelector("#state").textContent=JSON.stringify(engine.getState(),null,2);document.querySelector("#cookie").textContent=document.cookie||"(no cookie)";document.querySelector("#log").textContent=JSON.stringify(log,null,2)}
|
||||
function assert(c,m){if(!c)throw new Error(m)}
|
||||
function clean(){engine.reset();document.cookie="cc_consent=; Max-Age=0; Path=/; SameSite=Lax"}
|
||||
function test(name,fn){try{fn();return [name,true] }catch(e){return [name,false,e.message]}}
|
||||
function run(){
|
||||
const results=[];
|
||||
function setCookiePayload(payload){
|
||||
document.cookie="cc_consent="+encodeURIComponent(JSON.stringify(payload))+"; Path=/; SameSite=Lax";
|
||||
}
|
||||
function clearDirect(){ document.cookie="cc_consent=; Max-Age=0; Path=/; SameSite=Lax"; }
|
||||
clean(); engine.initialize();
|
||||
results.push(test("Default / unknown",()=>{const s=engine.getState();assert(s.categories.necessary==="granted","necessary");assert(s.categories.analytics==="unknown","analytics");assert(!s.hasDecision,"decision") }));
|
||||
results.push(test("Accept All",()=>{engine.acceptAll();const s=engine.getState();assert(s.categories.analytics==="granted","analytics");assert(s.hasDecision,"decision")}));
|
||||
results.push(test("Accept All idempotent",()=>{const t=engine.getState().timestamp;let n=0;const off=engine.on("consent:changed",()=>n++);engine.acceptAll();off();assert(n===0,"duplicate changed event");assert(engine.getState().timestamp===t,"timestamp changed")}));
|
||||
results.push(test("Persistence",()=>{const e=createConsentEngine(config);e.initialize();assert(e.getState().source==="storage","source");assert(e.hasConsent("analytics"),"analytics")}));
|
||||
results.push(test("Reject All",()=>{engine.rejectAll();const s=engine.getState();assert(s.categories.analytics==="denied","analytics");assert(s.categories.necessary==="granted","necessary")}));
|
||||
results.push(test("Reject All idempotent",()=>{let n=0;const off=engine.on("consent:changed",()=>n++);engine.rejectAll();off();assert(n===0,"duplicate changed event")}));
|
||||
results.push(test("Selective",()=>{engine.setConsent({preferences:"denied",analytics:"granted",marketing:"denied"});assert(engine.isComplete(),"complete");assert(engine.hasConsent("analytics"),"analytics")}));
|
||||
results.push(test("Invalid value rejected",()=>{let ok=false;try{engine.setConsent({analytics:"banana"})}catch{ok=true}assert(ok,"must throw")}));
|
||||
results.push(test("Necessary cannot be denied",()=>{let ok=false;try{engine.setConsent({necessary:"denied"})}catch{ok=true}assert(ok,"must throw")}));
|
||||
results.push(test("Revoke",()=>{engine.acceptAll();engine.revoke();assert(engine.getState().categories.analytics==="denied","analytics")}));
|
||||
results.push(test("Revoke idempotent",()=>{let n=0;const off=engine.on("consent:revoked",()=>n++);engine.revoke();off();assert(n===0,"duplicate revoke")}));
|
||||
results.push(test("Reset",()=>{engine.acceptAll();engine.reset();assert(!engine.isComplete(),"decision");assert(!document.cookie,"cookie remains")}));
|
||||
results.push(test("Initialize idempotent",()=>{let n=0;const e=createConsentEngine(config);e.on("initialized",()=>n++);e.initialize();e.initialize();assert(n===1,"initialized twice")}));
|
||||
results.push(test("Strict stored envelope validation",()=>{assert(!isStoragePayload({}),"empty payload accepted");assert(isStoragePayload({engineVersion:ENGINE_VERSION,policyVersion:"2026-01",timestamp:Date.now(),categories:{},jurisdiction:"global",gpc:false}),"valid envelope rejected")}));
|
||||
results.push(test("Strict category validation",()=>{assert(isValidCategoriesObject({necessary:"granted",preferences:"unknown",analytics:"granted",marketing:"denied"}),"valid categories rejected");assert(!isValidCategoriesObject({necessary:"denied",preferences:"unknown",analytics:"granted",marketing:"denied"}),"necessary denied accepted");assert(!isValidCategoriesObject({necessary:"granted",preferences:"banana",analytics:"granted",marketing:"denied"}),"invalid category accepted")}));
|
||||
results.push(test("Policy version mismatch",()=>{clean();const e=createConsentEngine(config);e.initialize();e.acceptAll();const fresh=createConsentEngine({...config,policyVersion:"2027-01"});fresh.initialize();assert(!fresh.isComplete(),"old policy accepted");assert(!document.cookie,"old cookie remains")}));
|
||||
results.push(test("Expired cookie",()=>{clean();const e=createConsentEngine(config);e.initialize();e.acceptAll();const raw=decodeURIComponent(document.cookie.split("=").slice(1).join("="));const payload=JSON.parse(raw);payload.timestamp=Date.now()-181*24*60*60*1000;setCookiePayload(payload);const fresh=createConsentEngine(config);let expired=0;fresh.on("consent:expired",()=>expired++);fresh.initialize();assert(expired===1,"expired event");assert(!fresh.isComplete(),"expired consent accepted");assert(!document.cookie,"expired cookie remains")}));
|
||||
results.push(test("Invalid persisted category payload",()=>{clean();const bad={engineVersion:ENGINE_VERSION,policyVersion:"2026-01",timestamp:Date.now(),categories:{necessary:"granted",preferences:"banana",analytics:"granted",marketing:"denied"},jurisdiction:"global",gpc:false};setCookiePayload(bad);const e=createConsentEngine(config);e.initialize();assert(!e.isComplete(),"invalid payload accepted");assert(!document.cookie,"invalid cookie remains")}));
|
||||
results.push(test("Future timestamp",()=>{clean();const e=createConsentEngine(config);e.initialize();e.acceptAll();const raw=decodeURIComponent(document.cookie.split("=").slice(1).join("="));const payload=JSON.parse(raw);payload.timestamp=Date.now()+60*60*1000;setCookiePayload(payload);const fresh=createConsentEngine(config);fresh.initialize();assert(!fresh.isComplete(),"future timestamp accepted");assert(!document.cookie,"future cookie remains")}));
|
||||
const failed=results.filter(x=>!x[1]);
|
||||
document.querySelector("#tests").innerHTML=results.map(x=>`<div class="test"><span>${x[0]}${x[2]?` — ${x[2]}`:""}</span><strong class="${x[1]?"pass":"fail"}">${x[1]?"PASS":"FAIL"}</strong></div>`).join("");
|
||||
document.querySelector("#summary").textContent=`${results.length-failed.length}/${results.length} tests passed`;
|
||||
document.querySelector("#summary").className=failed.length?"fail":"pass";
|
||||
render();
|
||||
}
|
||||
document.querySelector("#run").onclick=run;document.querySelector("#accept").onclick=()=>{engine.acceptAll();render()};document.querySelector("#reject").onclick=()=>{engine.rejectAll();render()};document.querySelector("#revoke").onclick=()=>{engine.revoke();render()};document.querySelector("#reset").onclick=()=>{engine.reset();render()};document.querySelector("#refresh").onclick=render;engine.initialize();render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user