feat: add local access control and managed point clouds
This commit is contained in:
181
server/pointcloudRename.test.ts
Normal file
181
server/pointcloudRename.test.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
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",
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user