11import '@testing-library/jest-dom' ;
22import React , { PropsWithChildren } from 'react' ;
3- import { render , fireEvent , screen , waitFor } from '@testing-library/react' ;
3+ import {
4+ render ,
5+ fireEvent ,
6+ screen ,
7+ waitFor ,
8+ renderHook ,
9+ within
10+ } from '@testing-library/react' ;
411import App from './App' ;
512import packageJson from '../package.json' ;
13+ import useOidcMfa from './hooks/useOidcMfa' ;
614
715const mockDescope = jest . fn ( ) ;
816const mockAuthProvider = jest . fn ( ) ;
917
1018jest . mock ( '@descope/react-sdk' , ( ) => ( {
1119 ...jest . requireActual ( '@descope/react-sdk' ) ,
12- Descope : ( props : unknown ) => {
20+ Descope : ( { onSuccess , ... props } : { onSuccess : ( ) => void } ) => {
1321 mockDescope ( props ) ;
14- return < div /> ;
22+ return (
23+ < button data-testid = "descope-button" type = "button" onClick = { onSuccess } >
24+ Descope
25+ </ button >
26+ ) ;
1527 } ,
1628 AuthProvider : ( props : PropsWithChildren < { [ key : string ] : string } > ) => {
1729 const { children } = props ;
@@ -26,6 +38,9 @@ const baseUrl = 'https://api.descope.test';
2638const flowId = 'test' ;
2739const debug = true ;
2840
41+ const mockFetch = jest . fn ( ) ;
42+ global . fetch = mockFetch ;
43+
2944describe ( 'App component' , ( ) => {
3045 beforeAll ( ( ) => {
3146 Object . defineProperty ( window , 'location' , {
@@ -147,4 +162,232 @@ describe('App component', () => {
147162 )
148163 ) ;
149164 } ) ;
165+
166+ describe ( 'onSuccess callback' , ( ) => {
167+ beforeEach ( ( ) => {
168+ jest . clearAllMocks ( ) ;
169+ process . env . DESCOPE_PROJECT_ID = 'P123456789012345678901234567' ;
170+ process . env . DESCOPE_FLOW_ID = 'saml-config' ;
171+ process . env . REACT_APP_DESCOPE_BASE_URL = baseUrl ;
172+
173+ // Set the ssoAppId URL parameter
174+ Object . defineProperty ( window , 'location' , {
175+ value : {
176+ ...window . location ,
177+ search : '?sso_app_id=testSsoAppId' ,
178+ pathname : '/test' ,
179+ assign : jest . fn ( )
180+ } ,
181+ writable : true
182+ } ) ;
183+ } ) ;
184+
185+ it ( 'should update the URL with done=true when onSuccess is triggered' , async ( ) => {
186+ render ( < App /> ) ;
187+
188+ const descopeButton = screen . getByTestId ( 'descope-button' ) ;
189+ fireEvent . click ( descopeButton ) ;
190+
191+ await waitFor ( ( ) => {
192+ expect ( window . location . assign ) . toHaveBeenCalledWith (
193+ `${ baseUrl } /test?sso_app_id=testSsoAppId&done=true`
194+ ) ;
195+ } ) ;
196+ } ) ;
197+
198+ it ( 'should update the URL with done=true when onSuccess is triggered without existing search params' , async ( ) => {
199+ Object . defineProperty ( window , 'location' , {
200+ value : {
201+ ...window . location ,
202+ search : '' ,
203+ pathname : '/test' ,
204+ assign : jest . fn ( )
205+ } ,
206+ writable : true
207+ } ) ;
208+
209+ render ( < App /> ) ;
210+
211+ const descopeButton = screen . getByTestId ( 'descope-button' ) ;
212+ fireEvent . click ( descopeButton ) ;
213+
214+ await waitFor ( ( ) => {
215+ expect ( window . location . assign ) . toHaveBeenCalledWith (
216+ `${ baseUrl } /test?done=true`
217+ ) ;
218+ } ) ;
219+ } ) ;
220+ } ) ;
221+
222+ describe ( 'favicon' , ( ) => {
223+ beforeEach ( ( ) => {
224+ jest . clearAllMocks ( ) ;
225+ process . env . REACT_APP_BASE_FUNCTIONS_URL = 'https://example.com' ;
226+ process . env . REACT_APP_FAVICON_URL = 'https://example.com/favicon.ico' ;
227+ process . env . DESCOPE_PROJECT_ID = 'P1234567890123456789012345678901' ;
228+
229+ Object . defineProperty ( window , 'location' , {
230+ value : {
231+ ...window . location ,
232+ search : '?sso_app_id=testSsoAppId' ,
233+ pathname : '/test'
234+ } ,
235+ writable : true
236+ } ) ;
237+ } ) ;
238+
239+ afterEach ( ( ) => {
240+ // clean head after each test
241+ document . head . innerHTML = '' ;
242+ } ) ;
243+
244+ it ( 'should update the favicon when all conditions are met' , async ( ) => {
245+ mockFetch . mockResolvedValueOnce ( {
246+ ok : true ,
247+ json : async ( ) => ( {
248+ faviconUrl : 'https://example.com/new-favicon.ico'
249+ } )
250+ } ) ;
251+
252+ render ( < App /> ) ;
253+
254+ await waitFor ( ( ) => {
255+ // eslint-disable-next-line testing-library/no-node-access -- can't query head with screen
256+ const link = document . head . querySelector (
257+ "link[rel~='icon']"
258+ ) as HTMLLinkElement ;
259+ expect ( link ) . toBeInTheDocument ( ) ;
260+ } ) ;
261+
262+ await waitFor ( ( ) => {
263+ // eslint-disable-next-line testing-library/no-node-access -- can't query head with screen
264+ const link = document . head . querySelector (
265+ "link[rel~='icon']"
266+ ) as HTMLLinkElement ;
267+ expect ( link . href ) . toBe ( 'https://example.com/new-favicon.ico' ) ;
268+ } ) ;
269+ } ) ;
270+
271+ it ( 'should not update the favicon if the response is not ok' , async ( ) => {
272+ mockFetch . mockResolvedValueOnce ( {
273+ ok : false
274+ } ) ;
275+
276+ render ( < App /> ) ;
277+
278+ await waitFor ( ( ) => {
279+ // eslint-disable-next-line testing-library/no-node-access -- can't query head with screen
280+ const link = document . head . querySelector ( "link[rel~='icon']" ) ;
281+ expect ( link ) . not . toBeInTheDocument ( ) ;
282+ } ) ;
283+ } ) ;
284+
285+ it ( 'should not update the favicon if the URL is not secure' , async ( ) => {
286+ process . env . REACT_APP_FAVICON_URL = 'http://example.com/favicon.ico' ;
287+
288+ render ( < App /> ) ;
289+
290+ await waitFor ( ( ) => {
291+ // eslint-disable-next-line testing-library/no-node-access -- can't query head with screen
292+ const link = document . querySelector ( "link[rel~='icon']" ) ;
293+ expect ( link ) . not . toBeInTheDocument ( ) ;
294+ } ) ;
295+ } ) ;
296+
297+ it ( 'should not update the favicon if the URL is not valid' , async ( ) => {
298+ process . env . REACT_APP_FAVICON_URL = 'invalid-url' ;
299+
300+ render ( < App /> ) ;
301+
302+ await waitFor ( ( ) => {
303+ // eslint-disable-next-line testing-library/no-node-access -- can't query head with screen
304+ const link = document . querySelector ( "link[rel~='icon']" ) ;
305+ expect ( link ) . not . toBeInTheDocument ( ) ;
306+ } ) ;
307+ } ) ;
308+
309+ it ( 'should not update the favicon if fetch throws an error' , async ( ) => {
310+ mockFetch . mockRejectedValueOnce ( new Error ( 'test error' ) ) ;
311+
312+ render ( < App /> ) ;
313+
314+ await waitFor ( ( ) => {
315+ // eslint-disable-next-line testing-library/no-node-access -- can't query head with screen
316+ const link = document . querySelector ( "link[rel~='icon']" ) ;
317+ expect ( link ) . not . toBeInTheDocument ( ) ;
318+ } ) ;
319+ } ) ;
320+
321+ it ( 'should not update the favicon if faviconUrl is missing' , async ( ) => {
322+ process . env . REACT_APP_FAVICON_URL = '' ;
323+
324+ render ( < App /> ) ;
325+
326+ await waitFor ( ( ) => {
327+ // eslint-disable-next-line testing-library/no-node-access -- can't query head with screen
328+ const link = document . querySelector ( "link[rel~='icon']" ) ;
329+ expect ( link ) . not . toBeInTheDocument ( ) ;
330+ } ) ;
331+ } ) ;
332+ } ) ;
333+
334+ describe ( 'useOidcMfa' , ( ) => {
335+ beforeEach ( ( ) => {
336+ Object . defineProperty ( window , 'location' , {
337+ value : {
338+ ...window . location ,
339+ search :
340+ '?oidc_mfa_state=testState&oidc_mfa_id_token=testIdToken&oidc_mfa_redirect_url=https://login.microsoftonline.com/common/federation/externalauthprovider' ,
341+ pathname : '/test'
342+ } ,
343+ writable : true
344+ } ) ;
345+
346+ // Mock window.history.replaceState
347+ window . history . replaceState = jest . fn ( ) ;
348+
349+ // Mock form.submit
350+ HTMLFormElement . prototype . submit = jest . fn ( ) ;
351+ } ) ;
352+
353+ afterEach ( ( ) => {
354+ // Clean up the DOM after each test
355+ document . body . innerHTML = '' ;
356+ } ) ;
357+
358+ it ( 'should create and submit a form with the correct parameters' , ( ) => {
359+ renderHook ( ( ) => useOidcMfa ( ) ) ;
360+
361+ const form = screen . getByTestId ( 'oidc-mfa-form' ) ;
362+ expect ( form ) . toBeInTheDocument ( ) ;
363+ expect ( form ) . toHaveAttribute (
364+ 'action' ,
365+ 'https://login.microsoftonline.com/common/federation/externalauthprovider'
366+ ) ;
367+ expect ( form ) . toHaveAttribute ( 'method' , 'POST' ) ;
368+
369+ const stateInput = within ( form ) . getByTestId ( 'state' ) ;
370+ expect ( stateInput ) . toBeInTheDocument ( ) ;
371+ expect ( stateInput ) . toHaveValue ( 'testState' ) ;
372+
373+ const idTokenInput = within ( form ) . getByTestId ( 'id_token' , { } ) ;
374+ expect ( idTokenInput ) . toBeInTheDocument ( ) ;
375+ expect ( idTokenInput ) . toHaveValue ( 'testIdToken' ) ;
376+ } ) ;
377+ it ( 'should not create form post if the URL is not approved' , ( ) => {
378+ Object . defineProperty ( window , 'location' , {
379+ value : {
380+ ...window . location ,
381+ search :
382+ '?oidc_mfa_state=testState&oidc_mfa_id_token=testIdToken&oidc_mfa_redirect_url=https://example.com' ,
383+ pathname : '/test'
384+ } ,
385+ writable : true
386+ } ) ;
387+
388+ renderHook ( ( ) => useOidcMfa ( ) ) ;
389+
390+ expect ( screen . queryByTestId ( 'oidc-mfa-form' ) ) . not . toBeInTheDocument ( ) ;
391+ } ) ;
392+ } ) ;
150393} ) ;
0 commit comments