文件
hyb-oa/scripts/qa/test-invoice-first-http.cjs
T

423 行
33 KiB
JavaScript

const assert = require("node:assert/strict");
const { execFileSync } = require("node:child_process");
const { randomUUID } = require("node:crypto");
const { readFileSync, readdirSync, mkdtempSync, rmSync } = require("node:fs");
const { tmpdir } = require("node:os");
const { createRequire } = require("node:module");
const path = require("node:path");
const { setTimeout: delay } = require("node:timers/promises");
const root = path.resolve(__dirname, "../..");
const serverRequire = createRequire(path.join(root, "server/package.json"));
const docker = (args, input) => execFileSync("docker", args, { input, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 60000 }).trim();
async function main() {
assert.equal(process.env.RUN_ISOLATED_DB_TEST, "1", "Explicit disposable database opt-in required");
const password = randomUUID();
let container;
let prisma;
let pool;
let app;
let storageDir;
let browser;
let preview;
const passed = [];
try {
container = docker(["run", "-d", "--rm", "--pull=never", "--tmpfs", "/var/lib/postgresql/data", "-p", "127.0.0.1::5432", "-e", `POSTGRES_PASSWORD=${password}`, "postgres:16-alpine"]);
let ready = false;
for (let attempt = 0; attempt < 30; attempt += 1) {
try { docker(["exec", container, "pg_isready", "-U", "postgres"]); ready = true; break; } catch { await delay(1000); }
}
assert.ok(ready, "Disposable PostgreSQL readiness");
const migrationDir = path.join(root, "server/prisma/migrations");
for (const directory of readdirSync(migrationDir).sort()) {
if (directory === "migration_lock.toml") continue;
docker(["exec", "-i", container, "psql", "-X", "-q", "-v", "ON_ERROR_STOP=1", "-U", "postgres"], readFileSync(path.join(migrationDir, directory, "migration.sql")));
}
const address = docker(["port", container, "5432/tcp"]);
assert.match(address, /^127\.0\.0\.1:\d+$/);
process.env.DATABASE_URL = `postgresql://postgres:${password}@${address}/postgres`;
({ prisma, pool } = serverRequire("./src/prismaClient").createPrismaClient());
const department = await prisma.department.create({ data: { name: "synthetic-http" } });
const employee = await prisma.employee.create({ data: {
name: "synthetic", email: "[email protected]", employee_no: "synthetic-http",
department_id: department.id, title: "synthetic", hire_date: new Date("2026-09-09"),
bank_account_encrypted: "synthetic-not-account", bank_name: "synthetic", password_hash: "not-a-login-hash",
} });
const expenseType = await prisma.expenseType.create({ data: { code: "synthetic_http", name: "synthetic", category: "office", sort_order: 1 } });
const manager = await prisma.employee.create({ data: {
name: "synthetic-manager", email: "[email protected]", employee_no: "synthetic-manager",
department_id: department.id, title: "synthetic", hire_date: new Date("2026-09-09"),
bank_account_encrypted: "synthetic-not-account", bank_name: "synthetic", password_hash: "not-a-login-hash",
} });
await prisma.employee.update({ where: { id: employee.id }, data: { direct_manager_id: manager.id } });
const approvers = { manager };
for (const role of ["ceo", "finance"]) {
approvers[role] = await prisma.employee.create({ data: {
name: `synthetic-${role}`, email: `invoice-${role}@example.invalid`, employee_no: `synthetic-${role}`,
department_id: department.id, title: "synthetic", hire_date: new Date("2026-09-09"),
bank_account_encrypted: "synthetic-not-account", bank_name: "synthetic", password_hash: "not-a-login-hash",
} });
}
for (const [role, actor] of Object.entries(approvers)) {
await prisma.employeeRole.create({ data: { employee_id: actor.id, role_code: role } });
}
await prisma.employeeRole.create({ data: { employee_id: employee.id, role_code: "employee" } });
const loginPassword = `Synthetic!${randomUUID()}`;
const passwordHash = await serverRequire("./src/utils/crypto").hashPassword(loginPassword);
await prisma.employee.updateMany({ data: { password_hash: passwordHash, must_change_password: false } });
await prisma.workflowDefinition.create({ data: { name: "synthetic-reimbursement", entity_type: "reimbursement", definition_json: { steps: [] }, version: 1 } });
await prisma.approvalRule.create({ data: { name: "synthetic-three-step", min_amount: 0, level1_role: "manager", level2_role: "ceo", level3_role: "finance", priority: 1 } });
await prisma.featureFlag.create({ data: { flag_key: "reimbursement_invoice_guard_v1", override_mode: "force_on", rollout_type: "global", department_ids: [] } });
const makeForm = () => prisma.reimbursementForm.create({ data: { form_number: `SYN-${randomUUID()}`, owner_id: employee.id, submitter_id: employee.id, department_id: department.id, reimbursement_date: new Date("2026-09-09") } });
const makeInvoice = (overrides = {}) => prisma.invoicePool.create({ data: {
owner_employee_id: employee.id, uploaded_by_employee_id: employee.id, status: "available",
storage_path: "synthetic-unused.pdf", original_name: "synthetic.pdf", mime_type: "application/pdf",
file_size: 1, file_sha256: randomUUID(), invoice_number: randomUUID(),
invoice_date: new Date("2026-09-09"), total_amount: "100.00", ocr_attempts: 1, ocr_completed_at: new Date("2026-09-09"), ...overrides,
} });
app = serverRequire("fastify")({ logger: false });
app.decorate("prisma", prisma);
storageDir = mkdtempSync(path.join(tmpdir(), "invoice-http-"));
app.decorate("config", { storageDir, storageDriver: "local", maxUploadSizeBytes: 10 * 1024 * 1024, ocrProvider: process.env.RUN_JOB_QUEUE === "1" ? "mock" : "tencent", reimbursementInvoiceGuardEnabled: true, appTimezone: "Asia/Shanghai", jwtSecret: randomUUID(), accessTokenTtl: "15m", refreshTtlSeconds: 3600, refreshCookieName: "synthetic_refresh", cookieSecure: false, loginFailMaxAttempts: 5, loginFailWindowSeconds: 300, loginLockSeconds: 60 });
await app.register(serverRequire("@fastify/cookie"));
await app.register(serverRequire("./src/plugins/auth"));
await app.register(serverRequire("./src/routes/auth"), { prefix: "/api/auth" });
app.setErrorHandler((error, request, reply) => reply.status(error.statusCode ?? 500).send({ error: { code: error.code, details: error.details } }));
const envelope = (data) => ({ requestId: "synthetic-http", data });
app.get("/api/me", { preHandler: app.authenticate }, async (request) => envelope({ id: request.employee.id, employeeNo: request.employee.employee_no, name: "synthetic", email: request.employee.email, departmentId: department.id, directManagerId: manager.id, status: "active", roleCodes: request.employee.roleCodes, notificationPushEnabled: false }));
app.get("/api/system/navigation", async () => envelope({ workspace: { id: "synthetic", profile: "small_team_internal", orgMode: "single_company", moduleVisibility: {} }, sections: { main: [], admin: [], backup: [] }, recommendedRolePresets: [] }));
app.get("/api/notifications", async () => envelope({ items: [], total: 0, unreadCount: 0, page: 1, pageSize: 20 }));
await app.register(serverRequire("./src/routes/expenseTypes"), { prefix: "/api" });
await app.register(serverRequire("@fastify/multipart"), { limits: { fileSize: 10 * 1024 * 1024 } });
await app.register(serverRequire("./src/routes/invoicePool"), { prefix: "/api" });
await app.register(serverRequire("./src/routes/reimbursements"), { prefix: "/api" });
await app.register(serverRequire("./src/routes/workflow"), { prefix: "/api" });
if (process.env.RUN_PAYMENT_EXPERIENCE_TEST === "1") {
await app.register(serverRequire("./src/routes/paymentBatches"), { prefix: "/api" });
}
if (process.env.RUN_JOB_QUEUE === "1") {
Object.assign(app.config, { jobDriver: "pgboss", pgBossSchema: "synthetic_jobs", ocrMaxAttempts: 3, ocrPollCron: "0 0 1 1 *", delegationRevokeCron: "0 0 1 1 *", invoiceArchiveCron: "0 0 1 1 *", integrationOutboxCron: "0 0 1 1 *" });
await serverRequire("./src/jobs/pgBossJobs").startPgBossJobs(app);
}
const origin = await app.listen({ host: "127.0.0.1", port: 0 });
const tokens = new Map();
for (const actor of [employee, ...Object.values(approvers)]) {
const login = await fetch(`${origin}/api/auth/login`, { method: "POST", headers: { "content-type": "application/json", "x-device-fingerprint": `synthetic-${actor.id}` }, body: JSON.stringify({ identifier: actor.email, password: loginPassword }) });
assert.equal(login.status, 200, "password login");
tokens.set(actor.id, (await login.json()).data.accessToken);
}
assert.equal((await fetch(`${origin}/api/me`)).status, 401);
assert.equal((await fetch(`${origin}/api/me`, { headers: { authorization: "Bearer invalid-synthetic-token" } })).status, 401);
passed.push("real password login issues tokens; absent and invalid JWT rejected");
const post = async (url, body, actor, method = "POST") => {
const token = tokens.get(actor ?? employee.id) ?? app.issueAccessToken({ id: actor, token_version: 0 });
const response = await fetch(`${origin}/api${url}`, { method, headers: { "content-type": "application/json", authorization: `Bearer ${token}` }, body: JSON.stringify(body), signal: AbortSignal.timeout(15000) });
return { status: response.status, body: await response.json() };
};
const bind = (invoice, form, actor) => post(`/invoice-pool/${invoice.id}/bind`, { reimbursementId: form.id, createItem: true }, actor);
const objects = [
"<< /Type /Catalog /Pages 2 0 R >>",
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 200] /Contents 4 0 R >>",
"<< /Length 0 >>\nstream\n\nendstream",
];
let pdf = "%PDF-1.4\n";
const offsets = [0];
for (const [index, content] of objects.entries()) {
offsets.push(Buffer.byteLength(pdf));
pdf += `${index + 1} 0 obj\n${content}\nendobj\n`;
}
const xrefOffset = Buffer.byteLength(pdf);
pdf += `xref\n0 5\n0000000000 65535 f \n${offsets.slice(1).map((offset) => `${String(offset).padStart(10, "0")} 00000 n \n`).join("")}trailer\n<< /Size 5 /Root 1 0 R >>\nstartxref\n${xrefOffset}\n%%EOF\n`;
const upload = async () => {
const body = new FormData();
body.append("file", new Blob([pdf], { type: "application/pdf" }), "synthetic-blank.pdf");
body.append("ocr_type", "vat");
const response = await fetch(`${origin}/api/invoice-pool/upload`, { method: "POST", headers: { authorization: `Bearer ${tokens.get(employee.id)}` }, body, signal: AbortSignal.timeout(15000) });
assert.equal(response.status, 200);
return (await response.json()).data;
};
const uploaded = await upload();
assert.equal(uploaded.status, "pending_ocr");
const uploadedRecord = await prisma.invoicePool.findUnique({ where: { id: uploaded.invoicePoolId } });
assert.equal(uploadedRecord.linked_reimbursement_id, null);
assert.deepEqual(readFileSync(path.join(storageDir, uploadedRecord.storage_path)), Buffer.from(pdf));
assert.equal((await upload()).invoicePoolId, uploaded.invoicePoolId);
assert.equal(await prisma.invoicePool.count(), 1);
passed.push("multipart PDF upload persists unbound pending OCR; duplicate upload reuses invoice without extra record");
if (process.env.RUN_BROWSER_TEST === "1") {
const clientRequire = createRequire(path.join(root, "client/package.json"));
const { pathToFileURL } = require("node:url");
const vite = await import(pathToFileURL(clientRequire.resolve("vite")).href);
preview = await (vite.default ?? vite).preview({ configFile: false, root: path.join(root, "client"), preview: { host: "127.0.0.1", port: 0, proxy: { "/api": origin } } });
const uiOrigin = `http://127.0.0.1:${preview.httpServer.address().port}`;
app.config.appOrigin = uiOrigin;
const { chromium, expect } = clientRequire("@playwright/test");
browser = await chromium.launch({ headless: true });
const runBrowserFlow = async (mobile) => {
const context = await browser.newContext({ serviceWorkers: "block", viewport: mobile ? { width: 390, height: 844 } : { width: 1440, height: 960 } });
const fingerprint = `synthetic-browser-${mobile ? "mobile" : "desktop"}`;
await context.addCookies([{ name: "hyb_oa_device_fingerprint", value: fingerprint, url: uiOrigin }]);
const browserLogin = await context.request.post(`${uiOrigin}/api/auth/login`, { headers: { "x-device-fingerprint": fingerprint }, data: { identifier: employee.email, password: loginPassword } });
assert.equal(browserLogin.status(), 200);
const page = await context.newPage();
page.setDefaultTimeout(20000);
const browserForm = await makeForm();
await page.goto(`${uiOrigin}${mobile ? "/mobile" : ""}/reimbursements/${browserForm.id}`);
await page.getByTestId("reimbursement-invoice-add").click();
const browserPdf = pdf.replace("300 200", mobile ? "302 200" : "301 200");
await page.getByTestId("reimbursement-invoice-upload-file").setInputFiles({ name: "synthetic-blank.pdf", mimeType: "application/pdf", buffer: Buffer.from(browserPdf) });
const [uploadResponse] = await Promise.all([
page.waitForResponse((response) => response.request().method() === "POST" && response.url().endsWith("/api/invoice-pool/upload")),
page.getByTestId("reimbursement-invoice-upload-submit").click(),
]);
assert.equal(uploadResponse.status(), 200);
const browserInvoiceId = (await uploadResponse.json()).data.invoicePoolId;
if (process.env.RUN_JOB_QUEUE === "1") {
await expect.poll(async () => (await prisma.invoicePool.findUnique({ where: { id: browserInvoiceId } })).status, { timeout: 20000 }).toBe("available");
await expect.poll(async () => Number((await pool.query("SELECT count(*) FROM synthetic_jobs.job WHERE name = 'ocr.invoice' AND data->>'invoiceId' = $1 AND state = 'completed'", [browserInvoiceId])).rows[0].count), { timeout: 20000 }).toBe(1);
} else {
const workerConfig = { ...app.config, ocrProvider: "mock", ocrMaxAttempts: 3 };
const workerResult = await serverRequire("./src/jobs/ocrJob").runOcrForInvoiceId(prisma, workerConfig, app.log, browserInvoiceId, "synthetic-browser-worker");
assert.equal(workerResult.available, 1);
}
await expect(page.getByRole("dialog").getByRole("checkbox").first()).toBeChecked({ timeout: 20000 });
assert.equal(await prisma.reimbursementItem.count({ where: { reimbursement_id: browserForm.id } }), 0);
await page.getByRole("dialog").getByRole("button", { name: mobile ? "添加到报销单 1" : "添加到报销单", exact: true }).click();
if (mobile) await page.getByRole("dialog").getByRole("button", { name: "返回报销单" }).click();
await expect(page.getByText("费用分类:1 条待补充")).toBeVisible();
await page.getByRole("checkbox", { name: /明细 1/ }).check();
await page.getByLabel("批量费用类型").selectOption(expenseType.id);
await page.getByRole("button", { name: "设置所选 1 条分类" }).click();
await expect(page.getByText("费用分类:1 条待补充")).toHaveCount(0);
await page.getByTestId("reimbursement-approval-evidence").getByRole("button", { name: "编辑", exact: true }).first().click();
await (mobile ? page.getByLabel("说明", { exact: true }) : page.getByTestId("reimbursement-item-description")).fill("synthetic browser purpose");
const [savedItemResponse] = await Promise.all([
page.waitForResponse((response) => response.request().method() === "PATCH" && response.url().includes(`/reimbursements/${browserForm.id}/items/`)),
(mobile ? page.getByRole("button", { name: "保存", exact: true }) : page.getByTestId("reimbursement-item-save")).click(),
]);
assert.equal(savedItemResponse.status(), 200);
assert.equal((await prisma.reimbursementItem.findFirst({ where: { reimbursement_id: browserForm.id } })).description, "synthetic browser purpose");
await page.reload();
await expect(page.getByTestId("reimbursement-approval-evidence").getByText("synthetic browser purpose").first()).toBeVisible();
await page.getByTestId("reimbursement-submit").click();
await expect.poll(async () => (await prisma.reimbursementForm.findUnique({ where: { id: browserForm.id } })).status, { timeout: 15000 }).toBe("reviewing");
const browserWorkflow = await prisma.workflowInstance.findFirst({ where: { reimbursement_id: browserForm.id } });
assert.equal(await prisma.workflowTask.count({ where: { instance_id: browserWorkflow.id, assignee_id: manager.id, status: "pending" } }), 1);
for (const role of ["manager", "ceo", "finance"]) {
const approvalTask = await prisma.workflowTask.findFirst({ where: { instance_id: browserWorkflow.id, status: "pending" } });
assert.ok(approvalTask);
assert.equal((await post(`/workflow/tasks/${approvalTask.id}/approve`, {}, approvers[role].id)).status, 200);
}
assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: browserForm.id } })).status, "approved");
await page.reload();
await expect(page.getByTestId("reimbursement-approval-evidence")).toBeVisible();
await expect(page.getByTestId("reimbursement-invoice-add")).toHaveCount(0);
await context.close();
passed.push(`${mobile ? "mobile" : "desktop"} browser upload -> real OCR worker with mock provider -> select -> classify/edit -> submit -> three HTTP approvals -> read-only page`);
};
await runBrowserFlow(false);
await runBrowserFlow(true);
}
const form = await makeForm();
const invoice = await makeInvoice();
const repeated = await Promise.all(Array.from({ length: 4 }, () => bind(invoice, form)));
assert.deepEqual(repeated.map((result) => result.status), [200, 200, 200, 200]);
let items = await prisma.reimbursementItem.findMany({ where: { reimbursement_id: form.id } });
assert.equal(items.length, 1);
assert.equal(items[0].expense_type_id, null);
assert.equal(items[0].amount.toFixed(2), "100.00");
assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: form.id } })).total_amount.toFixed(2), "100.00");
passed.push("same-form concurrent retry creates one persisted draft item");
const missingClassification = await post(`/reimbursements/${form.id}/submit`, {});
assert.equal(missingClassification.status, 400);
assert.equal(missingClassification.body.error.details.reasonCode, "reimbursement.items_expense_type_required");
const classification = await post(`/reimbursements/${form.id}/items/classify`, { itemIds: [items[0].id], expenseTypeId: expenseType.id });
assert.equal(classification.status, 200);
const missingDescription = await post(`/reimbursements/${form.id}/submit`, {});
assert.equal(missingDescription.body.error.details.reasonCode, "reimbursement.items_description_required");
assert.equal((await post(`/reimbursements/${form.id}/items/${items[0].id}`, { description: "synthetic edited purpose", amount: "90.00" }, null, "PATCH")).status, 200);
assert.equal((await bind(invoice, form)).status, 200);
const edited = await prisma.reimbursementItem.findUnique({ where: { id: items[0].id } });
assert.equal(edited.expense_type_id, expenseType.id);
assert.equal(edited.description, "synthetic edited purpose");
assert.equal(edited.amount.toFixed(2), "90.00");
passed.push("batch classification persists and retry preserves edits");
const submit = await post(`/reimbursements/${form.id}/submit`, {});
assert.equal(submit.status, 200, `submit: ${submit.status} ${submit.body.error?.details?.reasonCode ?? submit.body.error?.code ?? ""}`);
assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: form.id } })).status, "reviewing");
const workflow = await prisma.workflowInstance.findFirst({ where: { reimbursement_id: form.id } });
assert.ok(workflow);
assert.equal(await prisma.workflowTask.count({ where: { instance_id: workflow.id, assignee_id: manager.id } }), 1);
assert.equal((await post(`/reimbursements/${form.id}/submit`, {})).status, 409);
assert.equal(await prisma.workflowInstance.count({ where: { reimbursement_id: form.id } }), 1);
passed.push("empty draft to classified edited submission assigns manager once");
const firstTask = await prisma.workflowTask.findFirst({ where: { instance_id: workflow.id, status: "pending" } });
assert.equal((await post(`/workflow/tasks/${firstTask.id}/approve`, {})).status, 403);
assert.equal((await post(`/workflow/tasks/${firstTask.id}/return`, { comment: "synthetic correction required" }, manager.id)).status, 200);
assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: form.id } })).status, "draft");
assert.equal((await prisma.invoicePool.findUnique({ where: { id: invoice.id } })).linked_reimbursement_id, form.id);
assert.equal((await post(`/reimbursements/${form.id}/items/${items[0].id}`, { description: "synthetic corrected purpose" }, null, "PATCH")).status, 200);
const returnedResubmit = await post(`/reimbursements/${form.id}/submit`, {});
assert.equal(returnedResubmit.status, 200);
const rejectTask = await prisma.workflowTask.findFirst({ where: { instance_id: returnedResubmit.body.data.workflowInstanceId, status: "pending" } });
assert.equal((await post(`/workflow/tasks/${rejectTask.id}/reject`, { comment: "synthetic rejected correction" }, manager.id)).status, 200);
assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: form.id } })).status, "rejected");
assert.equal((await post(`/reimbursements/${form.id}/items/${items[0].id}`, { description: "synthetic rejected item corrected" }, null, "PATCH")).status, 200);
const resubmit = await post(`/reimbursements/${form.id}/submit`, {});
assert.equal(resubmit.status, 200);
const newWorkflowId = resubmit.body.data.workflowInstanceId;
assert.notEqual(newWorkflowId, workflow.id);
for (const role of ["manager", "ceo", "finance"]) {
const task = await prisma.workflowTask.findFirst({ where: { instance_id: newWorkflowId, status: "pending" } });
assert.ok(task, `pending task for ${role}`);
if (role === "manager") assert.equal(task.assignee_id, manager.id);
else { assert.equal(task.assignee_id, null); assert.equal(task.assignee_role, role); }
if (role === "finance" && browser && preview) {
await prisma.workflowHistory.updateMany({ where: { instance_id: newWorkflowId, action: "approve" }, data: { comment: "synthetic historical comment ".repeat(12) } });
const clientRequire = createRequire(path.join(root, "client/package.json"));
const { expect } = clientRequire("@playwright/test");
const uiOrigin = `http://127.0.0.1:${preview.httpServer.address().port}`;
for (const mobile of [false, true]) {
const context = await browser.newContext({ serviceWorkers: "block", viewport: mobile ? { width: 390, height: 844 } : { width: 1440, height: 960 } });
try {
const fingerprint = `synthetic-approval-layout-${mobile}`;
await context.addCookies([{ name: "hyb_oa_device_fingerprint", value: fingerprint, url: uiOrigin }]);
const login = await context.request.post(`${uiOrigin}/api/auth/login`, { headers: { "x-device-fingerprint": fingerprint }, data: { identifier: approvers.finance.email, password: loginPassword } });
assert.equal(login.status(), 200);
const page = await context.newPage();
await page.goto(`${uiOrigin}${mobile ? "/mobile/approvals" : "/workflow/tasks"}/${task.id}`);
const progress = page.getByTestId("reimbursement-approval-processing-progress");
await expect(progress).toHaveCount(1);
await expect(page.getByTestId("reimbursement-workflow-history")).toContainText("synthetic historical comment");
if (!mobile) {
const actions = page.getByTestId("workflow-task-action-panel");
const actionBounds = await actions.boundingBox();
const progressBounds = await progress.boundingBox();
assert.ok(progressBounds.y > actionBounds.y + actionBounds.height);
assert.ok(Math.abs(progressBounds.x - actionBounds.x) < 35);
assert.notEqual(await actions.locator("..").evaluate((element) => getComputedStyle(element).position), "sticky");
} else {
await expect(page.getByTestId("mobile-approvals-sticky-actions")).toBeVisible();
}
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 2), true);
} finally { await context.close(); }
}
passed.push("approval: real finance desktop/mobile evidence, current progress and synthetic long-history fixture; non-overlapping aside and safe-area action");
}
const approval = await post(`/workflow/tasks/${task.id}/approve`, {}, approvers[role].id);
assert.equal(approval.status, 200, `${role} approval: ${approval.status} ${approval.body.error?.code ?? ""}`);
assert.equal((await post(`/workflow/tasks/${task.id}/approve`, {}, approvers[role].id)).status, 409);
}
assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: form.id } })).status, "approved");
assert.equal(await prisma.workflowTask.count({ where: { instance_id: newWorkflowId, status: "pending" } }), 0);
assert.equal((await prisma.workflowInstance.findUnique({ where: { id: newWorkflowId } })).current_status, "approved");
passed.push("owner cannot self-approve; return/edit/resubmit and manager/ceo/finance approval complete without payment");
if (process.env.RUN_PAYMENT_EXPERIENCE_TEST === "1") {
await require("./test-payment-experience.cjs")({ app, prisma, employee, approvers, form, post, origin, tokens, serverRequire, passed, browser, preview, loginPassword });
}
const competingForms = await Promise.all([makeForm(), makeForm()]);
const sharedInvoice = await makeInvoice();
const competition = await Promise.all(competingForms.map((candidate) => bind(sharedInvoice, candidate)));
assert.deepEqual(competition.map((result) => result.status).sort(), [200, 409]);
assert.equal(await prisma.reimbursementItem.count({ where: { reimbursement_id: { in: competingForms.map((candidate) => candidate.id) } } }), 1);
passed.push("cross-form competition has one winner and no orphan item");
const multipleForm = await makeForm();
const equalInvoices = await Promise.all([makeInvoice(), makeInvoice()]);
assert.deepEqual((await Promise.all(equalInvoices.map((candidate) => bind(candidate, multipleForm)))).map((result) => result.status), [200, 200]);
const multipleItems = await prisma.reimbursementItem.findMany({ where: { reimbursement_id: multipleForm.id } });
assert.equal(multipleItems.length, 2);
assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: multipleForm.id } })).total_amount.toFixed(2), "200.00");
const invalidBatch = await post(`/reimbursements/${multipleForm.id}/items/classify`, { itemIds: [multipleItems[0].id, items[0].id], expenseTypeId: expenseType.id });
assert.equal(invalidBatch.status, 409);
assert.equal((await prisma.reimbursementItem.findUnique({ where: { id: multipleItems[0].id } })).expense_type_id, null);
const additionalInvoice = await makeInvoice();
assert.equal((await post(`/invoice-pool/${additionalInvoice.id}/bind`, { reimbursementId: multipleForm.id, linkedItemId: multipleItems[0].id })).status, 200);
assert.equal(await prisma.reimbursementItem.count({ where: { reimbursement_id: multipleForm.id } }), 2);
assert.equal((await prisma.reimbursementItem.findUnique({ where: { id: multipleItems[0].id } })).amount.toFixed(2), "100.00");
passed.push("equal-amount invoices stay separate; existing-item attachment preserves amount; invalid classification is atomic");
const emptyForm = await makeForm();
const incompleteInvoice = await makeInvoice({ total_amount: null });
assert.equal((await bind(incompleteInvoice, emptyForm)).status, 409);
assert.equal(await prisma.reimbursementItem.count({ where: { reimbursement_id: emptyForm.id } }), 0);
assert.equal((await prisma.invoicePool.findUnique({ where: { id: incompleteInvoice.id } })).status, "available");
assert.equal((await bind(await makeInvoice(), emptyForm, randomUUID())).status, 403);
await prisma.reimbursementForm.update({ where: { id: emptyForm.id }, data: { status: "approved" } });
assert.equal((await bind(await makeInvoice(), emptyForm)).status, 409);
passed.push("missing OCR amount, nonparticipant and immutable state reject without items");
const rollbackForm = await makeForm();
const rollbackInvoice = await makeInvoice();
await prisma.$executeRawUnsafe("CREATE FUNCTION synthetic_reject_bind() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NEW.status = 'bound' THEN RAISE EXCEPTION 'synthetic injected failure'; END IF; RETURN NEW; END $$");
await prisma.$executeRawUnsafe("CREATE TRIGGER synthetic_reject_bind BEFORE UPDATE ON invoice_pool FOR EACH ROW EXECUTE FUNCTION synthetic_reject_bind()");
assert.equal((await bind(rollbackInvoice, rollbackForm)).status, 500);
assert.equal(await prisma.reimbursementItem.count({ where: { reimbursement_id: rollbackForm.id } }), 0);
assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: rollbackForm.id } })).total_amount.toFixed(2), "0.00");
assert.equal((await prisma.invoicePool.findUnique({ where: { id: rollbackInvoice.id } })).status, "available");
await prisma.$executeRawUnsafe("DROP TRIGGER synthetic_reject_bind ON invoice_pool");
assert.equal((await bind(rollbackInvoice, rollbackForm)).status, 200);
passed.push("database failure rolls back item, occupancy and total; retry succeeds");
const blocker = await pool.connect();
const waitForLockedRequests = async (count) => {
for (let attempt = 0; attempt < 100; attempt += 1) {
const result = await pool.query("SELECT count(*)::int AS count FROM pg_stat_activity WHERE datname = current_database() AND wait_event_type = 'Lock' AND query LIKE '%reimbursement_forms%'");
if (result.rows[0].count >= count) return;
await delay(20);
}
assert.fail("synthetic lock barrier timed out");
};
let retryRequest;
let unbindRequest;
try {
await blocker.query("BEGIN");
await blocker.query("SELECT id FROM reimbursement_forms WHERE id = $1 FOR UPDATE", [rollbackForm.id]);
retryRequest = bind(rollbackInvoice, rollbackForm);
await waitForLockedRequests(1);
unbindRequest = post(`/invoice-pool/${rollbackInvoice.id}/unbind`, {});
await waitForLockedRequests(2);
} finally {
await blocker.query("ROLLBACK");
blocker.release();
}
const simultaneous = await Promise.all([retryRequest, unbindRequest]);
assert.deepEqual(simultaneous.map((result) => result.status), [200, 200], "retry and unbind must not deadlock");
passed.push("create retry and legacy unbind share a consistent lock order");
if (process.env.RUN_RETAINED_DELETE_TEST === "1") {
await require("./test-reimbursement-retained-delete.cjs")({
prisma, pool, app, origin, tokens, employee, manager, expenseType,
makeForm, makeInvoice, bind, post, uploadedRecord, storageDir, passed,
root, browser, preview, loginPassword,
});
}
await prisma.employee.update({ where: { id: employee.id }, data: { token_version: { increment: 1 } } });
assert.equal((await fetch(`${origin}/api/me`, { headers: { authorization: `Bearer ${tokens.get(employee.id)}` } })).status, 401);
passed.push("revoked token version rejected by real auth plugin");
console.log(JSON.stringify({ status: "PASS_REAL_DB_HTTP", passed, authentication: "REAL_PASSWORD_LOGIN_JWT_SYNTHETIC_ACCOUNTS", liveOcr: "NOT_TESTED", ocrWorker: process.env.RUN_JOB_QUEUE === "1" ? "REAL_PGBOSS_MOCK_PROVIDER" : process.env.RUN_BROWSER_TEST === "1" ? "REAL_WORKER_MOCK_PROVIDER_MANUAL_DISPATCH" : "NOT_TESTED", approvalFlow: "RETURN_RESUBMIT_THREE_STEP_APPROVAL", payment: process.env.RUN_PAYMENT_EXPERIENCE_TEST === "1" ? "CREATE_REVIEW_DOWNLOAD_STILL_UNPAID" : "NOT_TESTED" }, null, 2));
} finally {
if (browser) await browser.close().catch(() => { process.exitCode = 1; console.error("Browser cleanup failed"); });
if (preview) await new Promise((resolve, reject) => preview.httpServer.close((error) => error ? reject(error) : resolve())).catch(() => { process.exitCode = 1; console.error("Preview cleanup failed"); });
try { if (app) await app.close(); }
finally {
try { if (prisma) await prisma.$disconnect(); }
finally {
try { if (pool) await pool.end(); }
finally {
try { if (container) { docker(["rm", "-f", container]); console.log("Disposable database removed"); } }
finally { if (storageDir) { rmSync(storageDir, { recursive: true, force: true }); console.log("Synthetic upload storage removed"); } }
}
}
}
}
}
main().catch((error) => { console.error("Isolated HTTP verification failed:", error.code ?? error.name, error.code === "ERR_ASSERTION" ? error.message : "", error.stack?.match(/test-(?:invoice-first-http|reimbursement-retained-delete)\.cjs:\d+:\d+/)?.[0] ?? ""); process.exitCode = 1; });