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); }); }); });