import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it, vi } from 'vitest';
import { App } from './app';
import { ServerProvider } from './context/server-context';
import { ApiClient, ApiError } from './lib/api';
import { I18nProvider } from './lib/i18n';
import type { SessionInfo } from './types';
describe('App connectivity states', () => {
it('shows an explicit offline state without substituting demo metrics', async () => {
localStorage.setItem('xfesm.locale', 'en-US');
const client = new ApiClient();
vi.spyOn(client, 'session').mockRejectedValue(new ApiError('Connection refused'));
vi.spyOn(client, 'subscribe').mockReturnValue(() => undefined);
render(<I18nProvider><ServerProvider client={client}><App /></ServerProvider></I18nProvider>);
await waitFor(() => expect(screen.getAllByText('Management API offline').length).toBeGreaterThan(0));
expect(screen.getByText('Connection refused')).toBeInTheDocument();
expect(screen.queryByText('20.00')).not.toBeInTheDocument();
});
it('checks the anonymous session before protected resources and does not open SSE while logged out', async () => {
localStorage.setItem('xfesm.locale', 'en-US');
const client = new ApiClient();
const session = vi.spyOn(client, 'session').mockResolvedValue({ authenticated: false, bootstrapRequired: true });
const status = vi.spyOn(client, 'status');
const features = vi.spyOn(client, 'features');
const subscribe = vi.spyOn(client, 'subscribe').mockReturnValue(() => undefined);
render(<I18nProvider><ServerProvider client={client}><App /></ServerProvider></I18nProvider>);
expect(await screen.findByRole('heading', { name: 'Create the first owner' })).toBeInTheDocument();
expect(session).toHaveBeenCalled();
expect(status).not.toHaveBeenCalled();
expect(features).not.toHaveBeenCalled();
expect(subscribe).not.toHaveBeenCalled();
});
it('submits recovery codes without applying numeric TOTP filtering', async () => {
localStorage.setItem('xfesm.locale', 'en-US');
const client = new ApiClient();
vi.spyOn(client, 'session').mockResolvedValue({ authenticated: false, bootstrapRequired: false });
vi.spyOn(client, 'subscribe').mockReturnValue(() => undefined);
const login = vi.spyOn(client, 'login').mockRejectedValue(new ApiError('rejected', 401));
const user = userEvent.setup();
render(<I18nProvider><ServerProvider client={client}><App /></ServerProvider></I18nProvider>);
await user.type(await screen.findByLabelText('Username'), 'owner');
await user.type(screen.getByLabelText('Password'), 'a-strong-password');
await user.click(screen.getByRole('button', { name: 'Use a recovery code' }));
await user.type(screen.getByLabelText('Recovery code'), 'word-AB_19');
await user.click(screen.getByRole('button', { name: 'Sign in securely' }));
await waitFor(() => expect(login).toHaveBeenCalledWith('owner', 'a-strong-password', 'word-AB_19', true));
});
it('loads protected data and starts the reconnecting SSE stream only after authentication', async () => {
localStorage.setItem('xfesm.locale', 'en-US');
const client = new ApiClient();
const session = vi.spyOn(client, 'session').mockResolvedValue({
authenticated: true,
csrfToken: 'csrf',
actor: { id: 'a1', displayName: 'owner', roles: ['owner'], permissions: [], totpVerified: true },
});
const status = vi.spyOn(client, 'status').mockResolvedValue({ sampledAt: '2026-01-01T00:00:00Z', uptimeSeconds: 1, playersOnline: 0 });
const features = vi.spyOn(client, 'features').mockResolvedValue({ consoleEnabled: true, moderationEnabled: false, claimsEnabled: false, worldTrackingEnabled: false, rollbackEnabled: false });
const unsubscribe = vi.fn();
const subscribe = vi.spyOn(client, 'subscribe').mockReturnValue(unsubscribe);
const rendered = render(<I18nProvider><ServerProvider client={client}><App /></ServerProvider></I18nProvider>);
expect(await screen.findByRole('heading', { name: 'Server overview' })).toBeInTheDocument();
expect(session.mock.invocationCallOrder[0]).toBeLessThan(status.mock.invocationCallOrder[0]);
expect(session.mock.invocationCallOrder[0]).toBeLessThan(features.mock.invocationCallOrder[0]);
await waitFor(() => expect(subscribe).toHaveBeenCalledOnce());
rendered.unmount();
expect(unsubscribe).toHaveBeenCalledOnce();
});
it('creates the first owner with the physical-console bootstrap code', async () => {
localStorage.setItem('xfesm.locale', 'en-US');
const client = new ApiClient();
const unauthenticated = { authenticated: false, bootstrapRequired: true } as const;
const authenticated: SessionInfo = {
authenticated: true,
csrfToken: 'csrf',
actor: { id: 'a1', displayName: 'owner', roles: ['owner'], permissions: [], totpVerified: false, reauthenticationRequired: false },
};
vi.spyOn(client, 'session').mockResolvedValueOnce(unauthenticated).mockResolvedValue(authenticated);
const setup = vi.spyOn(client, 'setup').mockResolvedValue(authenticated);
vi.spyOn(client, 'status').mockResolvedValue({ sampledAt: '2026-01-01T00:00:00Z', uptimeSeconds: 1, playersOnline: 0 });
vi.spyOn(client, 'features').mockResolvedValue({ consoleEnabled: false, moderationEnabled: false, claimsEnabled: false, worldTrackingEnabled: false, rollbackEnabled: false });
vi.spyOn(client, 'subscribe').mockReturnValue(() => undefined);
const user = userEvent.setup();
render(<I18nProvider><ServerProvider client={client}><App /></ServerProvider></I18nProvider>);
await user.type(await screen.findByLabelText('Console bootstrap code'), 'terminal-code');
await user.type(screen.getByLabelText('Username'), 'owner');
await user.type(screen.getByLabelText(/^Password/), 'a-strong-password');
await user.type(screen.getByLabelText('Confirm password'), 'a-strong-password');
await user.click(screen.getByRole('button', { name: 'Create owner' }));
await waitFor(() => expect(setup).toHaveBeenCalledWith('terminal-code', 'owner', 'a-strong-password'));
await waitFor(() => expect(screen.getByRole('heading', { name: 'Server overview' })).toBeInTheDocument());
expect(sessionStorage.getItem('xfesm.csrf')).toBe('csrf');
});
it('revokes the session, clears CSRF state, and closes SSE on logout', async () => {
localStorage.setItem('xfesm.locale', 'en-US');
sessionStorage.setItem('xfesm.csrf', 'stale');
const client = new ApiClient();
vi.spyOn(client, 'session').mockResolvedValue({
authenticated: true,
csrfToken: 'csrf',
actor: { id: 'a1', displayName: 'owner', roles: ['owner'], permissions: [], totpVerified: true },
});
vi.spyOn(client, 'status').mockResolvedValue({ sampledAt: '2026-01-01T00:00:00Z', uptimeSeconds: 1, playersOnline: 0 });
vi.spyOn(client, 'features').mockResolvedValue({ consoleEnabled: true, moderationEnabled: false, claimsEnabled: false, worldTrackingEnabled: false, rollbackEnabled: false });
const logout = vi.spyOn(client, 'logout').mockResolvedValue(undefined);
const unsubscribe = vi.fn();
vi.spyOn(client, 'subscribe').mockReturnValue(unsubscribe);
const user = userEvent.setup();
render(<I18nProvider><ServerProvider client={client}><App /></ServerProvider></I18nProvider>);
await user.click(await screen.findByRole('button', { name: 'Sign out' }));
await waitFor(() => expect(logout).toHaveBeenCalledOnce());
expect(await screen.findByRole('heading', { name: 'Administrator authentication' })).toBeInTheDocument();
expect(sessionStorage.getItem('xfesm.csrf')).toBeNull();
expect(unsubscribe).toHaveBeenCalledOnce();
});
});
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it, vi } from 'vitest';
import { App } from './app';
import { ServerProvider } from './context/server-context';
import { ApiClient, ApiError } from './lib/api';
import { I18nProvider } from './lib/i18n';
import type { SessionInfo } from './types';
describe('App connectivity states', () => {
it('shows an explicit offline state without substituting demo metrics', async () => {
localStorage.setItem('xfesm.locale', 'en-US');
const client = new ApiClient();
vi.spyOn(client, 'session').mockRejectedValue(new ApiError('Connection refused'));
vi.spyOn(client, 'subscribe').mockReturnValue(() => undefined);
render(<I18nProvider><ServerProvider client={client}><App /></ServerProvider></I18nProvider>);
await waitFor(() => expect(screen.getAllByText('Management API offline').length).toBeGreaterThan(0));
expect(screen.getByText('Connection refused')).toBeInTheDocument();
expect(screen.queryByText('20.00')).not.toBeInTheDocument();
});
it('checks the anonymous session before protected resources and does not open SSE while logged out', async () => {
localStorage.setItem('xfesm.locale', 'en-US');
const client = new ApiClient();
const session = vi.spyOn(client, 'session').mockResolvedValue({ authenticated: false, bootstrapRequired: true });
const status = vi.spyOn(client, 'status');
const features = vi.spyOn(client, 'features');
const subscribe = vi.spyOn(client, 'subscribe').mockReturnValue(() => undefined);
render(<I18nProvider><ServerProvider client={client}><App /></ServerProvider></I18nProvider>);
expect(await screen.findByRole('heading', { name: 'Create the first owner' })).toBeInTheDocument();
expect(session).toHaveBeenCalled();
expect(status).not.toHaveBeenCalled();
expect(features).not.toHaveBeenCalled();
expect(subscribe).not.toHaveBeenCalled();
});
it('submits recovery codes without applying numeric TOTP filtering', async () => {
localStorage.setItem('xfesm.locale', 'en-US');
const client = new ApiClient();
vi.spyOn(client, 'session').mockResolvedValue({ authenticated: false, bootstrapRequired: false });
vi.spyOn(client, 'subscribe').mockReturnValue(() => undefined);
const login = vi.spyOn(client, 'login').mockRejectedValue(new ApiError('rejected', 401));
const user = userEvent.setup();
render(<I18nProvider><ServerProvider client={client}><App /></ServerProvider></I18nProvider>);
await user.type(await screen.findByLabelText('Username'), 'owner');
await user.type(screen.getByLabelText('Password'), 'a-strong-password');
await user.click(screen.getByRole('button', { name: 'Use a recovery code' }));
await user.type(screen.getByLabelText('Recovery code'), 'word-AB_19');
await user.click(screen.getByRole('button', { name: 'Sign in securely' }));
await waitFor(() => expect(login).toHaveBeenCalledWith('owner', 'a-strong-password', 'word-AB_19', true));
});
it('loads protected data and starts the reconnecting SSE stream only after authentication', async () => {
localStorage.setItem('xfesm.locale', 'en-US');
const client = new ApiClient();
const session = vi.spyOn(client, 'session').mockResolvedValue({
authenticated: true,
csrfToken: 'csrf',
actor: { id: 'a1', displayName: 'owner', roles: ['owner'], permissions: [], totpVerified: true },
});
const status = vi.spyOn(client, 'status').mockResolvedValue({ sampledAt: '2026-01-01T00:00:00Z', uptimeSeconds: 1, playersOnline: 0 });
const features = vi.spyOn(client, 'features').mockResolvedValue({ consoleEnabled: true, moderationEnabled: false, claimsEnabled: false, worldTrackingEnabled: false, rollbackEnabled: false });
const unsubscribe = vi.fn();
const subscribe = vi.spyOn(client, 'subscribe').mockReturnValue(unsubscribe);
const rendered = render(<I18nProvider><ServerProvider client={client}><App /></ServerProvider></I18nProvider>);
expect(await screen.findByRole('heading', { name: 'Server overview' })).toBeInTheDocument();
expect(session.mock.invocationCallOrder[0]).toBeLessThan(status.mock.invocationCallOrder[0]);
expect(session.mock.invocationCallOrder[0]).toBeLessThan(features.mock.invocationCallOrder[0]);
await waitFor(() => expect(subscribe).toHaveBeenCalledOnce());
rendered.unmount();
expect(unsubscribe).toHaveBeenCalledOnce();
});
it('creates the first owner with the physical-console bootstrap code', async () => {
localStorage.setItem('xfesm.locale', 'en-US');
const client = new ApiClient();
const unauthenticated = { authenticated: false, bootstrapRequired: true } as const;
const authenticated: SessionInfo = {
authenticated: true,
csrfToken: 'csrf',
actor: { id: 'a1', displayName: 'owner', roles: ['owner'], permissions: [], totpVerified: false, reauthenticationRequired: false },
};
vi.spyOn(client, 'session').mockResolvedValueOnce(unauthenticated).mockResolvedValue(authenticated);
const setup = vi.spyOn(client, 'setup').mockResolvedValue(authenticated);
vi.spyOn(client, 'status').mockResolvedValue({ sampledAt: '2026-01-01T00:00:00Z', uptimeSeconds: 1, playersOnline: 0 });
vi.spyOn(client, 'features').mockResolvedValue({ consoleEnabled: false, moderationEnabled: false, claimsEnabled: false, worldTrackingEnabled: false, rollbackEnabled: false });
vi.spyOn(client, 'subscribe').mockReturnValue(() => undefined);
const user = userEvent.setup();
render(<I18nProvider><ServerProvider client={client}><App /></ServerProvider></I18nProvider>);
await user.type(await screen.findByLabelText('Console bootstrap code'), 'terminal-code');
await user.type(screen.getByLabelText('Username'), 'owner');
await user.type(screen.getByLabelText(/^Password/), 'a-strong-password');
await user.type(screen.getByLabelText('Confirm password'), 'a-strong-password');
await user.click(screen.getByRole('button', { name: 'Create owner' }));
await waitFor(() => expect(setup).toHaveBeenCalledWith('terminal-code', 'owner', 'a-strong-password'));
await waitFor(() => expect(screen.getByRole('heading', { name: 'Server overview' })).toBeInTheDocument());
expect(sessionStorage.getItem('xfesm.csrf')).toBe('csrf');
});
it('revokes the session, clears CSRF state, and closes SSE on logout', async () => {
localStorage.setItem('xfesm.locale', 'en-US');
sessionStorage.setItem('xfesm.csrf', 'stale');
const client = new ApiClient();
vi.spyOn(client, 'session').mockResolvedValue({
authenticated: true,
csrfToken: 'csrf',
actor: { id: 'a1', displayName: 'owner', roles: ['owner'], permissions: [], totpVerified: true },
});
vi.spyOn(client, 'status').mockResolvedValue({ sampledAt: '2026-01-01T00:00:00Z', uptimeSeconds: 1, playersOnline: 0 });
vi.spyOn(client, 'features').mockResolvedValue({ consoleEnabled: true, moderationEnabled: false, claimsEnabled: false, worldTrackingEnabled: false, rollbackEnabled: false });
const logout = vi.spyOn(client, 'logout').mockResolvedValue(undefined);
const unsubscribe = vi.fn();
vi.spyOn(client, 'subscribe').mockReturnValue(unsubscribe);
const user = userEvent.setup();
render(<I18nProvider><ServerProvider client={client}><App /></ServerProvider></I18nProvider>);
await user.click(await screen.findByRole('button', { name: 'Sign out' }));
await waitFor(() => expect(logout).toHaveBeenCalledOnce());
expect(await screen.findByRole('heading', { name: 'Administrator authentication' })).toBeInTheDocument();
expect(sessionStorage.getItem('xfesm.csrf')).toBeNull();
expect(unsubscribe).toHaveBeenCalledOnce();
});
});