Files
consentos/apps/banner/src/__tests__/gpp-api.test.ts
James Cottrill fbf26453f2 feat: initial public release
ConsentOS — a privacy-first cookie consent management platform.

Self-hosted, source-available alternative to OneTrust, Cookiebot, and
CookieYes. Full standards coverage (IAB TCF v2.2, GPP v1, Google
Consent Mode v2, GPC, Shopify Customer Privacy API), multi-tenant
architecture with role-based access, configuration cascade
(system → org → group → site → region), dark-pattern detection in
the scanner, and a tamper-evident consent record audit trail.

This is the initial public release. Prior development history is
retained internally.

See README.md for the feature list, architecture overview, and
quick-start instructions. Licensed under the Elastic Licence 2.0 —
self-host freely; do not resell as a managed service.
2026-04-14 09:18:18 +00:00

502 lines
16 KiB
TypeScript

/**
* Tests for the GPP CMP API (__gpp() global function).
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
installGppApi,
removeGppApi,
updateGppConsent,
setGppDisplayStatus,
setGppSignalStatus,
getGppString,
isGppApiInstalled,
type GppPingReturn,
type GppData,
type GppEventData,
type GppApiCallback,
} from '../gpp-api';
import {
type GppString,
createDefaultSectionData,
US_NATIONAL,
US_CALIFORNIA,
SECTION_REGISTRY,
} from '../gpp';
// Section IDs (US_NATIONAL and US_CALIFORNIA are SectionDef objects, not numbers)
const US_NATIONAL_ID = US_NATIONAL.id; // 7
const US_CALIFORNIA_ID = US_CALIFORNIA.id; // 8
// ── Helpers ───────────────────────────────────────────────────────────
/** Call __gpp and return the callback args as a promise. */
function callGpp(
command: string,
parameter?: unknown,
): Promise<{ data: unknown; success: boolean }> {
return new Promise((resolve) => {
window.__gpp!(command, (data: unknown, success: boolean) => {
resolve({ data, success });
}, parameter);
});
}
/** Build a minimal GppString for testing. */
function buildTestGpp(sectionIds: number[] = [US_NATIONAL_ID]): GppString {
const sections = new Map<number, { data: Record<string, number | number[]> }>();
for (const id of sectionIds) {
const def = SECTION_REGISTRY.get(id);
if (def) {
const data = createDefaultSectionData(def);
sections.set(id, { data });
}
}
return {
header: {
version: 1,
sectionIds,
applicableSections: sectionIds,
},
sections,
};
}
// ── Tests ─────────────────────────────────────────────────────────────
describe('GPP CMP API', () => {
beforeEach(() => {
removeGppApi();
});
afterEach(() => {
removeGppApi();
});
// ── Installation ──────────────────────────────────────────────────
describe('installGppApi / removeGppApi', () => {
it('installs __gpp on window', () => {
expect(window.__gpp).toBeUndefined();
installGppApi(42, ['usnat']);
expect(typeof window.__gpp).toBe('function');
expect(isGppApiInstalled()).toBe(true);
});
it('removes __gpp from window', () => {
installGppApi(42);
removeGppApi();
expect(window.__gpp).toBeUndefined();
expect(isGppApiInstalled()).toBe(false);
});
it('clears __gppQueue on remove', () => {
(window as { __gppQueue?: unknown[] }).__gppQueue = [['ping', vi.fn()]];
installGppApi(42);
removeGppApi();
expect((window as { __gppQueue?: unknown[] }).__gppQueue).toBeUndefined();
});
});
// ── ping command ──────────────────────────────────────────────────
describe('ping command', () => {
it('returns CMP metadata', async () => {
installGppApi(42, ['usnat', 'usca']);
const { data, success } = await callGpp('ping');
expect(success).toBe(true);
const ping = data as GppPingReturn;
expect(ping.gppVersion).toBe('1.1');
expect(ping.cmpStatus).toBe('loaded');
expect(ping.cmpDisplayStatus).toBe('hidden');
expect(ping.signalStatus).toBe('not ready');
expect(ping.supportedAPIs).toEqual(['usnat', 'usca']);
expect(ping.cmpId).toBe(42);
expect(ping.gppString).toBe('');
expect(ping.applicableSections).toEqual([]);
});
it('reflects updated display status', async () => {
installGppApi(1);
setGppDisplayStatus('visible');
const { data } = await callGpp('ping');
expect((data as GppPingReturn).cmpDisplayStatus).toBe('visible');
});
it('reflects updated signal status', async () => {
installGppApi(1);
setGppSignalStatus('ready');
const { data } = await callGpp('ping');
expect((data as GppPingReturn).signalStatus).toBe('ready');
});
it('includes GPP string after consent update', async () => {
installGppApi(1, ['usnat']);
const gpp = buildTestGpp();
const encoded = updateGppConsent(gpp);
expect(encoded).toBeTruthy();
const { data } = await callGpp('ping');
expect((data as GppPingReturn).gppString).toBe(encoded);
expect((data as GppPingReturn).applicableSections).toEqual([US_NATIONAL_ID]);
});
});
// ── getGPPData command ────────────────────────────────────────────
describe('getGPPData command', () => {
it('returns empty data before consent', async () => {
installGppApi(1);
const { data, success } = await callGpp('getGPPData');
expect(success).toBe(true);
const gppData = data as GppData;
expect(gppData.gppString).toBe('');
expect(gppData.applicableSections).toEqual([]);
expect(gppData.parsedSections).toEqual({});
});
it('returns populated data after consent update', async () => {
installGppApi(1, ['usnat']);
const gpp = buildTestGpp();
updateGppConsent(gpp);
const { data } = await callGpp('getGPPData');
const gppData = data as GppData;
expect(gppData.gppString).toBeTruthy();
expect(gppData.applicableSections).toEqual([US_NATIONAL_ID]);
expect(gppData.parsedSections[US_NATIONAL_ID]).toBeDefined();
});
});
// ── getSection command ────────────────────────────────────────────
describe('getSection command', () => {
it('returns null for missing prefix', async () => {
installGppApi(1);
const { data, success } = await callGpp('getSection', 'usnat');
expect(success).toBe(false);
expect(data).toBeNull();
});
it('returns null when no parameter given', async () => {
installGppApi(1);
const { data, success } = await callGpp('getSection');
expect(success).toBe(false);
expect(data).toBeNull();
});
it('returns section data by API prefix', async () => {
installGppApi(1, ['usnat']);
const gpp = buildTestGpp();
updateGppConsent(gpp);
const { data, success } = await callGpp('getSection', 'usnat');
expect(success).toBe(true);
expect(data).toBeDefined();
expect((data as Record<string, unknown>).Version).toBeDefined();
});
it('returns null for unregistered prefix', async () => {
installGppApi(1);
updateGppConsent(buildTestGpp());
const { data, success } = await callGpp('getSection', 'nonexistent');
expect(success).toBe(false);
expect(data).toBeNull();
});
});
// ── hasSection command ────────────────────────────────────────────
describe('hasSection command', () => {
it('returns false when section not present', async () => {
installGppApi(1);
const { data, success } = await callGpp('hasSection', 'usnat');
expect(success).toBe(true);
expect(data).toBe(false);
});
it('returns true when section present', async () => {
installGppApi(1, ['usnat']);
updateGppConsent(buildTestGpp());
const { data, success } = await callGpp('hasSection', 'usnat');
expect(success).toBe(true);
expect(data).toBe(true);
});
it('returns false for empty parameter', async () => {
installGppApi(1);
const { data, success } = await callGpp('hasSection');
expect(success).toBe(true);
expect(data).toBe(false);
});
});
// ── addEventListener / removeEventListener ────────────────────────
describe('addEventListener', () => {
it('registers a listener and fires immediately', async () => {
installGppApi(1, ['usnat']);
const { data, success } = await callGpp('addEventListener');
expect(success).toBe(true);
const event = data as GppEventData;
expect(event.eventName).toBe('listenerRegistered');
expect(event.listenerId).toBe(1);
expect(event.pingData.cmpStatus).toBe('loaded');
});
it('assigns unique listener IDs', async () => {
installGppApi(1);
const { data: d1 } = await callGpp('addEventListener');
const { data: d2 } = await callGpp('addEventListener');
expect((d1 as GppEventData).listenerId).toBe(1);
expect((d2 as GppEventData).listenerId).toBe(2);
});
it('notifies listeners on consent update', () => {
installGppApi(1, ['usnat']);
const events: GppEventData[] = [];
window.__gpp!('addEventListener', (data: unknown) => {
events.push(data as GppEventData);
});
// First event is listenerRegistered
expect(events).toHaveLength(1);
expect(events[0].eventName).toBe('listenerRegistered');
// Update consent — should fire signalStatus event
updateGppConsent(buildTestGpp());
expect(events).toHaveLength(2);
expect(events[1].eventName).toBe('signalStatus');
expect(events[1].data).toBeDefined();
expect((events[1].data as GppData).gppString).toBeTruthy();
});
it('swallows errors thrown by listeners', () => {
installGppApi(1);
const throwingCallback: GppApiCallback = () => {
throw new Error('Listener error');
};
// Should not throw
expect(() => {
window.__gpp!('addEventListener', throwingCallback);
}).not.toThrow();
// Should not throw during consent update either
expect(() => {
updateGppConsent(buildTestGpp());
}).not.toThrow();
});
});
describe('removeEventListener', () => {
it('removes an existing listener', async () => {
installGppApi(1);
// Add listener
const { data } = await callGpp('addEventListener');
const listenerId = (data as GppEventData).listenerId;
// Remove it
const { data: removed, success } = await callGpp('removeEventListener', listenerId);
expect(success).toBe(true);
expect(removed).toBe(true);
});
it('returns false for non-existent listener', async () => {
installGppApi(1);
const { data, success } = await callGpp('removeEventListener', 999);
expect(success).toBe(false);
expect(data).toBe(false);
});
it('stops notifications after removal', () => {
installGppApi(1, ['usnat']);
const events: GppEventData[] = [];
let listenerId: number;
window.__gpp!('addEventListener', (data: unknown) => {
const event = data as GppEventData;
events.push(event);
listenerId = event.listenerId;
});
// Remove the listener
window.__gpp!('removeEventListener', (_d: unknown, _s: boolean) => {}, listenerId!);
// Update consent — should NOT fire to removed listener
const eventsBefore = events.length;
updateGppConsent(buildTestGpp());
expect(events.length).toBe(eventsBefore);
});
});
// ── Unknown commands ──────────────────────────────────────────────
describe('unknown commands', () => {
it('returns false for unknown commands', async () => {
installGppApi(1);
const { data, success } = await callGpp('unknownCommand');
expect(success).toBe(false);
expect(data).toBe(false);
});
});
// ── Uninitialised state ───────────────────────────────────────────
describe('uninitialised state', () => {
it('returns false when API not installed', () => {
// Manually set __gpp to the handler without initialising state
// by calling removeGppApi first to clear state, then manually assigning
removeGppApi();
// Directly import and call the handler — simulate calling before install
let callbackData: unknown;
let callbackSuccess: boolean | undefined;
// We can't call __gpp because it's removed, but we can test via getGppString
expect(getGppString()).toBe('');
expect(isGppApiInstalled()).toBe(false);
});
});
// ── updateGppConsent ──────────────────────────────────────────────
describe('updateGppConsent', () => {
it('returns the encoded GPP string', () => {
installGppApi(1, ['usnat']);
const gpp = buildTestGpp();
const result = updateGppConsent(gpp);
expect(result).toBeTruthy();
expect(typeof result).toBe('string');
// GPP strings start with 'D' (header prefix)
expect(result.startsWith('D')).toBe(true);
});
it('updates the gppString accessible via getGppString', () => {
installGppApi(1);
expect(getGppString()).toBe('');
updateGppConsent(buildTestGpp());
expect(getGppString()).toBeTruthy();
});
it('sets signalStatus to ready', async () => {
installGppApi(1);
updateGppConsent(buildTestGpp());
const { data } = await callGpp('ping');
expect((data as GppPingReturn).signalStatus).toBe('ready');
});
it('works with multiple sections', () => {
installGppApi(1, ['usnat', 'usca']);
const gpp = buildTestGpp([US_NATIONAL_ID, US_CALIFORNIA_ID]);
const result = updateGppConsent(gpp);
expect(result).toBeTruthy();
// Should contain ~ separator for sections
expect(result).toContain('~');
});
});
// ── Queue processing ──────────────────────────────────────────────
describe('queue processing', () => {
it('processes queued calls on install', () => {
// Set up a queue before installing
const results: Array<{ data: unknown; success: boolean }> = [];
const cb = (data: unknown, success: boolean) => {
results.push({ data, success });
};
(window as { __gppQueue?: unknown[] }).__gppQueue = [
['ping', cb],
];
installGppApi(1, ['usnat']);
// The queued ping should have been processed
expect(results).toHaveLength(1);
expect(results[0].success).toBe(true);
expect((results[0].data as GppPingReturn).cmpStatus).toBe('loaded');
});
it('clears the queue after processing', () => {
(window as { __gppQueue?: unknown[] }).__gppQueue = [
['ping', vi.fn()],
];
installGppApi(1);
expect((window as { __gppQueue?: unknown[] }).__gppQueue).toEqual([]);
});
});
// ── Coexistence with __tcfapi ─────────────────────────────────────
describe('coexistence', () => {
it('does not interfere with other window globals', () => {
// Set a mock __tcfapi
const mockTcf = vi.fn();
(window as { __tcfapi?: unknown }).__tcfapi = mockTcf;
installGppApi(1);
// __tcfapi should still be intact
expect((window as { __tcfapi?: unknown }).__tcfapi).toBe(mockTcf);
removeGppApi();
// __tcfapi should still be intact after removal
expect((window as { __tcfapi?: unknown }).__tcfapi).toBe(mockTcf);
// Clean up
delete (window as { __tcfapi?: unknown }).__tcfapi;
});
});
// ── setGppDisplayStatus / setGppSignalStatus ──────────────────────
describe('status setters', () => {
it('setGppDisplayStatus updates display status', async () => {
installGppApi(1);
setGppDisplayStatus('visible');
const { data } = await callGpp('ping');
expect((data as GppPingReturn).cmpDisplayStatus).toBe('visible');
setGppDisplayStatus('disabled');
const { data: d2 } = await callGpp('ping');
expect((d2 as GppPingReturn).cmpDisplayStatus).toBe('disabled');
});
it('setGppSignalStatus updates signal status', async () => {
installGppApi(1);
setGppSignalStatus('ready');
const { data } = await callGpp('ping');
expect((data as GppPingReturn).signalStatus).toBe('ready');
});
it('no-ops when API not installed', () => {
// Should not throw
expect(() => setGppDisplayStatus('visible')).not.toThrow();
expect(() => setGppSignalStatus('ready')).not.toThrow();
});
});
});