汇总已落盘的借款本金与还款、兼容历史编号的周期化编号、付款责任与回单核验、报销进度和排序、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项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
22 行
2.8 KiB
JavaScript
22 行
2.8 KiB
JavaScript
const Fastify = require("fastify");
|
|
const authPlugin = require("../../../src/plugins/auth");
|
|
const { AppError } = require("../../../src/utils/AppError");
|
|
const apps = [];
|
|
afterEach(async () => { await Promise.all(apps.splice(0).map((app) => app.close())); });
|
|
async function build() {
|
|
const app = Fastify(); apps.push(app);
|
|
const employee = { id: "synthetic-user", status: "active", token_version: 2, roles: [{ role_code: "employee" }] };
|
|
const prisma = { employee: { findUnique: vi.fn(async () => employee) } };
|
|
app.decorate("config", { jwtSecret: "synthetic-local-auth-contract-key", accessTokenTtl: "5m" }); app.decorate("prisma", prisma);
|
|
await app.register(authPlugin);
|
|
app.setErrorHandler((e, _, reply) => reply.status(e instanceof AppError ? e.statusCode : 500).send({ error: { code: e.code, details: e.details } }));
|
|
app.get("/synthetic-unsupported", { preHandler: app.requireEntityPermission("synthetic-unknown", "submit") }, async () => ({ ok: true }));
|
|
return { app, prisma, employee };
|
|
}
|
|
describe("auth plugin entity action contract with synthetic JWT only", () => {
|
|
it("keeps invalid/absent token rejection before entity lookup", async () => { const { app, prisma } = await build(); const r = await app.inject({ url: "/synthetic-unsupported" }); expect(r.statusCode).toBe(401); expect(r.json().error.code).toBe("AUTH_004"); expect(prisma.employee.findUnique).not.toHaveBeenCalled(); });
|
|
it("explains unsupported entity/action after authentication", async () => { const { app, employee } = await build(); const token = app.issueAccessToken(employee); const r = await app.inject({ url: "/synthetic-unsupported", headers: { authorization: `Bearer ${token}` } }); expect(r.statusCode).toBe(409); expect(r.json().error).toEqual({ code: "VALID_001", details: { entityType: "synthetic-unknown", action: "submit", permission: "unsupported_entity_action", reasonCode: "auth.entity_action_unsupported" } }); });
|
|
it("preserves valid entity mapping and missing permission 403", async () => { const { app } = await build(); expect(app.assertEntityPermission({ permissionCodes: ["procurement.submit"] }, "purchase_request", "submit")).toBe("procurement.submit"); expect(() => app.assertEntityPermission({ permissionCodes: [] }, "purchase_request", "submit")).toThrow(expect.objectContaining({ code: "PERM_001", statusCode: 403 })); });
|
|
it.each(["inactive", "stale"])("keeps %s account/token blocked before entity action", async (kind) => { const { app, employee } = await build(); const token = app.issueAccessToken(employee); if (kind === "inactive") employee.status = "archived"; else employee.token_version += 1; const r = await app.inject({ url: "/synthetic-unsupported", headers: { authorization: `Bearer ${token}` } }); expect(r.statusCode).toBe(kind === "inactive" ? 403 : 401); expect(r.json().error.code).toBe(kind === "inactive" ? "AUTH_003" : "AUTH_004"); });
|
|
});
|