forked from EduCraft/curriculum-project-hub
feat(database): init database folder frontend and permission
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* 纯权限 reducer 单测(契约 P6 / D11 / 2.3)。
|
||||
* 矩阵覆盖:个人/Group/祖先继承/max 取最高/不降权/空权限/toggle 冻结;
|
||||
* 外加确定性随机化不变量(单调性:任何可用 grant 都不超过 effective)。
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { checkAccess, effectiveRole, type EffectiveRoleInput, type FileLibGrantFact } from "../../src/database/filelib/permission.js";
|
||||
|
||||
const base: EffectiveRoleInput = {
|
||||
nodeId: "N",
|
||||
nodeKind: "FOLDER",
|
||||
ancestorIds: ["A", "R"], // N ⊂ A ⊂ R
|
||||
independentPermissionsEnabled: false,
|
||||
userId: "u1",
|
||||
groupIds: ["g1"],
|
||||
grants: [],
|
||||
};
|
||||
|
||||
function grant(partial: Partial<FileLibGrantFact>): FileLibGrantFact {
|
||||
return {
|
||||
nodeId: "N",
|
||||
principalType: "USER",
|
||||
principalId: "u1",
|
||||
role: "VIEW",
|
||||
isCreatorGrant: false,
|
||||
...partial,
|
||||
};
|
||||
}
|
||||
|
||||
describe("effectiveRole · 契约 P6 矩阵", () => {
|
||||
it("无任何 grant → null(无权限)", () => {
|
||||
expect(effectiveRole(base)).toBeNull();
|
||||
});
|
||||
|
||||
it("个人直接 grant 在 self 上 → 生效", () => {
|
||||
expect(effectiveRole({ ...base, grants: [grant({ role: "EDIT" })] })).toBe("EDIT");
|
||||
});
|
||||
|
||||
it("Group grant 且用户在组内 → 生效", () => {
|
||||
expect(effectiveRole({ ...base, grants: [grant({ principalType: "GROUP", principalId: "g1", role: "MANAGE" })] })).toBe("MANAGE");
|
||||
});
|
||||
|
||||
it("Group grant 但用户不在组内 → null", () => {
|
||||
expect(effectiveRole({ ...base, grants: [grant({ principalType: "GROUP", principalId: "g9" })] })).toBeNull();
|
||||
});
|
||||
|
||||
it("祖先链上的 grant 向下继承", () => {
|
||||
expect(effectiveRole({ ...base, grants: [grant({ nodeId: "A", role: "EDIT" })] })).toBe("EDIT");
|
||||
expect(effectiveRole({ ...base, grants: [grant({ nodeId: "R", role: "VIEW" })] })).toBe("VIEW");
|
||||
});
|
||||
|
||||
it("取最高:个人低权限 + Group 高权限 → 高权限(个人不能降权)", () => {
|
||||
const grants = [
|
||||
grant({ role: "VIEW" }),
|
||||
grant({ principalType: "GROUP", principalId: "g1", role: "MANAGE" }),
|
||||
];
|
||||
expect(effectiveRole({ ...base, grants })).toBe("MANAGE");
|
||||
});
|
||||
|
||||
it("取最高:链上多处 grant,最高者胜", () => {
|
||||
const grants = [
|
||||
grant({ nodeId: "R", role: "MANAGE" }),
|
||||
grant({ nodeId: "A", role: "VIEW" }),
|
||||
grant({ nodeId: "N", role: "EDIT" }),
|
||||
];
|
||||
expect(effectiveRole({ ...base, grants })).toBe("MANAGE");
|
||||
});
|
||||
|
||||
it("链外节点的 grant 不参与", () => {
|
||||
expect(effectiveRole({ ...base, grants: [grant({ nodeId: "X", role: "MANAGE" })] })).toBeNull();
|
||||
});
|
||||
|
||||
it("他人的个人 grant 不参与", () => {
|
||||
expect(effectiveRole({ ...base, grants: [grant({ principalId: "u2", role: "MANAGE" })] })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("effectiveRole · D11 独立权限开关", () => {
|
||||
const project: EffectiveRoleInput = { ...base, nodeKind: "PROJECT", nodeId: "P" };
|
||||
|
||||
it("开关关闭:项目级非创建者 grant 冻结", () => {
|
||||
const grants = [grant({ nodeId: "P", role: "EDIT" })];
|
||||
expect(effectiveRole({ ...project, grants })).toBeNull();
|
||||
});
|
||||
|
||||
it("开关关闭:创建者 grant 仍生效", () => {
|
||||
const grants = [grant({ nodeId: "P", role: "MANAGE", isCreatorGrant: true })];
|
||||
expect(effectiveRole({ ...project, grants })).toBe("MANAGE");
|
||||
});
|
||||
|
||||
it("开关关闭:祖先链 grant 不受影响", () => {
|
||||
const grants = [
|
||||
grant({ nodeId: "P", role: "MANAGE" }), // 冻结
|
||||
grant({ nodeId: "A", role: "VIEW" }), // 生效
|
||||
];
|
||||
expect(effectiveRole({ ...project, grants })).toBe("VIEW");
|
||||
});
|
||||
|
||||
it("开关开启:项目级 grant 恢复参与", () => {
|
||||
const grants = [grant({ nodeId: "P", role: "EDIT" })];
|
||||
expect(effectiveRole({ ...project, independentPermissionsEnabled: true, grants })).toBe("EDIT");
|
||||
});
|
||||
|
||||
it("文件夹忽略开关(self grant 照常参与)", () => {
|
||||
const grants = [grant({ nodeId: "N", role: "EDIT" })];
|
||||
expect(effectiveRole({ ...base, grants })).toBe("EDIT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("effectiveRole · 随机化不变量", () => {
|
||||
// 确定性 LCG,替代 property-based 框架(不新增依赖)。
|
||||
function lcg(seed: number): () => number {
|
||||
let s = seed;
|
||||
return () => {
|
||||
s = (s * 1103515245 + 12345) % 2147483648;
|
||||
return s / 2147483648;
|
||||
};
|
||||
}
|
||||
const roles = ["VIEW", "EDIT", "MANAGE"] as const;
|
||||
|
||||
it("effective 恰为所有可用 grant 的最高秩(单调、顺序无关)", () => {
|
||||
const rand = lcg(42);
|
||||
for (let round = 0; round < 200; round += 1) {
|
||||
const nodes = ["N", "A", "R", "X"];
|
||||
const principals = ["u1", "u2", "g1", "g9"];
|
||||
const grants: FileLibGrantFact[] = Array.from({ length: Math.floor(rand() * 12) }, () => {
|
||||
const principalId = principals[Math.floor(rand() * principals.length)]!;
|
||||
return grant({
|
||||
nodeId: nodes[Math.floor(rand() * nodes.length)]!,
|
||||
principalType: principalId.startsWith("g") ? "GROUP" : "USER",
|
||||
principalId,
|
||||
role: roles[Math.floor(rand() * roles.length)]!,
|
||||
});
|
||||
});
|
||||
const input: EffectiveRoleInput = { ...base, grants };
|
||||
const applicableRanks = grants
|
||||
.filter((g) => ["N", "A", "R"].includes(g.nodeId))
|
||||
.filter((g) => (g.principalType === "USER" ? g.principalId === "u1" : g.principalId === "g1"))
|
||||
.map((g) => ({ VIEW: 1, EDIT: 2, MANAGE: 3 })[g.role]);
|
||||
const expected = applicableRanks.length === 0 ? 0 : Math.max(...applicableRanks);
|
||||
const actual = effectiveRole(input);
|
||||
expect(actual === null ? 0 : { VIEW: 1, EDIT: 2, MANAGE: 3 }[actual]).toBe(expected);
|
||||
// 顺序无关
|
||||
expect(effectiveRole({ ...input, grants: [...grants].reverse() })).toBe(actual);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkAccess · D8 语义", () => {
|
||||
it("无权限 → not_found(404 不泄露)", () => {
|
||||
expect(checkAccess(null, "VIEW")).toEqual({ allowed: false, reason: "not_found" });
|
||||
});
|
||||
it("权限不足 → forbidden(403)", () => {
|
||||
expect(checkAccess("VIEW", "EDIT")).toEqual({ allowed: false, reason: "forbidden" });
|
||||
});
|
||||
it("权限足够 → allowed 并带回实际角色", () => {
|
||||
expect(checkAccess("MANAGE", "EDIT")).toEqual({ allowed: true, role: "MANAGE" });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user