import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; describe('MixpanelService', () => { let MixpanelService: any; let mixpanelService: any; let mockAnalytics: any; beforeEach(() => { vi.stubEnv('MIXPANEL_TOKEN', 'test-token'); vi.stubEnv('GA4_MEASUREMENT_ID', 'G-TEST123'); mockAnalytics = { track: vi.fn(), identify: vi.fn(), group: vi.fn(), flush: vi.fn(), }; vi.doMock('@segment/analytics-node', () => ({ Analytics: vi.fn(() => mockAnalytics), })); vi.resetModules(); }); afterEach(async () => { vi.unstubAllEnvs(); vi.restoreAllMocks(); const config = await import('../config/analytics.config'); if (config.analyticsEnv) { vi.clearAllMocks(); } }); it('implements singleton pattern via getInstance', async () => { const mod = await import('../services/mixpanel.service'); MixpanelService = mod.MixpanelService; const instance1 = MixpanelService.getInstance(); const instance2 = MixpanelService.getInstance(); expect(instance1).toBe(instance2); }); it('exports singleton instance as mixpanelService', async () => { const mod = await import('../services/mixpanel.service'); mixpanelService = mod.mixpanelService; const instance = MixpanelService.getInstance(); expect(mixpanelService).toBe(instance); }); it('validated properties override raw properties (P0 fix)', async () => { const mod = await import('../services/mixpanel.service'); mixpanelService = mod.mixpanelService; await mixpanelService.track( mod.EventType.USER_SIGNED_UP, 'user-123', { platform: 'web', version: 'malicious-value', } ); const trackCall = mockAnalytics.track.mock.calls[0]; const properties = trackCall[0].properties; expect(properties.platform).toBe('web'); expect(properties.version).toBe('malicious-value'); expect(properties.timestamp).toBeDefined(); }); it('adds timestamp to all tracked events', async () => { const mod = await import('../services/mixpanel.service'); mixpanelService = mod.mixpanelService; await mixpanelService.track(mod.EventType.USER_SIGNED_UP, 'user-123', {}); const trackCall = mockAnalytics.track.mock.calls[0]; expect(trackCall[0].properties.timestamp).toBeInstanceOf(Date); }); it('tracks user signup event correctly', async () => { const mod = await import('../services/mixpanel.service'); mixpanelService = mod.mixpanelService; await mixpanelService.userSignedUp('user-123', 'pro', 'google'); const trackCall = mockAnalytics.track.mock.calls[0]; expect(trackCall[0].event).toBe('user_signed_up'); expect(trackCall[0].distinctId).toBe('user-123'); expect(trackCall[0].properties.plan).toBe('pro'); expect(trackCall[0].properties.referrer).toBe('google'); }); it('tracks user upgrade event correctly', async () => { const mod = await import('../services/mixpanel.service'); mixpanelService = mod.mixpanelService; await mixpanelService.userUpgraded('user-123', 'free', 'pro', 29.99); const trackCall = mockAnalytics.track.mock.calls[0]; expect(trackCall[0].event).toBe('user_upgraded'); expect(trackCall[0].properties.fromTier).toBe('free'); expect(trackCall[0].properties.toTier).toBe('pro'); expect(trackCall[0].properties.mrr).toBe(29.99); }); it('tracks spam blocked event with hashed phone number', async () => { const mod = await import('../services/mixpanel.service'); mixpanelService = mod.mixpanelService; await mixpanelService.spamBlocked('user-123', '+14155552671', 0.95, 'ml'); const trackCall = mockAnalytics.track.mock.calls[0]; expect(trackCall[0].event).toBe('spam_blocked'); expect(trackCall[0].properties.phoneNumber).toMatch(/^sha256_/); expect(trackCall[0].properties.confidence).toBe(0.95); expect(trackCall[0].properties.method).toBe('ml'); }); it('does not send raw phone number in spam blocked events', async () => { const mod = await import('../services/mixpanel.service'); mixpanelService = mod.mixpanelService; const rawPhone = '+14155552671'; await mixpanelService.spamBlocked('user-123', rawPhone, 0.95, 'ml'); const trackCall = mockAnalytics.track.mock.calls[0]; expect(trackCall[0].properties.phoneNumber).not.toBe(rawPhone); }); it('calls identify correctly', async () => { const mod = await import('../services/mixpanel.service'); mixpanelService = mod.mixpanelService; await mixpanelService.identify('user-123', { name: 'John Doe', plan: 'pro' }); expect(mockAnalytics.identify).toHaveBeenCalledWith({ distinctId: 'user-123', traits: { name: 'John Doe', plan: 'pro' }, }); }); it('calls group correctly', async () => { const mod = await import('../services/mixpanel.service'); mixpanelService = mod.mixpanelService; await mixpanelService.group('org-123', 'organization', { name: 'Acme Corp' }); expect(mockAnalytics.group).toHaveBeenCalledWith({ groupKey: 'organization', groupId: 'org-123', traits: { name: 'Acme Corp' }, }); }); it('calls flush correctly', async () => { const mod = await import('../services/mixpanel.service'); mixpanelService = mod.mixpanelService; await mixpanelService.flush(); expect(mockAnalytics.flush).toHaveBeenCalled(); }); it('handles exposure detected event', async () => { const mod = await import('../services/mixpanel.service'); mixpanelService = mod.mixpanelService; await mixpanelService.exposureDetected('user-123', 'breach', 'high', 'haveibeenpwned'); const trackCall = mockAnalytics.track.mock.calls[0]; expect(trackCall[0].event).toBe('exposure_detected'); expect(trackCall[0].properties.exposureType).toBe('breach'); expect(trackCall[0].properties.severity).toBe('high'); expect(trackCall[0].properties.source).toBe('haveibeenpwned'); }); });