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>
This commit is contained in:
@@ -0,0 +1,605 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user