feat(admin): migrate system diagnostics and retain SMTP drafts
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
import React from 'react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
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(),
|
||||
@@ -19,9 +23,11 @@ jest.mock('./api', () => ({
|
||||
}));
|
||||
|
||||
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') {
|
||||
@@ -180,6 +186,223 @@ describe('AdminSystemPage', () => {
|
||||
}
|
||||
});
|
||||
|
||||
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>
|
||||
@@ -230,7 +453,9 @@ describe('AdminSystemPage', () => {
|
||||
mockedApi.post.mockResolvedValueOnce({ data: { jobId: 'cv-benchmark', status: 'queued' } } as any);
|
||||
render(<I18nProvider><AdminSystemPage /></I18nProvider>);
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: /run benchmark/i }));
|
||||
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');
|
||||
|
||||
Reference in New Issue
Block a user