const Fastify = require("fastify"); const performanceRoutes = require("../../../src/routes/performance"); const { AppError } = require("../../../src/utils/AppError"); const ADMIN_ID = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"; const MANAGER_ID = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"; const EMPLOYEE_ID = "cccccccc-cccc-4ccc-8ccc-cccccccccccc"; const OTHER_EMPLOYEE_ID = "dddddddd-dddd-4ddd-8ddd-dddddddddddd"; const DEPARTMENT_ID = "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee"; const ACTIVE_CYCLE_ID = "11111111-1111-4111-8111-111111111111"; const COMPLETED_CYCLE_ID = "22222222-2222-4222-8222-222222222222"; 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 buildCycleRow(overrides = {}) { return { id: ACTIVE_CYCLE_ID, name: "2026 Q2", cycle_type: "quarterly", start_date: new Date("2026-04-01T00:00:00.000Z"), end_date: new Date("2026-06-30T00:00:00.000Z"), status: "active", activated_at: new Date("2026-04-01T00:00:00.000Z"), completed_at: null, created_at: new Date("2026-03-29T08:00:00.000Z"), created_by: ADMIN_ID, updated_at: new Date("2026-03-29T08:00:00.000Z"), updated_by: ADMIN_ID, ...overrides, }; } function buildEmployeeRow(overrides = {}) { return { id: EMPLOYEE_ID, employee_no: "E001", name: "Alice", title: "专员", status: "active", direct_manager_id: MANAGER_ID, avatar_path: null, department: { id: DEPARTMENT_ID, name: "行政部" }, ...overrides, }; } function buildReviewRow(overrides = {}) { return { id: "33333333-3333-4333-8333-333333333333", cycle_id: COMPLETED_CYCLE_ID, employee_id: EMPLOYEE_ID, reviewer_employee_id: MANAGER_ID, overall_score: 4, comment: "完成良好", created_at: new Date("2026-03-28T08:00:00.000Z"), created_by: MANAGER_ID, updated_at: new Date("2026-03-28T08:00:00.000Z"), updated_by: MANAGER_ID, ...overrides, }; } async function buildApp({ actor = { id: MANAGER_ID, department_id: DEPARTMENT_ID, permissionCodes: ["employee"] }, } = {}) { const app = Fastify(); const state = { cycles: [ buildCycleRow(), buildCycleRow({ id: COMPLETED_CYCLE_ID, name: "2026 Q1", start_date: new Date("2026-01-01T00:00:00.000Z"), end_date: new Date("2026-03-31T00:00:00.000Z"), status: "completed", activated_at: new Date("2026-01-01T00:00:00.000Z"), completed_at: new Date("2026-03-31T10:00:00.000Z"), }), ], employees: [ buildEmployeeRow(), buildEmployeeRow({ id: MANAGER_ID, employee_no: "M001", name: "Bob", title: "经理", direct_manager_id: null, }), buildEmployeeRow({ id: OTHER_EMPLOYEE_ID, employee_no: "E002", name: "Carol", direct_manager_id: ADMIN_ID, }), ], reviews: [buildReviewRow()], dimensions: [ { id: "dim-1", review_id: "33333333-3333-4333-8333-333333333333", dimension_type: "work_quality", score: 4, }, ], }; function hydrateReview(review) { const employee = state.employees.find((item) => item.id === review.employee_id); const reviewer = state.employees.find((item) => item.id === review.reviewer_employee_id); return { ...review, employee, reviewer_employee: reviewer ? { id: reviewer.id, employee_no: reviewer.employee_no, name: reviewer.name, } : null, dimensions: state.dimensions.filter((item) => item.review_id === review.id), }; } const performanceReview = { findMany: vi.fn(async ({ where }) => { let rows = [...state.reviews]; if (typeof where?.cycle_id === "string") { rows = rows.filter((item) => item.cycle_id === where.cycle_id); } if (where?.cycle_id?.in) { rows = rows.filter((item) => where.cycle_id.in.includes(item.cycle_id)); } if (where?.employee_id) { rows = rows.filter((item) => item.employee_id === where.employee_id); } return rows.map(hydrateReview); }), findFirst: vi.fn(async ({ where }) => { const review = state.reviews.find((item) => item.cycle_id === where.cycle_id && item.employee_id === where.employee_id); return review ? hydrateReview(review) : null; }), findUnique: vi.fn(async ({ where }) => { const review = state.reviews.find((item) => item.id === where.id); return review ? hydrateReview(review) : null; }), create: vi.fn(async ({ data }) => { const review = buildReviewRow({ id: "44444444-4444-4444-8444-444444444444", cycle_id: data.cycle_id, employee_id: data.employee_id, reviewer_employee_id: data.reviewer_employee_id, overall_score: data.overall_score, comment: data.comment, created_by: data.created_by, updated_by: data.updated_by, }); state.reviews.push(review); return review; }), update: vi.fn(async ({ where, data }) => { const index = state.reviews.findIndex((item) => item.id === where.id); state.reviews[index] = { ...state.reviews[index], ...data, }; return state.reviews[index]; }), }; const performanceReviewDimension = { deleteMany: vi.fn(async ({ where }) => { state.dimensions = state.dimensions.filter((item) => item.review_id !== where.review_id); return { count: 1 }; }), createMany: vi.fn(async ({ data }) => { state.dimensions.push( ...data.map((item, index) => ({ id: `dim-${state.dimensions.length + index + 1}`, review_id: item.review_id, dimension_type: item.dimension_type, score: item.score, })), ); return { count: data.length }; }), }; app.decorate("config", { appTimezone: "Asia/Shanghai", employeeLifecycleEnabled: true }); app.decorate("authenticate", async (request) => { request.employee = actor; }); app.decorate("requirePermission", (permissionCode) => async (request) => { request.employee = actor; const codes = actor.permissionCodes ?? []; if (!codes.includes("*") && !codes.includes(permissionCode)) { throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403 }); } }); app.decorate("hasPermission", (employee, permissionCode) => { const codes = employee.permissionCodes ?? []; return codes.includes("*") || codes.includes(permissionCode); }); app.decorate("prisma", { featureFlag: { findUnique: vi.fn().mockResolvedValue(buildFeatureFlagRecord()), }, employee: { findUnique: vi.fn(async ({ where }) => state.employees.find((item) => item.id === where.id) ?? null), findMany: vi.fn(async ({ where }) => { if (where?.status === "active") { return state.employees.filter((item) => item.status === "active"); } return [...state.employees]; }), }, performanceReviewCycle: { findMany: vi.fn(async ({ where }) => state.cycles.filter((item) => { if (!where) { return true; } if (where?.status?.in) { return where.status.in.includes(item.status); } return item.status === where?.status; }), ), findUnique: vi.fn(async ({ where }) => state.cycles.find((item) => item.id === where.id) ?? null), findFirst: vi.fn(async ({ where }) => state.cycles.find((item) => { if (where?.status && item.status !== where.status) { return false; } if (where?.id?.not && item.id === where.id.not) { return false; } return true; }) ?? null, ), }, performanceReview, performanceReviewDimension, auditLog: { create: vi.fn().mockResolvedValue({}), }, $transaction: vi.fn(async (callback) => callback(app.prisma)), }); 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(performanceRoutes, { prefix: "/api" }); return { app, state, performanceReview, performanceReviewDimension }; } describe("performance review routes", () => { it("rejects direct managers while performance management is frozen before launch", async () => { const { app, state, performanceReviewDimension } = await buildApp(); const response = await app.inject({ method: "PUT", url: `/api/performance/cycles/${ACTIVE_CYCLE_ID}/reviews/${EMPLOYEE_ID}`, payload: { overallScore: 4.5, comment: "表现稳定", dimensions: [ { dimensionType: "work_quality", score: 4.5 }, { dimensionType: "teamwork", score: 4 }, ], }, }); expect(response.statusCode).toBe(403); expect(state.reviews.find((item) => item.cycle_id === ACTIVE_CYCLE_ID && item.employee_id === EMPLOYEE_ID)).toBeFalsy(); expect(performanceReviewDimension.createMany).not.toHaveBeenCalled(); await app.close(); }); it("rejects self viewers while performance management is frozen before launch", async () => { const { app } = await buildApp({ actor: { id: EMPLOYEE_ID, department_id: DEPARTMENT_ID, permissionCodes: ["employee"], }, }); const response = await app.inject({ method: "GET", url: `/api/performance/employees/${EMPLOYEE_ID}/reviews`, }); expect(response.statusCode).toBe(403); await app.close(); }); it("rejects non-manager non-admin access to other employees", async () => { const { app } = await buildApp({ actor: { id: OTHER_EMPLOYEE_ID, department_id: DEPARTMENT_ID, permissionCodes: ["employee"], }, }); const response = await app.inject({ method: "PUT", url: `/api/performance/cycles/${ACTIVE_CYCLE_ID}/reviews/${EMPLOYEE_ID}`, payload: { overallScore: 4, comment: "无权限", }, }); expect(response.statusCode).toBe(403); await app.close(); }); it("blocks updates on completed cycles and lets admins inspect cycle overviews", async () => { const { app } = await buildApp({ actor: { id: ADMIN_ID, department_id: DEPARTMENT_ID, permissionCodes: ["system.settings"], }, }); const overviewResponse = await app.inject({ method: "GET", url: `/api/performance/cycles/${ACTIVE_CYCLE_ID}/reviews`, }); expect(overviewResponse.statusCode).toBe(200); expect(overviewResponse.json().data.items).toEqual( expect.arrayContaining([ expect.objectContaining({ employee: expect.objectContaining({ id: EMPLOYEE_ID }), }), ]), ); const completedResponse = await app.inject({ method: "PUT", url: `/api/performance/cycles/${COMPLETED_CYCLE_ID}/reviews/${EMPLOYEE_ID}`, payload: { overallScore: 4.5, comment: "不允许修改", }, }); expect(completedResponse.statusCode).toBe(409); expect(completedResponse.json().error.details).toMatchObject({ cycle: "active_cycle_required", }); await app.close(); }); });