fix: 完善回单失败引导及真实认证验证(09-08-reimbursement-payment-closeout)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-09-15 15:26:08 +08:00
父节点 8d08d46d16
当前提交 b7d2285d8d
共修改 9 个文件,包含 383 行新增和 13 行删除
+54 -12
查看文件
@@ -22,6 +22,7 @@ const docker = (args, input) => {
async function main() {
assert.equal(process.env.RUN_ISOLATED_DB_TEST, "1");
const realAuthentication = process.env.RUN_RECEIPT_REAL_AUTH === "1";
let container;
let prisma;
let pool;
@@ -428,20 +429,29 @@ async function main() {
const app = serverRequire("fastify")();
app.decorate("prisma", prisma);
app.decorate("config", { ...config, paymentBatchEnabled: true, appTimezone: "Asia/Shanghai",
erpSyncEnabled: false, emailEnabled: false, imNotifierEnabled: false, appOrigin: "http://127.0.0.1" });
app.decorate("authenticate", async (request) => {
const actorId = request.headers.authorization === "Bearer synthetic-finance" ? employee.id :
request.headers.authorization === "Bearer synthetic-reviewer" ? reviewer.id : null;
if (!actorId) throw new AppError({ code: "AUTH_001", message: "未登录", statusCode: 401 });
const actor = await prisma.employee.findUnique({ where: { id: actorId }, include: { roles: true } });
request.employee = { ...actor, roleCodes: actor.roles.map((role) => role.role_code) };
});
app.decorate("hasPermission", (actor, permission) => hasPermission(resolvePermissionsForRoles(actor.roleCodes), permission));
erpSyncEnabled: false, emailEnabled: false, imNotifierEnabled: false, appOrigin: "http://127.0.0.1",
jwtSecret: randomUUID(), accessTokenTtl: "15m", refreshTtlSeconds: 3600,
refreshCookieName: "synthetic_refresh", cookieSecure: false,
loginFailMaxAttempts: 5, loginFailWindowSeconds: 300, loginLockSeconds: 60 });
if (realAuthentication) {
await app.register(serverRequire("@fastify/cookie"));
await app.register(serverRequire("./src/plugins/auth"));
await app.register(serverRequire("./src/routes/auth"), { prefix: "/api/auth" });
} else {
app.decorate("authenticate", async (request) => {
const actorId = request.headers.authorization === "Bearer synthetic-finance" ? employee.id :
request.headers.authorization === "Bearer synthetic-reviewer" ? reviewer.id : null;
if (!actorId) throw new AppError({ code: "AUTH_001", message: "未登录", statusCode: 401 });
const actor = await prisma.employee.findUnique({ where: { id: actorId }, include: { roles: true } });
request.employee = { ...actor, roleCodes: actor.roles.map((role) => role.role_code) };
});
app.decorate("hasPermission", (actor, permission) => hasPermission(resolvePermissionsForRoles(actor.roleCodes), permission));
}
app.setErrorHandler((error, request, reply) => reply.code(error instanceof AppError ? error.statusCode : 500)
.send({ error: { code: error.code ?? "unexpected", details: error instanceof AppError ? error.details : undefined } }));
await app.register(serverRequire("./src/routes/paymentBatches"), { prefix: "/api" });
const financeHeaders = { authorization: "Bearer synthetic-finance" };
const reviewerHeaders = { authorization: "Bearer synthetic-reviewer" };
let financeHeaders = { authorization: "Bearer synthetic-finance" };
let reviewerHeaders = { authorization: "Bearer synthetic-reviewer" };
async function readyHttp(context) {
await prisma.paymentBatch.update({ where: { id: context.batch.id }, data: {
reviewed_by: reviewer.id, reviewed_at: new Date(), default_payment_channel: "bank",
@@ -449,6 +459,30 @@ async function main() {
return `/api/payment-batches/${context.batch.id}`;
}
try {
if (realAuthentication) {
const loginPassword = `Synthetic!${randomUUID()}`;
const passwordHash = await serverRequire("./src/utils/crypto").hashPassword(loginPassword);
await prisma.employee.updateMany({ where: { id: { in: [employee.id, reviewer.id] } },
data: { password_hash: passwordHash, must_change_password: false } });
async function loginActor(actorId) {
const actor = await prisma.employee.findUniqueOrThrow({ where: { id: actorId } });
const response = await app.inject({ method: "POST", url: "/api/auth/login",
headers: { "x-device-fingerprint": `synthetic-${actorId}` },
payload: { identifier: actor.email, password: loginPassword } });
assert.equal(response.statusCode, 200, "synthetic password login failed");
const token = response.json().data.accessToken;
assert.equal(typeof token, "string");
assert.ok(response.cookies.some(cookie => cookie.name === "synthetic_refresh"));
return { authorization: `Bearer ${token}` };
}
financeHeaders = await loginActor(employee.id);
reviewerHeaders = await loginActor(reviewer.id);
passed.push("actual_password_login_jwt_and_refresh_cookie");
const forged = await app.inject({ method: "GET", url: `/api/payment-batches/${randomUUID()}/receipt-readiness`,
headers: { authorization: "Bearer synthetic-finance" } });
assert.equal(forged.statusCode, 401);
passed.push("synthetic_actor_header_rejected_by_real_authentication");
}
const httpManual = await manualFixture("SYN-HTTP-ORIGINAL");
const httpBase = await readyHttp(httpManual);
const httpProposalUrl = `${httpBase}/receipt-attachments/${httpManual.attachment.id}/manual-verification`;
@@ -489,6 +523,12 @@ async function main() {
assert.equal((await prisma.reimbursementForm.findUnique({ where: { id: rollback.batch.items[0].entity_id } })).status, "approved");
passed.push("real_db_http_source_amount_change_rolls_back_claim_alias_and_payment");
} finally { await app.close(); }
if (process.env.RUN_RECEIPT_BROWSER === "1") {
assert.ok(realAuthentication);
await require("./test-payment-receipt-browser.cjs")({
root, prisma, config, employee, reviewer, manualFixture, serverRequire, passed,
});
}
} finally {
try { if (prisma) await prisma.$disconnect(); }
finally {
@@ -504,7 +544,9 @@ async function main() {
}
}
console.log(JSON.stringify({ status: "PASS_ISOLATED_RECEIPT_DB", passed, cleanupComplete: true,
productionAccess: false, cloudCalls: false, httpTested: true, authentication: "synthetic_actor_fixture" }));
productionAccess: false, cloudCalls: false, httpTested: true,
authentication: realAuthentication ? "actual_password_login_and_jwt" : "synthetic_actor_fixture",
browserTested: process.env.RUN_RECEIPT_BROWSER === "1" }));
}
main().catch((error) => {