/**
* Tests for AuthContext - auth-context.tsx
*
* TDD RED phase: Write failing tests first.
*/
import { describe, it, expect, vi } from 'vitest'
import { render, screen, waitFor, act } from '@testing-library/react'
import { AuthProvider, useAuth, type AuthContextValue } from '../../context/auth-context'
import type { AuthProvider as AuthProviderInterface } from '../../types'
// Helper component to consume auth context
function AuthConsumer({ onAuth }: { onAuth: (auth: AuthContextValue) => void }) {
const auth = useAuth()
onAuth(auth)
return (
{auth.isAuthenticated.toString()}
{auth.isLoading.toString()}
{auth.identity?.fullName ?? 'none'}
)
}
function createMockAuthProvider(overrides: Partial = {}): AuthProviderInterface {
return {
login: vi.fn().mockResolvedValue(undefined),
logout: vi.fn().mockResolvedValue(undefined),
checkAuth: vi.fn().mockResolvedValue(undefined),
checkError: vi.fn().mockResolvedValue(undefined),
getIdentity: vi.fn().mockResolvedValue({ id: '1', fullName: 'Test User' }),
getPermissions: vi.fn().mockResolvedValue(['admin']),
...overrides,
}
}
describe('AuthContext', () => {
describe('AuthProvider', () => {
it('should check auth on mount and set authenticated state', async () => {
const mockProvider = createMockAuthProvider()
render(
{}} />
)
// Initially loading
expect(screen.getByTestId('loading').textContent).toBe('true')
// Wait for auth check to complete
await waitFor(() => {
expect(screen.getByTestId('loading').textContent).toBe('false')
})
expect(mockProvider.checkAuth).toHaveBeenCalled()
expect(mockProvider.getIdentity).toHaveBeenCalled()
expect(mockProvider.getPermissions).toHaveBeenCalled()
expect(screen.getByTestId('authenticated').textContent).toBe('true')
expect(screen.getByTestId('identity').textContent).toBe('Test User')
})
it('should set unauthenticated state when checkAuth fails', async () => {
const mockProvider = createMockAuthProvider({
checkAuth: vi.fn().mockRejectedValue(new Error('Not authenticated')),
})
render(
{}} />
)
await waitFor(() => {
expect(screen.getByTestId('loading').textContent).toBe('false')
})
expect(screen.getByTestId('authenticated').textContent).toBe('false')
expect(screen.getByTestId('identity').textContent).toBe('none')
})
it('should call authProvider.login and refresh auth on login', async () => {
const mockProvider = createMockAuthProvider()
let _authValue: AuthContextValue | undefined
render(
{ _authValue = auth }} />
)
await waitFor(() => {
expect(screen.getByTestId('loading').textContent).toBe('false')
})
// Call login
await act(async () => {
await _authValue!.login({ username: 'test', password: 'pass' })
})
expect(mockProvider.login).toHaveBeenCalledWith({ username: 'test', password: 'pass' })
// checkAuth called twice: once on mount, once after login
expect(mockProvider.checkAuth).toHaveBeenCalledTimes(2)
})
it('should call authProvider.logout and clear state on logout', async () => {
const mockProvider = createMockAuthProvider()
let _authValue: AuthContextValue | undefined
render(
{ _authValue = auth }} />
)
await waitFor(() => {
expect(screen.getByTestId('authenticated').textContent).toBe('true')
})
// Call logout
await act(async () => {
await _authValue!.logout()
})
expect(mockProvider.logout).toHaveBeenCalled()
expect(screen.getByTestId('authenticated').textContent).toBe('false')
expect(screen.getByTestId('identity').textContent).toBe('none')
})
it('should provide permissions from authProvider', async () => {
const mockProvider = createMockAuthProvider({
getPermissions: vi.fn().mockResolvedValue(['admin', 'user', 'editor']),
})
let _authValue: AuthContextValue | undefined
render(
{ _authValue = auth }} />
)
await waitFor(() => {
expect(screen.getByTestId('loading').textContent).toBe('false')
})
expect(_authValue!.permissions).toEqual(['admin', 'user', 'editor'])
})
})
describe('useAuth', () => {
it('should throw error when used outside AuthProvider', () => {
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
expect(() => {
render( {}} />)
}).toThrow('useAuth must be used within an AuthProvider')
consoleError.mockRestore()
})
})
})