import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { App } from '../app';
import { ServerProvider } from '../context/server-context';
import { ApiClient } from '../lib/api';
import { I18nProvider } from '../lib/i18n';
import type { ServerSettings, SessionInfo } from '../types';
const flags = {
consoleEnabled: true,
moderationEnabled: false,
claimsEnabled: false,
worldTrackingEnabled: false,
rollbackEnabled: false,
};
const settingsValue: ServerSettings = {
bindAddress: '127.0.0.1', port: 8765, publicBaseUrl: 'http://127.0.0.1:8765',
trustedProxyCidrs: [], idleSessionSeconds: 1800, absoluteSessionSeconds: 28800,
recentAuthenticationSeconds: 300, httpThreads: 8, maximumSseClients: 20,
webConsoleEnabled: true, rawItemEditingEnabled: false, metricsEnabled: false,
moderationEnabled: false, claimsEnabled: false, worldTrackingEnabled: false,
rollbackEnabled: false, joinExperienceEnabled: false,
joinWelcomeEnabled: true, joinWelcomeMessage: 'Welcome {player}',
firstJoinMessageEnabled: true, firstJoinMessage: 'First join {player}',
dailyAnnouncementsEnabled: true, dailyAnnouncements: ['Daily {date}'],
announcementTimeZone: 'UTC', rulesReminderEnabled: true, rulesMessage: 'Rules',
maintenanceJoinReminderEnabled: true, defaultMessageSender: 'XFEServerManager', features: flags,
crashProtection: { enabled: true, scanIntervalTicks: 100, itemEntitiesPerDimension: 2000,
itemEntitiesPerChunk: 256, livingEntitiesPerDimension: 2000, livingEntitiesPerChunk: 128,
totalEntitiesPerDimension: 5000, totalEntitiesPerChunk: 512,
entitiesPerNamespacePerDimension: 512, loadedChunksPerDimension: 8000,
spawnBurstLimit: 400, spawnBurstWindowTicks: 100, commandBlockCommandsPerSecond: 100,
slowTickMillis: 200, consecutiveSlowTicks: 3, heapUsagePercent: 90,
blockExcessSpawns: true, removeExcessItems: true, stopRunawayCommandBlocks: true },
};
describe('account security settings', () => {
afterEach(() => {
vi.restoreAllMocks();
location.hash = '';
});
it('enrolls TOTP, confirms it, and exposes the one-time recovery codes', async () => {
localStorage.setItem('xfesm.locale', 'en-US');
location.hash = '/overview';
const client = new ApiClient();
const initial: SessionInfo = {
authenticated: true,
csrfToken: 'csrf',
actor: { id: 'a1', displayName: 'owner', roles: ['owner'], permissions: [], totpVerified: false, totpEnabled: false },
};
const secured: SessionInfo = {
...initial,
actor: { ...initial.actor!, totpVerified: true, totpEnabled: true, reauthenticationRequired: false },
};
vi.spyOn(client, 'session').mockResolvedValueOnce(initial).mockResolvedValue(secured);
vi.spyOn(client, 'status').mockResolvedValue({ sampledAt: '2026-01-01T00:00:00Z', uptimeSeconds: 1, playersOnline: 0 });
vi.spyOn(client, 'features').mockResolvedValue(flags);
vi.spyOn(client, 'settings').mockResolvedValue(settingsValue);
vi.spyOn(client, 'subscribe').mockReturnValue(() => undefined);
const begin = vi.spyOn(client, 'beginTotpEnrollment').mockResolvedValue({ secret: 'JBSWY3DPEHPK3PXP', provisioningUri: 'otpauth://totp/XFE', expiresAt: '2026-01-01T00:10:00Z' });
const confirm = vi.spyOn(client, 'confirmTotpEnrollment').mockResolvedValue({ recoveryCodes: ['recover-one', 'recover-two'] });
const user = userEvent.setup();
render(<I18nProvider><ServerProvider client={client}><App /></ServerProvider></I18nProvider>);
await user.click(await screen.findByRole('button', { name: 'Personal settings' }));
expect(screen.getByText(/Optional. Sign in with your password/)).toBeInTheDocument();
expect(screen.queryByLabelText('Password')).not.toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'Enable two-factor authentication' }));
await user.type(screen.getByLabelText('Password'), 'a-strong-password');
await user.click(screen.getByRole('button', { name: 'Generate TOTP secret' }));
await waitFor(() => expect(begin).toHaveBeenCalledWith('a-strong-password'));
expect(await screen.findByDisplayValue('JBSWY3DPEHPK3PXP')).toBeInTheDocument();
await user.type(screen.getByLabelText('TOTP (when enabled)'), '123456');
await user.click(screen.getByRole('button', { name: 'Confirm and enable' }));
await waitFor(() => expect(confirm).toHaveBeenCalledWith('123456'));
expect(await screen.findByText('recover-one')).toBeInTheDocument();
expect(screen.getByText(/Recovery codes are shown once/)).toBeInTheDocument();
});
it('lets an enrolled account turn off two-factor authentication using a recovery code', async () => {
localStorage.setItem('xfesm.locale', 'en-US');
location.hash = '/profile';
const client = new ApiClient();
const enrolled: SessionInfo = { authenticated: true, csrfToken: 'csrf', actor: {
id: 'observer', displayName: 'observer', roles: ['observer'], permissions: ['status_read'],
totpVerified: true, totpEnabled: true,
} };
vi.spyOn(client, 'session').mockResolvedValueOnce(enrolled).mockResolvedValue({
...enrolled, actor: { ...enrolled.actor!, totpEnabled: false, totpVerified: false },
});
vi.spyOn(client, 'status').mockResolvedValue({ sampledAt: '2026-01-01T00:00:00Z', uptimeSeconds: 1, playersOnline: 0 });
vi.spyOn(client, 'features').mockResolvedValue(flags);
vi.spyOn(client, 'subscribe').mockReturnValue(() => undefined);
const disable = vi.spyOn(client, 'disableTotp').mockResolvedValue(undefined);
const user = userEvent.setup();
render(<I18nProvider><ServerProvider client={client}><App /></ServerProvider></I18nProvider>);
await user.click(await screen.findByRole('button', { name: 'Disable two-factor authentication' }));
await user.type(screen.getByLabelText('Password'), 'a-strong-password');
expect(screen.getByRole('button', { name: 'Confirm disable' })).toBeDisabled();
await user.click(screen.getByRole('button', { name: 'Use a recovery code' }));
await user.type(screen.getByLabelText('Recovery code'), 'recover-AB_123');
await user.click(screen.getByRole('button', { name: 'Confirm disable' }));
await waitFor(() => expect(disable).toHaveBeenCalledWith('a-strong-password', 'recover-AB_123', true));
expect(await screen.findByRole('button', { name: 'Enable two-factor authentication' })).toBeInTheDocument();
expect(screen.queryByDisplayValue('a-strong-password')).not.toBeInTheDocument();
expect(screen.queryByDisplayValue('recover-AB_123')).not.toBeInTheDocument();
});
it.each([false, true])('retries a sensitive operation with the configured authentication method (TOTP=%s)', async (enabled) => {
localStorage.setItem('xfesm.locale', 'en-US');
location.hash = '/console';
const client = new ApiClient();
vi.spyOn(client, 'session').mockResolvedValue({ authenticated: true, csrfToken: 'csrf', actor: {
id: 'owner', displayName: 'owner', roles: ['owner'], permissions: [],
totpVerified: enabled, totpEnabled: enabled, reauthenticationRequired: true,
} });
vi.spyOn(client, 'status').mockResolvedValue({ sampledAt: '2026-01-01T00:00:00Z', uptimeSeconds: 1, playersOnline: 0 });
vi.spyOn(client, 'features').mockResolvedValue(flags);
vi.spyOn(client, 'subscribe').mockReturnValue(() => undefined);
let attempts = 0;
const fetch = vi.spyOn(globalThis, 'fetch').mockImplementation(async (url) => {
if (String(url).endsWith('/auth/reauthenticate')) return new Response(null, { status: 204 });
attempts++;
return attempts === 1
? new Response(JSON.stringify({ detail: 'Recent authentication required' }), { status: 428, headers: { 'content-type': 'application/problem+json' } })
: new Response(JSON.stringify({ id: 'command-1', kind: 'console', state: 'QUEUED', createdAt: '2026-01-01T00:00:00Z' }), { status: 202, headers: { 'content-type': 'application/json' } });
});
const user = userEvent.setup();
render(<I18nProvider><ServerProvider client={client}><App /></ServerProvider></I18nProvider>);
await user.type(await screen.findByLabelText(/^Minecraft command/), 'list');
await user.type(screen.getByLabelText(/Audit reason/), 'Check connected players');
await user.click(screen.getByRole('button', { name: 'Preview' }));
await user.click(screen.getByRole('button', { name: 'Confirm execution' }));
expect(await screen.findByRole('dialog')).toBeInTheDocument();
await user.type(screen.getByLabelText(enabled ? '6-digit code' : 'Password'), enabled ? '654321' : 'a-strong-password');
await user.click(screen.getByRole('button', { name: 'Verify and continue' }));
await waitFor(() => expect(attempts).toBe(2));
const reauth = fetch.mock.calls.find(([url]) => String(url).endsWith('/auth/reauthenticate'));
expect(JSON.parse(reauth?.[1]?.body as string)).toEqual(enabled ? { totp: '654321' } : { password: 'a-strong-password' });
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
const commands = fetch.mock.calls.filter(([url]) => String(url).endsWith('/console/commands'));
expect(commands).toHaveLength(2);
expect(new Headers(commands[0][1]?.headers).get('Idempotency-Key')).toBe(new Headers(commands[1][1]?.headers).get('Idempotency-Key'));
});
});
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { App } from '../app';
import { ServerProvider } from '../context/server-context';
import { ApiClient } from '../lib/api';
import { I18nProvider } from '../lib/i18n';
import type { ServerSettings, SessionInfo } from '../types';
const flags = {
consoleEnabled: true,
moderationEnabled: false,
claimsEnabled: false,
worldTrackingEnabled: false,
rollbackEnabled: false,
};
const settingsValue: ServerSettings = {
bindAddress: '127.0.0.1', port: 8765, publicBaseUrl: 'http://127.0.0.1:8765',
trustedProxyCidrs: [], idleSessionSeconds: 1800, absoluteSessionSeconds: 28800,
recentAuthenticationSeconds: 300, httpThreads: 8, maximumSseClients: 20,
webConsoleEnabled: true, rawItemEditingEnabled: false, metricsEnabled: false,
moderationEnabled: false, claimsEnabled: false, worldTrackingEnabled: false,
rollbackEnabled: false, joinExperienceEnabled: false,
joinWelcomeEnabled: true, joinWelcomeMessage: 'Welcome {player}',
firstJoinMessageEnabled: true, firstJoinMessage: 'First join {player}',
dailyAnnouncementsEnabled: true, dailyAnnouncements: ['Daily {date}'],
announcementTimeZone: 'UTC', rulesReminderEnabled: true, rulesMessage: 'Rules',
maintenanceJoinReminderEnabled: true, defaultMessageSender: 'XFEServerManager', features: flags,
crashProtection: { enabled: true, scanIntervalTicks: 100, itemEntitiesPerDimension: 2000,
itemEntitiesPerChunk: 256, livingEntitiesPerDimension: 2000, livingEntitiesPerChunk: 128,
totalEntitiesPerDimension: 5000, totalEntitiesPerChunk: 512,
entitiesPerNamespacePerDimension: 512, loadedChunksPerDimension: 8000,
spawnBurstLimit: 400, spawnBurstWindowTicks: 100, commandBlockCommandsPerSecond: 100,
slowTickMillis: 200, consecutiveSlowTicks: 3, heapUsagePercent: 90,
blockExcessSpawns: true, removeExcessItems: true, stopRunawayCommandBlocks: true },
};
describe('account security settings', () => {
afterEach(() => {
vi.restoreAllMocks();
location.hash = '';
});
it('enrolls TOTP, confirms it, and exposes the one-time recovery codes', async () => {
localStorage.setItem('xfesm.locale', 'en-US');
location.hash = '/overview';
const client = new ApiClient();
const initial: SessionInfo = {
authenticated: true,
csrfToken: 'csrf',
actor: { id: 'a1', displayName: 'owner', roles: ['owner'], permissions: [], totpVerified: false, totpEnabled: false },
};
const secured: SessionInfo = {
...initial,
actor: { ...initial.actor!, totpVerified: true, totpEnabled: true, reauthenticationRequired: false },
};
vi.spyOn(client, 'session').mockResolvedValueOnce(initial).mockResolvedValue(secured);
vi.spyOn(client, 'status').mockResolvedValue({ sampledAt: '2026-01-01T00:00:00Z', uptimeSeconds: 1, playersOnline: 0 });
vi.spyOn(client, 'features').mockResolvedValue(flags);
vi.spyOn(client, 'settings').mockResolvedValue(settingsValue);
vi.spyOn(client, 'subscribe').mockReturnValue(() => undefined);
const begin = vi.spyOn(client, 'beginTotpEnrollment').mockResolvedValue({ secret: 'JBSWY3DPEHPK3PXP', provisioningUri: 'otpauth://totp/XFE', expiresAt: '2026-01-01T00:10:00Z' });
const confirm = vi.spyOn(client, 'confirmTotpEnrollment').mockResolvedValue({ recoveryCodes: ['recover-one', 'recover-two'] });
const user = userEvent.setup();
render(<I18nProvider><ServerProvider client={client}><App /></ServerProvider></I18nProvider>);
await user.click(await screen.findByRole('button', { name: 'Personal settings' }));
expect(screen.getByText(/Optional. Sign in with your password/)).toBeInTheDocument();
expect(screen.queryByLabelText('Password')).not.toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'Enable two-factor authentication' }));
await user.type(screen.getByLabelText('Password'), 'a-strong-password');
await user.click(screen.getByRole('button', { name: 'Generate TOTP secret' }));
await waitFor(() => expect(begin).toHaveBeenCalledWith('a-strong-password'));
expect(await screen.findByDisplayValue('JBSWY3DPEHPK3PXP')).toBeInTheDocument();
await user.type(screen.getByLabelText('TOTP (when enabled)'), '123456');
await user.click(screen.getByRole('button', { name: 'Confirm and enable' }));
await waitFor(() => expect(confirm).toHaveBeenCalledWith('123456'));
expect(await screen.findByText('recover-one')).toBeInTheDocument();
expect(screen.getByText(/Recovery codes are shown once/)).toBeInTheDocument();
});
it('lets an enrolled account turn off two-factor authentication using a recovery code', async () => {
localStorage.setItem('xfesm.locale', 'en-US');
location.hash = '/profile';
const client = new ApiClient();
const enrolled: SessionInfo = { authenticated: true, csrfToken: 'csrf', actor: {
id: 'observer', displayName: 'observer', roles: ['observer'], permissions: ['status_read'],
totpVerified: true, totpEnabled: true,
} };
vi.spyOn(client, 'session').mockResolvedValueOnce(enrolled).mockResolvedValue({
...enrolled, actor: { ...enrolled.actor!, totpEnabled: false, totpVerified: false },
});
vi.spyOn(client, 'status').mockResolvedValue({ sampledAt: '2026-01-01T00:00:00Z', uptimeSeconds: 1, playersOnline: 0 });
vi.spyOn(client, 'features').mockResolvedValue(flags);
vi.spyOn(client, 'subscribe').mockReturnValue(() => undefined);
const disable = vi.spyOn(client, 'disableTotp').mockResolvedValue(undefined);
const user = userEvent.setup();
render(<I18nProvider><ServerProvider client={client}><App /></ServerProvider></I18nProvider>);
await user.click(await screen.findByRole('button', { name: 'Disable two-factor authentication' }));
await user.type(screen.getByLabelText('Password'), 'a-strong-password');
expect(screen.getByRole('button', { name: 'Confirm disable' })).toBeDisabled();
await user.click(screen.getByRole('button', { name: 'Use a recovery code' }));
await user.type(screen.getByLabelText('Recovery code'), 'recover-AB_123');
await user.click(screen.getByRole('button', { name: 'Confirm disable' }));
await waitFor(() => expect(disable).toHaveBeenCalledWith('a-strong-password', 'recover-AB_123', true));
expect(await screen.findByRole('button', { name: 'Enable two-factor authentication' })).toBeInTheDocument();
expect(screen.queryByDisplayValue('a-strong-password')).not.toBeInTheDocument();
expect(screen.queryByDisplayValue('recover-AB_123')).not.toBeInTheDocument();
});
it.each([false, true])('retries a sensitive operation with the configured authentication method (TOTP=%s)', async (enabled) => {
localStorage.setItem('xfesm.locale', 'en-US');
location.hash = '/console';
const client = new ApiClient();
vi.spyOn(client, 'session').mockResolvedValue({ authenticated: true, csrfToken: 'csrf', actor: {
id: 'owner', displayName: 'owner', roles: ['owner'], permissions: [],
totpVerified: enabled, totpEnabled: enabled, reauthenticationRequired: true,
} });
vi.spyOn(client, 'status').mockResolvedValue({ sampledAt: '2026-01-01T00:00:00Z', uptimeSeconds: 1, playersOnline: 0 });
vi.spyOn(client, 'features').mockResolvedValue(flags);
vi.spyOn(client, 'subscribe').mockReturnValue(() => undefined);
let attempts = 0;
const fetch = vi.spyOn(globalThis, 'fetch').mockImplementation(async (url) => {
if (String(url).endsWith('/auth/reauthenticate')) return new Response(null, { status: 204 });
attempts++;
return attempts === 1
? new Response(JSON.stringify({ detail: 'Recent authentication required' }), { status: 428, headers: { 'content-type': 'application/problem+json' } })
: new Response(JSON.stringify({ id: 'command-1', kind: 'console', state: 'QUEUED', createdAt: '2026-01-01T00:00:00Z' }), { status: 202, headers: { 'content-type': 'application/json' } });
});
const user = userEvent.setup();
render(<I18nProvider><ServerProvider client={client}><App /></ServerProvider></I18nProvider>);
await user.type(await screen.findByLabelText(/^Minecraft command/), 'list');
await user.type(screen.getByLabelText(/Audit reason/), 'Check connected players');
await user.click(screen.getByRole('button', { name: 'Preview' }));
await user.click(screen.getByRole('button', { name: 'Confirm execution' }));
expect(await screen.findByRole('dialog')).toBeInTheDocument();
await user.type(screen.getByLabelText(enabled ? '6-digit code' : 'Password'), enabled ? '654321' : 'a-strong-password');
await user.click(screen.getByRole('button', { name: 'Verify and continue' }));
await waitFor(() => expect(attempts).toBe(2));
const reauth = fetch.mock.calls.find(([url]) => String(url).endsWith('/auth/reauthenticate'));
expect(JSON.parse(reauth?.[1]?.body as string)).toEqual(enabled ? { totp: '654321' } : { password: 'a-strong-password' });
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
const commands = fetch.mock.calls.filter(([url]) => String(url).endsWith('/console/commands'));
expect(commands).toHaveLength(2);
expect(new Headers(commands[0][1]?.headers).get('Idempotency-Key')).toBe(new Headers(commands[1][1]?.headers).get('Idempotency-Key'));
});
});