文件
hyb-oa/server/tests/unit/routes/read-export.errors.test.js
wangsli 1f8d5b658a 完善借款、编号、付款与预算并发及验收(09-06-release-gate-integrity)
汇总已落盘的借款本金与还款、兼容历史编号的周期化编号、付款责任与回单核验、报销进度和排序、PWA/采购事务、错误原因与日期校验及ERP退役。
统一共享预算池锁序和锁后判断,恢复差旅原预算选择,补原因码固定夹具及反向完整性检查。
纳入配套迁移、测试、受管QA、分层验收合同、规约及任务记录;原始证据留在ignored artifacts。

关联任务:09-17-company-loan-principal-repayment、09-17-erp-exclusive-retirement、09-18-reimbursement-periodic-numbering、09-08-reimbursement-payment-closeout、09-18-local-concurrency-product-fixes、09-19-budget-pool-concurrency、09-05-error-explainability-payment-readiness。
预算补审由Claude定向验证2文件928项通过,原4446/28及r6结果仍仅对应各自历史输入;本轮未重跑测试。
完整新候选门禁、R1真实入口接入与12项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
2026-09-19 13:11:04 +08:00

36 行
5.2 KiB
JavaScript

const Fastify = require("fastify");
const { AppError } = require("../../../src/utils/AppError");
const routes = [require("../../../src/routes/audit"), require("../../../src/routes/search"), require("../../../src/routes/exports")];
const apps = [];
afterEach(async () => { await Promise.all(apps.splice(0).map((app) => app.close())); });
async function build({ allowed = true, disabled } = {}) {
const app = Fastify(); apps.push(app);
const prisma = { featureFlag: { findUnique: vi.fn().mockResolvedValue({ override_mode: "force_on", rollout_type: "global", department_ids: [] }) } };
for (const name of ["employee", "reimbursementForm", "invoice", "auditLog", "paymentBatch", "contractRegistry", "employeePresence"]) prisma[name] = { count: vi.fn().mockResolvedValue(0), findMany: vi.fn().mockResolvedValue([]), create: vi.fn().mockResolvedValue({}) };
const actor = { id: "synthetic", permissionCodes: allowed ? ["*"] : [] };
const authenticate = async (request) => { request.employee = actor; };
app.decorate("prisma", prisma); app.decorate("config", { appTimezone: "Asia/Shanghai", globalSearchEnabled: disabled !== "search", paymentBatchEnabled: disabled !== "payment", projectLinkingEnabled: disabled !== "project" });
app.decorate("authenticate", authenticate); app.decorate("hasPermission", () => allowed); app.decorate("requirePermission", () => async (request) => { await authenticate(request); if (!allowed) throw new AppError({ code: "PERM_001", statusCode: 403 }); });
app.setErrorHandler((e, _, reply) => reply.status(e.statusCode || 500).send({ error: { code: e.code, details: e.details } }));
for (const route of routes) await app.register(route, { prefix: "/api" });
return { app, prisma };
}
function noBusiness(p) { for (const [name, model] of Object.entries(p)) if (name !== "featureFlag") for (const fn of Object.values(model)) expect(fn).not.toHaveBeenCalled(); }
function rejected(r, reason, status = 400) { expect(r.statusCode).toBe(status); expect(r.json().error).toMatchObject({ code: "VALID_001", details: { reasonCode: reason } }); expect(r.headers["content-disposition"]).toBeUndefined(); }
const cases = [
["GET", "/audit-logs?page=0", undefined, "audit.query_invalid"], ["GET", "/audit-logs?dateFrom=2026-09-17T00:00:00Z&dateTo=2026-09-16T00:00:00Z", undefined, "audit.date_range_invalid"],
["GET", "/search?q=a", undefined, "search.query_invalid"], ["GET", "/search?q=ab&limit=21", undefined, "search.query_invalid"],
...["presence-history", "reimbursements", "invoice-pool", "audit-logs", "dashboard/reimbursements"].map((path, i) => ["GET", `/exports/${path}?format=bad`, undefined, `export.${["presence", "reimbursement", "invoice", "audit", "dashboard"][i]}_query_invalid`]),
["POST", "/exports/ap-operations", { format: "bad" }, "export.ap_input_invalid"],
["POST", "/exports/ap-operations", { pendingCreatedAtFrom: "2026-09-17", pendingCreatedAtTo: "2026-09-16" }, "export.ap_pending_date_range_invalid"],
["GET", "/exports/audit-logs?dateFrom=2026-09-17T00:00:00Z&dateTo=2026-09-16T00:00:00Z", undefined, "audit.date_range_invalid"],
["GET", "/exports/dashboard/reimbursements?dateFrom=2026-09-17&dateTo=2026-09-16", undefined, "export.dashboard_date_range_invalid"],
];
describe("live read and export error contracts", () => {
it.each(cases)("explains %s %s before reads/export audit", async (method, path, payload, reason) => { const { app, prisma } = await build(); rejected(await app.inject({ method, url: `/api${path}`, payload }), reason); noBusiness(prisma); });
it.each([["search", "GET", "/search?q=ab", "search.feature_disabled"], ["payment", "POST", "/exports/ap-operations", "export.payment_batch_disabled"], ["project", "POST", "/exports/contracts-registry", "export.project_linking_disabled"]])("retains %s disabled gate", async (disabled, method, path, reason) => { const { app, prisma } = await build({ disabled }); rejected(await app.inject({ method, url: `/api${path}`, payload: method === "POST" ? {} : undefined }), reason, 409); noBusiness(prisma); });
it.each(["/audit-logs", "/exports/audit-logs", "/exports/reimbursements", "/exports/invoice-pool"])("retains permission before reads %s", async (path) => { const { app, prisma } = await build({ allowed: false }); expect((await app.inject({ url: `/api${path}` })).statusCode).toBe(403); noBusiness(prisma); });
it("keeps audit defaults and equal datetime bounds", async () => { const { app, prisma } = await build(); const r = await app.inject({ url: "/api/audit-logs?dateFrom=2026-09-17T00:00:00Z&dateTo=2026-09-17T00:00:00Z" }); expect(r.statusCode).toBe(200); expect(r.json().data).toMatchObject({ page: 1, pageSize: 20, items: [] }); expect(prisma.auditLog.findMany.mock.calls[0][0]).toMatchObject({ take: 20, skip: 0, where: { created_at: { gte: new Date("2026-09-17"), lte: new Date("2026-09-17") } } }); expect(prisma.auditLog.create).not.toHaveBeenCalled(); });
it.each(["csv", "xlsx"])("keeps synthetic audit %s export and one success audit", async (format) => { const { app, prisma } = await build(); const r = await app.inject({ url: `/api/exports/audit-logs?format=${format}` }); expect(r.statusCode).toBe(200); expect(r.headers["content-disposition"]).toContain(format); expect(prisma.auditLog.create).toHaveBeenCalledOnce(); });
});