import { beforeEach, describe, expect, it, vi } from "vitest"; import { appRouter } from "./routers"; import * as db from "./db"; import type { TrpcContext } from "./_core/context"; vi.mock("./db", () => { class InspectionObjectReferencedError extends Error { constructor(public readonly references: { taskReferenceCount: number; pointcloudReferenceCount: number; cadReferenceCount: number }) { const parts = [ references.taskReferenceCount > 0 ? `任务 ${references.taskReferenceCount} 个` : null, references.pointcloudReferenceCount > 0 ? `点云 ${references.pointcloudReferenceCount} 个` : null, references.cadReferenceCount > 0 ? `CAD ${references.cadReferenceCount} 个` : null, ].filter(Boolean); super(`检测对象仍被以下数据引用:${parts.join("、")}。请先处理关联数据后再删除。`); } } return { deleteInspectionObject: vi.fn(async () => undefined), InspectionObjectReferencedError, }; }); type AuthenticatedUser = NonNullable; function createContext(role: AuthenticatedUser["role"]): TrpcContext { return { user: { id: 1, username: "test-user", passwordHash: null, email: "test@example.com", name: "Test User", role, isActive: 1, mustChangePassword: 0, sessionVersion: 0, createdAt: new Date(), updatedAt: new Date(), lastLoginAt: new Date(), }, req: { protocol: "https", headers: {} } as TrpcContext["req"], res: { clearCookie: vi.fn() } as unknown as TrpcContext["res"], }; } describe("inspection object delete router", () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(db.deleteInspectionObject).mockResolvedValue(undefined); }); it("allows an administrator to delete an empty object", async () => { await expect(appRouter.createCaller(createContext("admin")).inspectionObject.delete({ id: 7 })) .resolves.toEqual({ success: true }); expect(db.deleteInspectionObject).toHaveBeenCalledWith(7); }); it.each([ [{ taskReferenceCount: 2, pointcloudReferenceCount: 0, cadReferenceCount: 0 }, "任务 2 个"], [{ taskReferenceCount: 0, pointcloudReferenceCount: 3, cadReferenceCount: 0 }, "点云 3 个"], [{ taskReferenceCount: 0, pointcloudReferenceCount: 0, cadReferenceCount: 4 }, "CAD 4 个"], ])("rejects a single reference type with its count", async (references, message) => { vi.mocked(db.deleteInspectionObject).mockRejectedValueOnce(new db.InspectionObjectReferencedError(references)); await expect(appRouter.createCaller(createContext("admin")).inspectionObject.delete({ id: 7 })) .rejects.toMatchObject({ code: "PRECONDITION_FAILED", message: expect.stringContaining(message) }); }); it("reports all reference types when a concurrent reference is observed by the transaction", async () => { vi.mocked(db.deleteInspectionObject).mockRejectedValueOnce(new db.InspectionObjectReferencedError({ taskReferenceCount: 1, pointcloudReferenceCount: 2, cadReferenceCount: 3, })); await expect(appRouter.createCaller(createContext("admin")).inspectionObject.delete({ id: 7 })) .rejects.toMatchObject({ code: "PRECONDITION_FAILED", message: "检测对象仍被以下数据引用:任务 1 个、点云 2 个、CAD 3 个。请先处理关联数据后再删除。", }); }); it("forbids a non-administrator from deleting", async () => { await expect(appRouter.createCaller(createContext("user")).inspectionObject.delete({ id: 7 })) .rejects.toMatchObject({ code: "FORBIDDEN" }); expect(db.deleteInspectionObject).not.toHaveBeenCalled(); }); });