Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/asa_go_integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ jobs:
run: |
xcodebuild test \
-scheme "Keycloak" \
-destination "platform=iOS Simulator,name=iPhone 16 Pro" \
-destination "platform=iOS Simulator,name=iPhone 17" \
-enableCodeCoverage YES \
-resultBundlePath ${{ runner.temp }}/keycloak.xcresult
- name: Upload iOS coverage to Codecov
Expand Down
1 change: 0 additions & 1 deletion mobile/asa-go/src/app.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,6 @@ describe('App', () => {
authentication: {
sessionMode: 'authenticated',
authenticating: false,
tokenRefreshed: false,
token: undefined,
idToken: undefined,
idir: undefined,
Expand Down
3 changes: 0 additions & 3 deletions mobile/asa-go/src/components/AuthWrapper.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ describe('AuthWrapper', () => {
sessionMode: 'authenticated',
authenticating: false,
error: null,
tokenRefreshed: false,
idToken: undefined,
idir: undefined,
token: 'test-token',
Expand All @@ -58,7 +57,6 @@ describe('AuthWrapper', () => {
sessionMode: 'login',
authenticating: false,
error: null,
tokenRefreshed: false,
idToken: undefined,
idir: undefined,
token: 'test-token',
Expand All @@ -79,7 +77,6 @@ describe('AuthWrapper', () => {
sessionMode: 'login',
authenticating: false,
error: null,
tokenRefreshed: false,
idToken: undefined,
idir: undefined,
token: undefined,
Expand Down
1 change: 0 additions & 1 deletion mobile/asa-go/src/components/LoginActions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ const mockAuthState = (authenticating: boolean, error: string | null = null) =>
vi.spyOn(selectors, 'selectAuthentication').mockReturnValue({
authenticating,
error,
tokenRefreshed: false,
idToken: undefined,
idir: undefined,
token: undefined,
Expand Down
8 changes: 4 additions & 4 deletions mobile/asa-go/src/components/PublicLoginButton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { fireEvent, render, screen } from '@testing-library/react'
import { useDispatch } from 'react-redux'
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
import PublicLoginButton from '@/components/PublicLoginButton'
import { continueAsGuest } from '@/slices/authenticationSlice'
import { continueAsGuestSession } from '@/slices/authenticationSlice'

vi.mock('react-redux', async () => {
const actual = await vi.importActual('react-redux')
Expand All @@ -14,7 +14,7 @@ vi.mock('react-redux', async () => {
})

vi.mock('@/slices/authenticationSlice', () => ({
continueAsGuest: vi.fn(() => ({ type: 'CONTINUE_AS_GUEST' }))
continueAsGuestSession: vi.fn(() => ({ type: 'CONTINUE_AS_GUEST_SESSION' }))
}))

describe('PublicLoginButton', () => {
Expand All @@ -39,11 +39,11 @@ describe('PublicLoginButton', () => {
expect(screen.getByRole('button', { name: /continue as guest/i })).toBeInTheDocument()
})

it('dispatches continueAsGuest on click', () => {
it('dispatches continueAsGuestSession on click', () => {
renderComponent()

fireEvent.click(screen.getByRole('button', { name: /continue as guest/i }))

expect(mockDispatch).toHaveBeenCalledWith(continueAsGuest())
expect(mockDispatch).toHaveBeenCalledWith(continueAsGuestSession())
})
})
4 changes: 2 additions & 2 deletions mobile/asa-go/src/components/PublicLoginButton.tsx
Comment thread
conbrad marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Button } from '@mui/material'
import { useDispatch } from 'react-redux'
import { continueAsGuest } from '@/slices/authenticationSlice'
import { continueAsGuestSession } from '@/slices/authenticationSlice'
import type { AppDispatch } from '@/store'

const PublicLoginButton = () => {
const dispatch: AppDispatch = useDispatch()
const handleClick = () => {
dispatch(continueAsGuest())
dispatch(continueAsGuestSession())
}
return (
<Button onClick={handleClick} sx={{ color: 'white', textDecoration: 'underline' }}>
Expand Down
119 changes: 67 additions & 52 deletions mobile/asa-go/src/slices/authenticationSlice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ import authenticationSlice, {
authenticateFinished,
authenticateStart,
continueAsGuest,
continueAsGuestSession,
initialState,
refreshTokenFinished,
resetAuthentication
} from '@/slices/authenticationSlice'
import { createTestStore } from '@/testUtils'
import { Keycloak } from '../../../keycloak/src'

interface TokenResponse {
accessToken: string
idToken?: string
refreshToken?: string
tokenType?: string
expiresIn?: number
Expand All @@ -31,7 +32,8 @@ const mockValidToken =
vi.mock('../../../keycloak/src', () => ({
Keycloak: {
authenticate: vi.fn(),
addListener: vi.fn()
addListener: vi.fn(),
clearAuthState: vi.fn()
}
}))

Expand Down Expand Up @@ -61,6 +63,7 @@ describe('authenticationSlice', () => {

const createTokenResponse = (overrides: Partial<TokenResponse> = {}): TokenResponse => ({
accessToken: mockValidToken,
idToken: 'new-id-token',
refreshToken: 'new-refresh-token',
tokenType: 'Bearer',
expiresIn: 3600,
Expand Down Expand Up @@ -122,7 +125,8 @@ describe('authenticationSlice', () => {
sessionMode: 'authenticated',
token: 'existing-token',
idToken: 'existing-id-token',
idir: 'test-user'
idir: 'test-user',
email: 'test@example.com'
})

const nextState = authenticationSlice(previousState, continueAsGuest())
Expand All @@ -133,6 +137,7 @@ describe('authenticationSlice', () => {
token: undefined,
idToken: undefined,
idir: undefined,
email: undefined,
error: null
})
})
Expand All @@ -157,7 +162,11 @@ describe('authenticationSlice', () => {
it('should handle authenticateError', () => {
const previousState = createAuthState({
authenticating: true,
sessionMode: 'authenticated'
sessionMode: 'authenticated',
token: 'existing-token',
idToken: 'existing-id-token',
idir: 'test-user',
email: 'test@example.com'
})
const errorMessage = 'Authentication failed'

Expand All @@ -166,66 +175,35 @@ describe('authenticationSlice', () => {
expectAuthState(nextState, {
sessionMode: 'login',
authenticating: false,
error: errorMessage
})
})

it('should handle refreshTokenFinished', () => {
const previousState = createAuthState({
token: 'old-token',
idToken: 'old-id-token',
tokenRefreshed: false
})
const payload = {
tokenRefreshed: true,
token: mockValidToken,
idToken: 'new-id-token'
}

const nextState = authenticationSlice(previousState, refreshTokenFinished(payload))

expectAuthState(nextState, {
sessionMode: 'authenticated',
token: mockValidToken,
idToken: 'new-id-token',
tokenRefreshed: true
})
})

it('should handle refreshTokenFinished with undefined tokens', () => {
const previousState = createAuthState({
token: 'existing-token',
idToken: 'existing-id-token',
tokenRefreshed: false
})
const payload = {
tokenRefreshed: false,
error: errorMessage,
token: undefined,
idToken: undefined
}

const nextState = authenticationSlice(previousState, refreshTokenFinished(payload))

expect(nextState.token).toBeUndefined()
expect(nextState.idToken).toBeUndefined()
expect(nextState.tokenRefreshed).toBe(false)
idToken: undefined,
idir: undefined,
email: undefined
})
})

it('should handle resetAuthentication', () => {
const previousState = createAuthState({
sessionMode: 'authenticated',
authenticating: true,
token: 'existing-token',
idToken: 'existing-id-token',
idir: 'test-user'
idir: 'test-user',
email: 'test@example.com',
error: 'existing-error'
})

const nextState = authenticationSlice(previousState, resetAuthentication())

expectAuthState(nextState, {
sessionMode: 'login',
authenticating: false,
token: undefined,
idToken: undefined,
idir: undefined
idir: undefined,
email: undefined,
error: null
})
})
})
Expand Down Expand Up @@ -325,10 +303,9 @@ describe('authenticationSlice', () => {
tokenRefreshCallback(tokenResponse)

expectAuthState(store.getState().authentication, {
tokenRefreshed: true,
token: mockValidToken
token: mockValidToken,
idToken: 'new-id-token'
})
expect(store.getState().authentication.idToken).toBeUndefined()
expect(mockSetUser).toHaveBeenCalledWith({ email: 'john.doe@contact.com' })
})

Expand All @@ -345,9 +322,47 @@ describe('authenticationSlice', () => {
tokenRefreshCallback(tokenResponse)

const finalState = store.getState().authentication
expect(finalState.tokenRefreshed).toBe(initialState.tokenRefreshed)
expect(finalState.token).toBe(initialState.token)
})

it('clears native auth state before continuing as guest', async () => {
;(Keycloak.clearAuthState as Mock).mockResolvedValue(undefined)
const store = createTestStore({
authentication: createAuthState({
sessionMode: 'authenticated',
token: mockValidToken,
idToken: 'existing-id-token',
idir: 'test-user',
email: 'test@example.com'
})
})

await store.dispatch(continueAsGuestSession())

expect(Keycloak.clearAuthState).toHaveBeenCalled()
expectAuthState(store.getState().authentication, {
sessionMode: 'guest',
token: undefined,
idToken: undefined,
idir: undefined,
email: undefined
})
expect(mockSetUser).toHaveBeenCalledWith(null)
})

it('continues as guest when native auth clear fails', async () => {
;(Keycloak.clearAuthState as Mock).mockRejectedValue(new Error('clear failed'))
const store = createTestStore({
authentication: createAuthState({
sessionMode: 'authenticated',
token: mockValidToken
})
})

await store.dispatch(continueAsGuestSession())

expect(store.getState().authentication.sessionMode).toBe('guest')
})
})
})
})
Loading
Loading