Files
gibbz/frontend/tests/auth.test.tsx
T
2026-09-22 08:41:20 +02:00

118 lines
4.2 KiB
TypeScript

import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { UserMenu } from '../src/auth/UserMenu'
import { SessionCard } from '../src/auth/SessionCard'
import { login } from '../src/auth/useSession'
const anonymousBody = JSON.stringify({ authenticated: false, user: null })
const userBody = JSON.stringify({
authenticated: true,
user: {
sub: 'user-1',
name: 'Mats Test',
email: 'mats@example.com',
roles: ['admin'],
},
})
function stubFetch(body: string, ok = true): ReturnType<typeof vi.fn> {
const fetchMock = vi.fn().mockResolvedValue({
ok,
json: () => Promise.resolve(JSON.parse(body)),
})
vi.stubGlobal('fetch', fetchMock)
return fetchMock
}
// jsdom's window.location is unforgeable; replace the whole global so the
// login() navigation target can be asserted.
beforeEach(() => {
vi.stubGlobal('location', { href: '', pathname: '/patterns', search: '?pattern=table' })
})
afterEach(() => {
vi.unstubAllGlobals()
})
describe('login()', () => {
it('navigates to the backend login with the current path as returnTo', () => {
login()
expect(window.location.href).toBe('/api/auth/login?returnTo=' + encodeURIComponent('/patterns?pattern=table'))
})
})
describe('UserMenu', () => {
it('shows Sign in when anonymous', async () => {
stubFetch(anonymousBody)
render(<UserMenu />)
await waitFor(() => expect(screen.getByRole('button', { name: 'Sign in' })).toBeInTheDocument())
})
it('shows the avatar and a sign-out popover when authenticated', async () => {
stubFetch(userBody)
render(<UserMenu />)
const avatar = await waitFor(() => screen.getByRole('button', { name: 'Mats Test' }))
expect(avatar).toHaveTextContent('MT')
fireEvent.click(avatar)
expect(screen.getByRole('menu')).toBeInTheDocument()
expect(screen.getByText('mats@example.com')).toBeInTheDocument()
const fetchMock = vi.mocked(globalThis.fetch)
fireEvent.click(screen.getByRole('menuitem', { name: /sign out/i }))
await waitFor(() =>
expect(fetchMock).toHaveBeenCalledWith('/api/auth/logout', { method: 'POST' }),
)
// Back to anonymous.
await waitFor(() => expect(screen.getByRole('button', { name: 'Sign in' })).toBeInTheDocument())
})
it('treats an unreachable backend as anonymous', async () => {
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('backend down')))
render(<UserMenu />)
await waitFor(() => expect(screen.getByRole('button', { name: 'Sign in' })).toBeInTheDocument())
})
it('closes the popover on Escape', async () => {
stubFetch(userBody)
render(<UserMenu />)
fireEvent.click(await waitFor(() => screen.getByRole('button', { name: 'Mats Test' })))
expect(screen.getByRole('menu')).toBeInTheDocument()
fireEvent.keyDown(document, { key: 'Escape' })
expect(screen.queryByRole('menu')).not.toBeInTheDocument()
})
})
describe('SessionCard', () => {
it('offers sign-in with a note when anonymous', async () => {
stubFetch(anonymousBody)
render(<SessionCard />)
const button = await waitFor(() => screen.getByRole('button', { name: 'Sign in' }))
expect(screen.getByText(/not signed in/i)).toBeInTheDocument()
fireEvent.click(button)
expect(window.location.href).toBe('/api/auth/login?returnTo=' + encodeURIComponent('/'))
})
it('shows the signed-in user, roles, and sub when authenticated', async () => {
stubFetch(userBody)
render(<SessionCard />)
expect(await screen.findByText('Mats Test')).toBeInTheDocument()
expect(screen.getByText('mats@example.com')).toBeInTheDocument()
expect(screen.getByText('sub: user-1')).toBeInTheDocument()
expect(screen.getByText('admin')).toBeInTheDocument()
})
it('signs out and flips back to anonymous', async () => {
stubFetch(userBody)
render(<SessionCard />)
fireEvent.click(await screen.findByRole('button', { name: 'Sign out' }))
const fetchMock = vi.mocked(globalThis.fetch)
await waitFor(() =>
expect(fetchMock).toHaveBeenCalledWith('/api/auth/logout', { method: 'POST' }),
)
await waitFor(() => expect(screen.getByRole('button', { name: 'Sign in' })).toBeInTheDocument())
})
})