const Fastify = require("fastify"); const usersRoutes = require("../../../src/routes/users"); const { AppError } = require("../../../src/utils/AppError"); const ACTOR_ID = "11111111-1111-4111-8111-111111111111"; const DEPARTMENT_ID = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"; const USER_ID = "22222222-2222-4222-8222-222222222222"; async function buildApp({ createdEmployeeNo = "HYD000001", currentEmployee = null, } = {}) { const app = Fastify(); const departmentFindUnique = vi.fn().mockResolvedValue({ id: DEPARTMENT_ID, archived_at: null, }); const employeeCreate = vi.fn(async ({ data }) => ({ id: USER_ID, employee_no: data.employee_no ?? createdEmployeeNo, name: data.name, phone: data.phone, email: data.email, status: data.status, department_id: data.department_id, title: data.title, direct_manager_id: data.direct_manager_id ?? null, hire_date: data.hire_date, created_at: data.created_at, updated_at: data.updated_at, })); const employeeFindUnique = vi .fn() .mockResolvedValue( currentEmployee ?? { id: USER_ID, employee_no: "HYD000321", name: "Alice", phone: "13800000000", email: "alice@example.com", status: "active", department_id: DEPARTMENT_ID, title: "员工", direct_manager_id: null, hire_date: new Date("2026-05-01T00:00:00.000Z"), bank_account_encrypted: "", bank_name: "", id_number_encrypted: null, archived_at: null, created_at: new Date("2026-05-01T00:00:00.000Z"), updated_at: new Date("2026-05-01T00:00:00.000Z"), roles: [{ role_code: "employee" }], }, ); const employeeUpdate = vi.fn(); const employeeRoleCreateMany = vi.fn().mockResolvedValue({ count: 1 }); const employeeRoleFindMany = vi.fn().mockResolvedValue([{ role_code: "employee" }]); const auditCreate = vi.fn().mockResolvedValue({ id: "audit-1" }); const tx = { department: { findUnique: departmentFindUnique, }, employee: { create: employeeCreate, findUnique: employeeFindUnique, update: employeeUpdate, }, employeeRole: { createMany: employeeRoleCreateMany, }, auditLog: { create: auditCreate, }, authSession: { updateMany: vi.fn().mockResolvedValue({ count: 0 }), }, }; app.decorate("config", {}); app.decorate("prisma", { department: { findUnique: departmentFindUnique, }, employee: { findUnique: employeeFindUnique, }, employeeRole: { findMany: employeeRoleFindMany, }, $transaction: vi.fn(async (callback) => callback(tx)), }); app.decorate("requirePermission", (permissionCode) => async (request) => { request.employee = { id: ACTOR_ID, department_id: DEPARTMENT_ID, permissionCodes: ["system.users"], }; const permissions = request.employee.permissionCodes ?? []; if (!permissions.includes("*") && !permissions.includes(permissionCode)) { throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403 }); } }); app.decorate("authenticate", async (request) => { request.employee = { id: ACTOR_ID, department_id: DEPARTMENT_ID, permissionCodes: ["system.users"], }; }); app.decorate("hasPermission", () => true); app.setErrorHandler((error, request, reply) => { if (error instanceof AppError) { return reply.status(error.statusCode).send({ requestId: request.id, error: { code: error.code, message: error.message, details: error.details ?? {} }, }); } return reply.status(500).send({ requestId: request.id, error: { code: error.code ?? "SYS_001", message: error.message ?? "服务器内部错误", details: error.details ?? {} }, }); }); await app.register(usersRoutes, { prefix: "/api" }); return { app, departmentFindUnique, employeeCreate, employeeFindUnique, employeeUpdate, employeeRoleCreateMany, employeeRoleFindMany, }; } describe("usersRoutes write flows", () => { it("creates a user without employeeNo or password and returns generated credentials", async () => { const { app, employeeCreate, employeeRoleCreateMany } = await buildApp(); const response = await app.inject({ method: "POST", url: "/api/users", payload: { name: "张三", phone: "13800000001", email: "zhangsan@example.com", roles: ["employee"], }, }); expect(response.statusCode).toBe(200); expect(employeeCreate).toHaveBeenCalledWith( expect.objectContaining({ data: expect.not.objectContaining({ employee_no: expect.anything(), }), }), ); expect(employeeRoleCreateMany).toHaveBeenCalledWith( expect.objectContaining({ data: [ expect.objectContaining({ employee_id: USER_ID, role_code: "employee", }), ], }), ); const payload = response.json().data; expect(payload).toEqual( expect.objectContaining({ id: USER_ID, employeeNo: "HYD000001", name: "张三", email: "zhangsan@example.com", roleCodes: ["employee"], }), ); expect(payload.oneTimePassword).toMatch(/^ResetA1[a-f0-9]{12}$/); await app.close(); }); it("rejects employeeNo changes on patch", async () => { const { app, employeeFindUnique, employeeUpdate } = await buildApp(); const response = await app.inject({ method: "PATCH", url: `/api/users/${USER_ID}`, payload: { employeeNo: "HYD999999", name: "李四", }, }); expect(response.statusCode).toBe(409); expect(response.json().error).toEqual( expect.objectContaining({ code: "VALID_001", details: { employeeNo: "immutable" }, }), ); expect(employeeFindUnique).not.toHaveBeenCalled(); expect(employeeUpdate).not.toHaveBeenCalled(); await app.close(); }); });