const Fastify = require("fastify"); const employeesRoutes = require("../../../src/routes/employees"); const { AppError } = require("../../../src/utils/AppError"); const EMPLOYEE_ID = "11111111-1111-4111-8111-111111111111"; const DEPARTMENT_ID = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"; function buildFeatureFlagRecord() { return { id: "flag-1", flag_key: "employee_lifecycle_v1", override_mode: "force_on", rollout_type: "global", department_ids: [], note: "", version: 1, created_at: new Date("2026-03-28T00:00:00.000Z"), created_by: null, updated_at: new Date("2026-03-28T00:00:00.000Z"), updated_by: null, }; } function buildTrainingRow(overrides = {}) { return { id: "33333333-3333-4333-8333-333333333333", employee_id: EMPLOYEE_ID, name: "入职培训", training_type: "internal", start_date: new Date("2026-03-01T00:00:00.000Z"), end_date: new Date("2026-03-02T00:00:00.000Z"), status: "planned", notes: "首次培训", created_at: new Date("2026-03-01T00:00:00.000Z"), created_by: "actor-1", updated_at: new Date("2026-03-01T00:00:00.000Z"), updated_by: "actor-1", ...overrides, }; } async function buildApp({ actor = { id: "actor-1", department_id: DEPARTMENT_ID, permissionCodes: ["system.users"] } } = {}) { const app = Fastify(); const state = { training: null, }; const trainingCreate = vi.fn(async ({ data }) => { state.training = buildTrainingRow({ name: data.name, training_type: data.training_type, start_date: data.start_date, end_date: data.end_date, status: data.status, notes: data.notes, created_by: data.created_by, updated_by: data.updated_by, }); return state.training; }); const trainingUpdate = vi.fn(async ({ data }) => { state.training = { ...state.training, ...(data.name !== undefined ? { name: data.name } : {}), ...(data.training_type !== undefined ? { training_type: data.training_type } : {}), ...(data.start_date !== undefined ? { start_date: data.start_date } : {}), ...(data.end_date !== undefined ? { end_date: data.end_date } : {}), ...(data.status !== undefined ? { status: data.status } : {}), ...(data.notes !== undefined ? { notes: data.notes } : {}), ...(data.updated_by !== undefined ? { updated_by: data.updated_by } : {}), }; return state.training; }); app.decorate("config", { appTimezone: "Asia/Shanghai", employeeLifecycleEnabled: true, storageDir: "/tmp", }); app.decorate("authenticate", async (request) => { request.employee = actor; }); app.decorate("requirePermission", (permissionCode) => async (request) => { request.employee = actor; const permissionCodes = actor.permissionCodes ?? []; if (!permissionCodes.includes("*") && !permissionCodes.includes(permissionCode)) { throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403 }); } }); app.decorate("hasPermission", (employeeRecord, permissionCode) => { const permissionCodes = employeeRecord.permissionCodes ?? []; return permissionCodes.includes("*") || permissionCodes.includes(permissionCode); }); app.decorate("prisma", { featureFlag: { findUnique: vi.fn().mockResolvedValue(buildFeatureFlagRecord()), }, employee: { findUnique: vi.fn(async ({ where }) => (where.id === EMPLOYEE_ID ? { id: EMPLOYEE_ID } : null)), findMany: vi.fn().mockResolvedValue([]), count: vi.fn().mockResolvedValue(0), update: vi.fn(), }, employeeContract: { findFirst: vi.fn().mockResolvedValue(null), findMany: vi.fn().mockResolvedValue([]), count: vi.fn().mockResolvedValue(0), update: vi.fn(), delete: vi.fn(), create: vi.fn(), }, employeeTraining: { findMany: vi.fn(async () => (state.training ? [state.training] : [])), findFirst: vi.fn(async ({ where }) => (state.training?.id === where.id && state.training?.employee_id === where.employee_id ? state.training : null)), create: trainingCreate, update: trainingUpdate, }, employeeCertificate: { findMany: vi.fn().mockResolvedValue([]), findFirst: vi.fn().mockResolvedValue(null), create: vi.fn(), update: vi.fn(), groupBy: vi.fn().mockResolvedValue([]), count: vi.fn().mockResolvedValue(0), }, employeeChecklistTask: { findMany: vi.fn().mockResolvedValue([]), count: vi.fn().mockResolvedValue(0), }, employeeLifecycleEvent: { findMany: vi.fn().mockResolvedValue([]), }, assetCard: { findMany: vi.fn().mockResolvedValue([]), count: vi.fn().mockResolvedValue(0), }, workflowTask: { count: vi.fn().mockResolvedValue(0), }, auditLog: { create: vi.fn().mockResolvedValue({}), }, }); app.setErrorHandler((error, request, reply) => { reply.status(error.statusCode || 500).send({ requestId: request.id, error: { code: error.code || "SYS_001", message: error.message, details: error.details ?? null, }, }); }); await app.register(employeesRoutes, { prefix: "/api" }); return { app, state, trainingCreate, trainingUpdate }; } describe("employee training routes", () => { it("allows system.users to create, update and list employee trainings", async () => { const { app, state, trainingCreate, trainingUpdate } = await buildApp(); const createResponse = await app.inject({ method: "POST", url: `/api/employees/${EMPLOYEE_ID}/trainings`, payload: { name: "安全培训", trainingType: "external", startDate: "2026-03-10", endDate: "2026-03-12", status: "planned", notes: "第一阶段", }, }); expect(createResponse.statusCode).toBe(200); expect(createResponse.json().data).toMatchObject({ name: "安全培训", trainingType: "external", status: "planned", notes: "第一阶段", }); expect(trainingCreate).toHaveBeenCalledTimes(1); const patchResponse = await app.inject({ method: "PATCH", url: `/api/employees/${EMPLOYEE_ID}/trainings/${state.training.id}`, payload: { status: "completed", endDate: "2026-03-15", }, }); expect(patchResponse.statusCode).toBe(200); expect(patchResponse.json().data).toMatchObject({ status: "completed", endDate: "2026-03-15", }); expect(trainingUpdate).toHaveBeenCalledTimes(1); const listResponse = await app.inject({ method: "GET", url: `/api/employees/${EMPLOYEE_ID}/trainings`, }); expect(listResponse.statusCode).toBe(200); expect(listResponse.json().data.items).toEqual([ expect.objectContaining({ id: state.training.id, name: "安全培训", status: "completed", }), ]); await app.close(); }); it("rejects non-admin users managing trainings", async () => { const { app } = await buildApp({ actor: { id: "actor-2", department_id: DEPARTMENT_ID, permissionCodes: ["employee"] }, }); const response = await app.inject({ method: "GET", url: `/api/employees/${EMPLOYEE_ID}/trainings`, }); expect(response.statusCode).toBe(403); await app.close(); }); });