Files
stack/apps/web/src/spa/pages/admin.spec.tsx
T
2026-08-27 00:03:38 +00:00

207 lines
6.0 KiB
TypeScript

import { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { createMemoryRouter, RouterProvider, type RouteObject } from 'react-router-dom';
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
const { apiMock, useSessionMock } = vi.hoisted(() => ({
apiMock: vi.fn(),
useSessionMock: vi.fn(),
}));
vi.mock('@/lib/api', () => ({
api: apiMock,
}));
vi.mock('@/lib/auth-client', () => ({
useSession: useSessionMock,
authClient: {},
}));
import { AdminPage } from './admin';
import { AdminGuard } from '@/spa/guards';
const userFixtures = {
users: [
{
id: 'u-admin',
name: 'Ada Admin',
email: '[email protected]',
role: 'admin',
banned: false,
banReason: null,
createdAt: '2026-08-01T00:00:00.000Z',
updatedAt: '2026-08-01T00:00:00.000Z',
},
{
id: 'u-member',
name: 'Mel Member',
email: '[email protected]',
role: 'member',
banned: true,
banReason: 'spam',
createdAt: '2026-08-02T00:00:00.000Z',
updatedAt: '2026-08-02T00:00:00.000Z',
},
],
total: 2,
};
let root: Root | null = null;
let container: HTMLDivElement;
beforeAll(() => {
Object.defineProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT', {
configurable: true,
value: true,
});
});
afterAll(() => {
Reflect.deleteProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT');
});
afterEach(async () => {
await act(async () => {
root?.unmount();
});
document.body.replaceChildren();
root = null;
apiMock.mockReset();
useSessionMock.mockReset();
});
async function renderAdminRoute(): Promise<void> {
const routes: RouteObject[] = [
{
element: <AdminGuard />,
children: [{ path: '/admin', element: <AdminPage /> }],
},
{ path: '/', element: <div>home page</div> },
{ path: '/login', element: <div>login page</div> },
];
const router = createMemoryRouter(routes, { initialEntries: ['/admin'] });
container = document.createElement('div');
document.body.append(container);
root = createRoot(container);
await act(async () => {
root?.render(<RouterProvider router={router} />);
});
}
function sessionWithRole(role: string | undefined): { data: unknown; isPending: boolean } {
return {
data: { user: { id: 'u-1', name: 'Test', email: '[email protected]', role } },
isPending: false,
};
}
describe('AdminGuard', () => {
it('redirects unauthenticated visitors to /login', async () => {
useSessionMock.mockReturnValue({ data: null, isPending: false });
await renderAdminRoute();
expect(container.textContent).toContain('login page');
expect(apiMock).not.toHaveBeenCalled();
});
it('redirects non-admin users to /', async () => {
useSessionMock.mockReturnValue(sessionWithRole('member'));
await renderAdminRoute();
expect(container.textContent).toContain('home page');
expect(apiMock).not.toHaveBeenCalled();
});
it('renders the admin page for admin users', async () => {
useSessionMock.mockReturnValue(sessionWithRole('admin'));
apiMock.mockResolvedValueOnce(userFixtures);
await renderAdminRoute();
expect(container.textContent).toContain('Admin Panel');
});
});
describe('AdminPage users tab', () => {
it('lists users with role and ban status after load', async () => {
useSessionMock.mockReturnValue(sessionWithRole('admin'));
apiMock.mockResolvedValueOnce(userFixtures);
await renderAdminRoute();
expect(apiMock).toHaveBeenCalledWith('/api/admin/users');
expect(container.textContent).toContain('Ada Admin');
expect(container.textContent).toContain('Mel Member');
expect(container.textContent).toContain('Banned');
expect(container.textContent).toContain('2 user(s)');
});
it('shows the load error with a retry control', async () => {
useSessionMock.mockReturnValue(sessionWithRole('admin'));
apiMock.mockRejectedValueOnce(new Error('gateway unavailable'));
await renderAdminRoute();
expect(container.textContent).toContain('gateway unavailable');
apiMock.mockResolvedValueOnce(userFixtures);
const retry = [...container.querySelectorAll('button')].find((b) =>
b.textContent?.includes('Retry'),
);
expect(retry).toBeTruthy();
await act(async () => {
retry?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
});
expect(container.textContent).toContain('Ada Admin');
});
it('posts to the ban endpoint and reloads on Ban', async () => {
useSessionMock.mockReturnValue(sessionWithRole('admin'));
apiMock.mockResolvedValue(userFixtures);
await renderAdminRoute();
const banButton = [...container.querySelectorAll('button')].find(
(b) => b.textContent === 'Ban',
);
expect(banButton).toBeTruthy();
await act(async () => {
banButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
});
expect(apiMock).toHaveBeenCalledWith('/api/admin/users/u-admin/ban', { method: 'POST' });
});
});
describe('AdminPage health tab', () => {
it('loads health status when the tab is opened', async () => {
useSessionMock.mockReturnValue(sessionWithRole('admin'));
apiMock.mockResolvedValueOnce(userFixtures).mockResolvedValueOnce({
status: 'ok',
database: { status: 'ok', latencyMs: 3 },
cache: { status: 'ok', latencyMs: 1 },
agentPool: { activeSessions: 2 },
providers: [{ id: 'ollama', name: 'Ollama', available: true, modelCount: 4 }],
checkedAt: '2026-08-26T00:00:00.000Z',
});
await renderAdminRoute();
const healthTab = [...container.querySelectorAll('button')].find((b) =>
b.textContent?.includes('System Health'),
);
await act(async () => {
healthTab?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
});
expect(apiMock).toHaveBeenCalledWith('/api/admin/health');
expect(container.textContent).toContain('Database (PostgreSQL)');
expect(container.textContent).toContain('Active sessions: 2');
expect(container.textContent).toContain('4 models');
});
});