118 lines
5.2 KiB
TypeScript
118 lines
5.2 KiB
TypeScript
import { afterEach, expect, it, vi } from 'vitest';
|
|
import { flushSync, mount, unmount } from 'svelte';
|
|
import { writable } from 'svelte/store';
|
|
import MaintenancePage from '../../src/routes/maintenance/+page.svelte';
|
|
import type { MaintenanceOverview } from '../../src/lib/maintenance.js';
|
|
import type { PlutoniumRelease } from '../../src/lib/plutonium.js';
|
|
import type { UserRole } from '../../src/lib/auth.js';
|
|
|
|
const currentPage = writable({
|
|
url: new URL('http://localhost/maintenance'),
|
|
data: { user: { role: 'admin' as UserRole } }
|
|
});
|
|
vi.mock('$app/stores', () => ({
|
|
page: {
|
|
subscribe: (run: Parameters<typeof currentPage.subscribe>[0]) => currentPage.subscribe(run)
|
|
}
|
|
}));
|
|
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
|
|
vi.mock('../../src/lib/ui/poll.js', () => ({
|
|
pollVisible: (task: () => Promise<void>) => {
|
|
void task();
|
|
return () => {};
|
|
}
|
|
}));
|
|
|
|
let component: ReturnType<typeof mount> | undefined;
|
|
afterEach(async () => {
|
|
if (component) await unmount(component);
|
|
component = undefined;
|
|
document.body.replaceChildren();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
function overview(release: Partial<PlutoniumRelease> = {}): MaintenanceOverview {
|
|
return {
|
|
policies: { paused: false, health: true, drift: true, updates: false },
|
|
recovery: { enabled: true, lastScanAt: null, scanError: null, incidents: [] },
|
|
runtime: {
|
|
release: {
|
|
revision: null,
|
|
checkedAt: '2026-10-17T12:00:00Z',
|
|
error: 'Updater manifest request failed (HTTP 503).',
|
|
...release
|
|
},
|
|
schedule: { enabled: false, time: '06:00', timeZone: 'UTC', nextAt: null },
|
|
servers: [],
|
|
jobs: []
|
|
}
|
|
};
|
|
}
|
|
async function show(role: UserRole = 'admin') {
|
|
currentPage.update((value) => ({ ...value, data: { user: { role } } }));
|
|
component = mount(MaintenancePage, { target: document.body });
|
|
flushSync();
|
|
await vi.waitFor(() => expect(document.querySelector('#updates')).not.toBeNull());
|
|
return document.querySelector<HTMLElement>('#updates')!;
|
|
}
|
|
const button = (panel: HTMLElement, label: string) =>
|
|
[...panel.querySelectorAll('button')].find((node) => node.textContent?.trim() === label);
|
|
|
|
it('shows unavailable release information on the first failed check and keeps diagnostics closed', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(Response.json(overview())));
|
|
const panel = await show();
|
|
const alert = panel.querySelector('[role="alert"]')!;
|
|
expect(alert.textContent).toContain('Could not refresh the published Plutonium release.');
|
|
expect(alert.textContent).toContain('The published revision is unavailable.');
|
|
expect(alert.textContent).toContain('No previous release information is available.');
|
|
expect(panel.querySelector('p strong')?.textContent).toBe('Unavailable');
|
|
expect(panel.textContent).not.toMatch(/cached|Showing the last known release/);
|
|
const details = panel.querySelector('details')!;
|
|
expect(details.open).toBe(false);
|
|
expect(details.textContent).toContain('Updater manifest request failed (HTTP 503).');
|
|
const primary = panel.cloneNode(true) as HTMLElement;
|
|
primary.querySelectorAll('details').forEach((node) => node.remove());
|
|
expect(primary.textContent).not.toContain('HTTP 503');
|
|
expect(panel.textContent).toContain('Last attempted');
|
|
});
|
|
|
|
it('labels a retained revision honestly and clears the failure after retry succeeds', async () => {
|
|
let resolve!: (response: Response) => void;
|
|
const fetch = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(Response.json(overview({ revision: 5354 })))
|
|
.mockImplementationOnce(() => new Promise<Response>((done) => (resolve = done)));
|
|
vi.stubGlobal('fetch', fetch);
|
|
const panel = await show();
|
|
expect(panel.textContent).toContain('Last known published revision');
|
|
expect(panel.textContent).toContain('r5354');
|
|
expect(panel.querySelector('[role="alert"]')!.textContent).toContain(
|
|
'Showing the last known release; it may be out of date.'
|
|
);
|
|
button(panel, 'Retry checks')!.click();
|
|
flushSync();
|
|
expect(fetch).toHaveBeenLastCalledWith('/api/maintenance', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ action: 'scan' })
|
|
});
|
|
expect(button(panel, 'Retry checks')!.disabled).toBe(true);
|
|
resolve(
|
|
Response.json({ ...overview({ revision: 5355, error: null }), notice: 'Checks finished.' })
|
|
);
|
|
await vi.waitFor(() => expect(panel.querySelector('[role="alert"]')).toBeNull());
|
|
expect(panel.textContent).toContain('r5355');
|
|
expect(panel.textContent).toContain('Checked');
|
|
expect(panel.textContent).not.toMatch(/Last known|Last attempted|HTTP 503/);
|
|
});
|
|
|
|
it('keeps release failure details available without offering an administrator check to viewers', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(Response.json(overview())));
|
|
const panel = await show('viewer');
|
|
expect(panel.querySelector('[role="alert"]')!.textContent).toContain(
|
|
'Could not refresh the published Plutonium release.'
|
|
);
|
|
expect(panel.querySelector('details')?.textContent).toContain('HTTP 503');
|
|
expect(button(panel, 'Retry checks')).toBeUndefined();
|
|
});
|