@@ -367,6 +367,41 @@ describe('User profile MFA management', () => {
367367 }
368368 } ) ;
369369
370+ it ( 'blocks backup-code regeneration while the default SMS method is changing' , async ( ) => {
371+ const first = fapiPhoneNumber ( { ...phone , reserved_for_second_factor : true , default_second_factor : true } ) ;
372+ const second = fapiPhoneNumber ( {
373+ ...phone ,
374+ id : 'phone_2' ,
375+ phone_number : '+15555550202' ,
376+ reserved_for_second_factor : true ,
377+ } ) ;
378+ const fapi = await renderMfa (
379+ fapiUser ( { id : 'user_1' , phone_numbers : [ first , second ] , backup_code_enabled : true , two_factor_enabled : true } ) ,
380+ ) ;
381+ const held = holdRequests ( 'post' , '/v1/me/phone_numbers/phone_2' ) ;
382+ const user = userEvent . setup ( ) ;
383+ try {
384+ await openMenu ( user , screen . getByRole ( 'button' , { name : 'Manage SMS verification +1 (555) 555-0202' } ) ) ;
385+ await user . click ( screen . getByRole ( 'menuitem' , { name : 'Set as default' } ) ) ;
386+ await waitFor ( ( ) => expect ( held . requests ) . toHaveLength ( 1 ) ) ;
387+ await waitFor ( ( ) => expect ( screen . queryByRole ( 'menu' ) ) . toBeNull ( ) ) ;
388+ const manage = screen . getByRole ( 'button' , { name : 'Manage Backup codes' } ) ;
389+ expect ( manage ) . toHaveAttribute ( 'aria-disabled' , 'true' ) ;
390+ await user . click ( manage ) ;
391+ expect ( screen . queryByRole ( 'menu' ) ) . toBeNull ( ) ;
392+ expect ( fapi . mfa . backupCodeCreations ) . toBe ( 0 ) ;
393+ expect ( screen . getByRole ( 'button' , { name : 'Add verification method' } ) ) . toBeDisabled ( ) ;
394+ } finally {
395+ held . release ( ) ;
396+ }
397+ const manage = screen . getByRole ( 'button' , { name : 'Manage Backup codes' } ) ;
398+ await waitFor ( ( ) => expect ( manage ) . not . toHaveAttribute ( 'aria-disabled' , 'true' ) ) ;
399+ await openMenu ( user , manage ) ;
400+ await user . click ( screen . getByRole ( 'menuitem' , { name : 'Regenerate' } ) ) ;
401+ await waitFor ( ( ) => expect ( screen . getByText ( 'CODE0100' ) ) . toBeVisible ( ) ) ;
402+ expect ( fapi . mfa . backupCodeCreations ) . toBe ( 1 ) ;
403+ } ) ;
404+
370405 it ( 'returns failed regeneration to the section and retries from the menu' , async ( ) => {
371406 const fapi = await renderMfa (
372407 fapiUser ( { id : 'user_1' , totp_enabled : true , backup_code_enabled : true , two_factor_enabled : true } ) ,
@@ -865,6 +900,12 @@ describe('User profile MFA management', () => {
865900 expect ( screen . getAllByText ( 'SMS verification' ) [ 0 ] ?. closest ( 'li' ) ) . toHaveTextContent ( '+1 (555) 555-0202' ) ,
866901 ) ;
867902 expect ( screen . getAllByText ( 'SMS verification' ) [ 0 ] ?. closest ( 'li' ) ) . toHaveTextContent ( 'Default' ) ;
903+ await waitFor ( ( ) =>
904+ expect ( screen . getByRole ( 'button' , { name : 'Manage SMS verification +1 (555) 555-0101' } ) ) . not . toHaveAttribute (
905+ 'aria-disabled' ,
906+ 'true' ,
907+ ) ,
908+ ) ;
868909 await user . click ( screen . getByRole ( 'button' , { name : 'Manage SMS verification +1 (555) 555-0101' } ) ) ;
869910 await waitFor ( ( ) => expect ( screen . getByRole ( 'menuitem' , { name : 'Set as default' } ) ) . toBeVisible ( ) ) ;
870911 } ) ;
@@ -908,6 +949,12 @@ describe('User profile MFA management', () => {
908949 expect ( fapi . mfa . phoneUpdates ) . toContainEqual ( { id : 'phone_2' , default : true , reserved : undefined } ) ,
909950 ) ;
910951 expect ( attempts ) . toBe ( 2 ) ;
952+ await waitFor ( ( ) =>
953+ expect ( screen . getByRole ( 'button' , { name : 'Manage SMS verification +1 (555) 555-0101' } ) ) . not . toHaveAttribute (
954+ 'aria-disabled' ,
955+ 'true' ,
956+ ) ,
957+ ) ;
911958 await user . click ( screen . getByRole ( 'button' , { name : 'Manage SMS verification +1 (555) 555-0101' } ) ) ;
912959 await waitFor ( ( ) => expect ( screen . getByRole ( 'menuitem' , { name : 'Set as default' } ) ) . toBeVisible ( ) ) ;
913960 } ) ;
@@ -916,7 +963,7 @@ describe('User profile MFA management', () => {
916963 { phase : 'mutation' , method : 'post' , path : '/v1/me/phone_numbers/phone_2' , pendingPhone : '+1 (555) 555-0202' } ,
917964 { phase : 'refresh' , method : 'get' , path : '/v1/me' , pendingPhone : '+1 (555) 555-0101' } ,
918965 ] as const ) (
919- 'keeps method menus available but hides default changes during $phase' ,
966+ 'disables method menus while changing the default during $phase' ,
920967 async ( { method, path, pendingPhone } ) => {
921968 const first = fapiPhoneNumber ( { ...phone , reserved_for_second_factor : true , default_second_factor : true } ) ;
922969 const second = fapiPhoneNumber ( {
@@ -934,16 +981,21 @@ describe('User profile MFA management', () => {
934981 await waitFor ( ( ) => expect ( held . requests ) . toHaveLength ( 1 ) ) ;
935982 try {
936983 const pendingAction = screen . getByRole ( 'button' , { name : `Manage SMS verification ${ pendingPhone } ` } ) ;
937- await openMenu ( user , pendingAction ) ;
938- await waitFor ( ( ) => expect ( screen . getByRole ( 'menuitem' , { name : 'Remove method' } ) ) . toBeVisible ( ) ) ;
939- expect ( screen . queryByRole ( 'menuitem' , { name : 'Set as default' } ) ) . toBeNull ( ) ;
940- await user . click ( await screen . findByRole ( 'menuitem' , { name : 'Remove method' } ) ) ;
984+ expect ( pendingAction ) . toHaveAttribute ( 'aria-disabled' , 'true' ) ;
985+ await user . click ( pendingAction ) ;
986+ expect ( screen . queryByRole ( 'menu' ) ) . toBeNull ( ) ;
941987 expect ( screen . queryByRole ( 'alertdialog' ) ) . toBeNull ( ) ;
942988 } finally {
943989 held . release ( ) ;
944990 }
945991 await waitFor ( ( ) => expect ( screen . getByText ( '+1 (555) 555-0202' ) . closest ( 'li' ) ) . toHaveTextContent ( 'Default' ) ) ;
946992 await user . keyboard ( '{Escape}' ) ;
993+ await waitFor ( ( ) =>
994+ expect ( screen . getByRole ( 'button' , { name : 'Manage SMS verification +1 (555) 555-0101' } ) ) . not . toHaveAttribute (
995+ 'aria-disabled' ,
996+ 'true' ,
997+ ) ,
998+ ) ;
947999 await user . click ( screen . getByRole ( 'button' , { name : 'Manage SMS verification +1 (555) 555-0101' } ) ) ;
9481000 await waitFor ( ( ) => expect ( screen . getByRole ( 'menuitem' , { name : 'Set as default' } ) ) . toBeVisible ( ) ) ;
9491001 } ,
0 commit comments