From 6c73cf51c5e4dc99515a4bb2576fcef63c7bc56f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deyson=20Thom=C3=A9?= Date: Wed, 2 Sep 2026 15:14:10 -0300 Subject: [PATCH] Add adversarial test suite --- run-tests.sh | 50 ++ tests/adversarial/adversarial-test.mjs | 640 +++++++++++++++++++++++++ 2 files changed, 690 insertions(+) create mode 100755 run-tests.sh create mode 100644 tests/adversarial/adversarial-test.mjs diff --git a/run-tests.sh b/run-tests.sh new file mode 100755 index 0000000..e01d8bd --- /dev/null +++ b/run-tests.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +set -u + +echo "========================================" +echo "Universal Cookie Consent — Test Suite" +echo "========================================" +echo + +run_test() { + local name="$1" + local command="$2" + + echo "----------------------------------------" + echo "$name" + echo "----------------------------------------" + + if eval "$command"; then + echo + echo "PASS: $name" + echo + else + echo + echo "FAIL: $name" + echo + exit 1 + fi +} + +run_test "Core" \ + "node test-core.mjs" + +run_test "Jurisdiction" \ + "node tests/jurisdiction-test.mjs" + +run_test "Integration" \ + "node tests/integration/integration-test.mjs" + +run_test "Audit" \ + "node tests/audit/audit-test.mjs" + +run_test "Reliability" \ + "node tests/reliability/reliability-test.mjs" + +run_test "Adversarial" \ + "node tests/adversarial/adversarial-test.mjs" + +echo "========================================" +echo "ALL TEST GROUPS PASSED" +echo "========================================" diff --git a/tests/adversarial/adversarial-test.mjs b/tests/adversarial/adversarial-test.mjs new file mode 100644 index 0000000..7a288f7 --- /dev/null +++ b/tests/adversarial/adversarial-test.mjs @@ -0,0 +1,640 @@ +import assert from "node:assert/strict"; + +import { createConsentEngine } from "../../core/consent.js"; +import { isStoragePayload } from "../../core/storage.js"; +import { + isValidCategoriesObject +} from "../../core/categories.js"; +import { + resolveJurisdiction, + isValidResolution +} from "../../jurisdiction/resolver.js"; +import { + createIntegratedConsentEngine +} from "../../integration/index.js"; + + +/* -------------------------------------------------------------------------- */ +/* Browser-like storage environment */ +/* -------------------------------------------------------------------------- */ + +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_adversarial", + 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 boundary */ +/* -------------------------------------------------------------------------- */ + +test("null configuration falls back safely", () => { + const consent = createConsentEngine(null); + + consent.initialize(); + + assert.equal(consent.getState().jurisdiction, "global"); +}); + +test("array configuration falls back safely", () => { + const consent = createConsentEngine([]); + + consent.initialize(); + + assert.equal(consent.getState().jurisdiction, "global"); +}); + +test("invalid storage object does not corrupt defaults", () => { + const consent = createConsentEngine({ + storage: "invalid" + }); + + consent.initialize(); + + assert.equal(consent.getState().jurisdiction, "global"); +}); + +test("invalid storage duration does not corrupt initialization", () => { + const consent = make({ + storage: { + duration: "invalid" + } + }); + + consent.initialize(); + + assert.equal(consent.getState().hasDecision, false); +}); + + +/* -------------------------------------------------------------------------- */ +/* Consent operation boundaries */ +/* -------------------------------------------------------------------------- */ + +test("empty consent payload does not create a decision", () => { + const consent = make(); + + consent.initialize(); + + const state = consent.setConsent({}); + + assert.equal(state.hasDecision, false); + assert.equal(state.categories.analytics, "unknown"); + assert.equal(state.categories.marketing, "unknown"); +}); + +test("null consent payload is rejected", () => { + const consent = make(); + + consent.initialize(); + + assert.throws( + () => consent.setConsent(null), + TypeError + ); +}); + +test("array consent payload is rejected", () => { + const consent = make(); + + consent.initialize(); + + assert.throws( + () => consent.setConsent([]), + TypeError + ); +}); + +test("unknown consent property is ignored without changing state", () => { + const consent = make(); + + consent.initialize(); + + const before = consent.getState(); + + const after = consent.setConsent({ + unknownCategory: "granted" + }); + + assert.deepEqual(after.categories, before.categories); + assert.equal(after.hasDecision, before.hasDecision); + assert.equal(after.jurisdiction, before.jurisdiction); + assert.equal(after.timestamp, before.timestamp); +}); +test("necessary cannot be changed to unknown", () => { + const consent = make(); + + consent.initialize(); + + assert.throws( + () => consent.setConsent({ + necessary: "unknown" + }), + TypeError + ); +}); + +test("revoke before acceptance remains deterministic", () => { + const consent = make(); + + consent.initialize(); + + const first = consent.revoke(); + const second = consent.revoke(); + + assert.deepEqual(second.categories, first.categories); + assert.equal(second.hasDecision, true); +}); + +test("reset before any decision remains deterministic", () => { + const consent = make(); + + consent.initialize(); + + const first = consent.reset(); + const second = consent.reset(); + + assert.equal(first.hasDecision, false); + assert.equal(second.hasDecision, false); + assert.deepEqual(second.categories, first.categories); +}); + + +/* -------------------------------------------------------------------------- */ +/* Operation sequences */ +/* -------------------------------------------------------------------------- */ + +test("accept then reject produces the expected final state", () => { + const consent = make(); + + consent.initialize(); + consent.acceptAll(); + + const state = consent.rejectAll(); + + assert.equal(state.hasDecision, true); + assert.equal(state.categories.necessary, "granted"); + assert.equal(state.categories.preferences, "denied"); + assert.equal(state.categories.analytics, "denied"); + assert.equal(state.categories.marketing, "denied"); +}); + +test("reject then accept produces the expected final state", () => { + const consent = make(); + + consent.initialize(); + consent.rejectAll(); + + const state = consent.acceptAll(); + + assert.equal(state.hasDecision, true); + assert.equal(state.categories.necessary, "granted"); + assert.equal(state.categories.preferences, "granted"); + assert.equal(state.categories.analytics, "granted"); + assert.equal(state.categories.marketing, "granted"); +}); + +test("reset after acceptance removes the decision", () => { + 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("revoke followed by reset returns to undecided state", () => { + const consent = make(); + + consent.initialize(); + consent.acceptAll(); + consent.revoke(); + + const state = consent.reset(); + + assert.equal(state.hasDecision, false); + assert.equal(state.categories.preferences, "unknown"); + assert.equal(state.categories.analytics, "unknown"); + assert.equal(state.categories.marketing, "unknown"); +}); + + +/* -------------------------------------------------------------------------- */ +/* Deep isolation */ +/* -------------------------------------------------------------------------- */ + +test("returned state nested mutation cannot alter internal state", () => { + const consent = make(); + + consent.initialize(); + consent.acceptAll(); + + const state = consent.getState(); + + state.categories.preferences = "denied"; + state.categories.analytics = "unknown"; + + const current = consent.getState(); + + assert.equal(current.categories.preferences, "granted"); + assert.equal(current.categories.analytics, "granted"); +}); + +test("two engines remain isolated after independent transitions", () => { + const first = make({ + jurisdiction: "us-ma" + }); + + const second = make({ + jurisdiction: "eu" + }); + + first.initialize(); + second.initialize(); + + first.acceptAll(); + + assert.equal(first.getState().jurisdiction, "us-ma"); + assert.equal(second.getState().jurisdiction, "eu"); + assert.equal(first.getState().hasDecision, true); + assert.equal(second.getState().hasDecision, false); +}); + + +/* -------------------------------------------------------------------------- */ +/* Storage envelope adversarial cases */ +/* -------------------------------------------------------------------------- */ + +test("storage payload rejects NaN timestamp", () => { + assert.equal( + isStoragePayload({ + engineVersion: "1.0.1", + policyVersion: "2026-01", + timestamp: NaN, + categories: { + necessary: "granted", + preferences: "unknown", + analytics: "unknown", + marketing: "unknown" + }, + jurisdiction: "global", + gpc: false + }), + false + ); +}); + +test("storage payload rejects infinite timestamp", () => { + assert.equal( + isStoragePayload({ + engineVersion: "1.0.1", + policyVersion: "2026-01", + timestamp: Infinity, + categories: { + necessary: "granted", + preferences: "unknown", + analytics: "unknown", + marketing: "unknown" + }, + jurisdiction: "global", + gpc: false + }), + false + ); +}); + +test("storage payload rejects empty engine version", () => { + assert.equal( + isStoragePayload({ + engineVersion: "", + policyVersion: "2026-01", + timestamp: Date.now(), + categories: { + necessary: "granted", + preferences: "unknown", + analytics: "unknown", + marketing: "unknown" + }, + jurisdiction: "global", + gpc: false + }), + false + ); +}); + +test("storage payload rejects non-boolean GPC", () => { + assert.equal( + isStoragePayload({ + engineVersion: "1.0.1", + policyVersion: "2026-01", + timestamp: Date.now(), + categories: { + necessary: "granted", + preferences: "unknown", + analytics: "unknown", + marketing: "unknown" + }, + jurisdiction: "global", + gpc: "true" + }), + false + ); +}); + +test("storage payload rejects array categories", () => { + assert.equal( + isStoragePayload({ + engineVersion: "1.0.1", + policyVersion: "2026-01", + timestamp: Date.now(), + categories: [], + jurisdiction: "global", + gpc: false + }), + false + ); +}); + +test("storage payload rejects missing jurisdiction", () => { + assert.equal( + isStoragePayload({ + engineVersion: "1.0.1", + policyVersion: "2026-01", + timestamp: Date.now(), + categories: { + necessary: "granted", + preferences: "unknown", + analytics: "unknown", + marketing: "unknown" + }, + gpc: false + }), + false + ); +}); + + +/* -------------------------------------------------------------------------- */ +/* Category contract */ +/* -------------------------------------------------------------------------- */ + +test("category contract rejects null", () => { + assert.equal( + isValidCategoriesObject(null), + false + ); +}); + +test("category contract rejects arrays", () => { + assert.equal( + isValidCategoriesObject([]), + false + ); +}); + +test("category contract rejects unknown consent value", () => { + assert.equal( + isValidCategoriesObject({ + necessary: "granted", + preferences: "unknown", + analytics: "invalid", + marketing: "unknown" + }), + false + ); +}); + + +/* -------------------------------------------------------------------------- */ +/* Jurisdiction adversarial cases */ +/* -------------------------------------------------------------------------- */ + +test("jurisdiction resolver rejects null input", () => { + assert.throws( + () => resolveJurisdiction(null), + TypeError + ); +}); + +test("jurisdiction resolver rejects array input", () => { + assert.throws( + () => resolveJurisdiction([]), + TypeError + ); +}); + +test("fixed jurisdiction trims surrounding whitespace", () => { + const result = resolveJurisdiction({ + mode: "fixed", + jurisdiction: " us-ma " + }); + + assert.equal(result.jurisdiction.id, "us-ma"); + assert.equal(isValidResolution(result), true); +}); + +test("fixed jurisdiction rejects whitespace-only value", () => { + assert.throws( + () => resolveJurisdiction({ + mode: "fixed", + jurisdiction: " " + }), + TypeError + ); +}); + +test("invalid signal source is ignored during auto resolution", () => { + const result = resolveJurisdiction({ + mode: "auto", + request: { + jurisdiction: "us-ma", + source: "invalid-source" + } + }); + + assert.equal(result.source, "fallback"); + assert.equal(result.jurisdiction.id, "global"); +}); + +test("valid server signal defeats lower-trust 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 fixed mode defeats conflicting request signals", () => { + const result = resolveJurisdiction({ + mode: "fixed", + jurisdiction: "eu", + request: { + jurisdiction: "us-ma", + source: "server" + } + }); + + assert.equal(result.jurisdiction.id, "eu"); + assert.equal(result.source, "site-config"); +}); + + +/* -------------------------------------------------------------------------- */ +/* Integration boundary */ +/* -------------------------------------------------------------------------- */ + +test("resolved jurisdiction cannot be overridden by Core jurisdiction option", () => { + const engine = createIntegratedConsentEngine({ + jurisdiction: { + mode: "fixed", + jurisdiction: "eu" + }, + core: { + jurisdiction: "us-ma" + } + }); + + engine.initialize(); + + assert.equal( + engine.getState().jurisdiction, + "eu" + ); + + assert.equal( + engine.getJurisdictionContext().resolution.jurisdiction.id, + "eu" + ); +}); + +test("integration rejects invalid jurisdiction before Core creation", () => { + assert.throws( + () => createIntegratedConsentEngine({ + jurisdiction: { + mode: "fixed", + jurisdiction: "unknown" + } + }), + RangeError + ); +}); + + +/* -------------------------------------------------------------------------- */ +/* GPC boundary */ +/* -------------------------------------------------------------------------- */ + +test("GPC remains state information under adversarial consent sequence", () => { + const consent = make(); + + consent.initialize(); + consent.setGpc(true); + + const state = consent.getState(); + + assert.equal(state.gpc, true); + 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("explicit acceptance remains explicit after GPC activation", () => { + const consent = make(); + + consent.initialize(); + consent.setGpc(true); + consent.acceptAll(); + + const state = consent.getState(); + + assert.equal(state.gpc, true); + assert.equal(state.hasDecision, true); + assert.equal(state.categories.preferences, "granted"); + assert.equal(state.categories.analytics, "granted"); + assert.equal(state.categories.marketing, "granted"); +}); + + +console.log(); +console.log(`${passed} adversarial tests passed.`);