33 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44
55import React from 'react' ;
6- import { screen } from '@testing-library/react' ;
6+ import { screen , waitFor } from '@testing-library/react' ;
77import userEvent from '@testing-library/user-event' ;
8- import { renderWithRouter } from '../../../models/mocks' ;
8+ import { mockAppContext , renderWithRouter } from '../../../models/mocks' ;
99import { MfaGuard } from './index' ;
1010import { JwtTokenCache } from '../../../lib/cache' ;
1111import { AuthUiErrors } from '../../../lib/auth-errors/auth-errors' ;
12+ import { AppContext } from '../../../models' ;
1213
1314const mockSessionToken = 'session-xyz' ;
1415const mockOtp = '123456' ;
@@ -22,9 +23,8 @@ const mockAuthClient = {
2223 return Promise . reject ( AuthUiErrors . INVALID_EXPIRED_OTP_CODE ) ;
2324 } ) ,
2425} ;
25- const mockFtlMsgResolver = {
26- getMsg : ( id : string , fallback : string ) => fallback ,
27- } ;
26+ const mockAlertBar = { error : jest . fn ( ) } ;
27+ const mockNavigate = jest . fn ( ) ;
2828
2929jest . mock ( '../../../lib/cache' , ( ) => {
3030 const actual = jest . requireActual ( '../../../lib/cache' ) ;
@@ -36,11 +36,24 @@ jest.mock('../../../lib/cache', () => {
3636} ) ;
3737
3838jest . mock ( '../../../models' , ( ) => ( {
39- useAccount : ( ) => ( { email : '[email protected] ' } ) , 39+ ... jest . requireActual ( '../../../models' ) ,
4040 useAuthClient : ( ) => mockAuthClient ,
41- useFtlMsgResolver : ( ) => mockFtlMsgResolver ,
41+ useAlertBar : ( ) => mockAlertBar ,
42+ } ) ) ;
43+
44+ jest . mock ( '@reach/router' , ( ) => ( {
45+ ...jest . requireActual ( '@reach/router' ) ,
46+ useNavigate : ( ) => mockNavigate ,
4247} ) ) ;
4348
49+ async function submitCode ( otp : string = mockOtp ) {
50+ await userEvent . type (
51+ screen . getByRole ( 'textbox' , { name : 'Enter 6-digit code' } ) ,
52+ otp
53+ ) ;
54+ await userEvent . click ( screen . getByRole ( 'button' , { name : 'Confirm' } ) ) ;
55+ }
56+
4457describe ( 'MfaGuard' , ( ) => {
4558 beforeEach ( ( ) => {
4659 if ( JwtTokenCache . hasToken ( mockSessionToken , mockScope ) ) {
@@ -51,9 +64,11 @@ describe('MfaGuard', () => {
5164
5265 it ( 'requests OTP and shows modal when JWT missing' , async ( ) => {
5366 renderWithRouter (
54- < MfaGuard requiredScope = { mockScope } >
55- < div > secured</ div >
56- </ MfaGuard >
67+ < AppContext . Provider value = { mockAppContext ( ) } >
68+ < MfaGuard requiredScope = { mockScope } >
69+ < div > secured</ div >
70+ </ MfaGuard >
71+ </ AppContext . Provider >
5772 ) ;
5873
5974 expect ( mockAuthClient . mfaRequestOtp ) . toHaveBeenCalledWith (
@@ -65,12 +80,7 @@ describe('MfaGuard', () => {
6580 await screen . findByText ( 'Enter confirmation code' )
6681 ) . toBeInTheDocument ( ) ;
6782
68- // Submit a code to verify integration with onSubmit
69- await userEvent . type (
70- screen . getByRole ( 'textbox' , { name : 'Enter 6-digit code' } ) ,
71- mockOtp
72- ) ;
73- await userEvent . click ( screen . getByRole ( 'button' , { name : 'Confirm' } ) ) ;
83+ await submitCode ( ) ;
7484
7585 expect ( mockAuthClient . mfaOtpVerify ) . toHaveBeenCalledWith (
7686 mockSessionToken ,
@@ -83,9 +93,11 @@ describe('MfaGuard', () => {
8393 JwtTokenCache . setToken ( mockSessionToken , mockScope , 'jwt-present' ) ;
8494
8595 renderWithRouter (
86- < MfaGuard requiredScope = { mockScope } >
87- < div > secured</ div >
88- </ MfaGuard >
96+ < AppContext . Provider value = { mockAppContext ( ) } >
97+ < MfaGuard requiredScope = { mockScope } >
98+ < div > secured</ div >
99+ </ MfaGuard >
100+ </ AppContext . Provider >
89101 ) ;
90102
91103 expect ( screen . getByText ( 'secured' ) ) . toBeInTheDocument ( ) ;
@@ -97,17 +109,15 @@ describe('MfaGuard', () => {
97109
98110 it ( 'shows error banner on invalid OTP' , async ( ) => {
99111 renderWithRouter (
100- < MfaGuard requiredScope = { mockScope } >
101- < div > secured</ div >
102- </ MfaGuard >
112+ < AppContext . Provider value = { mockAppContext ( ) } >
113+ < MfaGuard requiredScope = { mockScope } >
114+ < div > secured</ div >
115+ </ MfaGuard >
116+ </ AppContext . Provider >
103117 ) ;
104118
105119 expect ( screen . queryByText ( 'Enter confirmation code' ) ) . toBeInTheDocument ( ) ;
106- await userEvent . type (
107- screen . getByRole ( 'textbox' , { name : 'Enter 6-digit code' } ) ,
108- '654321'
109- ) ;
110- await userEvent . click ( screen . getByRole ( 'button' , { name : 'Confirm' } ) ) ;
120+ await submitCode ( '654321' ) ;
111121
112122 expect (
113123 await screen . findByText ( 'Invalid or expired confirmation code' )
@@ -116,26 +126,98 @@ describe('MfaGuard', () => {
116126
117127 it ( 'clears error banner on input change' , async ( ) => {
118128 renderWithRouter (
119- < MfaGuard requiredScope = { mockScope } >
120- < div > secured</ div >
121- </ MfaGuard >
129+ < AppContext . Provider value = { mockAppContext ( ) } >
130+ < MfaGuard requiredScope = { mockScope } >
131+ < div > secured</ div >
132+ </ MfaGuard >
133+ </ AppContext . Provider >
122134 ) ;
123135
124136 expect ( screen . getByText ( 'Enter confirmation code' ) ) . toBeInTheDocument ( ) ;
137+ await submitCode ( '654321' ) ;
138+ expect (
139+ await screen . findByText ( 'Invalid or expired confirmation code' )
140+ ) . toBeInTheDocument ( ) ;
141+
142+ await userEvent . clear ( screen . getByRole ( 'textbox' ) ) ;
143+
144+ expect (
145+ screen . queryByText ( 'Invalid or expired confirmation code' )
146+ ) . not . toBeInTheDocument ( ) ;
147+ } ) ;
148+
149+ it ( 'shows resend success banner and hides error banner on resend success' , async ( ) => {
150+ renderWithRouter (
151+ < AppContext . Provider value = { mockAppContext ( ) } >
152+ < MfaGuard requiredScope = { mockScope } >
153+ < div > secured</ div >
154+ </ MfaGuard >
155+ </ AppContext . Provider >
156+ ) ;
157+
158+ // Trigger an error first
125159 await userEvent . type (
126160 screen . getByRole ( 'textbox' , { name : 'Enter 6-digit code' } ) ,
127161 '654321'
128162 ) ;
129163 await userEvent . click ( screen . getByRole ( 'button' , { name : 'Confirm' } ) ) ;
130-
131164 expect (
132165 await screen . findByText ( 'Invalid or expired confirmation code' )
133166 ) . toBeInTheDocument ( ) ;
134167
135- await userEvent . clear ( screen . getByRole ( 'textbox' ) ) ;
136-
168+ await userEvent . click (
169+ screen . getByRole ( 'button' , { name : 'Email new code.' } )
170+ ) ;
171+ expect (
172+ await screen . findByText ( 'A new code was sent to your email.' )
173+ ) . toBeInTheDocument ( ) ;
137174 expect (
138175 screen . queryByText ( 'Invalid or expired confirmation code' )
139176 ) . not . toBeInTheDocument ( ) ;
140177 } ) ;
178+
179+ it ( 'shows error banner and hide success banner on resend error' , async ( ) => {
180+ renderWithRouter (
181+ < AppContext . Provider value = { mockAppContext ( ) } >
182+ < MfaGuard requiredScope = { mockScope } >
183+ < div > secured</ div >
184+ </ MfaGuard >
185+ </ AppContext . Provider >
186+ ) ;
187+
188+ await userEvent . click (
189+ screen . getByRole ( 'button' , { name : 'Email new code.' } )
190+ ) ;
191+ expect (
192+ await screen . findByText ( 'A new code was sent to your email.' )
193+ ) . toBeInTheDocument ( ) ;
194+
195+ mockAuthClient . mfaRequestOtp . mockRejectedValueOnce (
196+ AuthUiErrors . UNEXPECTED_ERROR
197+ ) ;
198+
199+ await userEvent . click (
200+ screen . getByRole ( 'button' , { name : 'Email new code.' } )
201+ ) ;
202+ expect ( await screen . findByText ( 'Unexpected error' ) ) . toBeInTheDocument ( ) ;
203+ } ) ;
204+
205+ it ( 'goes home and shows error alert bar if request for OTP fails' , async ( ) => {
206+ mockAuthClient . mfaRequestOtp . mockRejectedValueOnce (
207+ AuthUiErrors . UNEXPECTED_ERROR
208+ ) ;
209+
210+ renderWithRouter (
211+ < AppContext . Provider value = { mockAppContext ( ) } >
212+ < MfaGuard requiredScope = { mockScope } >
213+ < div > secured</ div >
214+ </ MfaGuard >
215+ </ AppContext . Provider >
216+ ) ;
217+
218+ await waitFor ( ( ) => {
219+ expect ( mockNavigate ) . toHaveBeenCalledWith ( '/settings' ) ;
220+ expect ( mockAlertBar . error ) . toHaveBeenCalledWith ( 'Unexpected error' ) ;
221+ } ) ;
222+ } ) ;
141223} ) ;
0 commit comments