feat(admin): add safe processing monitor and operation correlation

This commit is contained in:
cesnimda
2026-10-28 19:31:32 +01:00
parent 1f87301ee9
commit d52403dba8
19 changed files with 1064 additions and 6 deletions
+112
View File
@@ -0,0 +1,112 @@
import { expect, test, type APIRequestContext, type TestInfo } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
import { spawnSync } from 'node:child_process';
import { realpathSync } from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { translations } from '../src/i18n/translations';
const api = 'http://localhost:5302/api';
const password = 'E2ePassword123!';
async function login(client: APIRequestContext, email: string) {
expect((await client.post(`${api}/auth/login`, { data: { email, password } })).status()).toBe(200);
const me = await (await client.get(`${api}/auth/me`)).json();
return { id: me.id as string, email, headers: { 'X-CSRF-TOKEN': (await client.storageState()).cookies.find(cookie => cookie.name === 'XSRF-TOKEN')!.value, 'X-Session-Context': me.sessionContext as string } };
}
function seed(info: TestInfo, owner: Awaited<ReturnType<typeof login>>) {
const root = info.config.metadata.isolatedDataRoot;
if (typeof root !== 'string' || process.env.PLAYWRIGHT_REUSE_SERVERS === 'true') throw new Error('Explicit isolated non-reused test data required.');
const dataRoot = realpathSync(root);
expect(path.dirname(dataRoot)).toBe(realpathSync(os.tmpdir()));
const result = spawnSync(process.env.PLAYWRIGHT_PYTHON_PATH || (process.platform === 'win32' ? 'python' : 'python3'), ['-X', 'utf8', path.resolve('e2e/helpers/seed-operation.py')],
{ input: JSON.stringify({ dataRoot, ownerId: owner.id, ownerEmail: owner.email }), encoding: 'utf8', timeout: 15000, maxBuffer: 1024 * 1024 });
expect(result.status, result.stderr).toBe(0);
return (JSON.parse(result.stdout) as { operationId: string }).operationId;
}
test('[D] Real administrator/owner boundaries, actual cancellation history and responsive EN/NB themes', async ({ page, playwright }, info) => {
test.setTimeout(180000);
const admin = await playwright.request.newContext();
const ownerClient = await playwright.request.newContext();
const otherClient = await playwright.request.newContext();
const anonymous = await playwright.request.newContext();
try {
const administrator = await login(admin, 'e2e@example.test');
const email = `processing-owner-${Date.now()}@example.test`;
const otherEmail = `processing-other-${Date.now()}@example.test`;
for (const userEmail of [email, otherEmail]) expect((await admin.post(`${api}/users`, { headers: administrator.headers, data: { email: userEmail, password, roles: [] } })).status()).toBe(200);
const owner = await login(ownerClient, email);
const other = await login(otherClient, otherEmail);
const id = seed(info, owner);
for (const endpoint of ['/admin/processing', `/admin/processing/${id}`, `/admin/processing/${id}/events`]) {
expect((await anonymous.get(`${api}${endpoint}`)).status()).toBe(401);
expect((await ownerClient.get(`${api}${endpoint}`)).status()).toBe(403);
}
expect((await otherClient.get(`${api}/operations/${id}`)).status()).toBe(404);
expect((await otherClient.post(`${api}/operations/${id}/cancel`, { headers: other.headers })).status()).toBe(404);
const pageAdmin = await login(page.request, 'e2e@example.test');
await page.context().route('**/*', route => ['localhost', '127.0.0.1'].includes(new URL(route.request().url()).hostname) ? route.continue() : route.abort());
await page.goto('/admin/processing');
await page.getByLabel('Owner ID', { exact: true }).fill(owner.id);
await page.getByRole('button', { name: 'Apply filters', exact: true }).click();
await page.getByRole('button', { name: `View processing details ${id}`, exact: true }).click();
await expect(page.getByLabel('Correlation ID', { exact: true })).toHaveValue(id);
await expect(page.getByText('No retained events. Older jobs may predate event recording.', { exact: true })).toBeVisible();
const cancellation = await ownerClient.post(`${api}/operations/${id}/cancel`, { headers: owner.headers });
expect(cancellation.status()).toBe(200);
expect((await cancellation.json()).status).toBe('cancelled');
await page.getByRole('button', { name: 'Refresh', exact: true }).click();
await expect(page.getByRole('list').filter({ has: page.getByText(/^Processing cancelled/) })).toBeVisible();
const history = await (await admin.get(`${api}/admin/processing/${id}/events`)).json();
expect(history.items.map((item: { kind: string }) => item.kind)).toEqual(['Cancelled']);
expect(history.items[0].atUtc).toMatch(/Z$/);
const detail = await (await admin.get(`${api}/admin/processing/${id}`)).json();
expect(detail.owner.id).toBe(owner.id);
for (const field of ['leaseToken', 'resultReference', 'failureMessage', 'subjectId', 'idempotencyKey']) expect(detail).not.toHaveProperty(field);
expect(detail.worker).toBe('untracked');
for (const language of ['en', 'nb'] as const) for (const mode of ['light', 'dark'] as const) {
const t = translations[language];
expect((await page.request.put(`${api}/auth/preferences/language`, { headers: pageAdmin.headers, data: { language: language === 'nb' ? 'nb-NO' : 'en' } })).status()).toBe(204);
await page.evaluate(({ language, mode }) => { localStorage.setItem('uiLanguage', language); localStorage.setItem('jobtracker.themeMode', mode); }, { language, mode });
await page.reload();
await page.getByLabel(t.processingOwnerFilter, { exact: true }).fill(owner.id);
await page.getByRole('button', { name: t.processingApplyFilters, exact: true }).click();
await page.getByRole('button', { name: t.processingViewDetails.replace('{id}', id), exact: true }).click();
await expect(page.getByLabel(t.processingCorrelation, { exact: true })).toHaveValue(id);
for (const width of [1440, 768, 390]) {
await page.setViewportSize({ width, height: 900 });
await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1)).toBe(true);
await page.screenshot({ path: info.outputPath(`processing-${language}-${mode}-${width}.png`), fullPage: true });
}
const axe = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']).analyze();
await info.attach(`processing-${language}-${mode}-axe`, { body: JSON.stringify({ violations: axe.violations, incomplete: axe.incomplete }), contentType: 'application/json' });
expect(axe.violations).toEqual([]);
}
await info.attach('fixture-evidence-limit', { body: 'Guarded insertion of one inert unregistered task; actual native cancel creates its only event. No historical Created event, model job, inference, human accessibility or physical-device acceptance is fabricated.', contentType: 'text/plain' });
} finally { await Promise.all([admin.dispose(), ownerClient.dispose(), otherClient.dispose(), anonymous.dispose()]); }
});
test('[D] Controlled unavailable/forbidden reads recover without exposing raw failures or cached metadata', async ({ page }) => {
const administrator = await login(page.request, 'e2e@example.test');
expect((await page.request.put(`${api}/auth/preferences/language`, { headers: administrator.headers, data: { language: 'en' } })).status()).toBe(204);
await page.addInitScript(() => localStorage.setItem('uiLanguage', 'en'));
await page.context().route('**/*', route => ['localhost', '127.0.0.1'].includes(new URL(route.request().url()).hostname) ? route.continue() : route.abort());
let failure: number | null = 503;
await page.route('**/api/admin/processing?**', route => failure ? route.fulfill({ status: failure, json: { message: 'SECRET_EXCEPTION_DOCUMENT_CONTENT' } }) : route.continue());
await page.goto('/admin/processing');
await expect(page.getByRole('alert').filter({ hasText: 'Processing information could not be loaded.' })).toBeVisible();
await expect(page.getByText(/SECRET_EXCEPTION/)).toHaveCount(0);
failure = null;
await page.getByRole('button', { name: 'Retry', exact: true }).click();
await expect(page.getByRole('region', { name: 'Processing jobs', exact: true })).toBeVisible();
let calls = 0; page.on('request', request => { if (new URL(request.url()).pathname === '/api/admin/processing') calls++; });
await page.getByRole('button', { name: 'Pause updates', exact: true }).click();
const paused = calls;
await page.waitForTimeout(5500); // Negative assertion across one real polling interval.
expect(calls).toBe(paused);
failure = 403;
await page.getByRole('button', { name: 'Resume updates', exact: true }).click();
await expect(page.getByRole('alert').filter({ hasText: 'Administrator access is required.' })).toBeVisible();
await expect(page.getByRole('region', { name: 'Processing jobs', exact: true })).toHaveCount(0);
await expect(page.getByText(/SECRET_EXCEPTION/)).toHaveCount(0);
});