test(e2e): isolate administrator roles and locale between journeys

This commit is contained in:
cesnimda
2026-10-21 07:21:29 +02:00
parent dda1687345
commit b39a5af4de
10 changed files with 260 additions and 13 deletions
@@ -0,0 +1,26 @@
# Shared native fixture isolation
## Recorded failure
The committed-source run on `b333ef55d5665223d2f570ec9681336fc3971c8a` completed20October19:15:16UTC with **103passed/248failed/0skipped/0flaky**, zero configured retries. This is a failed integrated run, retained in its raw JSON/log/trace evidence, not248 established application defects.
Earlier zoom and route-census cases created synthetic administrators without removing their elevated roles. The Users case assumed the bootstrap administrator was the last one, attempted self-demotion, received204instead of409 and consequently removed the shared fixture's administrative access. Most later setup requests returned403. No server authorization failure is established: demotion was permitted because other administrators existed.
Separately, Audit’s bilingual samples persisted Bokmål to the shared administrator. System’s English fixture seeded only localStorage; the application correctly hydrated authoritative `/auth/me` language and displayed `Prøv igjen`/`Innstillinger`, while the test expected English. Trace responses and accessible snapshots confirm this cause.
An earlier Audit GET of the restored owned job328 returned500. It does not follow from later self-demotion. A read-only SQLite backup into an isolated diagnostic data root and an authenticated GET returned200; the combined successor case also passes. Root cause remains unproven. Do not close the intermittent read debt from these successful continuations. The ordinary browser backend now pipes Warning/error logs so future500responses retain server diagnostics without verbose SQL logging.
## Bounded correction and preservation
- Zoom and route census retain the exact ID of their own created administrator, remove only that synthetic account's roles in `finally` using bootstrap credentials, assert204and still dispose contexts/attach evidence on cleanup failure. No account or database is deleted.
- Users first asserts the bootstrap is the sole administrator before attempting self-demotion. This prevents another future fixture leak from causing a cascading loss of test authority. Its original409, ordinary-user403, session revocation and deletion-disabled503assertions remain. Its promoted synthetic account is demoted in `finally` through the independent bootstrap context.
- Audit, System and Users bootstrap login helpers explicitly save the language required by their fixture through the existing protected API. The bilingual cases retain every explicit per-sample update and assertion. Ordinary owners are unaffected.
- No application/API/schema/security semantics, production data, model, renderer, test timeout, skip, expected-failure marker or configured retry is changed. No case is removed or reordered to hide the integration failure.
## Successor evidence
The combined ordinary run executes both zoom cases, all five route/Board cases, Audit3, System3and Users3in their normal order on one fresh fixture: **16/16PASS**,21October05:11:01–05:15:20UTC, zero skips/flaky/retries. This proves producers and consumers compose, including the last-admin precondition and original409. Raw artifacts are `.artifacts/phase7/current/ui/composition.json`, `composition.log` and `composition-results/`; the portable manifest records hashes.
Two earlier composition attempts correctly refused to reuse port5302left by the parent's isolated diagnostic process. That exact task-owned process was identified and stopped; neither failed setup is a passing run. Source collection is post-run; the late stdout/log-level diagnostic change is qualified separately and receives fresh type/lint/full-run proof.
Case inventories remain899frontend/101suites,1057backend,56sidecar and351ordinary browser. This repair adds0cases; the preceding confirmation repair accounts for the899th frontend case. Overlapping focused vectors are not summed. Full integrated successor execution and final post-run source reconciliation remain required. All57 PRES dispositions and33routes remain; production readiness is NO-GO, P5-13 withheld and Phase8 not started.
+6
View File
@@ -1,5 +1,11 @@
# P7-12 — Integrated migration acceptance
## Continuation — 21 October
The first complete successor run failed103/351, with248unexpected failures and zero skips/flaky/retries; it is not accepted. [Fixture investigation](FIXTURE_ISOLATION.md) and [portable evidence](evidence/fixture-repair.json) separate administrative role/language contamination from an unresolved isolated Audit500. The corrected shared-fixture composition passes16/16. Existing409/403/503, ownership, source/save and route assertions remain; no test is removed or relaxed. The final whole run and separate lanes still follow. The earlier18:52:58UTC clean publication of entry evidence `ddb9c3da4ff31447fa6bd579002f32199052937d` remains historical, not final acceptance.
Separately, parser native-exit diagnostics `dda16873455259bce23a744bd838c3e3e0fc6ee8` are pushed on development and equivalent main8699ae4 is normally pushed/directly verified21October05:13:39UTC. Release837 remains unqualified: it subsequently failed in native MSBuild/.NET test compilation after the parser process succeeded. No deployment/model activation is claimed. The [parser report](../../../infrastructure/2026-10-21-parser-native-exit.md) preserves the earlier distinct SIGSEGV and diagnostic-only scope.
## Entry and bounded scope
All P7-00–P7-11 presentation boundaries are implemented and published, with their original qualifications retained. P7-11 final error-owner commit `584530b694a2238928d10fddfc013499cf9b3fab` was pushed and directly verified clean at 2026-10-20T18:29:28 UTC. The normal main-repair reconciliation merge `8bbaf1f875f6d32dd7d379779a35188dca844fbb` was pushed and verified clean at18:29:38 UTC. These receipts supersede earlier pending-publication observations.
@@ -0,0 +1,75 @@
{
"observedUtc": "2026-10-21T05:18:18.993008+00:00",
"failure": {
"stats": {
"startTime": "2026-10-20T18:50:55.722Z",
"duration": 1460403.5180000002,
"expected": 103,
"skipped": 0,
"unexpected": 248,
"flaky": 0
},
"rawJsonSha256": "aef0c669476135ab655a30104bba9886cfe5fb64458285dd7a2be4254d1e5343",
"rawLogSha256": "f0280b5a9665af14952c583b3fab2af10d330574a0f48e25201e44f7e2fe5a27",
"source": "b333ef55d5665223d2f570ec9681336fc3971c8a",
"qualification": "Completed failed whole run. Successor runner archives these exact raw bytes under ui/attempts; final manifest must resolve the archive path by hash. No failure is converted to a skip or expected failure."
},
"composition": {
"stats": {
"startTime": "2026-10-21T05:11:01.774Z",
"duration": 259024.46,
"expected": 16,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"command": {
"name": "composition",
"command": [
"rtk",
"proxy",
"node",
"node_modules/@playwright/test/cli.js",
"test",
"e2e/browser-zoom-audit.spec.ts",
"e2e/functional-acceptance.spec.ts",
"e2e/phase7-admin-audit.spec.ts",
"e2e/phase7-admin-system.spec.ts",
"e2e/phase7-admin-users.spec.ts",
"--retries=0",
"--reporter=list,json",
"--output=E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\ui\\composition-results"
],
"startedUtc": "2026-10-21T05:11:01.254030+00:00",
"finishedUtc": "2026-10-21T05:15:20.899324+00:00",
"exitCode": 0
},
"path": ".artifacts/phase7/current/ui/composition.json",
"sha256": "e751f72a84bbde8340b4077782c415cae62d0d175ad3684766366a428c1b3d48"
},
"files": {
"job-tracker-ui/e2e/browser-zoom-audit.spec.ts": "d2e14c8fb86a068456c1bcebf090b707d6448561b8643f13204bcd22e1515b37",
"job-tracker-ui/e2e/functional-acceptance.spec.ts": "fbba35f4ccdae767e5a4d87623f190bb348ce3ecd1eb7a4248316d6822fa3dd0",
"job-tracker-ui/e2e/phase7-admin-audit.spec.ts": "3737f20446d56bc6e61ba249b2c13316314aa0bfde6e7ff2202a781d3f793d6f",
"job-tracker-ui/e2e/phase7-admin-system.spec.ts": "1e4dc4a33587adcc8f2f4cc23f83a2d7f47b589d0fa4b81977187d5849dbce37",
"job-tracker-ui/e2e/phase7-admin-users.spec.ts": "df6ca26e3608a7dd0772f7dbcb71258fa7d58e7535982ecd5eddec157fcc790c",
"job-tracker-ui/playwright.config.ts": "24f13510ab8d4f2718bfeaaa7c77bb95ae109c69d4a318abd1b5c87777306ee0"
},
"axe": {
"scans": 28,
"violations": 0,
"incompleteRules": {
"color-contrast": 2145,
"aria-prohibited-attr": 10
},
"incompleteNodes": 2155
},
"visualEvidence": "Screenshots are retained automated state evidence; no new human image review claimed for this fixture-only repair. Prior checkpoint visual inspections remain qualified.",
"sourceQualification": "Post-run source collection. Logging-only Playwright stdout/log-level change was made while composition was already running and did not affect that backend process; fresh successor full run validates it.",
"conditions": [
"Audit owned-read500 remains unreproduced/intermittent, not closed by successful rerun.",
"Role cleanup is finally-based; sole-admin precondition fails safely if forced termination prevents earlier cleanup.",
"Axe incomplete contexts remain raw triage evidence and do not establish human screen-reader/device acceptance.",
"Full351browser and separate HTTP/security lanes remain required; no final acceptance yet."
]
}
@@ -0,0 +1,104 @@
{
"schemaVersion": 1,
"checkpoint": "p7-12",
"status": "PASS",
"checkedUtc": "2026-10-21T05:21:16.714740+00:00",
"commands": [
{
"name": "secret-scan",
"command": [
"rtk",
"proxy",
"C:\\Users\\Connor\\AppData\\Local\\Programs\\Python\\Python312\\python.exe",
"-X",
"utf8",
"scripts/supply-chain.py",
"scan-secrets"
],
"startedUtc": "2026-10-21T05:21:11.487147+00:00",
"finishedUtc": "2026-10-21T05:21:15.631000+00:00",
"exitCode": 0,
"log": ".artifacts/phase7/current/static/20261021T052111Z/secret-scan.log",
"logSha256Raw": "be5119e6057287d397e87759f855c91e634e3bfac61c8be7584d05592ba2dfcd"
},
{
"name": "working-whitespace",
"command": [
"rtk",
"proxy",
"git",
"diff",
"--check"
],
"startedUtc": "2026-10-21T05:21:15.631500+00:00",
"finishedUtc": "2026-10-21T05:21:15.721641+00:00",
"exitCode": 0,
"log": ".artifacts/phase7/current/static/20261021T052111Z/working-whitespace.log",
"logSha256Raw": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"name": "staged-whitespace",
"command": [
"rtk",
"proxy",
"git",
"diff",
"--cached",
"--check"
],
"startedUtc": "2026-10-21T05:21:15.722142+00:00",
"finishedUtc": "2026-10-21T05:21:15.811199+00:00",
"exitCode": 0,
"log": ".artifacts/phase7/current/static/20261021T052111Z/staged-whitespace.log",
"logSha256Raw": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"name": "phase7-whitespace",
"command": [
"rtk",
"proxy",
"git",
"diff",
"--check",
"b5319981ad3d1bd635daff861453d23c0de5320b"
],
"startedUtc": "2026-10-21T05:21:15.811199+00:00",
"finishedUtc": "2026-10-21T05:21:16.134813+00:00",
"exitCode": 0,
"log": ".artifacts/phase7/current/static/20261021T052111Z/phase7-whitespace.log",
"logSha256Raw": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
],
"untrackedSecretScan": {
"files": 0,
"findings": 0
},
"documentation": {
"files": 42,
"relativeLinks": 668,
"errors": []
},
"preservation": {
"groups": 57,
"statuses": {
"PARTIAL": 56,
"PASS": 0,
"FAIL": 0,
"DEFERRED": 1
},
"originalRoutesPreserved": true,
"currentConcreteRoutes": 33
},
"production": {
"main": "8699ae4f421bb1d049b937e735e40a4797a4b719",
"originMain": "8699ae4f421bb1d049b937e735e40a4797a4b719",
"status": "NO-GO",
"qualification": "Local fetched refs; direct push observation recorded separately."
},
"hashPolicy": "Portable JSON UTF8 LF; raw log hashes measure exact saved bytes.",
"limits": [
"Pattern scanner skips binary and files over2MiB; not historical Git/production secrets.",
"Local links check target existence, not URL/heading anchors.",
"Static evidence is not runtime/manual/device/provider/production acceptance."
]
}
+14 -5
View File
@@ -61,6 +61,8 @@ for (const [language, mode] of [['en', 'light'], ['nb', 'dark']] as const) {
args: ['--window-size=1280,900', `--disable-extensions-except=${extension}`, `--load-extension=${extension}`],
});
const admin = await playwright.request.newContext();
let adminHeaders: Record<string, string> | undefined;
let createdAdminId: string | undefined;
const records: unknown[] = [];
try {
await context.route('**/*', route => {
@@ -98,9 +100,12 @@ for (const [language, mode] of [['en', 'light'], ['nb', 'dark']] as const) {
await zoom(factor);
records.push({ area: 'Authentication', route: '/login', language, mode, zoom: factor, geometry: await geometry(page) });
}
const adminHeaders = await login(admin, 'e2e@example.test');
adminHeaders = await login(admin, 'e2e@example.test');
const email = `zoom-${language}-${Date.now()}@example.test`;
expect((await admin.post(`${apiUrl}/users`, { headers: adminHeaders, data: { email, password, roles: ['Admin'] } })).status()).toBe(200);
const created = await admin.post(`${apiUrl}/users`, { headers: adminHeaders, data: { email, password, roles: ['Admin'] } });
expect(created.status()).toBe(200);
createdAdminId = (await created.json()).id;
expect(createdAdminId).toEqual(expect.any(String));
const headers = await login(page.request, email);
expect((await page.request.put(`${apiUrl}/auth/preferences/language`, { headers, data: { language: language === 'nb' ? 'nb-NO' : 'en' } })).status()).toBe(204);
const profile = { version: '1', contact: { fullName: 'Åse Zoom Fixture', headline: 'Documented application workflows', location: 'Oslo' }, summary: ['Supported source summary.'], skills: ['C#', 'React'], jobs: [{ id: 'zoom-role', title: 'Engineer', company: 'Synthetic company', bullets: ['Built and maintained internal web applications.'] }] };
@@ -153,9 +158,13 @@ for (const [language, mode] of [['en', 'light'], ['nb', 'dark']] as const) {
expect(await (await page.request.get(`${apiUrl}/career/profile`)).json()).toEqual(source);
expect(await (await page.request.get(`${apiUrl}/cv/variants/${cv.id}`)).json()).toEqual(cv);
} finally {
await info.attach('actual-browser-zoom-observations', { body: JSON.stringify({ method: 'chrome.tabs.setZoom; viewport:null; dpr and actual zoom asserted; not human acceptance', browser: context.browser()?.version(), records }, null, 2), contentType: 'application/json' });
await admin.dispose();
await context.close();
try {
if (createdAdminId) expect((await admin.put(`${apiUrl}/users/${createdAdminId}/roles`, { headers: adminHeaders, data: { roles: [] } })).status()).toBe(204);
} finally {
await info.attach('actual-browser-zoom-observations', { body: JSON.stringify({ method: 'chrome.tabs.setZoom; viewport:null; dpr and actual zoom asserted; not human acceptance', browser: context.browser()?.version(), records }, null, 2), contentType: 'application/json' });
await admin.dispose();
await context.close();
}
}
});
}
@@ -90,10 +90,18 @@ for (const identity of ['anonymous', 'normal', 'admin'] as const) {
test(`[P5-15-01 E02 E07 A01] all 33 routes retain native entry, refresh and ${identity} permissions`, async ({ page, playwright }, info) => {
test.setTimeout(300_000);
const admin = await playwright.request.newContext();
let adminHeaders: Record<string, string> | undefined;
let createdAdminId: string | undefined;
const evidence: { pattern: string; mode: string; destination: string; status?: number }[] = [];
try {
const adminHeaders = await login(admin, 'e2e@example.test');
const email = await createAccount(admin, adminHeaders, identity === 'admin' ? ['Admin'] : []);
adminHeaders = await login(admin, 'e2e@example.test');
const email = `acceptance-${Date.now()}-${Math.random().toString(36).slice(2)}@example.test`;
const created = await admin.post(`${apiUrl}/users`, { headers: adminHeaders, data: { email, password, roles: identity === 'admin' ? ['Admin'] : [] } });
expect(created.status()).toBe(200);
if (identity === 'admin') {
createdAdminId = (await created.json()).id;
expect(createdAdminId).toEqual(expect.any(String));
}
let jobId = 2147483647;
let cvId = 2147483647;
if (identity !== 'anonymous') {
@@ -163,8 +171,12 @@ for (const identity of ['anonymous', 'normal', 'admin'] as const) {
await expect(page.getByTestId('admin-version-badge')).toHaveCount(identity === 'admin' ? 1 : 0);
}
} finally {
await info.attach(`route-census-${identity}`, { body: JSON.stringify(evidence, null, 2), contentType: 'application/json' });
await admin.dispose();
try {
if (createdAdminId) expect((await admin.put(`${apiUrl}/users/${createdAdminId}/roles`, { headers: adminHeaders, data: { roles: [] } })).status()).toBe(204);
} finally {
await info.attach(`route-census-${identity}`, { body: JSON.stringify(evidence, null, 2), contentType: 'application/json' });
await admin.dispose();
}
}
});
}
@@ -21,7 +21,9 @@ async function login(records: RequestRecord[], api: APIRequestContext, email: st
expect((await call(records, api, 'POST', '/auth/login', undefined, { email, password })).status()).toBe(200);
const me = await json<{ id: string; sessionContext: string }>(await call(records, api, 'GET', '/auth/me'));
const token = (await api.storageState()).cookies.find(value => value.name === 'XSRF-TOKEN'); expect(token).toBeDefined();
return { id: me.id, email, headers: { 'X-CSRF-TOKEN': token!.value, 'X-Session-Context': me.sessionContext } };
const headers = { 'X-CSRF-TOKEN': token!.value, 'X-Session-Context': me.sessionContext };
if (email === 'e2e@example.test') expect((await call(records, api, 'PUT', '/auth/preferences/language', headers, { language: 'en' })).status()).toBe(204);
return { id: me.id, email, headers };
}
async function createOwner(records: RequestRecord[], admin: APIRequestContext, headers: Headers) {
const email = `p710-audit-${Date.now()}-${Math.random().toString(36).slice(2)}@example.test`;
@@ -21,7 +21,9 @@ async function login(records: RequestRecord[], api: APIRequestContext, email: st
expect((await call(records, api, 'POST', '/auth/login', undefined, { email, password })).status()).toBe(200);
const me = await json<{ id: string; sessionContext: string }>(await call(records, api, 'GET', '/auth/me'));
const token = (await api.storageState()).cookies.find(value => value.name === 'XSRF-TOKEN'); expect(token).toBeDefined();
return { id: me.id, email, headers: { 'X-CSRF-TOKEN': token!.value, 'X-Session-Context': me.sessionContext } };
const headers = { 'X-CSRF-TOKEN': token!.value, 'X-Session-Context': me.sessionContext };
if (email === 'e2e@example.test') expect((await call(records, api, 'PUT', '/auth/preferences/language', headers, { language: 'en' })).status()).toBe(204);
return { id: me.id, email, headers };
}
async function createOwner(records: RequestRecord[], admin: APIRequestContext, headers: Headers) {
const email = `p710-system-${Date.now()}-${Math.random().toString(36).slice(2)}@example.test`;
+11 -2
View File
@@ -21,7 +21,9 @@ async function login(records: RequestRecord[], api: APIRequestContext, email: st
expect((await call(records, api, 'POST', '/auth/login', undefined, { email, password })).status()).toBe(200);
const me = await json<{ id: string; sessionContext: string }>(await call(records, api, 'GET', '/auth/me'));
const token = (await api.storageState()).cookies.find(value => value.name === 'XSRF-TOKEN'); expect(token).toBeDefined();
return { id: me.id, email, headers: { 'X-CSRF-TOKEN': token!.value, 'X-Session-Context': me.sessionContext } };
const headers = { 'X-CSRF-TOKEN': token!.value, 'X-Session-Context': me.sessionContext };
if (email === 'e2e@example.test') expect((await call(records, api, 'PUT', '/auth/preferences/language', headers, { language: 'en' })).status()).toBe(204);
return { id: me.id, email, headers };
}
async function createOwner(records: RequestRecord[], admin: APIRequestContext, headers: Headers) {
const email = `p710-admin-${Date.now()}-${Math.random().toString(36).slice(2)}@example.test`;
@@ -68,6 +70,9 @@ function response(page: Page, method: string, path: string) { return page.waitFo
test('[P7-10 Users A01 E09] real admin creation/role persistence revoke sessions while final-admin and disabled deletion remain protected', async ({ page, playwright, session }) => {
const administrator=await login(session.requests, page.request, 'e2e@example.test');
const administrators=(await json<Array<{ id: string; roles: string[] }>>(await call(session.requests, page.request, 'GET', '/users'))).filter(value => value.roles.includes('Admin'));
// A contaminated fixture must fail before a successful self-demotion can break later suites.
expect(administrators.map(value => value.id)).toEqual([administrator.id]);
expect((await call(session.requests, page.request, 'PUT', `/users/${administrator.id}/roles`, administrator.headers, { roles: [] })).status()).toBe(409);
await start(page, '/admin/users', 390);
const email=`p710-created-${Date.now()}@example.test`;
@@ -90,7 +95,11 @@ test('[P7-10 Users A01 E09] real admin creation/role persistence revoke sessions
expect((await call(session.requests, other, 'GET', '/auth/me')).status()).toBe(401);
expect((await page.request.delete(`${apiUrl}/users/${created.id}`, { headers: { ...administrator.headers, 'X-Confirm-Account-Deletion': email } })).status()).toBe(503);
expect((await json<Array<{ id: string }>>(await call(session.requests, page.request, 'GET', '/users'))).some(value => value.id===created.id)).toBe(true);
} finally { await other.dispose(); }
} finally {
try {
expect((await call(session.requests, session.admin, 'PUT', `/users/${created.id}/roles`, session.adminHeaders, { roles: [] })).status()).toBe(204);
} finally { await other.dispose(); }
}
});
test('[P7-10 Users A01 E08] unavailable user list recovers by GET without fabricating empty state', async ({ page, session }, info) => {
+2
View File
@@ -35,8 +35,10 @@ export default defineConfig({
url: `${apiUrl}/health`,
timeout: 120_000,
reuseExistingServer: reuseServers,
stdout: "pipe",
env: {
ASPNETCORE_ENVIRONMENT: "Development",
Logging__LogLevel__Default: "Warning",
Auth__Require: "true",
Auth__JwtKey: "e2e-only-key-at-least-thirty-two-characters-long",
Auth__AdminEmail: "e2e@example.test",