Files
jobtrackingapp/job-tracker-ui/src/admin-system-page.test.tsx
T

479 lines
24 KiB
TypeScript

import React from 'react';
import '@testing-library/jest-dom';
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import AdminSystemPage from './views/AdminSystemPage';
import { I18nProvider } from './i18n/I18nProvider';
import { api } from './api';
let mockSessionContext = 'system-owner';
let mockSessionInvalidated = false;
jest.mock('./sessionBoundary', () => ({ checkSessionCookie: jest.fn(), getSessionContext: () => mockSessionContext, isSessionInvalidated: () => mockSessionInvalidated }));
jest.mock('./api', () => ({
api: {
get: jest.fn(),
post: jest.fn(),
put: jest.fn(),
patch: jest.fn(),
delete: jest.fn(),
interceptors: { request: { use: jest.fn() }, response: { use: jest.fn() } },
},
getApiErrorMessage: (error: any, fallback?: string) => fallback || 'Request failed.',
}));
const mockedApi = api as jest.Mocked<typeof api>;
function deferred<T>() { let resolve!: (value: T) => void; const promise = new Promise<T>(done => { resolve = done; }); return { promise, resolve }; }
describe('AdminSystemPage', () => {
beforeEach(() => {
mockSessionContext = 'system-owner'; mockSessionInvalidated = false;
window.localStorage.clear();
mockedApi.get.mockImplementation((url: string) => {
if (url === '/admin/system') {
return Promise.resolve({
data: {
environment: 'Production',
contentRoot: '/app',
version: '1.2.3',
commitSha: 'abc1234',
buildStamp: '2026-03-23 11:00 UTC',
storage: { dataRoot: '/data', dbPath: '/data/jobtracker.db', dbExists: true, dbSizeBytes: 2048, companyCount: 3, jobCount: 7, deletedCount: 1 },
email: { enabled: true, host: 'smtp.example.test', port: 587, enableSsl: true, from: 'noreply@example.test', fromName: 'Jobbjakt' },
database: { provider: 'mariadb', looksConfigured: true, canConnect: true, target: 'server=db', usesFileStorage: false, warning: null },
runtime: { framework: '.NET 9', osDescription: 'Linux', processArchitecture: 'X64', machineName: 'app-01' },
auth: { required: true, hasJwtKey: true, googleConfigured: true, gmailConfigured: true },
ai: {
healthy: true,
model: 'distilbart',
device: 'cpu',
gpuAvailable: false,
gpuName: null,
ocrAvailable: true,
ocrLanguages: 'eng',
ollamaConfigured: true,
ollamaReachable: true,
ollamaModel: 'qwen2.5:7b',
ollamaModelAvailable: true,
ollamaVersion: '0.7.0',
ollamaInstalledModels: ['qwen2.5:7b', 'nomic-embed-text'],
ollamaLoadedModels: ['qwen2.5:7b'],
ollamaLoadedCount: 1,
healthLatencyMs: 12.4,
probeLatencyMs: 25.8,
lastProbeAt: '2026-03-23T10:00:00Z',
lastProbeSuccessAt: '2026-03-23T10:00:00Z',
lastProbeFailureAt: null,
probeFailures: 0,
requests: 18,
cacheHits: 9,
cacheMisses: 9,
failures: 0,
averageLatencyMs: 42.2,
ocrRequests: 5,
ocrFailures: 0,
averageOcrLatencyMs: 88.4,
lastOcrSuccessAt: '2026-03-23T10:05:00Z',
lastOcrFailureAt: null,
lastSuccessAt: '2026-03-23T10:04:00Z',
lastFailureAt: null,
lastError: null,
},
},
} as any);
}
if (url === '/admin/system/email-settings') {
return Promise.resolve({
data: {
enabled: true,
host: 'smtp.example.test',
port: 587,
user: 'mailer@example.test',
from: 'noreply@example.test',
fromName: 'Jobbjakt',
enableSsl: true,
timeoutMs: 15000,
usesOverrides: false,
hasPassword: true,
},
} as any);
}
if (url === '/admin/system/cv-benchmark') {
return Promise.resolve({
data: {
rootPath: '/data/CvBenchmarks/latest',
lastUpdatedAtUtc: '2026-03-23T10:10:00Z',
reportMarkdown: '# CV benchmark report\n\n- Files: 4',
indexJson: JSON.stringify({
CorpusRoot: '/home/pi/cvs',
OutputRoot: '/data/CvBenchmarks/latest',
GeneratedAtUtc: '2026-03-23T10:10:00Z',
TotalFiles: 4,
AverageCoverage: 0.72,
AverageConfidence: 0.66,
AverageConsistency: 0.94,
FilesWithSuspiciousLocations: 1,
MissingApprovedFixtures: 4,
Entries: [
{
FileName: 'cv.txt',
Slug: 'cv-txt',
Extension: '.txt',
Characters: 2000,
OutputPath: '/data/CvBenchmarks/latest/outputs/cv-txt.json',
ApprovedFixturePath: null,
CandidateFixturePath: '/data/CvBenchmarks/latest/candidate-fixtures/cv-txt.json',
ContactLocation: 'San Francisco, Hobbies',
FirstJob: '* July',
FirstJobLocation: null,
FirstEducation: '* September',
FirstEducationLocation: null,
QualificationLevels: ['Other'],
SuspiciousLocations: [],
CoverageScore: 0.5,
ConfidenceScore: 0.65,
ConsistencyScore: 0.8,
DiffSummary: 'No approved fixture yet — candidate fixture written.',
},
],
}),
},
} as any);
}
if (url === '/admin/system/maintenance-jobs') {
return Promise.resolve({
data: [{
id: 'cv-benchmark',
title: 'CV extraction benchmark',
description: 'Runs the CV corpus benchmark.',
available: true,
runsUrl: 'https://git.example.test/actions',
}],
} as any);
}
return Promise.resolve({ data: {} } as any);
});
mockedApi.put.mockResolvedValue({
data: {
enabled: true,
host: 'smtp.changed.test',
port: 2525,
user: 'mailer@example.test',
from: 'noreply@example.test',
fromName: 'Jobbjakt',
enableSsl: true,
timeoutMs: 15000,
usesOverrides: true,
hasPassword: true,
},
} as any);
});
afterEach(() => {
jest.clearAllMocks();
});
it('uses contextual system headings and keeps metric values out of heading navigation', async () => {
render(<I18nProvider><AdminSystemPage /></I18nProvider>);
await screen.findByText('Production');
expect(screen.getByRole('heading', { name: 'System status', level: 2 })).toBeInTheDocument();
expect(screen.queryByRole('heading', { name: 'Production' })).not.toBeInTheDocument();
const levels = screen.getAllByRole('heading').map(heading => Number(heading.tagName.slice(1)));
let previous = 1;
for (const level of levels) {
expect(level).toBeLessThanOrEqual(previous + 1);
previous = level;
}
});
it('associates each tab with its named panel', async () => {
render(<I18nProvider><AdminSystemPage /></I18nProvider>);
await screen.findByText('Production');
const statusTab = screen.getByRole('tab', { name: 'Status' });
const statusPanel = screen.getByRole('tabpanel', { name: 'Status' });
expect(statusTab).toHaveAttribute('aria-controls', statusPanel.id);
expect(statusPanel).toHaveAttribute('aria-labelledby', statusTab.id);
fireEvent.click(screen.getByRole('tab', { name: 'Settings' }));
const settingsTab = screen.getByRole('tab', { name: 'Settings' });
expect(settingsTab).toHaveAttribute('aria-controls', screen.getByRole('tabpanel', { name: 'Settings' }).id);
expect(screen.queryByRole('tabpanel', { name: 'Status' })).not.toBeInTheDocument();
});
it('never fabricates offline metrics after an initial status failure and retries only the failed read', async () => {
const get = mockedApi.get.getMockImplementation()!;
mockedApi.get.mockImplementation((url: string, config?: any) => url === '/admin/system' ? Promise.reject(new Error('offline')) : get(url, config));
render(<I18nProvider><AdminSystemPage /></I18nProvider>);
const alert = await screen.findByRole('alert');
expect(screen.queryByText('Offline')).not.toBeInTheDocument();
expect(screen.queryByText('Requests')).not.toBeInTheDocument();
mockedApi.get.mockImplementation(get);
mockedApi.get.mockClear();
fireEvent.click(within(alert).getByRole('button', { name: 'Retry' }));
expect(await screen.findByText('Production')).toBeVisible();
expect(mockedApi.get.mock.calls.map(call => call[0])).toEqual(['/admin/system']);
expect(mockedApi.post).not.toHaveBeenCalled();
expect(mockedApi.put).not.toHaveBeenCalled();
});
it('keeps SMTP edits and write-only password through explicit refresh', async () => {
render(<I18nProvider><AdminSystemPage /></I18nProvider>);
await screen.findByText('Production');
fireEvent.click(screen.getByRole('tab', { name: 'Settings' }));
fireEvent.change(screen.getByLabelText('Host'), { target: { value: 'unsaved.example.test' } });
fireEvent.change(screen.getByLabelText('SMTP password'), { target: { value: 'new-secret' } });
fireEvent.click(screen.getByRole('button', { name: 'Refresh' }));
await waitFor(() => expect(screen.getByRole('button', { name: 'Refresh' })).toBeEnabled());
expect(screen.getByLabelText('Host')).toHaveValue('unsaved.example.test');
expect(screen.getByLabelText('SMTP password')).toHaveValue('new-secret');
expect(screen.queryByText('new-secret')).not.toBeInTheDocument();
expect(mockedApi.put).not.toHaveBeenCalled();
});
it('submits one SMTP snapshot, retains newer edits and prevents competing operational actions', async () => {
const pending = deferred<any>();
mockedApi.put.mockReturnValueOnce(pending.promise);
render(<I18nProvider><AdminSystemPage /></I18nProvider>);
expect(screen.queryByText('Unsaved SMTP changes.')).not.toBeInTheDocument();
await screen.findByText('Production');
fireEvent.click(screen.getByRole('tab', { name: 'Settings' }));
expect(screen.queryByText('Unsaved SMTP changes.')).not.toBeInTheDocument();
fireEvent.change(screen.getByLabelText('Host'), { target: { value: 'submitted.example.test' } });
fireEvent.change(screen.getByLabelText('SMTP password'), { target: { value: 'submitted-secret' } });
fireEvent.click(screen.getByRole('button', { name: 'Save settings' }));
fireEvent.change(screen.getByLabelText('Host'), { target: { value: 'newer.example.test' } });
fireEvent.change(screen.getByLabelText('SMTP password'), { target: { value: 'newer-secret' } });
expect(screen.getByRole('button', { name: 'Send test email' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'Run probe now' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'Refresh' })).toBeDisabled();
expect(mockedApi.put).toHaveBeenCalledTimes(1);
expect(mockedApi.put).toHaveBeenCalledWith('/admin/system/email-settings', expect.objectContaining({ host: 'submitted.example.test', password: 'submitted-secret', clearPassword: false }));
await act(async () => pending.resolve({ data: { enabled: true, host: 'submitted.example.test', hasPassword: true } }));
expect(screen.getByLabelText('Host')).toHaveValue('newer.example.test');
expect(screen.getByLabelText('SMTP password')).toHaveValue('newer-secret');
expect(screen.getByText('SMTP settings saved.')).toBeVisible();
expect(screen.getByText('Unsaved SMTP changes.')).toBeVisible();
expect(mockedApi.post).not.toHaveBeenCalled();
});
it.each([
['/admin/system/cv-benchmark', 'CV benchmark results could not be loaded.'],
['/admin/system/maintenance-jobs', 'Maintenance availability could not be loaded.'],
])('reports an optional %s failure and retries that GET without operational writes', async (path, message) => {
const get = mockedApi.get.getMockImplementation()!;
mockedApi.get.mockImplementation((url: string, config?: any) => url === path ? Promise.reject(new Error('offline')) : get(url, config));
render(<I18nProvider><AdminSystemPage /></I18nProvider>);
expect(await screen.findByText(message)).toBeVisible();
expect(screen.getByText('Production')).toBeVisible();
expect(screen.queryByText('Maintenance dispatch is unavailable.')).not.toBeInTheDocument();
mockedApi.get.mockImplementation(get); mockedApi.get.mockClear();
fireEvent.click(within(screen.getByText(message).closest('[role="alert"]')!).getByRole('button', { name: 'Retry' }));
await waitFor(() => expect(screen.queryByText(message)).not.toBeInTheDocument());
expect(mockedApi.get.mock.calls.map(call => call[0])).toEqual([path]);
expect(mockedApi.post).not.toHaveBeenCalled();
});
it('keeps explicit password clearing through refresh and acknowledges only the submitted secret', async () => {
render(<I18nProvider><AdminSystemPage /></I18nProvider>);
await screen.findByText('Production');
fireEvent.click(screen.getByRole('tab', { name: 'Settings' }));
const clear = screen.getByRole('checkbox', { name: /clear stored password/i });
fireEvent.click(clear);
fireEvent.click(screen.getByRole('button', { name: 'Refresh' }));
await waitFor(() => expect(screen.getByRole('button', { name: 'Refresh' })).toBeEnabled());
expect(clear).toBeChecked();
fireEvent.click(screen.getByRole('button', { name: 'Save settings' }));
await screen.findByText('SMTP settings saved.');
expect(mockedApi.put).toHaveBeenCalledWith('/admin/system/email-settings', expect.objectContaining({ password: null, clearPassword: true }));
expect(clear).not.toBeChecked();
expect(screen.getByLabelText('SMTP password')).toHaveValue('');
expect(screen.queryByText('Unsaved SMTP changes.')).not.toBeInTheDocument();
expect(screen.getByLabelText('Host')).toHaveValue('smtp.changed.test');
});
it('retains failed SMTP edits and only writes again after an explicit save', async () => {
mockedApi.put.mockRejectedValueOnce({ response: { status: 503, data: { title: 'Private object must not stringify' } } });
render(<I18nProvider><AdminSystemPage /></I18nProvider>);
await screen.findByText('Production');
fireEvent.click(screen.getByRole('tab', { name: 'Settings' }));
fireEvent.change(screen.getByLabelText('Host'), { target: { value: 'keep.example.test' } });
fireEvent.change(screen.getByLabelText('SMTP password'), { target: { value: 'keep-secret' } });
fireEvent.click(screen.getByRole('button', { name: 'Save settings' }));
expect(await screen.findByRole('alert')).toHaveTextContent('Failed to save email settings.');
expect(screen.getByLabelText('Host')).toHaveValue('keep.example.test');
expect(screen.getByLabelText('SMTP password')).toHaveValue('keep-secret');
expect(screen.getByText('Unsaved SMTP changes.')).toBeVisible();
expect(screen.queryByText('[object Object]')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Refresh' }));
await waitFor(() => expect(screen.getByRole('button', { name: 'Save settings' })).toBeEnabled());
expect(mockedApi.put).toHaveBeenCalledTimes(1);
expect(screen.getByLabelText('Host')).toHaveValue('keep.example.test');
});
it('waits for one probe before allowing another action and never runs on entry or tab changes', async () => {
const pending = deferred<any>(); mockedApi.post.mockReturnValueOnce(pending.promise);
render(<I18nProvider><AdminSystemPage /></I18nProvider>);
await screen.findByText('Production');
expect(mockedApi.post).not.toHaveBeenCalled();
const probe = screen.getByRole('button', { name: 'Run probe now' });
fireEvent.click(probe); fireEvent.click(probe);
fireEvent.click(screen.getByRole('tab', { name: 'Settings' }));
expect(screen.getByRole('button', { name: 'Send test email' })).toBeDisabled();
expect(mockedApi.post).toHaveBeenCalledTimes(1);
expect(mockedApi.post).toHaveBeenCalledWith('/admin/system/ai/probe');
await act(async () => pending.resolve({ data: {} }));
expect(await screen.findByText('AI probe completed.')).toBeVisible();
});
it('refreshes probe metrics after success and a failed follow-up read retries GET without replaying the probe', async () => {
mockedApi.post.mockResolvedValueOnce({ data: {} } as any);
render(<I18nProvider><AdminSystemPage /></I18nProvider>);
await screen.findByText('Production');
const get = mockedApi.get.getMockImplementation()!;
mockedApi.get.mockImplementation((url: string, config?: any) => url === '/admin/system' ? Promise.reject(new Error('offline')) : get(url, config));
mockedApi.get.mockClear();
fireEvent.click(screen.getByRole('button', { name: 'Run probe now' }));
expect(await screen.findByText('System status could not be refreshed. Previously loaded values remain.')).toBeVisible();
expect(mockedApi.get.mock.calls.map(call => call[0])).toEqual(['/admin/system']);
mockedApi.get.mockImplementation(get);
fireEvent.click(screen.getByRole('button', { name: 'Retry' }));
await waitFor(() => expect(screen.queryByRole('button', { name: 'Retry' })).not.toBeInTheDocument());
expect(mockedApi.post).toHaveBeenCalledTimes(1);
});
it('does not acknowledge an SMTP write into a changed session', async () => {
const pending = deferred<any>(); mockedApi.put.mockReturnValueOnce(pending.promise);
render(<I18nProvider><AdminSystemPage /></I18nProvider>);
await screen.findByText('Production');
fireEvent.click(screen.getByRole('tab', { name: 'Settings' }));
fireEvent.change(screen.getByLabelText('Host'), { target: { value: 'owner-a.example.test' } });
fireEvent.click(screen.getByRole('button', { name: 'Save settings' }));
mockSessionContext = 'second-session';
await act(async () => pending.resolve({ data: { enabled: true, host: 'must-not-apply.example.test', hasPassword: true } }));
expect(screen.getByLabelText('Host')).toHaveValue('owner-a.example.test');
expect(screen.queryByText('SMTP settings saved.')).not.toBeInTheDocument();
});
it('does not refresh or announce a completed probe after unmount', async () => {
const pending = deferred<any>(); mockedApi.post.mockReturnValueOnce(pending.promise);
const view = render(<I18nProvider><AdminSystemPage /></I18nProvider>);
await screen.findByText('Production');
fireEvent.click(screen.getByRole('button', { name: 'Run probe now' }));
const count = mockedApi.get.mock.calls.length;
view.unmount();
await act(async () => pending.resolve({ data: {} }));
expect(mockedApi.get).toHaveBeenCalledTimes(count);
});
it('clears sensitive diagnostics after a denied refresh and offers no retry or operational action', async () => {
render(<I18nProvider><AdminSystemPage /></I18nProvider>);
await screen.findByText('Production');
mockedApi.get.mockRejectedValue({ response: { status: 403 } });
fireEvent.click(screen.getByRole('button', { name: 'Refresh' }));
await waitFor(() => expect(screen.queryByText('Production')).not.toBeInTheDocument());
expect(screen.queryByRole('button', { name: 'Retry' })).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Run probe now' })).toBeDisabled();
fireEvent.click(screen.getByRole('tab', { name: 'Settings' }));
expect(screen.queryByLabelText('SMTP password')).not.toBeInTheDocument();
expect(mockedApi.post).not.toHaveBeenCalled();
});
it('keeps the last diagnostic values under a qualified refresh error', async () => {
render(<I18nProvider><AdminSystemPage /></I18nProvider>);
await screen.findByText('Production');
const get = mockedApi.get.getMockImplementation()!;
mockedApi.get.mockImplementation((url: string, config?: any) => url === '/admin/system' ? Promise.reject({ response: { status: 503, data: 'Temporary failure' } }) : get(url, config));
fireEvent.click(screen.getByRole('button', { name: 'Refresh' }));
expect(await screen.findByText('System status could not be refreshed. Previously loaded values remain.')).toBeVisible();
expect(screen.getByText('Production')).toBeVisible();
expect(mockedApi.post).not.toHaveBeenCalled();
});
it('does not present unavailable SMTP settings as a blank editable configuration', async () => {
const get = mockedApi.get.getMockImplementation()!;
mockedApi.get.mockImplementation((url: string, config?: any) => url === '/admin/system/email-settings' ? Promise.reject(new Error('offline')) : get(url, config));
render(<I18nProvider><AdminSystemPage /></I18nProvider>);
await screen.findByText('Production');
fireEvent.click(screen.getByRole('tab', { name: 'Settings' }));
expect(await screen.findByText('SMTP settings could not be loaded.')).toBeVisible();
expect(screen.queryByLabelText('Host')).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Send test email' })).toBeDisabled();
mockedApi.get.mockImplementation(get); mockedApi.get.mockClear();
fireEvent.click(screen.getByRole('button', { name: 'Retry' }));
expect(await screen.findByLabelText('Host')).toHaveValue('smtp.example.test');
expect(mockedApi.get.mock.calls.map(call => call[0])).toEqual(['/admin/system/email-settings']);
});
it('renders AI service health, latency, and OCR readiness', async () => {
render(
<I18nProvider>
<AdminSystemPage />
</I18nProvider>,
);
await waitFor(() => {
expect(screen.getByText('Production')).toBeTruthy();
});
expect(screen.getByText(/25.8 ms probe/i)).toBeTruthy();
expect(screen.getByText('OCR eng')).toBeTruthy();
expect(screen.getAllByText(/ollama configured/i).length).toBeGreaterThan(0);
expect(screen.getByText(/ollama version/i)).toBeTruthy();
expect(screen.getByText(/model · qwen2.5:7b/i)).toBeTruthy();
expect(screen.getByText(/cv benchmark review/i)).toBeTruthy();
expect(screen.getByText(/top parser findings/i)).toBeTruthy();
expect(screen.getByText(/suspicious contact location: san francisco, hobbies/i)).toBeTruthy();
expect(screen.getByText('OCR avg latency')).toBeTruthy();
expect(screen.getByText('88.4 ms')).toBeTruthy();
});
it('loads and saves smtp settings from the settings tab', async () => {
render(
<I18nProvider>
<AdminSystemPage />
</I18nProvider>,
);
expect(await screen.findByText('AI service')).toBeTruthy();
fireEvent.click(screen.getByRole('tab', { name: /settings/i }));
const hostInput = await screen.findByLabelText(/host/i);
fireEvent.change(hostInput, { target: { value: 'smtp.changed.test' } });
fireEvent.change(screen.getByLabelText(/port/i), { target: { value: '2525' } });
fireEvent.click(screen.getByRole('button', { name: /save settings/i }));
await waitFor(() => {
expect(mockedApi.put).toHaveBeenCalledWith('/admin/system/email-settings', expect.objectContaining({
host: 'smtp.changed.test',
port: 2525,
}));
});
});
it('dispatches the allowlisted CV benchmark from the system panel', async () => {
mockedApi.post.mockResolvedValueOnce({ data: { jobId: 'cv-benchmark', status: 'queued' } } as any);
render(<I18nProvider><AdminSystemPage /></I18nProvider>);
const run = await screen.findByRole('button', { name: /run benchmark/i });
await waitFor(() => expect(run).toBeEnabled());
fireEvent.click(run);
await waitFor(() => {
expect(mockedApi.post).toHaveBeenCalledWith('/admin/system/maintenance-jobs/cv-benchmark/runs');
});
expect(await screen.findByText(/queued in Gitea Actions/i)).toBeTruthy();
expect(screen.getByRole('link', { name: /view runs/i }).getAttribute('href')).toBe('https://git.example.test/actions');
});
it('localizes benchmark diagnostics without changing runtime values', async () => {
window.localStorage.setItem('uiLanguage', 'nb');
render(<I18nProvider><AdminSystemPage /></I18nProvider>);
expect(await screen.findByText('Gjennomgang av CV-referansetest')).toBeTruthy();
expect(await screen.findByText(/Mistenkelig kontaktsted: san francisco, hobbies/i)).toBeTruthy();
expect(screen.getByText('Production')).toBeTruthy();
expect(screen.getByText(/Modell · qwen2.5:7b/i)).toBeTruthy();
fireEvent.click(screen.getByRole('tab', { name: /innstillinger/i }));
expect(await screen.findByDisplayValue('Dette er en test-e-post fra systempanelet i Jobbjakt.')).toBeTruthy();
});
});