Initial reliable release of consent engine
This commit is contained in:
+180
@@ -0,0 +1,180 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
createConsentEngine,
|
||||
ENGINE_VERSION
|
||||
} 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(([k,v]) => `${k}=${v}`).join('; ');
|
||||
},
|
||||
set(value) {
|
||||
if (blockWrites) return;
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
function clearCookie() { jar.clear(); blockWrites = false; }
|
||||
function make() {
|
||||
return createConsentEngine({
|
||||
policyVersion: '2026-01',
|
||||
jurisdiction: 'global',
|
||||
storage: { name: 'cc_consent', duration: 180, path: '/', sameSite: 'Lax' }
|
||||
});
|
||||
}
|
||||
|
||||
let passed = 0;
|
||||
function test(name, fn) {
|
||||
try { fn(); passed++; console.log(`PASS ${name}`); }
|
||||
catch (e) { console.error(`FAIL ${name}\n`, e); process.exitCode = 1; }
|
||||
}
|
||||
|
||||
clearCookie();
|
||||
|
||||
test('default state is undecided', () => {
|
||||
const e = make(); e.initialize(); const s = e.getState();
|
||||
assert.equal(s.categories.necessary, 'granted');
|
||||
assert.equal(s.categories.analytics, 'unknown');
|
||||
assert.equal(s.hasDecision, false);
|
||||
});
|
||||
|
||||
test('accept all', () => {
|
||||
clearCookie();
|
||||
const e = make(); e.initialize(); let changed = 0, accepted = 0;
|
||||
e.on('consent:changed', () => changed++); e.on('consent:accepted', () => accepted++);
|
||||
e.acceptAll();
|
||||
assert.equal(e.getState().hasDecision, true);
|
||||
assert.equal(e.getState().categories.analytics, 'granted');
|
||||
assert.equal(changed, 1); assert.equal(accepted, 1);
|
||||
});
|
||||
|
||||
test('accept all is idempotent', () => {
|
||||
clearCookie();
|
||||
const e = make(); e.initialize(); let events = 0; e.on('consent:changed', () => events++);
|
||||
e.acceptAll(); const before = e.getState().timestamp; e.acceptAll();
|
||||
assert.equal(events, 1); assert.equal(e.getState().timestamp, before);
|
||||
});
|
||||
|
||||
test('persistence loads from storage', () => {
|
||||
clearCookie();
|
||||
const e = make(); e.initialize(); e.rejectAll();
|
||||
const fresh = make(); fresh.initialize(); const s = fresh.getState();
|
||||
assert.equal(s.source, 'storage'); assert.equal(s.categories.analytics, 'denied');
|
||||
});
|
||||
|
||||
test('reject all is idempotent', () => {
|
||||
clearCookie(); const e = make(); e.initialize(); let events = 0; e.on('consent:changed', () => events++);
|
||||
e.rejectAll(); e.rejectAll(); assert.equal(events, 1);
|
||||
});
|
||||
|
||||
test('selective consent', () => {
|
||||
clearCookie(); const e = make(); e.initialize(); e.setConsent({ analytics: 'granted', marketing: 'denied', preferences: 'denied' });
|
||||
assert.equal(e.getState().categories.analytics, 'granted'); assert.equal(e.isComplete(), true);
|
||||
});
|
||||
|
||||
test('invalid consent value throws', () => {
|
||||
clearCookie(); const e = make(); e.initialize(); assert.throws(() => e.setConsent({ analytics: 'banana' }), TypeError);
|
||||
});
|
||||
|
||||
test('necessary cannot be denied', () => {
|
||||
clearCookie(); const e = make(); e.initialize();
|
||||
assert.throws(() => e.setConsent({ necessary: 'denied', analytics: 'granted' }), TypeError);
|
||||
assert.equal(e.getState().categories.necessary, 'granted');
|
||||
assert.equal(e.getState().categories.analytics, 'unknown');
|
||||
});
|
||||
|
||||
test('revoke is idempotent', () => {
|
||||
clearCookie(); const e = make(); e.initialize(); e.acceptAll(); let revoked = 0; e.on('consent:revoked', () => revoked++);
|
||||
e.revoke(); e.revoke(); assert.equal(revoked, 1); assert.equal(e.getState().categories.analytics, 'denied');
|
||||
});
|
||||
|
||||
test('reset removes persistence', () => {
|
||||
clearCookie(); const e = make(); e.initialize(); e.acceptAll(); e.reset();
|
||||
assert.equal(e.isComplete(), false); assert.equal(document.cookie, '');
|
||||
const fresh = make(); fresh.initialize(); assert.equal(fresh.isComplete(), false); assert.equal(fresh.getState().source, 'default');
|
||||
});
|
||||
|
||||
test('reset does not lie when storage removal fails', () => {
|
||||
clearCookie(); const e = make(); e.initialize(); e.acceptAll(); blockWrites = true; let reset = 0, errors = 0;
|
||||
e.on('consent:reset', () => reset++); e.on('storage:error', () => errors++);
|
||||
const before = e.getState(); e.reset();
|
||||
assert.equal(reset, 0); assert.equal(errors, 1); assert.equal(e.getState().categories.analytics, before.categories.analytics);
|
||||
});
|
||||
|
||||
test('strict stored envelope validation', () => {
|
||||
assert.equal(isStoragePayload({}), false);
|
||||
assert.equal(isStoragePayload({ engineVersion: ENGINE_VERSION, policyVersion: '2026-01', timestamp: Date.now(), categories: {}, jurisdiction: 'global', gpc: false }), true);
|
||||
});
|
||||
|
||||
test('strict category validation', () => {
|
||||
assert.equal(isValidCategoriesObject({ necessary:'granted', preferences:'unknown', analytics:'granted', marketing:'denied' }), true);
|
||||
assert.equal(isValidCategoriesObject({ necessary:'denied', preferences:'unknown', analytics:'granted', marketing:'denied' }), false);
|
||||
assert.equal(isValidCategoriesObject({ necessary:'granted', preferences:'banana', analytics:'granted', marketing:'denied' }), false);
|
||||
});
|
||||
|
||||
test('initialize is idempotent', () => {
|
||||
clearCookie(); const e = make(); let initialized = 0; e.on('initialized', () => initialized++); e.initialize(); e.initialize(); assert.equal(initialized, 1);
|
||||
});
|
||||
|
||||
test('policy mismatch removes valid cookie', () => {
|
||||
clearCookie(); const e = make(); e.initialize(); e.acceptAll();
|
||||
const fresh = createConsentEngine({ policyVersion:'2027-01', jurisdiction:'global', storage:{ name:'cc_consent', duration:180, path:'/', sameSite:'Lax' } });
|
||||
fresh.initialize(); assert.equal(fresh.isComplete(), false); assert.equal(document.cookie, '');
|
||||
});
|
||||
|
||||
test('expired cookie is rejected', () => {
|
||||
clearCookie(); const e = make(); 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;
|
||||
document.cookie = `cc_consent=${encodeURIComponent(JSON.stringify(payload))}; Path=/; SameSite=Lax`;
|
||||
const fresh = make(); let expired = 0; fresh.on('consent:expired', () => expired++); fresh.initialize();
|
||||
assert.equal(expired, 1); assert.equal(fresh.isComplete(), false); assert.equal(document.cookie, '');
|
||||
});
|
||||
|
||||
|
||||
test('necessary denial is explicitly rejected', () => {
|
||||
clearCookie(); const e = make(); e.initialize();
|
||||
assert.throws(() => e.setConsent({ necessary: 'denied' }), TypeError);
|
||||
assert.equal(e.getState().categories.necessary, 'granted');
|
||||
});
|
||||
|
||||
test('invalid persisted category payload is ignored and removed', () => {
|
||||
clearCookie();
|
||||
const bad = {
|
||||
engineVersion: ENGINE_VERSION,
|
||||
policyVersion: '2026-01',
|
||||
timestamp: Date.now(),
|
||||
categories: { necessary: 'granted', preferences: 'banana', analytics: 'granted', marketing: 'denied' },
|
||||
jurisdiction: 'global',
|
||||
gpc: false
|
||||
};
|
||||
document.cookie = `cc_consent=${encodeURIComponent(JSON.stringify(bad))}; Path=/; SameSite=Lax`;
|
||||
const e = make(); e.initialize();
|
||||
assert.equal(e.isComplete(), false); assert.equal(document.cookie, '');
|
||||
});
|
||||
|
||||
test('future timestamp is rejected', () => {
|
||||
clearCookie(); const e = make(); 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;
|
||||
document.cookie = `cc_consent=${encodeURIComponent(JSON.stringify(payload))}; Path=/; SameSite=Lax`;
|
||||
const fresh = make(); fresh.initialize();
|
||||
assert.equal(fresh.isComplete(), false); assert.equal(document.cookie, '');
|
||||
});
|
||||
|
||||
console.log(`\n${passed} tests passed.`);
|
||||
if (process.exitCode) process.exit(1);
|
||||
Reference in New Issue
Block a user