Files
breakpilot-compliance/consent-sdk/src/core/ConsentManager.test.ts
Benjamin Boenisch 4435e7ea0a Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance,
AI-Compliance-SDK, Consent-SDK, Developer-Portal,
PCA-Platform, DSMS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:28 +01:00

606 lines
16 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { ConsentManager } from './ConsentManager';
import type { ConsentConfig, ConsentState } from '../types';
describe('ConsentManager', () => {
let manager: ConsentManager;
const mockConfig: ConsentConfig = {
apiEndpoint: 'https://api.example.com',
siteId: 'test-site',
debug: false,
};
beforeEach(() => {
localStorage.clear();
vi.clearAllMocks();
// Mock successful API response
vi.mocked(fetch).mockResolvedValue({
ok: true,
status: 200,
json: () =>
Promise.resolve({
consentId: 'consent-123',
timestamp: '2024-01-15T10:00:00.000Z',
expiresAt: '2025-01-15T10:00:00.000Z',
}),
} as Response);
manager = new ConsentManager(mockConfig);
});
afterEach(() => {
vi.clearAllMocks();
});
describe('constructor', () => {
it('should create manager with merged config', () => {
expect(manager).toBeDefined();
});
it('should apply default config values', () => {
// Default consent config should be applied
expect(manager).toBeDefined();
});
});
describe('init', () => {
it('should initialize the manager', async () => {
await manager.init();
// Should have generated fingerprint and be initialized
expect(manager.needsConsent()).toBe(true); // No consent stored
});
it('should only initialize once', async () => {
await manager.init();
await manager.init(); // Second call should be skipped
expect(manager.needsConsent()).toBe(true);
});
it('should emit init event', async () => {
const callback = vi.fn();
manager.on('init', callback);
await manager.init();
expect(callback).toHaveBeenCalled();
});
it('should load existing consent from storage', async () => {
// Pre-set consent in storage
const storageKey = `bp_consent_${mockConfig.siteId}`;
const mockConsent = {
categories: {
essential: true,
functional: true,
analytics: true,
marketing: false,
social: false,
},
vendors: {},
timestamp: new Date().toISOString(),
version: '1.0.0',
};
// Create a simple hash for signature
const data = JSON.stringify(mockConsent) + mockConfig.siteId;
let hash = 5381;
for (let i = 0; i < data.length; i++) {
hash = (hash * 33) ^ data.charCodeAt(i);
}
const signature = (hash >>> 0).toString(16);
localStorage.setItem(
storageKey,
JSON.stringify({
version: '1',
consent: mockConsent,
signature,
})
);
manager = new ConsentManager(mockConfig);
await manager.init();
expect(manager.hasConsent('analytics')).toBe(true);
});
it('should show banner when no consent exists', async () => {
const callback = vi.fn();
manager.on('banner_show', callback);
await manager.init();
expect(callback).toHaveBeenCalled();
expect(manager.isBannerVisible()).toBe(true);
});
});
describe('hasConsent', () => {
it('should return true for essential without initialization', () => {
expect(manager.hasConsent('essential')).toBe(true);
});
it('should return false for other categories without consent', () => {
expect(manager.hasConsent('analytics')).toBe(false);
expect(manager.hasConsent('marketing')).toBe(false);
});
});
describe('hasVendorConsent', () => {
it('should return false when no consent exists', () => {
expect(manager.hasVendorConsent('google-analytics')).toBe(false);
});
});
describe('getConsent', () => {
it('should return null when no consent exists', () => {
expect(manager.getConsent()).toBeNull();
});
it('should return a copy of consent state', async () => {
await manager.init();
await manager.acceptAll();
const consent1 = manager.getConsent();
const consent2 = manager.getConsent();
expect(consent1).not.toBe(consent2); // Different objects
expect(consent1).toEqual(consent2); // Same content
});
});
describe('setConsent', () => {
it('should set consent categories', async () => {
await manager.init();
await manager.setConsent({
essential: true,
functional: true,
analytics: true,
marketing: false,
social: false,
});
expect(manager.hasConsent('analytics')).toBe(true);
expect(manager.hasConsent('marketing')).toBe(false);
});
it('should always keep essential enabled', async () => {
await manager.init();
await manager.setConsent({
essential: false, // Attempting to disable
functional: false,
analytics: false,
marketing: false,
social: false,
});
expect(manager.hasConsent('essential')).toBe(true);
});
it('should emit change event', async () => {
await manager.init();
const callback = vi.fn();
manager.on('change', callback);
await manager.setConsent({
essential: true,
functional: true,
analytics: true,
marketing: false,
social: false,
});
expect(callback).toHaveBeenCalled();
});
it('should save consent locally even on API error', async () => {
vi.mocked(fetch).mockRejectedValueOnce(new Error('Network error'));
await manager.init();
await manager.setConsent({
essential: true,
functional: false,
analytics: true,
marketing: false,
social: false,
});
expect(manager.hasConsent('analytics')).toBe(true);
});
});
describe('acceptAll', () => {
it('should enable all categories', async () => {
await manager.init();
await manager.acceptAll();
expect(manager.hasConsent('essential')).toBe(true);
expect(manager.hasConsent('functional')).toBe(true);
expect(manager.hasConsent('analytics')).toBe(true);
expect(manager.hasConsent('marketing')).toBe(true);
expect(manager.hasConsent('social')).toBe(true);
});
it('should emit accept_all event', async () => {
await manager.init();
const callback = vi.fn();
manager.on('accept_all', callback);
await manager.acceptAll();
expect(callback).toHaveBeenCalled();
});
it('should hide banner', async () => {
await manager.init();
expect(manager.isBannerVisible()).toBe(true);
await manager.acceptAll();
expect(manager.isBannerVisible()).toBe(false);
});
});
describe('rejectAll', () => {
it('should only keep essential enabled', async () => {
await manager.init();
await manager.rejectAll();
expect(manager.hasConsent('essential')).toBe(true);
expect(manager.hasConsent('functional')).toBe(false);
expect(manager.hasConsent('analytics')).toBe(false);
expect(manager.hasConsent('marketing')).toBe(false);
expect(manager.hasConsent('social')).toBe(false);
});
it('should emit reject_all event', async () => {
await manager.init();
const callback = vi.fn();
manager.on('reject_all', callback);
await manager.rejectAll();
expect(callback).toHaveBeenCalled();
});
it('should hide banner', async () => {
await manager.init();
await manager.rejectAll();
expect(manager.isBannerVisible()).toBe(false);
});
});
describe('revokeAll', () => {
it('should clear all consent', async () => {
await manager.init();
await manager.acceptAll();
await manager.revokeAll();
expect(manager.getConsent()).toBeNull();
});
it('should try to revoke on server', async () => {
await manager.init();
await manager.acceptAll();
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
status: 204,
} as Response);
await manager.revokeAll();
// DELETE request should have been made
expect(fetch).toHaveBeenCalledWith(
expect.stringContaining('/consent/'),
expect.objectContaining({ method: 'DELETE' })
);
});
});
describe('exportConsent', () => {
it('should export consent data as JSON', async () => {
await manager.init();
await manager.acceptAll();
const exported = await manager.exportConsent();
const parsed = JSON.parse(exported);
expect(parsed.currentConsent).toBeDefined();
expect(parsed.exportedAt).toBeDefined();
expect(parsed.siteId).toBe('test-site');
});
});
describe('needsConsent', () => {
it('should return true when no consent exists', () => {
expect(manager.needsConsent()).toBe(true);
});
it('should return false when valid consent exists', async () => {
await manager.init();
await manager.acceptAll();
// After acceptAll, consent should exist
expect(manager.getConsent()).not.toBeNull();
// needsConsent checks for currentConsent and expiration
// Since we just accepted all, consent should be valid
const consent = manager.getConsent();
expect(consent?.categories?.essential).toBe(true);
});
});
describe('banner control', () => {
it('should show banner', async () => {
await manager.init();
manager.hideBanner();
manager.showBanner();
expect(manager.isBannerVisible()).toBe(true);
});
it('should hide banner', async () => {
await manager.init();
manager.hideBanner();
expect(manager.isBannerVisible()).toBe(false);
});
it('should emit banner_show event', async () => {
const callback = vi.fn();
manager.on('banner_show', callback);
await manager.init(); // This shows banner
expect(callback).toHaveBeenCalled();
});
it('should emit banner_hide event', async () => {
await manager.init();
const callback = vi.fn();
manager.on('banner_hide', callback);
manager.hideBanner();
expect(callback).toHaveBeenCalled();
});
it('should not show banner if already visible', async () => {
await manager.init();
const callback = vi.fn();
manager.on('banner_show', callback);
callback.mockClear();
manager.showBanner();
manager.showBanner();
expect(callback).toHaveBeenCalledTimes(0); // Already visible from init
});
});
describe('showSettings', () => {
it('should emit settings_open event', async () => {
await manager.init();
const callback = vi.fn();
manager.on('settings_open', callback);
manager.showSettings();
expect(callback).toHaveBeenCalled();
});
});
describe('event handling', () => {
it('should register event listeners', async () => {
await manager.init();
const callback = vi.fn();
manager.on('change', callback);
await manager.acceptAll();
expect(callback).toHaveBeenCalled();
});
it('should unregister event listeners', async () => {
await manager.init();
const callback = vi.fn();
manager.on('change', callback);
manager.off('change', callback);
await manager.acceptAll();
expect(callback).not.toHaveBeenCalled();
});
it('should return unsubscribe function', async () => {
await manager.init();
const callback = vi.fn();
const unsubscribe = manager.on('change', callback);
unsubscribe();
await manager.acceptAll();
expect(callback).not.toHaveBeenCalled();
});
});
describe('callbacks', () => {
it('should call onConsentChange callback', async () => {
const onConsentChange = vi.fn();
manager = new ConsentManager({
...mockConfig,
onConsentChange,
});
await manager.init();
await manager.acceptAll();
expect(onConsentChange).toHaveBeenCalled();
});
it('should call onBannerShow callback', async () => {
const onBannerShow = vi.fn();
manager = new ConsentManager({
...mockConfig,
onBannerShow,
});
await manager.init();
expect(onBannerShow).toHaveBeenCalled();
});
it('should call onBannerHide callback', async () => {
const onBannerHide = vi.fn();
manager = new ConsentManager({
...mockConfig,
onBannerHide,
});
await manager.init();
manager.hideBanner();
expect(onBannerHide).toHaveBeenCalled();
});
});
describe('static methods', () => {
it('should return SDK version', () => {
const version = ConsentManager.getVersion();
expect(version).toBeDefined();
expect(typeof version).toBe('string');
});
});
describe('Google Consent Mode', () => {
it('should update Google Consent Mode when gtag is available', async () => {
const gtag = vi.fn();
(window as unknown as { gtag: typeof gtag }).gtag = gtag;
await manager.init();
await manager.acceptAll();
expect(gtag).toHaveBeenCalledWith(
'consent',
'update',
expect.objectContaining({
analytics_storage: 'granted',
ad_storage: 'granted',
})
);
delete (window as unknown as { gtag?: typeof gtag }).gtag;
});
});
describe('consent expiration', () => {
it('should clear expired consent on init', async () => {
const storageKey = `bp_consent_${mockConfig.siteId}`;
const expiredConsent = {
categories: {
essential: true,
functional: true,
analytics: true,
marketing: false,
social: false,
},
vendors: {},
timestamp: '2020-01-01T00:00:00.000Z', // Very old
version: '1.0.0',
expiresAt: '2020-06-01T00:00:00.000Z', // Expired
};
const data = JSON.stringify(expiredConsent) + mockConfig.siteId;
let hash = 5381;
for (let i = 0; i < data.length; i++) {
hash = (hash * 33) ^ data.charCodeAt(i);
}
const signature = (hash >>> 0).toString(16);
localStorage.setItem(
storageKey,
JSON.stringify({
version: '1',
consent: expiredConsent,
signature,
})
);
manager = new ConsentManager(mockConfig);
await manager.init();
expect(manager.needsConsent()).toBe(true);
});
});
describe('debug mode', () => {
it('should log when debug is enabled', async () => {
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const debugManager = new ConsentManager({
...mockConfig,
debug: true,
});
await debugManager.init();
expect(consoleSpy).toHaveBeenCalled();
consoleSpy.mockRestore();
});
});
describe('consent input normalization', () => {
it('should accept categories object directly', async () => {
await manager.init();
await manager.setConsent({
essential: true,
functional: true,
analytics: false,
marketing: false,
social: false,
});
expect(manager.hasConsent('functional')).toBe(true);
});
it('should accept nested categories object', async () => {
await manager.init();
await manager.setConsent({
categories: {
essential: true,
functional: false,
analytics: true,
marketing: false,
social: false,
},
});
expect(manager.hasConsent('analytics')).toBe(true);
expect(manager.hasConsent('functional')).toBe(false);
});
it('should accept vendors in consent input', async () => {
await manager.init();
await manager.setConsent({
categories: {
essential: true,
functional: true,
analytics: true,
marketing: false,
social: false,
},
vendors: {
'google-analytics': true,
},
});
const consent = manager.getConsent();
expect(consent?.vendors['google-analytics']).toBe(true);
});
});
});