import { PrivacyManager } from '../PrivacyManager'; import { ConversionIQConfig, DataType } from '../../types'; // Mock ShadowDOMManager jest.mock('../ShadowDOMManager', () => ({ ShadowDOMManager: jest.fn().mockImplementation(() => ({ createModal: jest.fn().mockReturnValue(document.createElement('div')), show: jest.fn(), hideAll: jest.fn(), destroy: jest.fn() })) })); describe('PrivacyManager', () => { let privacyManager: PrivacyManager; let mockConfig: ConversionIQConfig; beforeEach(() => { mockConfig = { websiteId: 'test-website-id', endpoint: 'https://api.conversioniq.com', debug: false, enablePrivacyMode: false }; privacyManager = new PrivacyManager(mockConfig); // Clear localStorage localStorage.clear(); // Reset mocks jest.clearAllMocks(); }); afterEach(() => { privacyManager.destroy(); localStorage.clear(); }); describe('canTrack', () => { it('should allow essential tracking by default', () => { expect(privacyManager.canTrack('essential')).toBe(true); }); it('should not allow non-essential tracking without consent', () => { expect(privacyManager.canTrack('analytics')).toBe(false); expect(privacyManager.canTrack('marketing')).toBe(false); expect(privacyManager.canTrack('functional')).toBe(false); }); it('should respect consent settings', () => { privacyManager.updateConsent({ analytics: true, marketing: false, functional: true }); expect(privacyManager.canTrack('analytics')).toBe(true); expect(privacyManager.canTrack('marketing')).toBe(false); expect(privacyManager.canTrack('functional')).toBe(true); expect(privacyManager.canTrack('essential')).toBe(true); // Always true }); it('should not allow tracking after consent expires', () => { // Set consent that expires immediately const expiredConsent = { version: '1.0', timestamp: Date.now() - 1000, settings: { essential: true, analytics: true, marketing: true, functional: true }, jurisdiction: 'US', expiresAt: Date.now() - 1000 // Expired }; localStorage.setItem('conversioniq_consent', JSON.stringify(expiredConsent)); // Reinitialize to load expired consent privacyManager = new PrivacyManager(mockConfig); expect(privacyManager.canTrack('analytics')).toBe(false); expect(privacyManager.canTrack('essential')).toBe(true); // Always allowed }); }); describe('updateConsent', () => { it('should update consent settings', () => { privacyManager.updateConsent({ analytics: true, marketing: true }); const status = privacyManager.getConsentStatus(); expect(status.analytics).toBe(true); expect(status.marketing).toBe(true); expect(status.essential).toBe(true); // Always true }); it('should persist consent to localStorage', () => { privacyManager.updateConsent({ analytics: true, functional: true }); const stored = localStorage.getItem('conversioniq_consent'); expect(stored).toBeTruthy(); const parsedConsent = JSON.parse(stored!); expect(parsedConsent.settings.analytics).toBe(true); expect(parsedConsent.settings.functional).toBe(true); }); it('should not allow essential to be set to false', () => { privacyManager.updateConsent({ essential: false, // This should be ignored analytics: true }); const status = privacyManager.getConsentStatus(); expect(status.essential).toBe(true); // Should remain true expect(status.analytics).toBe(true); }); }); describe('needsConsent', () => { it('should not need consent when privacy mode is disabled and not in regulated jurisdiction', () => { // Mock non-EU timezone jest.spyOn(Intl.DateTimeFormat.prototype, 'resolvedOptions').mockReturnValue({ timeZone: 'America/New_York', locale: 'en-US', calendar: 'gregory', numberingSystem: 'latn' } as any); expect(privacyManager.needsConsent()).toBe(false); }); it('should need consent when privacy mode is explicitly enabled', () => { const configWithPrivacy = { ...mockConfig, enablePrivacyMode: true }; const privacyEnabledManager = new PrivacyManager(configWithPrivacy); expect(privacyEnabledManager.needsConsent()).toBe(true); privacyEnabledManager.destroy(); }); it('should need consent in EU jurisdiction', () => { // Mock EU timezone jest.spyOn(Intl.DateTimeFormat.prototype, 'resolvedOptions').mockReturnValue({ timeZone: 'Europe/Berlin', locale: 'de-DE', calendar: 'gregory', numberingSystem: 'latn' } as any); const euManager = new PrivacyManager(mockConfig); expect(euManager.needsConsent()).toBe(true); euManager.destroy(); }); it('should not need consent when valid consent exists', () => { // First update consent privacyManager.updateConsent({ analytics: true }); // Then check if consent is still needed expect(privacyManager.needsConsent()).toBe(false); }); }); describe('getConsentStatus', () => { it('should return default consent when no consent exists', () => { const status = privacyManager.getConsentStatus(); expect(status).toEqual({ essential: true, functional: false, analytics: false, marketing: false }); }); it('should return current consent settings', () => { privacyManager.updateConsent({ analytics: true, marketing: true }); const status = privacyManager.getConsentStatus(); expect(status.analytics).toBe(true); expect(status.marketing).toBe(true); expect(status.functional).toBe(false); expect(status.essential).toBe(true); }); }); describe('getPrivacyCompliantUserId', () => { it('should return null when analytics tracking is not allowed', () => { expect(privacyManager.getPrivacyCompliantUserId()).toBeNull(); }); it('should return user ID when analytics tracking is allowed', () => { privacyManager.updateConsent({ analytics: true }); const userId = privacyManager.getPrivacyCompliantUserId(); expect(userId).toBeTruthy(); expect(userId).toMatch(/^ciq_/); }); it('should return the same user ID on subsequent calls', () => { privacyManager.updateConsent({ analytics: true }); const userId1 = privacyManager.getPrivacyCompliantUserId(); const userId2 = privacyManager.getPrivacyCompliantUserId(); expect(userId1).toBe(userId2); }); it('should generate session-only ID when localStorage fails', () => { privacyManager.updateConsent({ analytics: true }); // Mock localStorage to fail const setItemSpy = jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { throw new Error('Storage quota exceeded'); }); const getItemSpy = jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => { throw new Error('Storage unavailable'); }); const userId = privacyManager.getPrivacyCompliantUserId(); expect(userId).toBeTruthy(); expect(userId).toMatch(/^ciq_session_/); setItemSpy.mockRestore(); getItemSpy.mockRestore(); }); }); describe('jurisdiction detection', () => { it('should detect EU timezone correctly', () => { jest.spyOn(Intl.DateTimeFormat.prototype, 'resolvedOptions').mockReturnValue({ timeZone: 'Europe/London', locale: 'en-GB', calendar: 'gregory', numberingSystem: 'latn' } as any); const euManager = new PrivacyManager(mockConfig); expect(euManager.needsConsent()).toBe(true); euManager.destroy(); }); it('should detect California timezone correctly', () => { jest.spyOn(Intl.DateTimeFormat.prototype, 'resolvedOptions').mockReturnValue({ timeZone: 'America/Los_Angeles', locale: 'en-US', calendar: 'gregory', numberingSystem: 'latn' } as any); const caManager = new PrivacyManager(mockConfig); expect(caManager.needsConsent()).toBe(true); caManager.destroy(); }); it('should fallback to language detection when timezone fails', () => { jest.spyOn(Intl.DateTimeFormat.prototype, 'resolvedOptions').mockImplementation(() => { throw new Error('Timezone unavailable'); }); Object.defineProperty(navigator, 'language', { value: 'en-GB', writable: true }); const langManager = new PrivacyManager(mockConfig); expect(langManager.needsConsent()).toBe(true); langManager.destroy(); }); }); describe('persistence', () => { it('should load consent from localStorage on initialization', () => { const consentData = { version: '1.0', timestamp: Date.now(), settings: { essential: true, analytics: true, marketing: false, functional: true }, jurisdiction: 'US', expiresAt: Date.now() + (13 * 30 * 24 * 60 * 60 * 1000) // 13 months }; localStorage.setItem('conversioniq_consent', JSON.stringify(consentData)); const newManager = new PrivacyManager(mockConfig); expect(newManager.canTrack('analytics')).toBe(true); expect(newManager.canTrack('marketing')).toBe(false); expect(newManager.canTrack('functional')).toBe(true); newManager.destroy(); }); it('should handle corrupted localStorage data gracefully', () => { localStorage.setItem('conversioniq_consent', 'invalid-json'); expect(() => { const newManager = new PrivacyManager(mockConfig); newManager.destroy(); }).not.toThrow(); }); }); });