4435e7ea0a
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>
606 lines
16 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|