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; function deferred() { let resolve!: (value: T) => void; const promise = new Promise(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(); 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(); 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(); 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(); 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(); mockedApi.put.mockReturnValueOnce(pending.promise); render(); 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(); 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(); 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(); 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(); mockedApi.post.mockReturnValueOnce(pending.promise); render(); 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(); 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(); mockedApi.put.mockReturnValueOnce(pending.promise); render(); 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(); mockedApi.post.mockReturnValueOnce(pending.promise); const view = render(); 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(); 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(); 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(); 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( , ); 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( , ); 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(); 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(); 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(); }); });