0954876295
Implement B01-B06 with regression coverage and retained validation evidence. Preserve the production ingress preflight; F04 runtime verification remains open.
91 lines
5.7 KiB
TypeScript
91 lines
5.7 KiB
TypeScript
import { expect, test, type APIRequestContext, type Playwright } from "@playwright/test";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
const url = "http://localhost:5302/api";
|
|
const password = "E2ePassword123!";
|
|
type Account = { api: APIRequestContext; headers: Record<string, string>; email: string };
|
|
async function login(playwright: Playwright, email: string): Promise<Account> {
|
|
const api = await playwright.request.newContext();
|
|
expect((await api.post(`${url}/auth/login`, { data: { email, password } })).status()).toBe(200);
|
|
const me = await (await api.get(`${url}/auth/me`)).json();
|
|
const csrf = (await api.storageState()).cookies.find(cookie => cookie.name === "XSRF-TOKEN")!.value;
|
|
return { api, email, headers: { "X-CSRF-TOKEN": csrf, "X-Session-Context": me.sessionContext } };
|
|
}
|
|
const root = () => process.env.PDF_STUB_ROOT!;
|
|
const starts = () => fs.readdirSync(root()).filter(file => file.endsWith(".started"));
|
|
function running() {
|
|
return starts().filter(file => { try { process.kill(Number.parseInt(file), 0); return true; } catch { return false; } }).length;
|
|
}
|
|
|
|
test("authenticated bursts across variant, job and public routes share actual render capacity and recover", async ({ playwright }, info) => {
|
|
const admin = await login(playwright, "e2e@example.test");
|
|
const accounts: Account[] = [];
|
|
const pending: Array<Promise<unknown>> = [];
|
|
fs.writeFileSync(path.join(root(), "mode"), "hold");
|
|
let maximumActualRenders = 0;
|
|
const sampler = setInterval(() => { maximumActualRenders = Math.max(maximumActualRenders, running()); }, 10);
|
|
try {
|
|
for (const label of ["a", "b", "c"]) {
|
|
const email = `pdf-${label}-${Date.now()}@example.test`;
|
|
expect((await admin.api.post(`${url}/users`, { headers: admin.headers, data: { email, password, roles: [] } })).status()).toBe(200);
|
|
accounts.push(await login(playwright, email));
|
|
}
|
|
const [a, b, c] = accounts;
|
|
const aSecondSession = await login(playwright, a.email);
|
|
accounts.push(aSecondSession);
|
|
const variantIds: number[] = [];
|
|
for (const owner of [a, b, c]) {
|
|
const response = await owner.api.post(`${url}/cv/variants`, { headers: owner.headers, data: { name: "Synthetic PDF" } });
|
|
expect(response.status()).toBe(200);
|
|
variantIds.push((await response.json()).id);
|
|
}
|
|
const secondVariant = await (await a.api.post(`${url}/cv/variants`, { headers: a.headers, data: { name: "Another A variant" } })).json();
|
|
const published = await (await a.api.put(`${url}/cv/variants/${variantIds[0]}/public`, { headers: a.headers, data: { isPublic: true } })).json();
|
|
const company = await (await c.api.post(`${url}/companies`, { headers: c.headers, data: { name: "Synthetic employer" } })).json();
|
|
const job = await (await c.api.post(`${url}/jobapplications`, { headers: c.headers, data: { companyId: company.id, jobTitle: "Engineer", status: "Saved" } })).json();
|
|
const variant = (owner: Account, id: number) => owner.api.post(`${url}/cv/variants/${id}/export-pdf`, { headers: owner.headers, data: {} });
|
|
const first = variant(a, variantIds[0]); pending.push(first);
|
|
await expect.poll(() => starts().length).toBe(1);
|
|
const sameOwner = await variant(aSecondSession, secondVariant.id);
|
|
expect(sameOwner.status()).toBe(503); // Even with a spare global slot.
|
|
expect((await sameOwner.json()).code).toBe("pdf_capacity_exceeded");
|
|
const second = variant(b, variantIds[1]); pending.push(second);
|
|
await expect.poll(() => starts().length).toBe(2);
|
|
const anonymous = await playwright.request.newContext();
|
|
accounts.push({ api: anonymous, headers: {}, email: "anonymous" });
|
|
const burst = await Promise.all([
|
|
...Array.from({ length: 6 }, () => variant(aSecondSession, secondVariant.id)),
|
|
variant(c, variantIds[2]),
|
|
c.api.post(`${url}/jobapplications/${job.id}/export-tailored-cv-pdf`, { headers: c.headers, data: {} }),
|
|
anonymous.get(`${url}/public-cv/${published.publicSlug}/pdf`),
|
|
]);
|
|
for (const response of burst) {
|
|
expect(response.status()).toBe(503);
|
|
expect(response.headers()["retry-after"]).toBe("3");
|
|
expect(await response.json()).toMatchObject({ code: "pdf_capacity_exceeded" });
|
|
}
|
|
expect(starts()).toHaveLength(2);
|
|
maximumActualRenders = Math.max(maximumActualRenders, running());
|
|
expect(maximumActualRenders).toBe(2);
|
|
// Admission never substitutes for B02 or resource ownership.
|
|
expect((await c.api.post(`${url}/cv/variants/${variantIds[2]}/export-pdf`, { headers: { "X-CSRF-TOKEN": c.headers["X-CSRF-TOKEN"] }, data: {} })).status()).toBe(428);
|
|
expect((await c.api.post(`${url}/cv/variants/${variantIds[0]}/export-pdf`, { headers: c.headers, data: {} })).status()).toBe(404);
|
|
fs.writeFileSync(path.join(root(), "release"), "release");
|
|
for (const response of await Promise.all([first, second])) {
|
|
expect(response.status()).toBe(200);
|
|
expect((await response.body()).subarray(0, 5).toString()).toBe("%PDF-");
|
|
}
|
|
await expect.poll(running).toBe(0);
|
|
const recovered = await c.api.post(`${url}/jobapplications/${job.id}/export-tailored-cv-pdf`, { headers: c.headers, data: {} });
|
|
expect(recovered.status()).toBe(200);
|
|
await expect.poll(running).toBe(0);
|
|
await info.attach("bounded-admission-observations", { contentType: "application/json", body: JSON.stringify({ configured: 2, admittedDuringBurst: 2, rejected: burst.length + 1, queued: 0, maximumActualRenders, busyStatus: 503, retryAfterSeconds: 3, recoveryStatus: recovered.status() }, null, 2) });
|
|
} finally {
|
|
fs.writeFileSync(path.join(root(), "release"), "release");
|
|
await Promise.allSettled(pending);
|
|
clearInterval(sampler);
|
|
await Promise.all([admin, ...accounts].map(account => account.api.dispose()));
|
|
}
|
|
});
|