const Fastify = require("fastify"); const { AppError } = require("../../../src/utils/AppError"); const { resolveDocumentSequenceConfig } = require("../../../src/services/documentSequence"); const { ensureCutoverAllowed } = require("../../../src/services/cutover"); const { resolveEntityPrecheck, buildEntityPrecheck } = require("../../../src/services/feeControl"); const routeNames = ["configRead", "systemWorkspace", "confidentialDocRefs", "cutover", "migrationRuns", "featureFlags", "feeControl", "riskFlags"]; const ID = "11111111-1111-4111-8111-111111111111"; const FLAG = { id: ID, flag_key: "payment_batch_v1", override_mode: "force_on", rollout_type: "global", department_ids: [], version: 2, created_at: new Date("2026-01-01"), updated_at: new Date("2026-01-01") }; const FLAG_BODY = { overrideMode: "force_on", rolloutType: "global", departmentIds: [], expectedVersion: 2 }; const apps = []; afterEach(async () => { await Promise.all(apps.splice(0).map((app) => app.close())); }); async function build({ allowed = true, enabled = true } = {}) { const app = Fastify(); apps.push(app); const prisma = {}; for (const name of ["approvalRule", "documentSequence", "documentCategory", "workspaceSetting", "confidentialDocumentRef", "cutoverScope", "migrationRun", "entityRiskFlag", "featureFlag", "auditLog", "purchaseRequest"]) prisma[name] = { findUnique: vi.fn().mockResolvedValue(null), findFirst: vi.fn().mockResolvedValue(null), findMany: vi.fn().mockResolvedValue([]), count: vi.fn().mockResolvedValue(0), create: vi.fn(async ({ data }) => ({ id: ID, ...data })), update: vi.fn(async ({ data }) => ({ id: ID, ...data })), updateMany: vi.fn().mockResolvedValue({ count: 1 }), upsert: vi.fn() }; prisma.featureFlag.findUnique.mockImplementation(async ({ where }) => ({ ...FLAG, flag_key: where.flag_key, override_mode: enabled ? "force_on" : "force_off" })); prisma.$transaction = vi.fn(async (fn) => fn(prisma)); app.decorate("prisma", prisma); app.decorate("config", { appTimezone: "Asia/Shanghai", confidentialDocRefsEnabled: true, cutoverConsoleEnabled: true, paymentBatchEnabled: true }); const auth = async (request) => { request.employee = { id: ID, permissionCodes: allowed ? ["*"] : [] }; }; app.decorate("authenticate", auth); app.decorate("hasPermission", () => allowed); app.decorate("requirePermission", () => async (request) => { await auth(request); if (!allowed) throw new AppError({ code: "PERM_001", statusCode: 403 }); }); app.setErrorHandler((e, request, reply) => reply.status(e.statusCode || 500).send({ requestId: request.id, error: { code: e.code || "SYS_001", message: e.message, details: e.details ?? {} } })); await app.register(require("@fastify/multipart")); for (const name of routeNames) await app.register(require(`../../../src/routes/${name}`), { prefix: "/api" }); return { app, prisma }; } function noWrites(p) { for (const model of Object.values(p)) for (const op of ["create", "update", "upsert", "updateMany"]) if (model[op]) expect(model[op]).not.toHaveBeenCalled(); } function rejected(r, reason, status = 400, details = {}) { expect(r.statusCode, r.body).toBe(status); expect(r.json().error).toMatchObject({ code: "VALID_001", details: { reasonCode: reason, ...details } }); } const inject = (app, method, path, payload) => app.inject({ method, url: `/api${path}`, payload }); const cases = [ ["GET", "/approval-rules?enabled=bad", undefined, "config.enabled_query_invalid"], ["GET", "/document-sequences/bad-type", undefined, "config.document_type_invalid"], ["PATCH", "/system/workspace", {}, "workspace.update_input_invalid"], ["PATCH", "/system/workspace", { moduleVisibility: { portal: "bad" } }, "workspace.update_input_invalid"], ["POST", "/confidential-doc-refs", {}, "confidential_ref.create_input_invalid"], ["PATCH", `/confidential-doc-refs/${ID}`, {}, "confidential_ref.update_input_invalid"], ["GET", "/confidential-doc-refs?projectId=bad", undefined, "confidential_ref.query_invalid"], ["PATCH", "/cutover/scopes/bad", {}, "cutover.id_invalid"], ["POST", "/cutover/scopes", {}, "cutover.create_input_invalid"], ["PATCH", `/cutover/scopes/${ID}`, {}, "cutover.update_input_invalid"], ["GET", "/migration-runs/bad", undefined, "migration_run.id_invalid"], ["GET", "/system/feature-flags/bad/changelog", undefined, "feature_flag.key_invalid"], ["GET", "/system/feature-flags/payment_batch_v1/changelog?page=0", undefined, "feature_flag.page_invalid"], ["PUT", "/system/feature-flags/payment_batch_v1", { ...FLAG_BODY, note: "๐Ÿ˜€".repeat(251) }, "feature_flag.update_input_invalid"], ["PUT", "/system/feature-flags/payment_batch_v1", { ...FLAG_BODY, departmentIds: [ID] }, "feature_flag.global_departments_invalid"], ["POST", "/system/feature-flags/batch", { items: [] }, "feature_flag.batch_input_invalid"], ["POST", "/system/feature-flags/batch", { items: Array(2).fill({ flagKey: "payment_batch_v1", overrideMode: "force_on", expectedVersion: 2 }), note: "ๅˆๆˆ" }, "feature_flag.batch_input_invalid"], ["POST", "/fee-control/precheck", { entityType: "bad", entityId: ID }, "fee_control.input_invalid"], ["GET", "/risk-flags?status=bad", undefined, "risk_flag.query_invalid"], ["POST", "/risk-flags/bad/resolve", {}, "risk_flag.id_invalid"], ["POST", `/risk-flags/${ID}/dismiss`, { note: "x".repeat(501) }, "risk_flag.action_input_invalid"], ]; describe("system management route error contracts", () => { it.each(cases)("explains %s %s without writes", async (method, path, payload, reason) => { const { app, prisma } = await build(); rejected(await inject(app, method, path, payload), reason); noWrites(prisma); }); it.each([["/confidential-doc-refs", "confidential_ref.feature_disabled"], ["/cutover/scopes", "cutover.feature_disabled"], ["/migration-runs", "cutover.feature_disabled"]])("keeps gate %s", async (path, reason) => { const { app, prisma } = await build({ enabled: false }); rejected(await inject(app, "GET", path), reason, 409); noWrites(prisma); }); it.each(["resolve", "dismiss"])("rejects previously handled risk %s", async (action) => { const { app, prisma } = await build(); prisma.entityRiskFlag.findUnique.mockResolvedValue({ id: ID, status: "resolved" }); rejected(await inject(app, "POST", `/risk-flags/${ID}/${action}`, {}), "risk_flag.not_open", 409, { status: "risk_flag_not_open" }); noWrites(prisma); }); it.each(["single", "batch"])("preserves %s optimistic conflict", async (kind) => { const { app, prisma } = await build(); const r = kind === "single" ? await inject(app, "PUT", "/system/feature-flags/payment_batch_v1", { ...FLAG_BODY, expectedVersion: 1 }) : await inject(app, "POST", "/system/feature-flags/batch", { items: [{ flagKey: "payment_batch_v1", overrideMode: "force_on", expectedVersion: 1 }], note: "ๅˆๆˆ" }); rejected(r, "feature_flag.version_conflict", 409, { expectedVersion: 1, currentVersion: 2 }); noWrites(prisma); }); it.each(["single", "batch"])("preserves %s concurrent update conflict after attempt", async (kind) => { const { app, prisma } = await build(); prisma.featureFlag.updateMany.mockResolvedValue({ count: 0 }); const r = kind === "single" ? await inject(app, "PUT", "/system/feature-flags/payment_batch_v1", FLAG_BODY) : await inject(app, "POST", "/system/feature-flags/batch", { items: [{ flagKey: "payment_batch_v1", overrideMode: "force_on", expectedVersion: 2 }], note: "ๅˆๆˆ" }); rejected(r, "feature_flag.version_conflict", 409); expect(prisma.featureFlag.updateMany).toHaveBeenCalledOnce(); expect(prisma.auditLog.create).not.toHaveBeenCalled(); }); it.each([undefined, "true", "1", "false", "0"])("keeps enabled filter %s", async (value) => { const { app, prisma } = await build(); const r = await inject(app, "GET", `/approval-rules${value ? `?enabled=${value}` : ""}`); expect(r.statusCode).toBe(200); expect(prisma.approvalRule.findMany.mock.calls[0][0].where).toEqual(value ? { enabled: ["true", "1"].includes(value) } : {}); noWrites(prisma); }); it("preserves workspace create/defaults and audit", async () => { const { app, prisma } = await build(); expect((await inject(app, "PATCH", "/system/workspace", { moduleVisibility: { portal: "hidden" } })).statusCode).toBe(200); expect(prisma.workspaceSetting.create).toHaveBeenCalledOnce(); expect(prisma.auditLog.create).toHaveBeenCalledOnce(); }); it("keeps cutover create default disabled and partial clear", async () => { const { app, prisma } = await build(); const r = await inject(app, "POST", "/cutover/scopes", { scopeType: "domain", scopeKey: "reimbursement" }); expect(r.statusCode).toBe(201); expect(prisma.cutoverScope.create.mock.calls[0][0].data).toMatchObject({ enabled: false, starts_at: null }); expect((await inject(app, "PATCH", `/cutover/scopes/${ID}`, { startsAt: null, notes: null })).statusCode).toBe(200); expect(prisma.auditLog.create).not.toHaveBeenCalled(); }); it.each([["PATCH", "/system/workspace"], ["POST", "/confidential-doc-refs"], ["POST", "/cutover/scopes"], ["POST", "/system/feature-flags/batch"], ["GET", "/risk-flags"]])("preserves permission %s %s", async (method, path) => { const { app, prisma } = await build({ allowed: false }); expect((await inject(app, method, path, method === "GET" ? undefined : {})).statusCode).toBe(403); noWrites(prisma); }); }); function multipart(content) { return Buffer.from(`--synthetic\r\nContent-Disposition: form-data; name="file"; filename="synthetic.csv"\r\nContent-Type: text/csv\r\n\r\n${content}\r\n--synthetic--\r\n`); } describe("synthetic migration precheck only", () => { it.each([["--synthetic--\r\n", "employees", "migration_run.file_required"], [multipart("name\na"), "bad", "migration_run.type_invalid"]])("rejects missing part or type", async (payload, type, reason) => { const { app, prisma } = await build(); const r = await app.inject({ method: "POST", url: `/api/migration-runs/dry-run?runType=${type}`, headers: { "content-type": "multipart/form-data; boundary=synthetic" }, payload }); rejected(r, reason); noWrites(prisma); }); it.each([["name\nๅˆๆˆ้ƒจ้—จ", "dry_run_ok"], ["other\nๅˆๆˆ", "dry_run_failed"], ["name", "dry_run_failed"]])("retains CSV outcome %s", async (content, status) => { const { app, prisma } = await build(); const r = await app.inject({ method: "POST", url: "/api/migration-runs/dry-run?runType=departments", headers: { "content-type": "multipart/form-data; boundary=synthetic" }, payload: multipart(content) }); expect(r.statusCode).toBe(201); expect(r.json().data.item.status).toBe(status); expect(prisma.migrationRun.create).toHaveBeenCalledOnce(); expect(prisma.auditLog.create).not.toHaveBeenCalled(); }); }); describe("shared sequence, cutover and precheck guards", () => { it("rejects unsupported sequence type before sequence access", async () => { const { app, prisma } = await build(); rejected(await inject(app, "GET", "/document-sequences/unknown"), "document_sequence.type_unsupported", 409); expect(prisma.documentSequence.findUnique).not.toHaveBeenCalled(); noWrites(prisma); }); it.each([[null, "category_missing"], [{ entity_type: "leave", enabled: true }, "category_type_mismatch"], [{ entity_type: "purchase_request", enabled: false }, "category_unavailable"], [{ entity_type: "purchase_request", enabled: true, archived_at: new Date("2026-01-01") }, "category_unavailable"]])("rejects specified category %s", async (category, reason) => { const { prisma } = await build(); prisma.documentCategory.findUnique.mockResolvedValue(category); await expect(resolveDocumentSequenceConfig(prisma, { documentType: "purchase_request", documentCategoryCode: "SYNTHETIC" })).rejects.toMatchObject({ statusCode: 409, details: { reasonCode: `document_sequence.${reason}` } }); noWrites(prisma); }); it.each(["domain", "department", "legal_entity"])("keeps allowed cutover match %s", async (type) => { const { prisma, app } = await build(); prisma.cutoverScope.findMany.mockResolvedValue([{ id: ID, scope_type: type, scope_key: type === "domain" ? "reimbursement" : ID }]); const r = await ensureCutoverAllowed({ prisma, config: app.config, request: { employee: { id: ID } }, domain: "reimbursement", departmentId: ID, legalEntityId: ID }); expect(r).toMatchObject({ checked: true, allowed: true, matchedScope: { scopeType: type } }); noWrites(prisma); }); it("rejects missing active scope and leaves disabled gate permissive", async () => { const { prisma, app } = await build(); const input = { prisma, config: app.config, request: { employee: { id: ID } }, domain: "reimbursement" }; await expect(ensureCutoverAllowed(input)).rejects.toMatchObject({ statusCode: 409, details: { reasonCode: "cutover.scope_not_enabled", cutover: "not_enabled" } }); app.config.cutoverConsoleEnabled = false; prisma.cutoverScope.findMany.mockClear(); expect(await ensureCutoverAllowed(input)).toMatchObject({ checked: false, allowed: true }); expect(prisma.cutoverScope.findMany).not.toHaveBeenCalled(); noWrites(prisma); }); it("rejects unsupported precheck entity in actual service entry points", async () => { const { prisma, app } = await build(); const input = { entityType: "bad", entityId: ID, config: app.config }; await expect(resolveEntityPrecheck({ prisma, ...input })).rejects.toMatchObject({ statusCode: 400, details: { entityType: "unsupported", reasonCode: "fee_control.entity_unsupported" } }); await expect(buildEntityPrecheck(prisma, input)).rejects.toMatchObject({ details: { reasonCode: "fee_control.entity_unsupported" } }); noWrites(prisma); }); }); it("retains synthetic confidential index writes and their audits", async () => { const { app, prisma } = await build(); const created = await inject(app, "POST", "/confidential-doc-refs", { title: "ๅˆๆˆ็ดขๅผ•", storageType: "offline_archive", storagePath: "synthetic-index", confidentialityLevel: "internal" }); expect(created.statusCode).toBe(201); expect(prisma.confidentialDocumentRef.create.mock.calls[0][0].data).toMatchObject({ owner_employee_id: ID, storage_type: "offline_archive", project_id: null }); expect((await inject(app, "PATCH", `/confidential-doc-refs/${ID}`, { title: "ๅˆๆˆ็ดขๅผ•ไฟฎ่ฎข" })).statusCode).toBe(200); expect(prisma.auditLog.create).toHaveBeenCalledTimes(2); }); it("retains valid disabled purchase precheck and permission refusal", async () => { const { app, prisma } = await build(); prisma.purchaseRequest.findUnique.mockResolvedValue({ id: ID, department_id: null }); const r = await inject(app, "POST", "/fee-control/precheck", { entityType: "purchase_request", entityId: ID }); expect(r.statusCode).toBe(200); noWrites(prisma); const denied = await build({ allowed: false }); expect((await inject(denied.app, "POST", "/fee-control/precheck", { entityType: "purchase_request", entityId: ID })).statusCode).toBe(403); expect(denied.prisma.purchaseRequest.findUnique).not.toHaveBeenCalled(); });