const Fastify = require("fastify"); const featureFlagsRoutes = require("../../../src/routes/featureFlags"); const { AppError } = require("../../../src/utils/AppError"); const ACTOR_ID = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"; function buildAuditLog(overrides = {}) { return { id: "11111111-1111-4111-8111-111111111111", actor_id: ACTOR_ID, created_at: new Date("2026-04-07T10:00:00.000Z"), extra: { flagKey: "payment_batch_v1", from: { overrideMode: "force_off", rolloutType: "global", departmentIds: [], note: "old note", version: 1, }, to: { overrideMode: "force_on", rolloutType: "global", departmentIds: [], note: "new note", version: 2, }, }, actor: { id: ACTOR_ID, name: "系统管理员", }, ...overrides, }; } async function buildApp(options = {}) { const app = Fastify(); const actor = options.actor ?? { id: ACTOR_ID, permissionCodes: ["system.settings"], }; const findMany = options.findMany ?? vi.fn().mockResolvedValue([buildAuditLog()]); const count = options.count ?? vi.fn().mockResolvedValue(1); 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("config", {}); app.decorate("prisma", { auditLog: { findMany, count, create: vi.fn(), }, featureFlag: { findUnique: vi.fn(), }, $transaction: vi.fn(), }); 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(featureFlagsRoutes, { prefix: "/api" }); return { app, findMany, count }; } describe("feature flag changelog route", () => { it("lists paginated changelog records for a specific flag", async () => { const { app, findMany, count } = await buildApp(); const response = await app.inject({ method: "GET", url: "/api/system/feature-flags/payment_batch_v1/changelog?page=2&pageSize=10", }); expect(response.statusCode).toBe(200); expect(findMany).toHaveBeenCalledWith( expect.objectContaining({ where: { action: "FEATURE_FLAG_UPDATED", target_type: "feature_flag", extra: { path: ["flagKey"], equals: "payment_batch_v1", }, }, orderBy: { created_at: "desc" }, skip: 10, take: 10, }), ); expect(count).toHaveBeenCalledWith({ where: { action: "FEATURE_FLAG_UPDATED", target_type: "feature_flag", extra: { path: ["flagKey"], equals: "payment_batch_v1", }, }, }); const payload = response.json(); expect(payload.data.page).toBe(2); expect(payload.data.pageSize).toBe(10); expect(payload.data.total).toBe(1); expect(payload.data.items[0]).toMatchObject({ actorId: ACTOR_ID, actorName: "系统管理员", from: { overrideMode: "force_off", }, to: { overrideMode: "force_on", }, }); await app.close(); }); it("returns an empty list when no changelog records exist", async () => { const { app } = await buildApp({ findMany: vi.fn().mockResolvedValue([]), count: vi.fn().mockResolvedValue(0), }); const response = await app.inject({ method: "GET", url: "/api/system/feature-flags/cutover_console_v1/changelog", }); expect(response.statusCode).toBe(200); expect(response.json().data).toEqual({ page: 1, pageSize: 20, total: 0, items: [], }); await app.close(); }); it("rejects requests without system.settings permission", async () => { const { app, findMany, count } = await buildApp({ actor: { id: ACTOR_ID, permissionCodes: [], }, }); const response = await app.inject({ method: "GET", url: "/api/system/feature-flags/payment_batch_v1/changelog", }); expect(response.statusCode).toBe(403); expect(findMany).not.toHaveBeenCalled(); expect(count).not.toHaveBeenCalled(); await app.close(); }); });