@@ -338,6 +338,176 @@ test.describe('severity-1 #smoke', () => {
338338 await expect ( settings . recoveryKey . status ) . toHaveText ( 'Not Set' ) ;
339339 } ) ;
340340
341+ test ( 'provide invalid recovery key then reset with totp authenticator code' , async ( {
342+ page,
343+ target,
344+ pages : {
345+ signin,
346+ resetPassword,
347+ settings,
348+ totp,
349+ confirmTotpResetPassword,
350+ recoveryKey,
351+ } ,
352+ testAccountTracker,
353+ } ) => {
354+ const credentials = await testAccountTracker . signUp ( ) ;
355+
356+ // Sign Into Settings
357+ await signin . goto ( ) ;
358+ await signin . fillOutEmailFirstForm ( credentials . email ) ;
359+ await signin . fillOutPasswordForm ( credentials . password ) ;
360+ await expect ( page ) . toHaveURL ( / s e t t i n g s / ) ;
361+
362+ // Create Recovery Key
363+ await settings . recoveryKey . createButton . click ( ) ;
364+ await settings . confirmMfaGuard ( credentials . email ) ;
365+ await recoveryKey . createRecoveryKey ( credentials . password , 'hint' ) ;
366+ await expect ( settings . settingsHeading ) . toBeVisible ( ) ;
367+ await expect ( settings . recoveryKey . status ) . toHaveText ( 'Enabled' ) ;
368+
369+ // Enable 2FA
370+ await expect ( settings . totp . status ) . toHaveText ( 'Disabled' ) ;
371+ await settings . totp . addButton . click ( ) ;
372+ await settings . confirmMfaGuard ( credentials . email ) ;
373+ const { secret } =
374+ await totp . setUpTwoStepAuthWithQrAndBackupCodesChoice ( credentials ) ;
375+ await expect ( settings . settingsHeading ) . toBeVisible ( ) ;
376+ await expect ( settings . alertBar ) . toContainText (
377+ 'Two-step authentication has been enabled'
378+ ) ;
379+ await expect ( settings . totp . status ) . toHaveText ( 'Enabled' ) ;
380+
381+ // Start Reset Password Flow
382+ await settings . signOut ( ) ;
383+ await signin . goto ( ) ;
384+ await signin . fillOutEmailFirstForm ( credentials . email ) ;
385+ await signin . forgotPasswordLink . click ( ) ;
386+ await resetPassword . fillOutEmailForm ( credentials . email ) ;
387+ const code = await target . emailClient . getResetPasswordCode (
388+ credentials . email
389+ ) ;
390+ await await resetPassword . fillOutResetPasswordCodeForm ( code ) ;
391+
392+ // Enter Invalid Recovery Key
393+ await expect ( resetPassword . confirmRecoveryKeyHeading ) . toBeVisible ( ) ;
394+ await resetPassword . recoveryKeyTextbox . fill (
395+ '12345678-12345678-12345678-12345678'
396+ ) ;
397+ await resetPassword . confirmRecoveryKeyButton . click ( ) ;
398+ await expect ( resetPassword . errorBanner ) . toBeVisible ( ) ;
399+
400+ // Note! This is the start of edge case this test validates. When we provided
401+ // a recovery key, we took our password forgot token and exchange it for an
402+ // account reset token, which resulted in the passwordForgotToken becoming
403+ // invalid. We therefore must use the account reset token for the rest of
404+ // the web requests in this flow.
405+
406+ // Click Forgot Key Link
407+ await resetPassword . forgotKeyLink . click ( ) ;
408+
409+ // Provide TOTP Code from Authenticator
410+ await page . waitForURL ( / c o n f i r m _ t o t p _ r e s e t _ p a s s w o r d / ) ;
411+ await expect ( page . getByLabel ( 'Enter 6-digit code' ) ) . toBeVisible ( ) ;
412+ const totpCode = await getTotpCode ( secret ) ;
413+ await confirmTotpResetPassword . fillOutCodeForm ( totpCode ) ;
414+
415+ // Create a New Password
416+ await expect ( resetPassword . dataLossWarning ) . toBeVisible ( ) ;
417+ const newPassword = testAccountTracker . generatePassword ( ) ;
418+ await resetPassword . fillOutNewPasswordForm ( newPassword ) ;
419+ testAccountTracker . updateAccountPassword ( credentials . email , newPassword ) ;
420+
421+ // Observe Settings
422+ await expect ( settings . settingsHeading ) . toBeVisible ( ) ;
423+ await expect ( settings . alertBar ) . toHaveText ( 'Your password has been reset' ) ;
424+ } ) ;
425+
426+ test ( 'provide invalid recovery key then reset with totp back up code' , async ( {
427+ page,
428+ target,
429+ pages : { signin, resetPassword, settings, totp, recoveryKey } ,
430+ testAccountTracker,
431+ } ) => {
432+ const credentials = await testAccountTracker . signUp ( ) ;
433+
434+ // Sign Into Settings
435+ await signin . goto ( ) ;
436+ await signin . fillOutEmailFirstForm ( credentials . email ) ;
437+ await signin . fillOutPasswordForm ( credentials . password ) ;
438+
439+ // Create Recovery Key
440+ await expect ( page ) . toHaveURL ( / s e t t i n g s / ) ;
441+ await settings . recoveryKey . createButton . click ( ) ;
442+ await settings . confirmMfaGuard ( credentials . email ) ;
443+ await recoveryKey . createRecoveryKey ( credentials . password , 'hint' ) ;
444+
445+ await expect ( settings . settingsHeading ) . toBeVisible ( ) ;
446+ await expect ( settings . recoveryKey . status ) . toHaveText ( 'Enabled' ) ;
447+
448+ // Enable 2FA
449+ await expect ( settings . settingsHeading ) . toBeVisible ( ) ;
450+ await expect ( settings . totp . status ) . toHaveText ( 'Disabled' ) ;
451+ await settings . totp . addButton . click ( ) ;
452+ await settings . confirmMfaGuard ( credentials . email ) ;
453+ const { recoveryCodes } =
454+ await totp . setUpTwoStepAuthWithQrAndBackupCodesChoice ( credentials ) ;
455+
456+ await expect ( settings . settingsHeading ) . toBeVisible ( ) ;
457+ await expect ( settings . alertBar ) . toContainText (
458+ 'Two-step authentication has been enabled'
459+ ) ;
460+ await expect ( settings . totp . status ) . toHaveText ( 'Enabled' ) ;
461+
462+ // Start Reset Password Flow
463+ await settings . signOut ( ) ;
464+ await signin . goto ( ) ;
465+ await signin . fillOutEmailFirstForm ( credentials . email ) ;
466+ await signin . forgotPasswordLink . click ( ) ;
467+ await resetPassword . fillOutEmailForm ( credentials . email ) ;
468+ const code = await target . emailClient . getResetPasswordCode (
469+ credentials . email
470+ ) ;
471+ await resetPassword . fillOutResetPasswordCodeForm ( code ) ;
472+
473+ // Enter Invalid Recovery Key
474+ await expect ( resetPassword . confirmRecoveryKeyHeading ) . toBeVisible ( ) ;
475+ await resetPassword . recoveryKeyTextbox . fill (
476+ '12345678-12345678-12345678-12345678'
477+ ) ;
478+ await resetPassword . confirmRecoveryKeyButton . click ( ) ;
479+ await expect ( resetPassword . errorBanner ) . toBeVisible ( ) ;
480+
481+ /// Note! This is the start of edge case this test validates. When we provided
482+ // a recovery key, we took our password forgot token and exchange it for an
483+ // account reset token, which resulted in the passwordForgotToken becoming
484+ // invalid. We therefore must use the account reset token for the rest of
485+ // the web requests in this flow.
486+
487+ // Click Forgot Key Link
488+ await resetPassword . forgotKeyLink . click ( ) ;
489+
490+ // Verify TOTP code entry page is shown
491+ await page . waitForURL ( / c o n f i r m _ t o t p _ r e s e t _ p a s s w o r d / ) ;
492+ await expect ( page . getByLabel ( 'Enter 6-digit code' ) ) . toBeVisible ( ) ;
493+ await resetPassword . clickTroubleEnteringCode ( ) ;
494+
495+ // Provide a Backup TOTP Codes
496+ await expect ( totp . confirmBackupCodeHeading ) . toBeVisible ( ) ;
497+ await totp . confirmBackupCodeTextbox . fill ( recoveryCodes [ 0 ] ) ;
498+ await totp . confirmBackupCodeConfirmButton . click ( ) ;
499+
500+ // Create a New Password
501+ await expect ( resetPassword . dataLossWarning ) . toBeVisible ( ) ;
502+ const newPassword = testAccountTracker . generatePassword ( ) ;
503+ await resetPassword . fillOutNewPasswordForm ( newPassword ) ;
504+ testAccountTracker . updateAccountPassword ( credentials . email , newPassword ) ;
505+
506+ // Observe Settings Page
507+ await expect ( settings . settingsHeading ) . toBeVisible ( ) ;
508+ await expect ( settings . alertBar ) . toHaveText ( 'Your password has been reset' ) ;
509+ } ) ;
510+
341511 test ( 'can reset password with unverified 2FA and skip recovery key' , async ( {
342512 page,
343513 target,
@@ -570,4 +740,114 @@ test.describe('reset password with recovery phone', () => {
570740
571741 await expect ( settings . settingsHeading ) . toBeVisible ( ) ;
572742 } ) ;
743+
744+ test ( 'provide invalid recovery key then reset with recovery phone' , async ( {
745+ page,
746+ target,
747+ pages : {
748+ signin,
749+ resetPassword,
750+ settings,
751+ totp,
752+ recoveryKey,
753+ recoveryPhone,
754+ } ,
755+ testAccountTracker,
756+ } ) => {
757+ const credentials = await testAccountTracker . signUp ( ) ;
758+ const testNumber = target . smsClient . getPhoneNumber ( ) ;
759+
760+ // Sign Into Settings
761+ await signin . goto ( ) ;
762+ await signin . fillOutEmailFirstForm ( credentials . email ) ;
763+ await signin . fillOutPasswordForm ( credentials . password ) ;
764+ await expect ( page ) . toHaveURL ( / s e t t i n g s / ) ;
765+
766+ // Create Recovery Key
767+ await settings . recoveryKey . createButton . click ( ) ;
768+ await settings . confirmMfaGuard ( credentials . email ) ;
769+ await recoveryKey . createRecoveryKey ( credentials . password , 'hint' ) ;
770+ await expect ( settings . settingsHeading ) . toBeVisible ( ) ;
771+ await expect ( settings . recoveryKey . status ) . toHaveText ( 'Enabled' ) ;
772+
773+ // Enable 2FA
774+ await expect ( settings . settingsHeading ) . toBeVisible ( ) ;
775+ await expect ( settings . totp . status ) . toHaveText ( 'Disabled' ) ;
776+ await settings . totp . addButton . click ( ) ;
777+ await settings . confirmMfaGuard ( credentials . email ) ;
778+ await totp . setUpTwoStepAuthWithQrAndBackupCodesChoice ( credentials ) ;
779+ await expect ( settings . settingsHeading ) . toBeVisible ( ) ;
780+ await expect ( settings . alertBar ) . toContainText (
781+ 'Two-step authentication has been enabled'
782+ ) ;
783+ await expect ( settings . totp . status ) . toHaveText ( 'Enabled' ) ;
784+
785+ // Enable Recovery Phone
786+ await settings . totp . addRecoveryPhoneButton . click ( ) ;
787+ await page . waitForURL ( / r e c o v e r y _ p h o n e \/ s e t u p / ) ;
788+ await expect ( recoveryPhone . addHeader ( ) ) . toBeVisible ( ) ;
789+ await recoveryPhone . enterPhoneNumber ( testNumber ) ;
790+ await recoveryPhone . clickSendCode ( ) ;
791+ await expect ( recoveryPhone . confirmHeader ) . toBeVisible ( ) ;
792+ let smsCode = await target . smsClient . getCode ( { ...credentials } ) ;
793+ await recoveryPhone . enterCode ( smsCode ) ;
794+ await recoveryPhone . clickConfirm ( ) ;
795+ await page . waitForURL ( / s e t t i n g s / ) ;
796+ await expect ( settings . alertBar ) . toHaveText ( 'Recovery phone added' ) ;
797+
798+ // Start Reset Password Flow
799+ await settings . signOut ( ) ;
800+ await signin . goto ( ) ;
801+ await signin . fillOutEmailFirstForm ( credentials . email ) ;
802+ await signin . forgotPasswordLink . click ( ) ;
803+ await resetPassword . fillOutEmailForm ( credentials . email ) ;
804+ const code = await target . emailClient . getResetPasswordCode (
805+ credentials . email
806+ ) ;
807+ await resetPassword . fillOutResetPasswordCodeForm ( code ) ;
808+ await expect ( resetPassword . confirmRecoveryKeyHeading ) . toBeVisible ( ) ;
809+
810+ // Enter Invalid Recovery Key
811+ await resetPassword . recoveryKeyTextbox . fill (
812+ '12345678-12345678-12345678-12345678'
813+ ) ;
814+ await resetPassword . confirmRecoveryKeyButton . click ( ) ;
815+ await expect ( resetPassword . errorBanner ) . toBeVisible ( ) ;
816+
817+ // Note! This is the start of edge case this test validates. When we provided
818+ // a recovery key, we took our password forgot token and exchange it for an
819+ // account reset token, which resulted in the passwordForgotToken becoming
820+ // invalid. We therefore must use the account reset token for the rest of
821+ // the web requests in this flow.
822+
823+ // Click Forgot Key Link
824+ await resetPassword . forgotKeyLink . click ( ) ;
825+
826+ // Verify TOTP code entry page is shown
827+ await page . waitForURL ( / c o n f i r m _ t o t p _ r e s e t _ p a s s w o r d / ) ;
828+ await expect ( page . getByLabel ( 'Enter 6-digit code' ) ) . toBeVisible ( ) ;
829+ await resetPassword . clickTroubleEnteringCode ( ) ;
830+
831+ // Choose Recovery Phone Option
832+ await page . waitForURL ( / r e s e t _ p a s s w o r d _ t o t p _ r e c o v e r y _ c h o i c e / ) ;
833+ await resetPassword . clickChoosePhone ( ) ;
834+ await resetPassword . clickContinueButton ( ) ;
835+
836+ // Provide SMS Code
837+ await page . waitForURL ( / r e s e t _ p a s s w o r d _ r e c o v e r y _ p h o n e / ) ;
838+
839+ smsCode = await target . smsClient . getCode ( { ...credentials } ) ;
840+ await resetPassword . fillRecoveryPhoneCodeForm ( smsCode ) ;
841+ await resetPassword . clickConfirmButton ( ) ;
842+
843+ // Create a New Password
844+ await expect ( resetPassword . dataLossWarning ) . toBeVisible ( ) ;
845+ const newPassword = testAccountTracker . generatePassword ( ) ;
846+ await resetPassword . fillOutNewPasswordForm ( newPassword ) ;
847+ testAccountTracker . updateAccountPassword ( credentials . email , newPassword ) ;
848+
849+ // Observe Settings Page
850+ await expect ( settings . settingsHeading ) . toBeVisible ( ) ;
851+ await expect ( settings . alertBar ) . toHaveText ( 'Your password has been reset' ) ;
852+ } ) ;
573853} ) ;
0 commit comments