forked from EduCraft/curriculum-project-hub
229 lines
8.1 KiB
TypeScript
229 lines
8.1 KiB
TypeScript
import { afterAll, beforeEach, describe, expect, it } from "vitest";
|
|
import {
|
|
browseBindableFolder,
|
|
discoverBindableProjects,
|
|
normalizeProjectSearchQuery,
|
|
} from "../../src/projectDiscovery.js";
|
|
import { DEFAULT_ORG_ID, prisma, resetDb } from "./helpers.js";
|
|
|
|
describe("project discovery", () => {
|
|
beforeEach(async () => {
|
|
await resetDb();
|
|
await seedUser("owner", "ou_owner", "OWNER");
|
|
await seedUser("member", "ou_member", "MEMBER");
|
|
});
|
|
|
|
afterAll(async () => prisma.$disconnect());
|
|
|
|
it("normalizes project codes across punctuation, width and case", () => {
|
|
expect(normalizeProjectSearchQuery(" TH-141 ")).toBe("th141");
|
|
expect(normalizeProjectSearchQuery("th_141")).toBe("th141");
|
|
});
|
|
|
|
it("searches normalized codes, Chinese names and complete folder breadcrumbs", async () => {
|
|
const root = await createFolder("root-legacy", null, "旧教学资产");
|
|
const subject = await createFolder("folder-physics", root.id, "物理竞赛教研");
|
|
const thermal = await createFolder("folder-thermal", subject.id, "TH_热学专题");
|
|
await createProject("project-th141", thermal.id, "TH-141_表面张力的严肃理论");
|
|
await createProject("project-fullwidth", thermal.id, "TH-142_全角编号");
|
|
await createProject("project-explicit-code", thermal.id, "表面课程", "PHY-001");
|
|
await createProject("project-other", thermal.id, "TH-211_理想气体静力学");
|
|
|
|
const byCode = await discover("TH141");
|
|
expect(byCode.items[0]).toMatchObject({
|
|
projectId: "project-th141",
|
|
breadcrumb: "旧教学资产 / 物理竞赛教研 / TH_热学专题",
|
|
});
|
|
await expect(discover("表面张力")).resolves.toMatchObject({
|
|
totalItems: 1,
|
|
items: [{ projectId: "project-th141" }],
|
|
});
|
|
await expect(discover("TH142")).resolves.toMatchObject({
|
|
items: [expect.objectContaining({ projectId: "project-fullwidth" })],
|
|
});
|
|
await expect(discover("PHY001")).resolves.toMatchObject({
|
|
items: [expect.objectContaining({ projectId: "project-explicit-code" })],
|
|
});
|
|
const byPath = await discover("物理竞赛教研");
|
|
expect(byPath.items.map((item) => item.projectId)).toEqual(expect.arrayContaining([
|
|
"project-th141",
|
|
"project-other",
|
|
]));
|
|
await expect(discover("物理竞赛 TH141")).resolves.toMatchObject({
|
|
items: [expect.objectContaining({ projectId: "project-th141" })],
|
|
});
|
|
await expect(discover("%")).resolves.toMatchObject({ totalItems: 0 });
|
|
});
|
|
|
|
it("refreshes descendant breadcrumbs after a folder rename", async () => {
|
|
const root = await createFolder("root-before", null, "旧目录");
|
|
const child = await createFolder("child", root.id, "子目录");
|
|
await createProject("project-refresh", child.id, "项目");
|
|
|
|
await prisma.folder.update({ where: { id: root.id }, data: { name: "新目录" } });
|
|
|
|
await expect(discover("新目录")).resolves.toMatchObject({
|
|
totalItems: 1,
|
|
items: [{ projectId: "project-refresh", breadcrumb: "新目录 / 子目录" }],
|
|
});
|
|
await expect(discover("旧目录")).resolves.toMatchObject({ totalItems: 0 });
|
|
});
|
|
|
|
it("filters authorization before counting and paginating", async () => {
|
|
for (let index = 0; index < 12; index += 1) {
|
|
await createProject(`project-${index}`, null, `TH-${index}`);
|
|
}
|
|
await prisma.permissionGrant.create({
|
|
data: {
|
|
resourceType: "PROJECT",
|
|
resourceId: "project-11",
|
|
principalType: "USER",
|
|
principalId: "ou_member",
|
|
role: "MANAGE",
|
|
},
|
|
});
|
|
|
|
const result = await discoverBindableProjects({
|
|
prisma,
|
|
organizationId: DEFAULT_ORG_ID,
|
|
actorFeishuOpenId: "ou_member",
|
|
isOrgAdmin: false,
|
|
query: "TH",
|
|
page: 2,
|
|
pageSize: 5,
|
|
});
|
|
|
|
expect(result).toMatchObject({ page: 1, totalItems: 1, totalPages: 1 });
|
|
expect(result.items.map((item) => item.projectId)).toEqual(["project-11"]);
|
|
});
|
|
|
|
it("browses the preserved folder tree and paginates projects", async () => {
|
|
const root = await createFolder("root", null, "旧教学资产");
|
|
const child = await createFolder("child", root.id, "物理竞赛教研");
|
|
await createProject("project-root", root.id, "根目录项目");
|
|
|
|
const result = await browseBindableFolder({
|
|
prisma,
|
|
organizationId: DEFAULT_ORG_ID,
|
|
actorFeishuOpenId: "ou_owner",
|
|
isOrgAdmin: true,
|
|
folderId: root.id,
|
|
});
|
|
|
|
expect(result.breadcrumb).toBe("旧教学资产");
|
|
expect(result.parentFolderId).toBeNull();
|
|
expect(result.childFolders).toEqual([{ folderId: child.id, name: "物理竞赛教研" }]);
|
|
expect(result.projects).toEqual([{
|
|
projectId: "project-root",
|
|
name: "根目录项目",
|
|
breadcrumb: "旧教学资产",
|
|
}]);
|
|
});
|
|
|
|
it("hides the system Inbox at root and presents its projects as unclassified", async () => {
|
|
const inbox = await prisma.folder.findFirstOrThrow({
|
|
where: { organizationId: DEFAULT_ORG_ID, kind: "SYSTEM_INBOX" },
|
|
});
|
|
const businessRoot = await createFolder("business-root", null, "业务目录");
|
|
const businessInbox = await createFolder("business-inbox", businessRoot.id, "Inbox");
|
|
await createProject("project-inbox", inbox.id, "新项目");
|
|
|
|
const result = await browseBindableFolder({
|
|
prisma,
|
|
organizationId: DEFAULT_ORG_ID,
|
|
actorFeishuOpenId: "ou_owner",
|
|
isOrgAdmin: true,
|
|
folderId: null,
|
|
});
|
|
|
|
expect(result.childFolders.map((folder) => folder.folderId)).not.toContain(inbox.id);
|
|
expect(result.projects).toContainEqual({
|
|
projectId: "project-inbox",
|
|
name: "新项目",
|
|
breadcrumb: "未分类",
|
|
});
|
|
|
|
const businessResult = await browseBindableFolder({
|
|
prisma,
|
|
organizationId: DEFAULT_ORG_ID,
|
|
actorFeishuOpenId: "ou_owner",
|
|
isOrgAdmin: true,
|
|
folderId: businessRoot.id,
|
|
});
|
|
expect(businessResult.childFolders).toContainEqual({ folderId: businessInbox.id, name: "Inbox" });
|
|
});
|
|
|
|
it("shares one card page budget across folders and projects", async () => {
|
|
const root = await createFolder("folder-page-root", null, "目录分页");
|
|
for (let index = 0; index < 6; index += 1) {
|
|
await createFolder(`folder-page-${index}`, root.id, `目录-${index}`);
|
|
await createProject(`project-page-${index}`, root.id, `项目-${index}`);
|
|
}
|
|
|
|
const first = await browseBindableFolder({
|
|
prisma,
|
|
organizationId: DEFAULT_ORG_ID,
|
|
actorFeishuOpenId: "ou_owner",
|
|
isOrgAdmin: true,
|
|
folderId: root.id,
|
|
pageSize: 8,
|
|
});
|
|
const second = await browseBindableFolder({
|
|
prisma,
|
|
organizationId: DEFAULT_ORG_ID,
|
|
actorFeishuOpenId: "ou_owner",
|
|
isOrgAdmin: true,
|
|
folderId: root.id,
|
|
page: 2,
|
|
pageSize: 8,
|
|
});
|
|
|
|
expect(first).toMatchObject({ page: 1, totalPages: 2, totalFolders: 6, totalProjects: 6 });
|
|
expect(first.childFolders.length + first.projects.length).toBe(8);
|
|
expect(second.childFolders.length + second.projects.length).toBe(4);
|
|
expect([...first.projects, ...second.projects].map((project) => project.projectId).sort()).toEqual(
|
|
Array.from({ length: 6 }, (_, index) => `project-page-${index}`),
|
|
);
|
|
});
|
|
});
|
|
|
|
async function seedUser(id: string, feishuOpenId: string, role: "OWNER" | "MEMBER"): Promise<void> {
|
|
await prisma.user.create({
|
|
data: {
|
|
id,
|
|
feishuOpenId,
|
|
displayName: id,
|
|
organizationMemberships: { create: { organizationId: DEFAULT_ORG_ID, role } },
|
|
},
|
|
});
|
|
}
|
|
|
|
async function createFolder(id: string, parentId: string | null, name: string) {
|
|
return prisma.folder.create({
|
|
data: { id, organizationId: DEFAULT_ORG_ID, parentId, name },
|
|
});
|
|
}
|
|
|
|
async function createProject(id: string, folderId: string | null, name: string, code?: string) {
|
|
return prisma.project.create({
|
|
data: {
|
|
id,
|
|
organizationId: DEFAULT_ORG_ID,
|
|
folderId,
|
|
...(code !== undefined ? { code } : {}),
|
|
name,
|
|
workspaceDir: `/tmp/${id}`,
|
|
},
|
|
});
|
|
}
|
|
|
|
function discover(query: string) {
|
|
return discoverBindableProjects({
|
|
prisma,
|
|
organizationId: DEFAULT_ORG_ID,
|
|
actorFeishuOpenId: "ou_owner",
|
|
isOrgAdmin: true,
|
|
query,
|
|
});
|
|
}
|