文件
hyb-oa/server/tests/unit/routes/date-consumers.calendar.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

93 行
6.8 KiB
JavaScript

const Fastify = require("fastify");
const { AppError } = require("../../../src/utils/AppError");
const routes = Object.fromEntries(["payroll", "budgets", "performance", "attendance", "attendanceCorrections", "leave"].map((name) => [name, require(`../../../src/routes/${name}`)]));
const ID = "11111111-1111-4111-8111-111111111111";
const budgetBody = { code: "SYNTHETIC", name: "合成预算", periodStart: "2024-02-29", periodEnd: "2026-04-30", allocatedAmount: "100.00" };
const cycleBody = { name: "合成周期", cycleType: "quarterly", startDate: "2024-02-29", endDate: "2026-04-30" };
const leaveBody = { leaveTypeId: ID, startDate: "2024-02-29", endDate: "2026-04-30", startSession: "am", endSession: "pm", reason: "synthetic" };
const cases = [
["payroll", "PUT", `/payroll/employees/${ID}/profile`, { effectiveDate: "2024-02-29", items: [{ salaryItemId: ID, monthlyAmount: "100.00" }] }, "effectiveDate"],
...["periodStart", "periodEnd"].map((field) => ["budgets", "POST", "/budgets", budgetBody, field]),
...["startDate", "endDate"].flatMap((field) => [["performance", "POST", "/performance/cycles", cycleBody, field], ["performance", "PATCH", `/performance/cycles/${ID}`, {}, field]]),
["attendance", "GET", "/attendance/team", {}, "date"],
["attendance", "POST", "/attendance/admin/backfill", { employeeId: ID, clockInAt: "2026-04-30T09:00:00+08:00" }, "workDate"],
...["startDate", "endDate"].map((field) => ["attendance", "POST", "/attendance/wecom/sync", { startDate: "2026-04-01", endDate: "2026-04-30" }, field]),
["attendanceCorrections", "POST", "/attendance-corrections", { kind: "missed_punch", missedPunchAt: "2026-04-30T09:00:00+08:00", reason: "synthetic" }, "workDate"],
...["startDate", "endDate"].flatMap((field) => [["leave", "POST", "/leave-requests", leaveBody, field], ["leave", "PATCH", `/leave-requests/${ID}`, leaveBody, field], ["leave", "GET", "/leave-requests/working-days", { startDate: "2024-02-29", endDate: "2026-04-30", startSession: "am", endSession: "pm" }, field]]),
...["dateFrom", "dateTo"].map((field) => ["leave", "GET", "/leave-requests", {}, field]),
];
async function buildApp(name, { allowed = true, budgetSuccess = false } = {}) {
const app = Fastify();
const businessCalls = [];
const rejectCall = (...args) => { businessCalls.push(args); throw new Error("unexpected_business_access"); };
const prisma = new Proxy({
featureFlag: { findUnique: vi.fn(async ({ where }) => ({ flag_key: where.flag_key, override_mode: where.flag_key.includes("cutover") ? "force_off" : "force_on", rollout_type: "global", department_ids: [] })) },
$transaction: vi.fn(rejectCall),
}, { get(target, key) {
if (!(key in target)) target[key] = new Proxy({}, { get(model, operation) { return model[operation] ??= vi.fn(rejectCall); } });
return target[key];
} });
if (budgetSuccess) {
prisma.budgetPool = { create: vi.fn(async ({ data }) => ({ id: ID, ...data })) };
prisma.auditLog = { create: vi.fn().mockResolvedValue({}) };
}
const authenticate = async (request) => { request.employee = { id: ID, status: "active", roleCodes: ["admin"], permissionCodes: ["*"], department_id: null }; };
const permission = async (request) => { await authenticate(request); if (!allowed) throw new AppError({ code: "PERM_001", statusCode: 403, message: "无权限" }); };
app.decorate("config", { appTimezone: "Asia/Shanghai", employeeLifecycleEnabled: true, attendanceUnifiedEnabled: true });
app.decorate("prisma", prisma);
app.decorate("authenticate", permission);
app.decorate("requirePermission", () => permission);
app.decorate("requireEntityPermission", () => permission);
app.decorate("assertEntityPermission", () => {});
app.decorate("hasPermission", () => allowed);
await app.register(require("@fastify/multipart"));
app.setErrorHandler((error, request, reply) => reply.code(error.statusCode ?? 500).send({ error: { code: error.code, details: error.details } }));
await app.register(routes[name], { prefix: "/api" });
return { app, prisma, businessCalls };
}
describe("remaining calendar consumers", () => {
it.each(cases)("%s %s %s rejects calendar overflow before business access (%j / %s)", async (name, method, path, body, field) => {
const { app, prisma, businessCalls } = await buildApp(name);
try {
for (const date of ["2026-02-29", "2026-04-31"]) {
const input = { ...body, [field]: date };
const response = await app.inject({ method, url: `/api${path}${method === "GET" ? `?${new URLSearchParams(input)}` : ""}`, ...(method === "GET" ? {} : { payload: input }) });
const legacyDisabled = name === "attendance" && ["/attendance/team", "/attendance/admin/backfill"].includes(path);
expect(response.statusCode).toBe(legacyDisabled ? 409 : 400);
expect(response.json().error.code).toBe("VALID_001");
if (legacyDisabled) expect(response.json().error.details).toEqual({ featureFlag: "attendance_v1_disabled", reason: "env_hard_off", reasonCode: "attendance.feature_disabled" });
else expect(response.json().error.details.fieldErrors[field]).toContain("invalid_calendar_date");
expect(prisma.$transaction).not.toHaveBeenCalled();
expect(businessCalls).toEqual([]);
}
} finally { await app.close(); }
});
it("budget stores exact leap-day/month-end dates and retains reverse-range refusal", async () => {
const { app, prisma, businessCalls } = await buildApp("budgets", { budgetSuccess: true });
try {
const response = await app.inject({ method: "POST", url: "/api/budgets", payload: budgetBody });
expect(response.statusCode).toBe(200);
expect(response.json().data).toMatchObject({ periodStart: "2024-02-29", periodEnd: "2026-04-30", allocatedAmount: "100.00" });
expect(prisma.budgetPool.create.mock.calls[0][0].data).toMatchObject({ period_start: new Date("2024-02-29T00:00:00Z"), period_end: new Date("2026-04-30T00:00:00Z") });
expect(prisma.auditLog.create).toHaveBeenCalledTimes(1);
const reversed = await app.inject({ method: "POST", url: "/api/budgets", payload: { ...budgetBody, periodStart: "2026-05-01" } });
expect(reversed.statusCode).toBe(400);
expect(reversed.json().error.details).toEqual({ periodEnd: "must_be_after_periodStart", reasonCode: "budget.period_invalid" });
expect(prisma.budgetPool.create).toHaveBeenCalledTimes(1);
expect(prisma.auditLog.create).toHaveBeenCalledTimes(1);
expect(businessCalls).toEqual([]);
} finally { await app.close(); }
});
it("budget retains permission refusal without writes", async () => {
const { app, businessCalls } = await buildApp("budgets", { allowed: false });
try {
expect((await app.inject({ method: "POST", url: "/api/budgets", payload: budgetBody })).statusCode).toBe(403);
expect(businessCalls).toEqual([]);
} finally { await app.close(); }
});
});