Files
massive-cloud/server/pointcloudRename.test.ts

182 lines
6.6 KiB
TypeScript

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",
});
});
});