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 { 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() 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() 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() await waitFor(() => expect(screen.getByRole('button', { name: 'Sign in' })).toBeInTheDocument()) }) it('closes the popover on Escape', async () => { stubFetch(userBody) render() 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() 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() 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() 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()) }) })