import { beforeEach, describe, expect, it, vi } from "vitest"; import type { User } from "../drizzle/schema"; import type { TrpcContext } from "./_core/context"; import * as db from "./db"; import { appRouter } from "./routers"; vi.mock("./db", () => ({ getTaskPointcloudFileById: vi.fn(), findTaskPointcloudFileByTaskAndName: vi.fn(), updateTaskPointcloudFileName: vi.fn(), getModelPointcloudFileById: vi.fn(), findModelPointcloudFileByModelAndName: vi.fn(), updateModelPointcloudFileName: vi.fn(), })); const now = new Date("2026-07-21T00:00:00Z"); const taskPointcloud = { id: 11, taskId: 101, name: "原始点云.ply", fileSize: 123, filePath: "/data/tasks/101/physical-file.PLY", source: "uploaded", sourceResultId: null, sourcePointcloudFileId: null, sourceModelPointcloudId: null, createdAt: now, }; const modelPointcloud = { id: 22, modelId: 202, name: "车型点云.txt", fileSize: 456, filePath: "/data/models/202/physical-file.txt", createdAt: now, }; function user(role: User["role"]): User { return { id: 1, username: `${role}-account`, passwordHash: "unused", name: role, email: null, role, isActive: 1, mustChangePassword: 0, sessionVersion: 0, createdAt: now, updatedAt: now, lastLoginAt: null, }; } function caller(role: User["role"] = "admin") { const context: TrpcContext = { user: user(role), req: {} as TrpcContext["req"], res: {} as TrpcContext["res"], }; return appRouter.createCaller(context); } describe("点云重命名路由", () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(db.getTaskPointcloudFileById).mockResolvedValue(taskPointcloud); vi.mocked(db.findTaskPointcloudFileByTaskAndName).mockResolvedValue(undefined); vi.mocked(db.updateTaskPointcloudFileName).mockImplementation(async (id, name) => ({ ...taskPointcloud, id, name, })); vi.mocked(db.getModelPointcloudFileById).mockResolvedValue(modelPointcloud); vi.mocked(db.findModelPointcloudFileByModelAndName).mockResolvedValue(undefined); vi.mocked(db.updateModelPointcloudFileName).mockImplementation(async (id, name) => ({ ...modelPointcloud, id, name, })); }); it("任务点云去除首尾空格并按实际路径保留扩展名", async () => { const result = await caller().pointcloudFile.rename({ id: 11, baseName: " 新名称 " }); expect(result?.name).toBe("新名称.PLY"); expect(db.findTaskPointcloudFileByTaskAndName).toHaveBeenCalledWith(101, "新名称.PLY", 11); expect(db.updateTaskPointcloudFileName).toHaveBeenCalledWith(11, "新名称.PLY"); }); it("车型库点云按 modelId 检查重名并返回更新记录", async () => { const result = await caller("super_admin").modelPointcloudFile.rename({ id: 22, baseName: "扫描结果" }); expect(result?.name).toBe("扫描结果.txt"); expect(db.findModelPointcloudFileByModelAndName).toHaveBeenCalledWith(202, "扫描结果.txt", 22); expect(db.updateModelPointcloudFileName).toHaveBeenCalledWith(22, "扫描结果.txt"); }); it.each(["", " "])("拒绝空名称 %#", async (baseName) => { await expect(caller().pointcloudFile.rename({ id: 11, baseName })).rejects.toMatchObject({ code: "BAD_REQUEST", message: "点云名称不能为空", }); }); it.each(["目录/名称", "目录\\名称", "名称\n换行"])("拒绝非法名称 %s", async (baseName) => { await expect(caller().pointcloudFile.rename({ id: 11, baseName })).rejects.toMatchObject({ code: "BAD_REQUEST", }); }); it("拒绝拼接扩展名后超过 255 个字符的名称", async () => { await expect(caller().pointcloudFile.rename({ id: 11, baseName: "名".repeat(252) })).rejects.toMatchObject({ code: "BAD_REQUEST", message: "点云名称不能超过 255 个字符(含扩展名)", }); }); it("同一任务中忽略英文大小写禁止重名", async () => { vi.mocked(db.findTaskPointcloudFileByTaskAndName).mockResolvedValueOnce({ ...taskPointcloud, id: 12, name: "NEW-NAME.ply", }); await expect(caller().pointcloudFile.rename({ id: 11, baseName: "new-name" })).rejects.toMatchObject({ code: "CONFLICT", message: "当前任务下已存在同名点云", }); expect(db.updateTaskPointcloudFileName).not.toHaveBeenCalled(); }); it("同一车型中忽略英文大小写禁止重名", async () => { vi.mocked(db.findModelPointcloudFileByModelAndName).mockResolvedValueOnce({ ...modelPointcloud, id: 23, name: "SCAN.txt", }); await expect(caller().modelPointcloudFile.rename({ id: 22, baseName: "scan" })).rejects.toMatchObject({ code: "CONFLICT", message: "当前车型下已存在同名点云", }); }); it("记录不存在时返回 NOT_FOUND", async () => { vi.mocked(db.getTaskPointcloudFileById).mockResolvedValueOnce(undefined); vi.mocked(db.getModelPointcloudFileById).mockResolvedValueOnce(undefined); await expect(caller().pointcloudFile.rename({ id: 999, baseName: "名称" })).rejects.toMatchObject({ code: "NOT_FOUND" }); await expect(caller().modelPointcloudFile.rename({ id: 999, baseName: "名称" })).rejects.toMatchObject({ code: "NOT_FOUND" }); }); it("名称未变化时直接成功且不查询重名、不更新", async () => { const unchanged = { ...taskPointcloud, name: "原始点云.PLY" }; vi.mocked(db.getTaskPointcloudFileById).mockResolvedValueOnce(unchanged); await expect(caller().pointcloudFile.rename({ id: 11, baseName: "原始点云" })).resolves.toEqual(unchanged); expect(db.findTaskPointcloudFileByTaskAndName).not.toHaveBeenCalled(); expect(db.updateTaskPointcloudFileName).not.toHaveBeenCalled(); }); it("名称只改变大小写时排除自身后允许更新", async () => { await expect(caller().pointcloudFile.rename({ id: 11, baseName: "原始点云" })).resolves.toMatchObject({ name: "原始点云.PLY", }); expect(db.findTaskPointcloudFileByTaskAndName).toHaveBeenCalledWith(101, "原始点云.PLY", 11); }); it("普通用户无权调用,管理员和超级管理员可以调用", async () => { await expect(caller("user").pointcloudFile.rename({ id: 11, baseName: "无权修改" })).rejects.toMatchObject({ code: "FORBIDDEN", }); await expect(caller("admin").pointcloudFile.rename({ id: 11, baseName: "管理员修改" })).resolves.toMatchObject({ name: "管理员修改.PLY", }); await expect(caller("super_admin").pointcloudFile.rename({ id: 11, baseName: "超管修改" })).resolves.toMatchObject({ name: "超管修改.PLY", }); }); });