479 lines
24 KiB
TypeScript
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();
|
|
});
|
|
});
|