Skip to content

Commit

Permalink
Fix tests
Browse files Browse the repository at this point in the history
- logout should be required function
- handle auth error alert banner on Landing, not Header
  • Loading branch information
haworku committed Jan 8, 2024
1 parent c4745ee commit b76afa9
Show file tree
Hide file tree
Showing 8 changed files with 99 additions and 74 deletions.
31 changes: 0 additions & 31 deletions services/app-web/src/components/Header/Header.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,36 +141,5 @@ describe('Header', () => {

await waitFor(() => expect(spy).toHaveBeenCalledTimes(1))
})

it('calls setAlert when logout is unsuccessful', async () => {
const spy = jest
.spyOn(CognitoAuthApi, 'signOut')
.mockRejectedValue('This logout failed!')
const mockAlert = jest.fn()

renderWithProviders(
<Header authMode={'AWS_COGNITO'} setAlert={mockAlert} />,
{
apolloProvider: {
mocks: [
fetchCurrentUserMock({ statusCode: 200 }),
fetchCurrentUserMock({ statusCode: 403 }),
],
},
}
)

await waitFor(() => {
const signOutButton = screen.getByRole('button', {
name: /Sign out/i,
})

expect(signOutButton).toBeInTheDocument()
void userEvent.click(signOutButton)
})

await waitFor(() => expect(spy).toHaveBeenCalledTimes(1))
await waitFor(() => expect(mockAlert).toHaveBeenCalled())
})
})
})
16 changes: 2 additions & 14 deletions services/app-web/src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,9 @@ import { Logo } from '../Logo'
import styles from './Header.module.scss'
import { PageHeadingRow } from './PageHeadingRow/PageHeadingRow'
import { UserLoginInfo } from './UserLoginInfo/UserLoginInfo'
import { recordJSException } from '../../otelHelpers'
import { ErrorAlertSignIn } from '../ErrorAlert'

export type HeaderProps = {
authMode: AuthModeType
setAlert?: React.Dispatch<React.ReactElement>
disableLogin?: boolean
}

