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 } from '../lib/api';
import { I18nProvider } from '../lib/i18n';
import type { Role } from '../types';
function clientFor(role: Role, permissions: string[]): ApiClient {
const client = new ApiClient();
vi.spyOn(client, 'session').mockResolvedValue({
authenticated: true,
csrfToken: 'csrf',
actor: { id: `${role}-id`, displayName: role, roles: [role], permissions, totpVerified: false },
});
vi.spyOn(client, 'status').mockResolvedValue({
sampledAt: '2026-01-01T00:00:00Z', uptimeSeconds: 1, playersOnline: 1,
});
vi.spyOn(client, 'features').mockResolvedValue({
consoleEnabled: false,
moderationEnabled: false,
claimsEnabled: false,
worldTrackingEnabled: false,
rollbackEnabled: false,
});
vi.spyOn(client, 'subscribe').mockReturnValue(() => undefined);
return client;
}
function renderApp(client: ApiClient) {
return render(<I18nProvider><ServerProvider client={client}><App /></ServerProvider></I18nProvider>);
}
describe('permission-aware management UI', () => {
it('keeps observer player read access but does not expose mutation or inventory controls', async () => {
localStorage.setItem('xfesm.locale', 'en-US');
location.hash = '/players';
const client = clientFor('observer', ['status_read', 'player_read', 'claim_read']);
vi.spyOn(client, 'players').mockResolvedValue({
items: [{ uuid: '11111111-1111-1111-1111-111111111111', name: 'Alex', online: true }],
});
renderApp(client);
expect(await screen.findByText('Alex')).toBeInTheDocument();
expect(screen.getByText('Read only')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Manage' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Command policies' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Audit log' })).not.toBeInTheDocument();
});
it('shows helper direct messaging and hides every action the helper cannot perform', async () => {
localStorage.setItem('xfesm.locale', 'en-US');
location.hash = '/players';
const client = clientFor('helper', ['status_read', 'player_read', 'player_message', 'claim_read']);
vi.spyOn(client, 'players').mockResolvedValue({
items: [{ uuid: '11111111-1111-1111-1111-111111111111', name: 'Alex', online: true }],
});
const inventory = vi.spyOn(client, 'playerInventory');
const user = userEvent.setup();
renderApp(client);
await user.click(await screen.findByRole('button', { name: 'Manage' }));
expect(screen.getByRole('option', { name: 'Direct message' })).toBeInTheDocument();
expect(screen.getAllByRole('option')).toHaveLength(1);
expect(screen.queryByRole('tab', { name: 'Inventory' })).not.toBeInTheDocument();
expect(screen.queryByRole('tab', { name: 'Ender chest' })).not.toBeInTheDocument();
expect(inventory).not.toHaveBeenCalled();
});
it('blocks direct navigation before an unauthorized policy request is sent', async () => {
localStorage.setItem('xfesm.locale', 'en-US');
location.hash = '/policies';
const client = clientFor('observer', ['status_read', 'player_read', 'claim_read']);
const policies = vi.spyOn(client, 'policies');
renderApp(client);
expect(await screen.findByRole('heading', { name: 'Access denied' })).toBeInTheDocument();
await waitFor(() => expect(policies).not.toHaveBeenCalled());
});
});
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 } from '../lib/api';
import { I18nProvider } from '../lib/i18n';
import type { Role } from '../types';
function clientFor(role: Role, permissions: string[]): ApiClient {
const client = new ApiClient();
vi.spyOn(client, 'session').mockResolvedValue({
authenticated: true,
csrfToken: 'csrf',
actor: { id: `${role}-id`, displayName: role, roles: [role], permissions, totpVerified: false },
});
vi.spyOn(client, 'status').mockResolvedValue({
sampledAt: '2026-01-01T00:00:00Z', uptimeSeconds: 1, playersOnline: 1,
});
vi.spyOn(client, 'features').mockResolvedValue({
consoleEnabled: false,
moderationEnabled: false,
claimsEnabled: false,
worldTrackingEnabled: false,
rollbackEnabled: false,
});
vi.spyOn(client, 'subscribe').mockReturnValue(() => undefined);
return client;
}
function renderApp(client: ApiClient) {
return render(<I18nProvider><ServerProvider client={client}><App /></ServerProvider></I18nProvider>);
}
describe('permission-aware management UI', () => {
it('keeps observer player read access but does not expose mutation or inventory controls', async () => {
localStorage.setItem('xfesm.locale', 'en-US');
location.hash = '/players';
const client = clientFor('observer', ['status_read', 'player_read', 'claim_read']);
vi.spyOn(client, 'players').mockResolvedValue({
items: [{ uuid: '11111111-1111-1111-1111-111111111111', name: 'Alex', online: true }],
});
renderApp(client);
expect(await screen.findByText('Alex')).toBeInTheDocument();
expect(screen.getByText('Read only')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Manage' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Command policies' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Audit log' })).not.toBeInTheDocument();
});
it('shows helper direct messaging and hides every action the helper cannot perform', async () => {
localStorage.setItem('xfesm.locale', 'en-US');
location.hash = '/players';
const client = clientFor('helper', ['status_read', 'player_read', 'player_message', 'claim_read']);
vi.spyOn(client, 'players').mockResolvedValue({
items: [{ uuid: '11111111-1111-1111-1111-111111111111', name: 'Alex', online: true }],
});
const inventory = vi.spyOn(client, 'playerInventory');
const user = userEvent.setup();
renderApp(client);
await user.click(await screen.findByRole('button', { name: 'Manage' }));
expect(screen.getByRole('option', { name: 'Direct message' })).toBeInTheDocument();
expect(screen.getAllByRole('option')).toHaveLength(1);
expect(screen.queryByRole('tab', { name: 'Inventory' })).not.toBeInTheDocument();
expect(screen.queryByRole('tab', { name: 'Ender chest' })).not.toBeInTheDocument();
expect(inventory).not.toHaveBeenCalled();
});
it('blocks direct navigation before an unauthorized policy request is sent', async () => {
localStorage.setItem('xfesm.locale', 'en-US');
location.hash = '/policies';
const client = clientFor('observer', ['status_read', 'player_read', 'claim_read']);
const policies = vi.spyOn(client, 'policies');
renderApp(client);
expect(await screen.findByRole('heading', { name: 'Access denied' })).toBeInTheDocument();
await waitFor(() => expect(policies).not.toHaveBeenCalled());
});
});