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();
});
});