Expand All @@ -24,25 +21,16 @@ export type HeaderProps = {
*/
export const Header = ({
authMode,
setAlert,
disableLogin = false,
}: HeaderProps): React.ReactElement => {
const { logout, loggedInUser, loginStatus } = useAuth()
const { heading } = usePage()
const { currentRoute: route } = useCurrentRoute()

const handleLogout = (
const handleLogout = async (
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
if (!logout) {
console.info('Something went wrong ', e)
return
}

logout({ sessionTimeout: false }).catch((e) => {
recordJSException(`Error with logout: ${e}`)
setAlert && setAlert(<ErrorAlertSignIn />)
})
await logout({ sessionTimeout: false })
}

return (
Expand Down
53 changes: 45 additions & 8 deletions services/app-web/src/contexts/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,19 @@ export const MODAL_COUNTDOWN_DURATION = 2 * 60 // session expiration modal count

type AuthContextType = {
/* See docs/AuthContext.md for an explanation of some of these variables */
checkAuth: () => Promise<ApolloQueryResult<FetchCurrentUserQuery> | Error> // manually refetch to confirm auth status, logout if not authenticated
checkAuth: (
failureRedirect?: string
) => Promise<ApolloQueryResult<FetchCurrentUserQuery> | Error>
checkIfSessionsIsAboutToExpire: () => void
loggedInUser: UserType | undefined
loginStatus: LoginStatusType
logout: ({ sessionTimeout }: { sessionTimeout: boolean }) => Promise<void> // sessionTimeout true when logout is due to session ending
logout: ({
sessionTimeout,
redirectPath,
}: {
sessionTimeout: boolean
redirectPath?: string
}) => Promise<void>
logoutCountdownDuration: number
sessionExpirationTime: MutableRefObject<dayjs.Dayjs | undefined>
sessionIsExpiring: boolean
Expand Down Expand Up @@ -163,11 +171,25 @@ function AuthProvider({
}
}

const checkAuth = async () => {
/*
Refetches current user and confirms authentication
@param {failureRedirectPath} passed through to logout which is called on certain checkAuth failures
Use this function to reconfirm the user is logged in. Also used in CognitoLogin
*/
const checkAuth: AuthContextType['checkAuth'] = async (
failureRedirectPath = '/'
) => {
try {
return await refetch()
} catch (e) {
await logout({ sessionTimeout: true })
// if we fail auth at a time we expected logged in user, the session may have timed out. Logout fully to reflect that and force Reat state to update
if (loggedInUser) {
await logout({
sessionTimeout: true,
redirectPath: failureRedirectPath,
})
}
return new Error(e)
}
}
Expand Down Expand Up @@ -211,20 +233,35 @@ function AuthProvider({
}
}

const logout = async ({ sessionTimeout }: { sessionTimeout: boolean }) => {
/*
Close user session and handle redirect afterward
@param {sessionTimeout} will pass along a URL query param to display session expired alert
@param {redirectPath} optionally changes redirect path on logout - useful for cognito and local login\
Logout is called when user clicks to logout from header or session expiration modal
Also called in the background with session times out
*/
const logout: AuthContextType['logout'] = async ({
sessionTimeout,
redirectPath = '/',
}) => {
const realLogout =
authMode === 'LOCAL' ? logoutLocalUser : cognitoSignOut

updateSessionExpirationState(false)

try {
await realLogout()
if (sessionTimeout) {
window.location.href = '/?session-timeout=true'
window.location.href = `${redirectPath}?session-timeout=true'`
} else {
window.location.href = '/'
window.location.href = redirectPath
}
return
} catch (e) {
recordJSException(new Error(`Logout Failed. ${JSON.stringify(e)}`))
window.location.href = '/'
window.location.href = redirectPath
return
}
}
Expand Down
6 changes: 3 additions & 3 deletions services/app-web/src/pages/Auth/Auth.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
import { CognitoLogin } from './CognitoLogin'
import { LocalLogin } from '../../localAuth'
import { fetchCurrentUserMock } from '../../testHelpers/apolloMocks'
/*
/*
This file should only have basic user flows for auth. Form and implementation details are tested at the component level.
*/

Expand Down Expand Up @@ -243,7 +243,7 @@ describe('Auth', () => {
})
})

it('when login fails, stay on page and display error alert', async () => {
it('when login fails, kick back to landing page', async () => {
let testLocation: Location

renderWithProviders(
Expand All @@ -266,7 +266,7 @@ describe('Auth', () => {

await userClickByTestId(screen, 'TophButton')
await waitFor(() => {
expect(testLocation.pathname).toBe('/auth')
expect(testLocation.pathname).toBe('/')
})
})
})
Expand Down
14 changes: 8 additions & 6 deletions services/app-web/src/pages/Auth/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,19 @@ export function Login({ defaultEmail }: Props): React.ReactElement {
setShowFormAlert(true)
console.info('Error', result.message)
} else {
try {
await checkAuth()
} catch (e) {
console.info('UNEXPECTED NOT LOGGED IN AFTER LOGGIN', e)
const result = await checkAuth('/auth')
if (result instanceof Error) {
console.error(
'UNEXPECTED - NOT AUTHENTICATED BUT COGNITO PASSWORD LOGIN HAS RESOLVED WITHOUT ERROR',
result
)
setShowFormAlert(true)
}

navigate('/')
}
} catch (err) {
console.info('Unexpected error signing in:', err)
setShowFormAlert(true)
console.info('Sign in error:', err)
}
}

Expand Down
22 changes: 22 additions & 0 deletions services/app-web/src/pages/Landing/Landing.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,26 @@ describe('Landing', () => {
screen.queryByText(/You have been logged out due to inactivity/)
).toBeNull()
})

it('displays sign in error when query parameter included', async () => {
ldUseClientSpy({ 'site-under-maintenance-banner': false })
renderWithProviders(<Landing />, {
routerProvider: { route: '/?auth-error' },
})
expect(
screen.queryByRole('heading', { name: 'Sign in error' })
).toBeNull()
})

it('does not display sign in error by default', async () => {
ldUseClientSpy({ 'site-under-maintenance-banner': false })
renderWithProviders(<Landing />, {
routerProvider: { route: '/' },
})
expect(
screen.queryByRole('heading', {
name: 'Sign in error',
})
).toBeNull()
})
})
18 changes: 14 additions & 4 deletions services/app-web/src/pages/Landing/Landing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import {
ErrorAlertSiteUnavailable,
ErrorAlertSessionExpired,
ErrorAlertScheduledMaintenance,
ErrorAlertSignIn,
} from '../../components'

function maintenanceBannerForVariation(flag: string): React.ReactNode {
function maintenanceBannerForVariation(
flag: string
): React.ReactElement | undefined {
if (flag === 'UNSCHEDULED') {
return <ErrorAlertSiteUnavailable />
} else if (flag === 'SCHEDULED') {
Expand All @@ -27,22 +30,29 @@ export const Landing = (): React.ReactElement => {
featureFlags.SITE_UNDER_MAINTENANCE_BANNER.defaultValue
)

const maybeMaintenaceBanner = maintenanceBannerForVariation(
const maintainenceBanner = maintenanceBannerForVariation(
siteUnderMaintenanceBannerFlag
)

const redirectFromSessionTimeout = new URLSearchParams(location.search).get(
'session-timeout'
)

const redirectFromAuthError = new URLSearchParams(location.search).get(
'auth-error'
)

return (
<>
<section className={styles.detailsSection}>
<GridContainer className={styles.detailsSectionContent}>
{maybeMaintenaceBanner}
{redirectFromSessionTimeout && !maybeMaintenaceBanner && (
{maintainenceBanner}
{redirectFromSessionTimeout && !maintainenceBanner && (
<ErrorAlertSessionExpired />
)}
{redirectFromAuthError && !maintainenceBanner && (
<ErrorAlertSignIn />
)}
<Grid row gap className="margin-top-2">
<Grid tablet={{ col: 6 }}>
<div className={styles.detailsSteps}>
Expand Down
13 changes: 5 additions & 8 deletions services/app-web/src/pages/Wrapper/AuthenticatedRouteWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,12 @@ export const AuthenticatedRouteWrapper = ({

const logoutSession = useCallback(
(forcedSessionSignout: boolean) => {
updateSessionExpirationState(false)
if (logout) {
logout({ sessionTimeout: forcedSessionSignout }).catch((e) => {
recordJSException(`Error with logout: ${e}`)
setAlert && setAlert(<ErrorAlertSignIn />)
})
}
logout({ sessionTimeout: forcedSessionSignout }).catch((e) => {
recordJSException(`Error with logout: ${e}`)
setAlert && setAlert(<ErrorAlertSignIn />)
})
},
[logout, setAlert, updateSessionExpirationState]
[logout, setAlert]
)

const resetSessionTimeout = () => {
Expand Down

0 comments on commit b76afa9

Please sign in to comment.