// Independent audit validation. Synthetic local data only; no product behavior changes. // Requires existing Release API build and static UI export targeting http://localhost:5402/api. import { createRequire } from 'node:module'; import { spawn } from 'node:child_process'; import { mkdtemp, mkdir, writeFile } from 'node:fs/promises'; import { createWriteStream } from 'node:fs'; import path from 'node:path'; import os from 'node:os'; const root = process.cwd(), ui = path.join(root, 'job-tracker-ui'); const require = createRequire(path.join(ui, 'package.json')); const { chromium, request } = require('@playwright/test'); const AxeBuilder = require('@axe-core/playwright').default; const output = path.join(root, '.artifacts/audit-validation'); await mkdir(output, { recursive: true }); const data = await mkdtemp(path.join(os.tmpdir(), 'jobjakt-independent-')); const api = 'http://localhost:5402', site = 'http://localhost:3400'; const password = 'ValidationSynthetic123!'; const observations = [], children = []; let browser; const record = (caseName, facts) => { observations.push({ caseName, ...facts }); console.log(JSON.stringify({ caseName, ...facts })); }; function start(exe, args, cwd, env, log) { const child = spawn(exe, args, { cwd, env: { ...process.env, ...env }, windowsHide: true }); const sink = createWriteStream(path.join(output, log)); child.stdout.pipe(sink); child.stderr.pipe(sink); children.push(child); } async function ready(url) { for (let i = 0; i < 100; i++) { try { if ((await fetch(url, { signal: AbortSignal.timeout(500) })).ok) return; } catch {} await new Promise(r => setTimeout(r, 400)); } throw Error('Owned test service unavailable: ' + url); } async function json(response) { if (!response.ok()) throw Error('Fixture HTTP ' + response.status() + ': ' + response.url()); return response.json(); } async function client(email) { const c = await request.newContext({ baseURL: api }); await json(await c.post('/api/auth/login', { data: { email, password } })); const csrf = (await c.storageState()).cookies.find(x => x.name === 'XSRF-TOKEN').value; return { c, h: { 'X-CSRF-TOKEN': csrf } }; } async function uiLogin(page, email) { await page.goto(site + '/login'); await page.getByLabel('Email').fill(email); await page.getByLabel('Current password').fill(password); await page.getByRole('button', { name: 'Sign in', exact: true }).click(); await page.waitForURL('**/dashboard'); } try { for (const origin of [api, site]) { try { await fetch(origin + '/health', { signal: AbortSignal.timeout(300) }); throw Error('Refusing to reuse occupied audit port ' + origin); } catch (e) { if (String(e).includes('Refusing')) throw e; } } start(process.env.DOTNET_HOST_PATH || path.join(os.homedir(), '.dotnet/dotnet.exe'), [path.join(root, 'JobTrackerApi/bin/Release/net9.0/JobTrackerApi.dll'), '--urls', api], path.join(root, 'JobTrackerApi'), { ASPNETCORE_ENVIRONMENT: 'Development', DOTNET_CLI_TELEMETRY_OPTOUT: '1', Data__Root: data, Database__Provider: 'sqlite', ConnectionStrings__JobTracker: 'Data Source=' + path.join(data, 'jobtracker.db'), AccountLifecycle__TombstonesRoot: path.join(data, 'tombstones'), Auth__Require: 'true', Auth__JwtKey: 'independent-validation-synthetic-key-at-least32', Auth__AdminEmail: 'validation-admin@example.test', Auth__AdminPassword: password, Auth__TurnstileSecretKey: '', Auth__GoogleClientId: '', Auth__MicrosoftClientId: '', Auth__AllowRegistration: 'false', App__PublicBaseUrl: site, Cors__Origins__0: site, Ai__BaseUrl: 'http://127.0.0.1:1', Email__Enabled: 'false', Stripe__SecretKey: '', Workers__AiOperationsEnabled: 'false', Workers__RulesEnabled: 'false', Workers__FollowUpRemindersEnabled: 'false', Workers__DailyExportEnabled: 'false', Workers__JobEnrichmentEnabled: 'false', Backups__Enabled: 'false', CV_PDF_BROWSER_PATH: chromium.executablePath(), }, 'api.log'); await ready(api + '/health'); const admin = await client('validation-admin@example.test'); for (const email of ['validation-a@example.test', 'validation-b@example.test']) { await json(await admin.c.post('/api/users', { headers: admin.h, data: { email, password, roles: [] } })); } const a = await client('validation-a@example.test'), b = await client('validation-b@example.test'); for (const [c, name] of [[a, 'Private profile A'], [b, 'Private profile B']]) { await json(await c.c.put('/api/career/profile', { headers: c.h, data: { profile: { version: '1', contact: { fullName: name }, summary: [name + ' factual summary'], skills: ['C#'], } } })); } const variant = await json(await a.c.post('/api/cv/variants', { headers: a.h, data: { name: 'Independent variant' } })); const first = await json(await a.c.put('/api/cv/variants/' + variant.id, { headers: a.h, data: { name: 'First writer', settings: { ...variant.settings, titleOverride: 'New content' }, version: variant.version } })); const secondResponse = await a.c.put('/api/cv/variants/' + variant.id, { headers: a.h, data: { name: 'Stale writer', settings: variant.settings, version: variant.version } }); const second = await json(secondResponse); record('stale revision', { firstRevision: first.version, staleStatus: secondResponse.status(), resultingName: second.name, resultingRevision: second.version }); const wrongOwner = []; for (const route of ['', '/preview', '/versions']) { const response = await b.c.get('/api/cv/variants/' + variant.id + route); wrongOwner.push({ route, status: response.status(), body: await response.text() }); } wrongOwner.push({ route: '/export-pdf', status: (await b.c.post('/api/cv/variants/' + variant.id + '/export-pdf', { headers: b.h })).status() }); record('document owner boundaries', { results: wrongOwner }); const company = await json(await a.c.post('/api/companies', { headers: a.h, data: { name: 'Synthetic validation company' } })); const ids = []; for (let i = 0; i < 2; i++) ids.push((await json(await a.c.post('/api/jobapplications', { headers: { ...a.h, 'Idempotency-Key': 'same-intent-synthetic' }, data: { companyId: company.id, jobTitle: '=1+1', status: 'Saved' }, }))).id); record('repeated POST with same key', { ids, deduplicated: ids[0] === ids[1] }); for (const stamp of ['2026-03-29T09:00:00+02:00', '2026-03-29T07:00:00Z', '2026-03-29', '2026-10-25T02:30:00+02:00', '2026-10-25T02:30:00+01:00']) { const job = await json(await a.c.post('/api/jobapplications', { headers: a.h, data: { companyId: company.id, jobTitle: 'Date control', status: 'Saved', followUpAt: stamp } })); const reload = await json(await a.c.get('/api/jobapplications/' + job.id)); const ics = await (await a.c.get('/api/calendar/jobs/' + job.id + '/events.ics?kind=follow-up')).text(); record('date round trip', { input: stamp, createValue: job.followUpAt, reloadValue: reload.followUpAt, start: ics.match(/^DTSTART[^\r\n]*/m)?.[0] }); } const csv = await (await a.c.get('/api/export/jobs?format=csv')).text(); record('CSV arithmetic payload', { containsUnneutralizedFormula: csv.includes(',=1+1,') }); const exports = []; for (let i = 0; i < 2; i++) exports.push((await a.c.post('/api/export/account', { headers: a.h, data: {} })).status()); exports.push((await b.c.post('/api/export/account', { headers: b.h, data: {} })).status()); record('owner rate isolation', { accountA: exports.slice(0, 2), firstAccountB: exports[2] }); start(process.execPath, [path.join(ui, 'scripts/serve-export.mjs')], ui, { PORT: '3400' }, 'ui.log'); await ready(site); browser = await chromium.launch({ headless: true }); const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } }); await context.route('**/*', route => { const url = new URL(route.request().url()); return ['localhost', '127.0.0.1'].includes(url.hostname) || ['data:', 'about:', 'blob:'].includes(url.protocol) ? route.continue() : route.abort(); }); await context.addInitScript(() => { if (!localStorage.getItem('uiLanguage')) localStorage.setItem('uiLanguage', 'en'); }); const pageA = await context.newPage(); await uiLogin(pageA, 'validation-a@example.test'); await pageA.goto(site + '/career?section=profile'); const nameA = pageA.getByLabel('Full name', { exact: true }); await nameA.waitFor(); await nameA.fill('A edit must never enter B account'); const pageB = await context.newPage(); await pageB.goto(site + '/dashboard'); await pageB.getByRole('button', { name: 'User', exact: true }).click(); await pageB.getByRole('menuitem', { name: 'Sign out', exact: true }).click(); await pageB.waitForURL('**/login'); await uiLogin(pageB, 'validation-b@example.test'); const oldVisible = await nameA.count() ? await nameA.inputValue() : null; if (oldVisible === 'A edit must never enter B account') { const savedResponse = pageA.waitForResponse(r => r.url().endsWith('/api/career/profile') && r.request().method() === 'PUT'); await pageA.getByRole('button', { name: 'Save changes', exact: true }).click(); const saveStatus = (await savedResponse).status(); const actualB = await json(await b.c.get('/api/career/profile')); const actualA = await json(await a.c.get('/api/career/profile')); record('real UI cross-account stale form save', { oldVisible, saveStatus, profileB: actualB.profile.contact.fullName, profileA: actualA.profile.contact.fullName }); } else record('real UI cross-account stale form save', { oldVisible, protectedByClearing: true }); await pageB.screenshot({ path: path.join(output, 'account-b-after-switch.png'), fullPage: true }); // Reauthenticate A after the intentional UI logout revoked only its browser session. const editor = await context.newPage(); await uiLogin(editor, 'validation-a@example.test'); await editor.goto(site + '/career/builder/' + variant.id); await editor.getByLabel('CV name', { exact: true }).waitFor(); for (const theme of ['light', 'dark']) { await editor.evaluate(theme => localStorage.setItem('jobtracker.themeMode', theme), theme); await editor.reload(); await editor.getByLabel('CV name', { exact: true }).waitFor(); await editor.waitForFunction(() => [...document.querySelectorAll('[data-cv-page] iframe')].some(frame => frame.contentDocument?.body?.textContent?.includes('Private profile A'))); for (const [width, height] of [[1440, 1000], [1440, 900], [375, 900]]) { await editor.setViewportSize({ width, height }); const axe = await new AxeBuilder({ page: editor }).withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']).analyze(); record('builder axe ' + theme, { width, height, violations: axe.violations.map(v => ({ id: v.id, impact: v.impact, nodes: v.nodes.map(n => ({ target: n.target, summary: n.failureSummary })) })) }); for (const node of axe.violations.filter(v => v.id === 'scrollable-region-focusable').flatMap(v => v.nodes)) { if (node.target.length === 1 && typeof node.target[0] === 'string') record('scroll region structure', await editor.locator(node.target[0]).evaluate(el => ({ tag: el.tagName, tabIndex: el.tabIndex, overflow: getComputedStyle(el).overflow, maxHeight: getComputedStyle(el).maxHeight, clientHeight: el.clientHeight, scrollHeight: el.scrollHeight, previewPages: el.querySelectorAll('[data-cv-page]').length, frames: el.querySelectorAll('iframe').length, }))); } } } await context.close(); } catch (e) { record('probe failure', { error: String(e.stack || e) }); process.exitCode = 2; } finally { if (browser) await browser.close(); for (const child of children.reverse()) if (child.exitCode === null) { if (process.platform === 'win32') await new Promise(resolve => spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' }).on('exit', resolve)); else child.kill('SIGTERM'); } await writeFile(path.join(output, 'observations.json'), JSON.stringify({ date: new Date().toISOString(), syntheticDataRoot: data, observations }, null, 2)); }