@@ -7,6 +7,7 @@ import { verifyMFATOTPApi } from 'api';
77import { auth } from 'modules/Auth/state' ;
88import { navigateToLibrary } from 'modules/Auth/utils' ;
99import { setupStore } from 'redux/store' ;
10+ import { Mixpanel , MixpanelEventType , MixpanelProps } from 'shared/utils/mixpanel' ;
1011import type { MFAVerifyResponse } from 'modules/Auth/api/api.types' ;
1112
1213import { useMFAVerification } from './useMFAVerification' ;
@@ -250,4 +251,100 @@ describe('useMFAVerification', () => {
250251 expect ( result . current . isSessionExpired ) . toBe ( true ) ;
251252 expect ( result . current . displayError ) . toBe ( 'mfaSessionExpired' ) ;
252253 } ) ;
254+
255+ describe ( 'Mixpanel Tracking' , ( ) => {
256+ beforeEach ( ( ) => {
257+ vi . mocked ( Mixpanel . track ) . mockClear ( ) ;
258+ } ) ;
259+
260+ it ( 'should track LoginSuccessful with MFA via Authenticator App on successful TOTP verification' , async ( ) => {
261+ const mockDispatch = vi . spyOn ( store , 'dispatch' ) ;
262+ mockDispatch . mockResolvedValueOnce ( {
263+ type : 'auth/verifyMFATOTP/fulfilled' ,
264+ payload : { result : { user : { id : 'user-123' } } } ,
265+ } ) ;
266+
267+ const { result } = renderHook ( ( ) => useMFAVerification ( 'totp' ) , { wrapper } ) ;
268+
269+ await act ( async ( ) => {
270+ await result . current . verifyCode ( '123456' ) ;
271+ } ) ;
272+
273+ expect ( Mixpanel . track ) . toHaveBeenCalledWith ( {
274+ action : MixpanelEventType . LoginSuccessful ,
275+ [ MixpanelProps . MFAUsed ] : true ,
276+ [ MixpanelProps . MFAMethodUsed ] : 'Authenticator App' ,
277+ } ) ;
278+ } ) ;
279+
280+ it ( 'should track LoginSuccessful with MFA via Backup Codes on successful recovery code verification' , async ( ) => {
281+ const mockDispatch = vi . spyOn ( store , 'dispatch' ) ;
282+ mockDispatch . mockResolvedValueOnce ( {
283+ type : 'auth/verifyMFARecoveryCode/fulfilled' ,
284+ payload : { result : { user : { id : 'user-123' } } } ,
285+ } ) ;
286+
287+ const { result } = renderHook ( ( ) => useMFAVerification ( 'recovery' ) , { wrapper } ) ;
288+
289+ await act ( async ( ) => {
290+ await result . current . verifyCode ( 'ABCDE-12345' ) ;
291+ } ) ;
292+
293+ expect ( Mixpanel . track ) . toHaveBeenCalledWith ( {
294+ action : MixpanelEventType . LoginSuccessful ,
295+ [ MixpanelProps . MFAUsed ] : true ,
296+ [ MixpanelProps . MFAMethodUsed ] : 'Backup Codes' ,
297+ } ) ;
298+ } ) ;
299+
300+ it ( 'should track LoginFailed with MFA stage and Authenticator App on TOTP failure' , async ( ) => {
301+ const mockResponse = {
302+ data : {
303+ result : {
304+ message : 'Invalid TOTP code' ,
305+ } ,
306+ } ,
307+ status : 400 ,
308+ statusText : 'Bad Request' ,
309+ headers : { } as AxiosHeaders ,
310+ config : {
311+ headers : { } as AxiosHeaders ,
312+ } as any ,
313+ } satisfies AxiosResponse < MFAVerifyResponse > ;
314+
315+ mockVerifyAPI . mockResolvedValueOnce ( mockResponse ) ;
316+
317+ const { result } = renderHook ( ( ) => useMFAVerification ( 'totp' ) , { wrapper } ) ;
318+
319+ await act ( async ( ) => {
320+ await result . current . verifyCode ( '123456' ) ;
321+ } ) ;
322+
323+ expect ( Mixpanel . track ) . toHaveBeenCalledWith ( {
324+ action : MixpanelEventType . LoginFailed ,
325+ [ MixpanelProps . FailureStage ] : 'MFA' ,
326+ [ MixpanelProps . MFAMethodUsed ] : 'Authenticator App' ,
327+ } ) ;
328+ } ) ;
329+
330+ it ( 'should track LoginFailed with MFA stage and Backup Codes on recovery code failure' , async ( ) => {
331+ const mockDispatch = vi . spyOn ( store , 'dispatch' ) ;
332+ mockDispatch . mockResolvedValueOnce ( {
333+ type : 'auth/verifyMFARecoveryCode/rejected' ,
334+ payload : 'Invalid recovery code' ,
335+ } ) ;
336+
337+ const { result } = renderHook ( ( ) => useMFAVerification ( 'recovery' ) , { wrapper } ) ;
338+
339+ await act ( async ( ) => {
340+ await result . current . verifyCode ( 'INVALID-CODE' ) ;
341+ } ) ;
342+
343+ expect ( Mixpanel . track ) . toHaveBeenCalledWith ( {
344+ action : MixpanelEventType . LoginFailed ,
345+ [ MixpanelProps . FailureStage ] : 'MFA' ,
346+ [ MixpanelProps . MFAMethodUsed ] : 'Backup Codes' ,
347+ } ) ;
348+ } ) ;
349+ } ) ;
253350} ) ;
0 commit comments