Files
curriculum-project-hub/hub/test/integration/filelib-nav.test.ts
T
ymy 83a6b012b7 feat(filelib): 移除最近打开模块(ADR-0032,supersede ADR-0031 对应半部)
- FileLibRecentVisit 删表(手写迁移;表当日新建无生产数据)
- recentService/recentRoutes/RecentView 删除;GridLibraryView 埋点与
  navTarget 跳转一并移除;types 清 RecentEntry
- 左栏保留 文件库/回收站(ADR-0031 回收站半部不受影响)
- breadcrumb 的 role 字段保留(独立可用的增量字段)
2026-07-31 13:39:18 +08:00

116 lines
5.0 KiB
TypeScript

/**
* 回收站集成测试(真实 Postgres,ADR-0031;最近打开已由 ADR-0032 移除)。
* 覆盖:bin 列出(祖先全活跃顶点/直连 MANAGE 可见性/管理员)、restore 对称语义
* 与审计、purge 仅管理员 + 整支硬删(RESTRICT 顺序)。
* 运行前提:本地 PG(paradigm:paradigm@127.0.0.1:5432/cph_hub_test)且已 migrate。
*/
import { beforeEach, describe, expect, it } from "vitest";
import { prisma, resetDb, DEFAULT_ORG_ID } from "./helpers.js";
import {
createNode,
softDeleteNode,
listChildren,
type FileLibActor,
type TreeServiceDeps,
} from "../../src/database/filelib/treeService.js";
import { listBin, purgeBinEntry, restoreBinEntry, type BinDeps } from "../../src/database/filelib/binService.js";
import { createStaticGroupResolver } from "../../src/database/filelib/groupResolver.js";
import { createInMemoryVersionStore } from "../../src/database/filelib/versionStore.js";
import { FILE_LIB_AUDIT_ACTIONS } from "../../src/database/filelib/audit.js";
const ADMIN: FileLibActor = { userId: "u_admin", isWebsiteAdmin: true };
const ALICE: FileLibActor = { userId: "u_alice", isWebsiteAdmin: false };
const BOB: FileLibActor = { userId: "u_bob", isWebsiteAdmin: false };
function treeDeps(): TreeServiceDeps {
return {
prisma,
groupResolver: createStaticGroupResolver({ u_bob: ["g_physics"] }),
versionStore: createInMemoryVersionStore(),
organizationId: DEFAULT_ORG_ID,
storageRoot: "/tmp/filelib-test",
};
}
function binDeps(): BinDeps {
return {
prisma,
organizationId: DEFAULT_ORG_ID,
groupResolver: createStaticGroupResolver({ u_bob: ["g_physics"] }),
};
}
beforeEach(async () => {
await resetDb();
for (const [id, openId] of [["u_admin", "ou_admin"], ["u_alice", "ou_alice"], ["u_bob", "ou_bob"]] as const) {
await prisma.user.create({ data: { id, feishuOpenId: openId, displayName: id } });
}
});
describe("binService · 列出与可见性", () => {
it("只露每支已删子树的顶;管理员全见,直连 MANAGE 可见,无关者不见", async () => {
const root = await createNode(treeDeps(), ADMIN, { parentId: null, kind: "FOLDER", name: "物理" });
const child = await createNode(treeDeps(), ADMIN, { parentId: root.id, kind: "FOLDER", name: "必修一" });
await createNode(treeDeps(), ADMIN, { parentId: child.id, kind: "PROJECT", name: "TH-141" });
// alice 在 child 上直连 MANAGE。
const own = await createNode(treeDeps(), ADMIN, {
parentId: null, kind: "PROJECT", name: "alice 项目",
grants: [{ principalType: "USER", principalId: "u_alice", role: "MANAGE" }],
});
await softDeleteNode(treeDeps(), ADMIN, child.id); // 删中间层:child 是顶,孙项目不单列
await softDeleteNode(treeDeps(), ADMIN, own.id);
const adminBin = await listBin(binDeps(), ADMIN);
expect(adminBin.map((e) => e.name).sort()).toEqual(["alice 项目", "必修一"]);
const aliceBin = await listBin(binDeps(), ALICE);
expect(aliceBin.map((e) => e.name)).toEqual(["alice 项目"]); // 只见自己 MANAGE 的
const bobBin = await listBin(binDeps(), BOB);
expect(bobBin).toEqual([]);
void root;
});
});
describe("binService · 恢复", () => {
it("restore 只清本节点:整支立即可见,落 folder.restore 审计;无权者 404", async () => {
const root = await createNode(treeDeps(), ADMIN, { parentId: null, kind: "FOLDER", name: "物理" });
const child = await createNode(treeDeps(), ADMIN, { parentId: root.id, kind: "FOLDER", name: "必修一" });
await softDeleteNode(treeDeps(), ADMIN, child.id);
await expect(restoreBinEntry(binDeps(), BOB, child.id)).rejects.toMatchObject({ statusCode: 404 });
await restoreBinEntry(binDeps(), ADMIN, child.id);
const visible = await listChildren(treeDeps(), ADMIN, root.id);
expect(visible.map((c) => c.name)).toContain("必修一");
const audits = await prisma.auditEntry.findMany({
where: { action: FILE_LIB_AUDIT_ACTIONS.folderRestore },
});
expect(audits).toHaveLength(1);
});
});
describe("binService · 彻底删除", () => {
it("仅管理员;整支硬删(含子孙/授权),落 node.purge 审计", async () => {
const root = await createNode(treeDeps(), ADMIN, { parentId: null, kind: "FOLDER", name: "物理" });
const child = await createNode(treeDeps(), ADMIN, {
parentId: root.id, kind: "PROJECT", name: "TH-141",
grants: [{ principalType: "USER", principalId: "u_alice", role: "EDIT" }],
});
await softDeleteNode(treeDeps(), ADMIN, root.id); // 连根删:root 是顶
await expect(purgeBinEntry(binDeps(), ALICE, root.id)).rejects.toMatchObject({ statusCode: 404 });
const { removed } = await purgeBinEntry(binDeps(), ADMIN, root.id);
expect(removed).toBe(2);
expect(await prisma.fileLibNode.count({ where: { id: { in: [root.id, child.id] } } })).toBe(0);
expect(await prisma.fileLibGrant.count({ where: { nodeId: child.id } })).toBe(0);
const audits = await prisma.auditEntry.findMany({ where: { action: FILE_LIB_AUDIT_ACTIONS.nodePurge } });
expect(audits).toHaveLength(1);
});
});