import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { AuthGate } from './AuthGate'; import type { AuthConfig, AuthSession } from './api'; const APP_MARKER = 'APP CONTENT'; function jsonResponse(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { headers: { 'Content-Type': 'application/json' }, status }); } interface Scenario { config: AuthConfig | { fail: true }; session?: AuthSession; login?: AuthSession; } function mockFetch(scenario: Scenario) { return vi.spyOn(window, 'fetch').mockImplementation((input, init) => { const url = typeof input === 'string' ? input : (input as Request).url; const method = (init?.method ?? 'GET').toUpperCase(); if (url.endsWith('/api/v1/auth/config')) { if ('fail' in scenario.config) { return Promise.resolve(jsonResponse({ status: 500, title: 'Server error' }, 500)); } return Promise.resolve(jsonResponse(scenario.config)); } if (url.endsWith('/api/v1/auth/session')) { return Promise.resolve(jsonResponse(scenario.session ?? { authenticated: false, username: null, method: null })); } if (url.endsWith('/api/v1/auth/login') && method === 'POST') { return Promise.resolve(jsonResponse(scenario.login ?? { authenticated: true, username: 'admin', method: 'local' })); } return Promise.resolve(new Response(null, { status: 404 })); }); } const localConfig: AuthConfig = { oidcEnabled: false, localLoginEnabled: true, setupRequired: false }; describe('AuthGate', () => { beforeEach(() => { window.localStorage.clear(); vi.restoreAllMocks(); }); afterEach(() => { cleanup(); }); it('renders the setup screen when setup is required', async () => { mockFetch({ config: { oidcEnabled: false, localLoginEnabled: true, setupRequired: true } }); render(
{APP_MARKER}
); expect(await screen.findByText('Create the first administrator account')).toBeTruthy(); expect(screen.queryByText(APP_MARKER)).toBeNull(); }); it('renders the login screen when unauthenticated, with the SSO button only when OIDC is enabled', async () => { mockFetch({ config: { oidcEnabled: true, localLoginEnabled: true, setupRequired: false } }); render(
{APP_MARKER}
); expect(await screen.findByRole('button', { name: /Sign in with SSO/ })).toBeTruthy(); expect(screen.queryByText(APP_MARKER)).toBeNull(); }); it('omits the SSO button when OIDC is disabled', async () => { mockFetch({ config: localConfig }); render(
{APP_MARKER}
); expect(await screen.findByRole('button', { name: 'Sign in' })).toBeTruthy(); expect(screen.queryByRole('button', { name: /SSO/ })).toBeNull(); }); it('renders children when already authenticated and clears the legacy API key', async () => { window.localStorage.setItem('ctv-api-key', 'legacy-secret'); mockFetch({ config: localConfig, session: { authenticated: true, username: 'admin', method: 'local' } }); render(
{APP_MARKER}
); expect(await screen.findByText(APP_MARKER)).toBeTruthy(); await waitFor(() => expect(window.localStorage.getItem('ctv-api-key')).toBeNull()); }); it('transitions from login to the app after a successful sign-in', async () => { mockFetch({ config: localConfig, login: { authenticated: true, username: 'admin', method: 'local' } }); render(
{APP_MARKER}
); const button = await screen.findByRole('button', { name: 'Sign in' }); fireEvent.change(screen.getByPlaceholderText('Username'), { target: { value: 'admin' } }); fireEvent.change(screen.getByPlaceholderText('Password'), { target: { value: 'hunter2' } }); fireEvent.click(button); expect(await screen.findByText(APP_MARKER)).toBeTruthy(); }); it('shows an error state with Retry when the config fetch fails, then recovers', async () => { const scenario: Scenario = { config: { fail: true } }; mockFetch(scenario); render(
{APP_MARKER}
); expect(await screen.findByRole('alert')).toBeTruthy(); const retry = screen.getByRole('button', { name: 'Retry' }); // Config now succeeds — Retry should re-run the gate and land on the login screen. scenario.config = localConfig; fireEvent.click(retry); expect(await screen.findByRole('button', { name: 'Sign in' })).toBeTruthy(); }); });