Skip to content

Commit 3107df5

Browse files
Fix: [AEA-5833] - Correct which pages shown on invalid session check (#1747)
## Summary - Routine Change ### Details Resolve the incorrect pages being shown during the redirection & logout sequence of a user whose session is no longer valid. Previously: Session invalid -> Remove role state -> Caused select your role page to render with 401 error -> Redirect to no roles available -> You've been logged out Now: Session invalid -> No page drawn until awaited logout sequence completes -> You've been logged out --------- Signed-off-by: Connor Avery <214469360+connoravo-nhs@users.noreply.github.com>
1 parent cd41712 commit 3107df5

File tree

3 files changed

+182
-7
lines changed

3 files changed

+182
-7
lines changed
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import React, {useContext} from "react"
2+
import {render, waitFor, act} from "@testing-library/react"
3+
import {MemoryRouter} from "react-router-dom"
4+
5+
import {AuthContext, AuthProvider} from "@/context/AuthProvider"
6+
import {getTrackerUserInfo} from "@/helpers/userInfo"
7+
8+
// Mock the external dependencies that AuthProvider needs
9+
jest.mock("aws-amplify", () => ({
10+
Amplify: {
11+
configure: jest.fn()
12+
}
13+
}))
14+
15+
jest.mock("aws-amplify/auth", () => ({
16+
signInWithRedirect: jest.fn(),
17+
signOut: jest.fn()
18+
}))
19+
20+
jest.mock("aws-amplify/utils", () => ({
21+
Hub: {
22+
listen: jest.fn(() => () => {}) // Return unsubscribe function
23+
}
24+
}))
25+
26+
jest.mock("@/helpers/userInfo", () => ({
27+
getTrackerUserInfo: jest.fn(),
28+
updateRemoteSelectedRole: jest.fn()
29+
}))
30+
31+
jest.mock("@/constants/environment", () => ({
32+
PUBLIC_PATHS: ["/login"],
33+
FRONTEND_PATHS: {
34+
LOGIN: "/login"
35+
},
36+
API_ENDPOINTS: {
37+
CIS2_SIGNOUT_ENDPOINT: "/api/cis2-signout"
38+
},
39+
AUTH_CONFIG: {
40+
USER_POOL_ID: "mock-pool-id",
41+
USER_POOL_CLIENT_ID: "mock-client-id",
42+
HOSTED_LOGIN_DOMAIN: "mock-domain",
43+
REDIRECT_SIGN_IN: "mock-signin",
44+
REDIRECT_SIGN_OUT: "mock-signout"
45+
},
46+
APP_CONFIG: {
47+
REACT_LOG_LEVEL: "info"
48+
}
49+
}))
50+
51+
jest.mock("@/helpers/logger", () => ({
52+
logger: {
53+
debug: jest.fn(),
54+
info: jest.fn(),
55+
error: jest.fn()
56+
}
57+
}))
58+
59+
describe("updateTrackerUserInfo", () => {
60+
beforeEach(() => {
61+
jest.clearAllMocks()
62+
// Mock localStorage
63+
Storage.prototype.getItem = jest.fn(() => null)
64+
Storage.prototype.setItem = jest.fn()
65+
})
66+
67+
it("does not update role/user state if there is an error", async () => {
68+
const testObject = {
69+
error: "Some error",
70+
rolesWithAccess: [{name: "Role1"}],
71+
rolesWithoutAccess: [{name: "Role2"}],
72+
selectedRole: {name: "Role1"},
73+
userDetails: {username: "testuser"},
74+
isConcurrentSession: false,
75+
sessionId: "session123",
76+
invalidSessionCause: undefined
77+
}
78+
79+
// Mock getTrackerUserInfo to return an error response
80+
;(getTrackerUserInfo as jest.Mock).mockResolvedValue(testObject)
81+
82+
// Create a test component that captures the context value
83+
let contextValue: typeof AuthContext extends React.Context<infer T> ? T : never
84+
85+
const TestComponent = () => {
86+
contextValue = useContext(AuthContext)
87+
return null
88+
}
89+
90+
// Act: Render the provider with our test component
91+
await act(async () => {
92+
render(
93+
<MemoryRouter>
94+
<AuthProvider>
95+
<TestComponent />
96+
</AuthProvider>
97+
</MemoryRouter>
98+
)
99+
})
100+
101+
// Call the function we're testing
102+
await act(async () => {
103+
await contextValue!.updateTrackerUserInfo()
104+
})
105+
106+
// Assert: Verify that role/user details were NOT set (because of error)
107+
await waitFor(() => {
108+
expect(contextValue!.rolesWithAccess).toEqual([])
109+
expect(contextValue!.rolesWithoutAccess).toEqual([])
110+
expect(contextValue!.selectedRole).toBeUndefined()
111+
expect(contextValue!.userDetails).toBeUndefined()
112+
})
113+
114+
// Assert: Verify that session info WAS set (even with error)
115+
await waitFor(() => {
116+
expect(contextValue!.isConcurrentSession).toBe(testObject.isConcurrentSession)
117+
expect(contextValue!.sessionId).toBe(testObject.sessionId)
118+
expect(contextValue!.error).toBe(testObject.error)
119+
expect(contextValue!.invalidSessionCause).toBe(testObject.invalidSessionCause)
120+
})
121+
})
122+
123+
it("updates all state when there is no error", async () => {
124+
// Arrange: Set up test data WITHOUT an error
125+
const testObject = {
126+
error: null,
127+
rolesWithAccess: [{name: "Role1"}],
128+
rolesWithoutAccess: [{name: "Role2"}],
129+
selectedRole: {name: "Role1"},
130+
userDetails: {username: "testuser"},
131+
isConcurrentSession: true,
132+
sessionId: "session456",
133+
invalidSessionCause: undefined
134+
}
135+
136+
;(getTrackerUserInfo as jest.Mock).mockResolvedValue(testObject)
137+
138+
let contextValue: typeof AuthContext extends React.Context<infer T> ? T : never
139+
140+
const TestComponent = () => {
141+
contextValue = useContext(AuthContext)
142+
return null
143+
}
144+
145+
// Act
146+
await act(async () => {
147+
render(
148+
<MemoryRouter>
149+
<AuthProvider>
150+
<TestComponent />
151+
</AuthProvider>
152+
</MemoryRouter>
153+
)
154+
})
155+
156+
await act(async () => {
157+
await contextValue!.updateTrackerUserInfo()
158+
})
159+
160+
// Assert: ALL state should be set when there's no error
161+
await waitFor(() => {
162+
expect(contextValue!.rolesWithAccess).toEqual(testObject.rolesWithAccess)
163+
expect(contextValue!.rolesWithoutAccess).toEqual(testObject.rolesWithoutAccess)
164+
expect(contextValue!.selectedRole).toEqual(testObject.selectedRole)
165+
expect(contextValue!.userDetails).toEqual(testObject.userDetails)
166+
expect(contextValue!.isConcurrentSession).toBe(testObject.isConcurrentSession)
167+
expect(contextValue!.sessionId).toBe(testObject.sessionId)
168+
expect(contextValue!.error).toBe(testObject.error)
169+
expect(contextValue!.invalidSessionCause).toBe(testObject.invalidSessionCause)
170+
})
171+
})
172+
})

packages/cpt-ui/src/context/AccessProvider.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export const AccessProvider = ({children}: { children: ReactNode }) => {
6161
}
6262

6363
const loggedOut = !auth.isSignedIn && !auth.isSigningOut
64+
const loggingOut = auth.isSignedIn && auth.isSigningOut
6465
const concurrent = auth.isSignedIn && auth.isConcurrentSession
6566
const noRole = auth.isSignedIn && !auth.isSigningIn && !auth.selectedRole
6667
const authedAtRoot = auth.isSignedIn && !!auth.selectedRole && atRoot
@@ -78,7 +79,7 @@ export const AccessProvider = ({children}: { children: ReactNode }) => {
7879
return redirect(FRONTEND_PATHS.SESSION_SELECTION, "Concurrent session found - redirecting to session selection")
7980
}
8081

81-
if (noRole && (!inNoRoleAllowed || atRoot)) {
82+
if (!loggingOut && noRole && (!inNoRoleAllowed || atRoot)) {
8283
return redirect(FRONTEND_PATHS.SELECT_YOUR_ROLE, `No selected role - Redirecting from ${path}`)
8384
}
8485

packages/cpt-ui/src/context/AuthProvider.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,16 @@ export const AuthProvider = ({children}: { children: React.ReactNode }) => {
9191

9292
const updateTrackerUserInfo = async () => {
9393
const trackerUserInfo = await getTrackerUserInfo()
94-
setRolesWithAccess(trackerUserInfo.rolesWithAccess)
95-
setRolesWithoutAccess(trackerUserInfo.rolesWithoutAccess)
96-
setSelectedRole(trackerUserInfo.selectedRole)
97-
setUserDetails(trackerUserInfo.userDetails)
98-
setError(trackerUserInfo.error)
94+
if (!trackerUserInfo.error) {
95+
setRolesWithAccess(trackerUserInfo.rolesWithAccess)
96+
setRolesWithoutAccess(trackerUserInfo.rolesWithoutAccess)
97+
setSelectedRole(trackerUserInfo.selectedRole)
98+
setUserDetails(trackerUserInfo.userDetails)
99+
}
99100
setIsConcurrentSession(trackerUserInfo.isConcurrentSession)
100-
setInvalidSessionCause(trackerUserInfo.invalidSessionCause)
101101
setSessionId(trackerUserInfo.sessionId)
102+
setError(trackerUserInfo.error)
103+
setInvalidSessionCause(trackerUserInfo.invalidSessionCause)
102104
return trackerUserInfo
103105
}
104106

0 commit comments

Comments
 (0)