Add adversarial test suite
This commit is contained in:
Executable
+50
@@ -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 "========================================"
|
||||
@@ -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.`);
|
||||
Reference in New Issue
Block a user