1+ // @vitest -environment node
12/*
23 * Copyright (c) 2025 Ping Identity Corporation. All rights reserved.
34 *
@@ -75,7 +76,7 @@ function getUrlFromInput(input: RequestInfo | URL): string {
7576/**
7677 * Helper to setup mock fetch for wellknown + journey responses
7778 */
78- function setupMockFetch ( journeyResponse : Step | null = null ) {
79+ function setupMockFetch ( journeyResponse : Step | null = null , authenticateStatus = 200 ) {
7980 mockFetch . mockImplementation ( ( input : RequestInfo | URL ) => {
8081 const url = getUrlFromInput ( input ) ;
8182
@@ -85,8 +86,13 @@ function setupMockFetch(journeyResponse: Step | null = null) {
8586 }
8687
8788 // Journey authenticate endpoint
88- if ( journeyResponse && url . includes ( '/authenticate' ) ) {
89- return Promise . resolve ( new Response ( JSON . stringify ( journeyResponse ) ) ) ;
89+ if ( url . includes ( '/authenticate' ) ) {
90+ if ( journeyResponse === null ) {
91+ return Promise . reject ( new Error ( `Unexpected fetch: ${ url } ` ) ) ;
92+ }
93+ return Promise . resolve (
94+ new Response ( JSON . stringify ( journeyResponse ) , { status : authenticateStatus } ) ,
95+ ) ;
9096 }
9197
9298 return Promise . reject ( new Error ( `Unexpected fetch: ${ url } ` ) ) ;
@@ -152,6 +158,30 @@ describe('journey-client', () => {
152158 }
153159 } ) ;
154160
161+ test ( 'start_401WithCodeInBody_ReturnsLoginFailure' , async ( ) => {
162+ const failurePayload : Step = {
163+ code : 401 ,
164+ message : 'Access Denied' ,
165+ reason : 'Unauthorized' ,
166+ detail : { failureUrl : 'https://example.com/failure' } ,
167+ } ;
168+ setupMockFetch ( failurePayload , 401 ) ;
169+
170+ const client = await journey ( { config : mockConfig } ) ;
171+ const result = await client . start ( ) ;
172+
173+ expect ( result ) . toBeDefined ( ) ;
174+ expect ( isGenericError ( result ) ) . toBe ( false ) ;
175+ expect ( result ) . toHaveProperty ( 'type' , 'LoginFailure' ) ;
176+
177+ if ( ! isGenericError ( result ) && result . type === 'LoginFailure' ) {
178+ expect ( result . payload ) . toEqual ( failurePayload ) ;
179+ expect ( result . getCode ( ) ) . toBe ( 401 ) ;
180+ expect ( result . getMessage ( ) ) . toBe ( 'Access Denied' ) ;
181+ expect ( result . getReason ( ) ) . toBe ( 'Unauthorized' ) ;
182+ }
183+ } ) ;
184+
155185 test ( 'next_WellknownConfig_SendsStepAndReturnsNext' , async ( ) => {
156186 const initialStep = createJourneyStep ( {
157187 authId : 'test-auth-id' ,
@@ -192,6 +222,34 @@ describe('journey-client', () => {
192222 }
193223 } ) ;
194224
225+ test ( 'next_401WithCodeInBody_ReturnsLoginFailure' , async ( ) => {
226+ const initialStep = createJourneyStep ( {
227+ authId : 'test-auth-id' ,
228+ callbacks : [ ] ,
229+ } ) ;
230+ const failurePayload : Step = {
231+ code : 401 ,
232+ message : 'Access Denied' ,
233+ reason : 'Unauthorized' ,
234+ detail : { failureUrl : 'https://example.com/failure' } ,
235+ } ;
236+ setupMockFetch ( failurePayload , 401 ) ;
237+
238+ const client = await journey ( { config : mockConfig } ) ;
239+ const result = await client . next ( initialStep , { } ) ;
240+
241+ expect ( result ) . toBeDefined ( ) ;
242+ expect ( isGenericError ( result ) ) . toBe ( false ) ;
243+ expect ( result ) . toHaveProperty ( 'type' , 'LoginFailure' ) ;
244+
245+ if ( ! isGenericError ( result ) && result . type === 'LoginFailure' ) {
246+ expect ( result . payload ) . toEqual ( failurePayload ) ;
247+ expect ( result . getCode ( ) ) . toBe ( 401 ) ;
248+ expect ( result . getMessage ( ) ) . toBe ( 'Access Denied' ) ;
249+ expect ( result . getReason ( ) ) . toBe ( 'Unauthorized' ) ;
250+ }
251+ } ) ;
252+
195253 test ( 'redirect_WellknownConfig_StoresStepAndCallsLocationAssign' , async ( ) => {
196254 const mockStepPayload : Step = {
197255 callbacks : [
@@ -204,6 +262,15 @@ describe('journey-client', () => {
204262 } ;
205263 const step = createJourneyStep ( mockStepPayload ) ;
206264 const assignMock = vi . fn ( ) ;
265+ // Node test environment doesn't provide `window`, so create a minimal shim
266+ // with a real `location` getter so we can keep using vi.spyOn(..., 'get').
267+ ( globalThis as unknown as { window ?: unknown } ) . window = { } ;
268+ Object . defineProperty ( window , 'location' , {
269+ configurable : true ,
270+ get : ( ) => ( {
271+ assign : vi . fn ( ) ,
272+ } ) ,
273+ } ) ;
207274 const locationSpy = vi . spyOn ( window , 'location' , 'get' ) . mockReturnValue ( {
208275 ...window . location ,
209276 assign : assignMock ,
0 commit comments