44
55import { expect , test } from '../../lib/fixtures/standard' ;
66import { getCode } from 'fxa-settings/src/lib/totp' ;
7+ import { TargetName , getFromEnvWithFallback } from '../../lib/targets' ;
8+
9+ // Default test number, see Twilio test credentials phone numbers:
10+ // https://www.twilio.com/docs/iam/test-credentials
11+ const TEST_NUMBER = '4159929960' ;
12+
13+ /**
14+ * Checks the process env for a configured twilio test phone number. Defaults
15+ * to generic magic test number if one is not provided.
16+ * @param targetName The test target name. eg local, stage, prod.
17+ * @returns
18+ */
19+ function getPhoneNumber ( targetName : TargetName ) {
20+ if ( targetName === 'local' ) {
21+ return TEST_NUMBER ;
22+ }
23+ return getFromEnvWithFallback (
24+ 'FUNCTIONAL_TESTS__TWILIO__TEST_NUMBER' ,
25+ targetName ,
26+ TEST_NUMBER
27+ ) ;
28+ }
29+
30+ function usingRealTestPhoneNumber ( targetName : TargetName ) {
31+ return getPhoneNumber ( targetName ) !== TEST_NUMBER ;
32+ }
733
834test . describe ( 'severity-1 #smoke' , ( ) => {
935 test ( 'can reset password with 2FA enabled' , async ( {
@@ -332,3 +358,130 @@ test.describe('severity-1 #smoke', () => {
332358 credentials . password = newPassword ;
333359 } ) ;
334360} ) ;
361+
362+ test . describe ( 'reset password with recovery phone' , ( ) => {
363+ test . describe . configure ( { mode : 'serial' } ) ;
364+
365+ test . beforeAll ( async ( { target } ) => {
366+ /**
367+ * Important! Twilio does not allow you to fetch messages when using test
368+ * credentials. Twilio also does not allow you to send messages to magic
369+ * test numbers with real credentials.
370+ *
371+ * Therefore, if a 'magic' test number is configured, then we need to
372+ * use redis to peek at codes sent out, and if a 'real' testing phone
373+ * number is being being used, then we need to check the Twilio API for
374+ * the message sent out and look at the code within.
375+ */
376+ if (
377+ usingRealTestPhoneNumber ( target . name ) &&
378+ ! target . smsClient . isTwilioEnabled ( )
379+ ) {
380+ throw new Error (
381+ 'Twilio must be enabled when using a real test number.'
382+ ) ;
383+ }
384+ if (
385+ ! usingRealTestPhoneNumber ( target . name ) &&
386+ ! target . smsClient . isRedisEnabled ( )
387+ ) {
388+ throw new Error ( 'Redis must be enabled when using a real test number.' ) ;
389+ }
390+ } ) ;
391+
392+ test . beforeEach ( async ( { pages : { configPage } } ) => {
393+ // Ensure that the feature flag is enabled
394+ const config = await configPage . getConfig ( ) ;
395+ test . skip ( config . featureFlags . recoveryPhonePasswordReset2fa !== true ) ;
396+ } ) ;
397+
398+ test ( 'can reset password with 2FA enabled using recovery phone' , async ( {
399+ page,
400+ target,
401+ pages : { signin, resetPassword, settings, totp, recoveryPhone } ,
402+ testAccountTracker,
403+ } ) => {
404+ const credentials = await testAccountTracker . signUp ( ) ;
405+ const newPassword = testAccountTracker . generatePassword ( ) ;
406+
407+ await signin . goto ( ) ;
408+ await signin . fillOutEmailFirstForm ( credentials . email ) ;
409+ await signin . fillOutPasswordForm ( credentials . password ) ;
410+
411+ await expect ( settings . settingsHeading ) . toBeVisible ( ) ;
412+ await expect ( settings . totp . status ) . toHaveText ( 'Disabled' ) ;
413+
414+ await settings . totp . addButton . click ( ) ;
415+ await totp . fillOutTotpForms ( ) ;
416+
417+ await expect ( settings . settingsHeading ) . toBeVisible ( ) ;
418+ await expect ( settings . alertBar ) . toHaveText (
419+ 'Two-step authentication has been enabled'
420+ ) ;
421+ await expect ( settings . totp . status ) . toHaveText ( 'Enabled' ) ;
422+
423+ await settings . totp . addRecoveryPhoneButton . click ( ) ;
424+ await page . waitForURL ( / r e c o v e r y _ p h o n e \/ s e t u p / ) ;
425+
426+ await expect ( recoveryPhone . addHeader ( ) ) . toBeVisible ( ) ;
427+
428+ await recoveryPhone . enterPhoneNumber ( getPhoneNumber ( target . name ) ) ;
429+ await recoveryPhone . clickSendCode ( ) ;
430+
431+ await expect ( recoveryPhone . confirmHeader ) . toBeVisible ( ) ;
432+
433+ let smsCode = await target . smsClient . getCode (
434+ getPhoneNumber ( target . name ) ,
435+ credentials . uid
436+ ) ;
437+
438+ await recoveryPhone . enterCode ( smsCode ) ;
439+ await recoveryPhone . clickConfirm ( ) ;
440+
441+ await page . waitForURL ( / s e t t i n g s / ) ;
442+ await expect ( settings . alertBar ) . toHaveText ( 'Recovery phone added' ) ;
443+
444+ await settings . signOut ( ) ;
445+
446+ await resetPassword . goto ( ) ;
447+
448+ await resetPassword . fillOutEmailForm ( credentials . email ) ;
449+
450+ const code = await target . emailClient . getResetPasswordCode (
451+ credentials . email
452+ ) ;
453+ await resetPassword . fillOutResetPasswordCodeForm ( code ) ;
454+
455+ 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 / ) ;
456+
457+ await resetPassword . clickTroubleEnteringCode ( ) ;
458+
459+ 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 / ) ;
460+
461+ await resetPassword . clickChoosePhone ( ) ;
462+ await resetPassword . clickContinueButton ( ) ;
463+
464+ 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 / ) ;
465+
466+ smsCode = await target . smsClient . getCode (
467+ getPhoneNumber ( target . name ) ,
468+ credentials . uid
469+ ) ;
470+
471+ await resetPassword . fillRecoveryPhoneCodeForm ( smsCode ) ;
472+
473+ await resetPassword . clickConfirmButton ( ) ;
474+
475+ // Create and submit new password
476+ await resetPassword . fillOutNewPasswordForm ( newPassword ) ;
477+
478+ await expect ( settings . alertBar ) . toHaveText ( 'Your password has been reset' ) ;
479+
480+ await expect ( settings . settingsHeading ) . toBeVisible ( ) ;
481+
482+ // Remove TOTP before teardown
483+ await settings . disconnectTotp ( ) ;
484+ // Cleanup requires setting this value to correct password
485+ credentials . password = newPassword ;
486+ } ) ;
487+ } )
0 commit comments