Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
218 changes: 103 additions & 115 deletions frontend/src/__tests__/LoginPage.google-signin.test.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'

// authAPI.handleOIDCCallback calls `api.post` where `api` is the module-level
// authAPI.handleAuthCallback calls `api.post` where `api` is the module-level
// axios instance — the same object reference as the default export of ../services/api.
// We use vi.spyOn on the imported default to intercept calls made by the
// closed-over internal `api` variable (same object reference).
Expand All @@ -13,6 +13,11 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'

import api, { authAPI } from '../services/api'

// The provider-neutral Security API surface the browser talks to. No identity
// provider is named — the social callback exchanges an opaque `?code=` for a
// session at the same-origin `/api/v1/security/session/exchange` endpoint.
const EXCHANGE_PATH = '/v1/security/session/exchange'

// Mock localStorage
const localStorageMock = (() => {
let store: Record<string, string> = {}
Expand All @@ -37,7 +42,7 @@ function setSearch(search: string) {
})
}

describe('authAPI.handleOIDCCallback', () => {
describe('authAPI.handleAuthCallback', () => {
beforeEach(() => {
localStorageMock.clear()
vi.clearAllMocks()
Expand All @@ -51,25 +56,49 @@ describe('authAPI.handleOIDCCallback', () => {
vi.restoreAllMocks()
})

it('exchanges code for token and stores in localStorage', async () => {
it('exchanges code for an authenticated session and stores it in localStorage', async () => {
setSearch('?code=abc123')
vi.mocked(api.post).mockResolvedValueOnce({
data: { token: 'jwt-token', sessionId: 'sess-1' }
data: {
status: 'authenticated',
token: 'jwt-token',
sessionId: 'sess-1',
user: { id: 'u1' },
},
})

const result = await authAPI.handleOIDCCallback()
const result = await authAPI.handleAuthCallback()

expect(api.post).toHaveBeenCalledWith('/auth/token-exchange', { code: 'abc123' })
expect(api.post).toHaveBeenCalledWith(EXCHANGE_PATH, { code: 'abc123' })
expect(localStorageMock.getItem('authToken')).toBe('jwt-token')
expect(localStorageMock.getItem('sessionId')).toBe('sess-1')
expect(result).toEqual({ token: 'jwt-token', sessionId: 'sess-1' })
expect(result.result).toMatchObject({ status: 'authenticated', token: 'jwt-token' })
expect(result.error).toBeUndefined()
expect(mockReplaceState).toHaveBeenCalled()
})

it('returns an mfa_required challenge without persisting a session', async () => {
setSearch('?code=needs-mfa')
vi.mocked(api.post).mockResolvedValueOnce({
data: {
status: 'mfa_required',
challengeId: 'ch-1',
factors: [{ factorId: 'f1', type: 'totp' }],
},
})

const result = await authAPI.handleAuthCallback()

expect(api.post).toHaveBeenCalledWith(EXCHANGE_PATH, { code: 'needs-mfa' })
expect(result.result).toMatchObject({ status: 'mfa_required', challengeId: 'ch-1' })
// No session is established for an MFA challenge.
expect(localStorageMock.getItem('authToken')).toBeNull()
})

it('returns error from URL when error param present', async () => {
setSearch('?error=oidc_error&message=access_denied')
setSearch('?error=social_error&message=access_denied')

const result = await authAPI.handleOIDCCallback()
const result = await authAPI.handleAuthCallback()

expect(result).toEqual({ error: 'access_denied' })
expect(api.post).not.toHaveBeenCalled()
Expand All @@ -78,7 +107,7 @@ describe('authAPI.handleOIDCCallback', () => {
it('returns empty object when no params in URL', async () => {
setSearch('')

const result = await authAPI.handleOIDCCallback()
const result = await authAPI.handleAuthCallback()

expect(result).toEqual({})
expect(api.post).not.toHaveBeenCalled()
Expand All @@ -87,10 +116,23 @@ describe('authAPI.handleOIDCCallback', () => {
it('does not read token directly from URL', async () => {
setSearch('?token=some-jwt&sessionId=sess')

const result = await authAPI.handleOIDCCallback()
const result = await authAPI.handleAuthCallback()

expect(result).toEqual({})
expect(api.post).not.toHaveBeenCalled()
expect(localStorageMock.getItem('authToken')).toBeNull()
})

it('surfaces a friendly error when the exchange POST fails', async () => {
setSearch('?code=will-fail')
vi.mocked(api.post).mockRejectedValueOnce({
response: { data: { error: 'exchange_failed' } },
})

const result = await authAPI.handleAuthCallback()

expect(result.error).toBe('exchange_failed')
expect(result.result).toBeUndefined()
expect(localStorageMock.getItem('authToken')).toBeNull()
})
})
Loading
Loading