1- import type { User } from '@workos-inc/node' ;
2- import { data , redirect } from 'react-router' ;
3- import { getSignInUrl , getSignUpUrl , signOut , switchToOrganization } from './auth.js' ;
1+ import { User } from '@workos-inc/node' ;
2+ import { getSignInUrl , getSignUpUrl , signOut , switchToOrganization , withAuth } from './auth.js' ;
43import * as authorizationUrl from './get-authorization-url.js' ;
54import * as session from './session.js' ;
5+ import * as configModule from './config.js' ;
6+ import { data , redirect , LoaderFunctionArgs } from 'react-router' ;
67import { assertIsResponse } from './test-utils/test-helpers.js' ;
78
89const terminateSession = jest . mocked ( session . terminateSession ) ;
910const refreshSession = jest . mocked ( session . refreshSession ) ;
11+ const getSessionFromCookie = jest . mocked ( session . getSessionFromCookie ) ;
12+ const getClaimsFromAccessToken = jest . mocked ( session . getClaimsFromAccessToken ) ;
13+ const getConfig = jest . mocked ( configModule . getConfig ) ;
1014
1115jest . mock ( './session' , ( ) => ( {
1216 terminateSession : jest . fn ( ) . mockResolvedValue ( new Response ( ) ) ,
1317 refreshSession : jest . fn ( ) ,
18+ getSessionFromCookie : jest . fn ( ) ,
19+ getClaimsFromAccessToken : jest . fn ( ) ,
20+ } ) ) ;
21+
22+ jest . mock ( './config' , ( ) => ( {
23+ getConfig : jest . fn ( ) ,
1424} ) ) ;
1525
1626// Mock redirect and data from react-router
@@ -35,7 +45,6 @@ jest.mock('react-router', () => {
3545describe ( 'auth' , ( ) => {
3646 beforeEach ( ( ) => {
3747 jest . spyOn ( authorizationUrl , 'getAuthorizationUrl' ) ;
38- jest . clearAllMocks ( ) ;
3948 } ) ;
4049
4150 describe ( 'getSignInUrl' , ( ) => {
@@ -284,4 +293,193 @@ describe('auth', () => {
284293 } ) ;
285294 } ) ;
286295 } ) ;
296+
297+ describe ( 'withAuth' , ( ) => {
298+ const createMockRequest = ( cookie ?: string ) => {
299+ return {
300+ request : new Request ( 'https://example.com' , {
301+ headers : cookie ? { Cookie : cookie } : { } ,
302+ } ) ,
303+ } as LoaderFunctionArgs ;
304+ } ;
305+
306+ beforeEach ( ( ) => {
307+ jest . clearAllMocks ( ) ;
308+ getConfig . mockReturnValue ( 'wos-session' ) ;
309+ } ) ;
310+
311+ it ( 'should return user info when a valid session exists' , async ( ) => {
312+ // Mock session with valid access token
313+ const mockSession = {
314+ accessToken : 'valid-access-token' ,
315+ refreshToken : 'refresh-token' ,
316+ user : {
317+ id : 'user-1' ,
318+ 319+ firstName : 'Test' ,
320+ lastName : 'User' ,
321+ emailVerified : true ,
322+ profilePictureUrl : 'https://example.com/profile.jpg' ,
323+ object : 'user' as const ,
324+ createdAt : '2023-01-01T00:00:00Z' ,
325+ updatedAt : '2023-01-01T00:00:00Z' ,
326+ lastSignInAt : '2023-01-01T00:00:00Z' ,
327+ externalId : null ,
328+ } ,
329+ impersonator : {
330+ 331+ reason : 'testing' ,
332+ } ,
333+ headers : { } ,
334+ } ;
335+
336+ // Mock claims from access token
337+ const mockClaims = {
338+ sessionId : 'session-123' ,
339+ organizationId : 'org-456' ,
340+ role : 'admin' ,
341+ permissions : [ 'read' , 'write' ] ,
342+ entitlements : [ 'feature-1' , 'feature-2' ] ,
343+ exp : Date . now ( ) / 1000 + 3600 , // 1 hour from now
344+ iss : 'https://api.workos.com' ,
345+ } ;
346+
347+ getSessionFromCookie . mockResolvedValue ( mockSession ) ;
348+ getClaimsFromAccessToken . mockReturnValue ( mockClaims ) ;
349+
350+ const result = await withAuth ( createMockRequest ( 'wos-session=valid-session-data' ) ) ;
351+
352+ // Verify called with correct params
353+ expect ( getSessionFromCookie ) . toHaveBeenCalledWith ( 'wos-session=valid-session-data' ) ;
354+ expect ( getClaimsFromAccessToken ) . toHaveBeenCalledWith ( 'valid-access-token' ) ;
355+
356+ // Check result contains expected user info
357+ expect ( result ) . toEqual ( {
358+ user : mockSession . user ,
359+ sessionId : mockClaims . sessionId ,
360+ organizationId : mockClaims . organizationId ,
361+ role : mockClaims . role ,
362+ permissions : mockClaims . permissions ,
363+ entitlements : mockClaims . entitlements ,
364+ impersonator : mockSession . impersonator ,
365+ accessToken : mockSession . accessToken ,
366+ } ) ;
367+ } ) ;
368+
369+ it ( 'should handle expired access tokens' , async ( ) => {
370+ // Mock session with expired access token
371+ const mockSession = {
372+ accessToken : 'expired-access-token' ,
373+ refreshToken : 'refresh-token' ,
374+ user : {
375+ id : 'user-1' ,
376+ 377+ firstName : 'Test' ,
378+ lastName : 'User' ,
379+ emailVerified : true ,
380+ profilePictureUrl : 'https://example.com/profile.jpg' ,
381+ object : 'user' as const ,
382+ createdAt : '2023-01-01T00:00:00Z' ,
383+ updatedAt : '2023-01-01T00:00:00Z' ,
384+ lastSignInAt : '2023-01-01T00:00:00Z' ,
385+ externalId : null ,
386+ } ,
387+ headers : { } ,
388+ } ;
389+
390+ // Mock claims with expired token
391+ const mockClaims = {
392+ sessionId : 'session-123' ,
393+ organizationId : 'org-456' ,
394+ role : 'admin' ,
395+ permissions : [ 'read' , 'write' ] ,
396+ entitlements : [ 'feature-1' , 'feature-2' ] ,
397+ exp : Date . now ( ) / 1000 - 3600 , // 1 hour ago (expired)
398+ iss : 'https://api.workos.com' ,
399+ } ;
400+
401+ getSessionFromCookie . mockResolvedValue ( mockSession ) ;
402+ getClaimsFromAccessToken . mockReturnValue ( mockClaims ) ;
403+
404+ // Spy on console.warn
405+ const consoleWarnSpy = jest . spyOn ( console , 'warn' ) . mockImplementation ( ) ;
406+
407+ const result = await withAuth ( createMockRequest ( 'wos-session=expired-session-data' ) ) ;
408+
409+ // Should warn about expired token
410+ expect ( consoleWarnSpy ) . toHaveBeenCalledWith ( 'Access token expired for user' ) ;
411+
412+ // Result should still contain user info
413+ expect ( result ) . toEqual ( {
414+ user : mockSession . user ,
415+ sessionId : mockClaims . sessionId ,
416+ organizationId : mockClaims . organizationId ,
417+ role : mockClaims . role ,
418+ permissions : mockClaims . permissions ,
419+ entitlements : mockClaims . entitlements ,
420+ impersonator : undefined ,
421+ accessToken : mockSession . accessToken ,
422+ } ) ;
423+
424+ consoleWarnSpy . mockRestore ( ) ;
425+ } ) ;
426+
427+ it ( 'should return NoUserInfo when no session exists' , async ( ) => {
428+ // Mock no session
429+ getSessionFromCookie . mockResolvedValue ( null ) ;
430+
431+ const result = await withAuth ( createMockRequest ( ) ) ;
432+
433+ expect ( result ) . toEqual ( {
434+ user : null ,
435+ } ) ;
436+
437+ // getClaimsFromAccessToken should not be called
438+ expect ( getClaimsFromAccessToken ) . not . toHaveBeenCalled ( ) ;
439+ } ) ;
440+
441+ it ( 'should return NoUserInfo when session exists but has no access token' , async ( ) => {
442+ // Mock session with no access token - we'll add a dummy accessToken that will be ignored
443+ getSessionFromCookie . mockResolvedValue ( {
444+ user : {
445+ id : 'user-1' ,
446+ 447+ firstName : 'Test' ,
448+ lastName : 'User' ,
449+ emailVerified : true ,
450+ profilePictureUrl : 'https://example.com/profile.jpg' ,
451+ object : 'user' as const ,
452+ createdAt : '2023-01-01T00:00:00Z' ,
453+ updatedAt : '2023-01-01T00:00:00Z' ,
454+ lastSignInAt : '2023-01-01T00:00:00Z' ,
455+ externalId : null ,
456+ } ,
457+ refreshToken : 'refresh-token' ,
458+ headers : { } ,
459+ accessToken : '' , // Empty string to meet type requirement but it will be treated as falsy
460+ } ) ;
461+
462+ const result = await withAuth ( createMockRequest ( 'wos-session=invalid-session-data' ) ) ;
463+
464+ expect ( result ) . toEqual ( {
465+ user : null ,
466+ } ) ;
467+
468+ // getClaimsFromAccessToken should not be called
469+ expect ( getClaimsFromAccessToken ) . not . toHaveBeenCalled ( ) ;
470+ } ) ;
471+
472+ it ( 'should warn when no cookie header includes the cookie name' , async ( ) => {
473+ // Spy on console.warn
474+ const consoleWarnSpy = jest . spyOn ( console , 'warn' ) . mockImplementation ( ) ;
475+
476+ getSessionFromCookie . mockResolvedValue ( null ) ;
477+
478+ await withAuth ( createMockRequest ( 'other-cookie=value' ) ) ;
479+
480+ expect ( consoleWarnSpy ) . toHaveBeenCalledWith ( expect . stringContaining ( 'No session cookie "wos-session" found.' ) ) ;
481+
482+ consoleWarnSpy . mockRestore ( ) ;
483+ } ) ;
484+ } ) ;
287485} ) ;
0 commit comments