Files
Docker/uniapp/youjiala/src/permission/permission-kernel.spec.ts
T
2026-10-13 10:47:39 +08:00

397 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 权限内核的可执行校验(不引测试框架,纯 TS + 断言函数)
*
* 跑法(PowerShell,在项目根目录):
* & "C:\Program Files\nodejs\node.exe" ".\node_modules\esbuild\bin\esbuild" `
* src\permission\permission-kernel.spec.ts --bundle --platform=node --format=cjs `
* --alias:@=./src --outfile="$env:TEMP\permission-kernel.spec.cjs" --log-level=warning
* & "C:\Program Files\nodejs\node.exe" "$env:TEMP\permission-kernel.spec.cjs"
*
* 为什么要它:判定语义的正确性只能靠"跑一遍"来钉住,尤其 canView 是否检 ViewIndc
* 这条——它是本次移植相对旧 WebVue 唯一的行为变更,靠读代码很容易自我说服"应该没问题"。
* 项目目前还没接测试运行器,所以先写成"能被 esbuild 打包直接用 node 跑"的形态:
* 将来引入 vitest,只需把 check() 换成 expect(),用例本身不用动。
*
* 约定:所有用例都对照后端 CloudBuilder.Security.Service/Authorization/PermissionContext.cs
* 的既有行为写,断言的是"前端与后端一致",不是"前端自己的偏好"。
*/
import type {
DependentRowLike,
PermissionInfoLike,
PermissionRowLike,
} from "./permission-kernel";
import {
actionsOf,
buildPermissionIndex,
can,
canAny,
canOperate,
grantedPermissions,
} from "./permission-kernel";
let failed = 0;
let total = 0;
/** 断言:失败不中断,好让一次跑完能看清全部问题 */
function check(label: string, actual: unknown, expected: unknown): void {
total++;
if (actual !== expected) {
failed++;
// 本项目的 no-console 只放行 warn/error(页面代码里不该打印),
// 这是个只在 node 下跑的校验脚本,就沿用放行的那一个
console.warn(
` FAIL ${label} => expected ${String(expected)}, got ${String(actual)}`,
);
}
}
function row(
permission: string,
flags: Partial<Record<"amend" | "create" | "delete" | "view", string>>,
): PermissionRowLike {
return {
PermissionName: permission,
ViewIndc: flags.view,
CreateIndc: flags.create,
AmendIndc: flags.amend,
DeleteIndc: flags.delete,
};
}
function depRow(
permission: string,
dependent: string,
flags: Partial<Record<"amend" | "create" | "delete" | "view", string>>,
): DependentRowLike {
return { ...row(permission, flags), DependentDetailName: dependent };
}
function info(
permissions: PermissionRowLike[] = [],
dependents: DependentRowLike[] = [],
): PermissionInfoLike {
return { Permissions: permissions, PermissionDependents: dependents };
}
// ---------------------------------------------------------------- 1. view 必须检 ViewIndc
{
// view_indc='N' 而 create_indc='Y':后端 CanView 为 false。
// 旧前端只看"权限名在不在",会误放行,页面进来后列表接口一片 403。
const index = buildPermissionIndex(
info([row("ActorEnquiry", { view: "N", create: "Y" })]),
);
check(
"view=N 时 canView 为 false(修复旧前端漏检)",
can(index, "view", "ActorEnquiry"),
false,
);
check(
"view=N 时 canNone 仍为 true(只要求挂过权限)",
can(index, "none", "ActorEnquiry"),
true,
);
check(
"create=Y 时 canCreate 为 true",
can(index, "create", "ActorEnquiry"),
true,
);
}
// ---------------------------------------------------------------- 2. 四种动作各自独立
{
const index = buildPermissionIndex(
info([row("DmsFileEnquiry", { view: "Y" })]),
);
check("只给 view:view 通过", can(index, "view", "DmsFileEnquiry"), true);
check(
"只给 view:create 不通过",
can(index, "create", "DmsFileEnquiry"),
false,
);
check(
"只给 view:amend 不通过",
can(index, "amend", "DmsFileEnquiry"),
false,
);
check(
"只给 view:delete 不通过",
can(index, "delete", "DmsFileEnquiry"),
false,
);
const full = buildPermissionIndex(
info([
row("DmsFileEnquiry", {
view: "Y",
create: "Y",
amend: "Y",
delete: "Y",
}),
]),
);
check("四标全 Y:delete 通过", can(full, "delete", "DmsFileEnquiry"), true);
}
// ---------------------------------------------------------------- 3. 多用户组授权做 OR 合并
{
// 同一权限名由两个用户组分别授予 view 与 delete,合并后两者都应有
const index = buildPermissionIndex(
info([
row("FamilyGroupEnquiry", { view: "Y" }),
row("FamilyGroupEnquiry", { delete: "Y" }),
]),
);
check("多组合并:view 命中", can(index, "view", "FamilyGroupEnquiry"), true);
check(
"多组合并:delete 命中",
can(index, "delete", "FamilyGroupEnquiry"),
true,
);
check(
"多组合并:amend 仍未命中",
can(index, "amend", "FamilyGroupEnquiry"),
false,
);
const flags = actionsOf(index, "FamilyGroupEnquiry");
check(
"actionsOf 一次取全",
`${flags.view}/${flags.create}/${flags.amend}/${flags.delete}`,
"true/false/false/true",
);
}
// ---------------------------------------------------------------- 4. 数据级只查 dependents,不回退
{
const index = buildPermissionIndex(
info(
[row("PublishPhotoEnquiry", { view: "Y" })],
[depRow("PublishPhotoEnquiry", "F1001", { view: "N" })],
),
);
check("功能级命中", can(index, "view", "PublishPhotoEnquiry"), true);
// 后端 CanView(permission, dependent) 只查 PermissionDependents;
// 前端若"数据级没查到就退回功能级",就比后端宽,会出现前端放行后端 403
check(
"数据级 N:不回退到功能级",
can(index, "view", "PublishPhotoEnquiry", "F1001"),
false,
);
check(
"数据级整条不存在:false",
can(index, "view", "PublishPhotoEnquiry", "F9999"),
false,
);
check(
"数据级 Y:命中",
can(
buildPermissionIndex(
info([], [depRow("PublishPhotoEnquiry", "F1", { view: "Y" })]),
),
"view",
"PublishPhotoEnquiry",
"F1",
),
true,
);
}
// ---------------------------------------------------------------- 5. 严格比较(与后端一致)
{
const index = buildPermissionIndex(
info([
row("SecurityGroupEnquiry", { view: "y" }),
row(" security ", { view: "Y" }),
]),
);
check(
"小写 y 不算 YES(后端按 'Y' 精确比较)",
can(index, "view", "SecurityGroupEnquiry"),
false,
);
check("权限名区分大小写", can(index, "view", "securitygroupenquiry"), false);
check("权限名不做 trim", can(index, "view", "security"), false);
check("原样存在的名字可命中", can(index, "view", " security "), true);
}
// ---------------------------------------------------------------- 6. 档案缺失或损坏
{
const broken: PermissionInfoLike[] = [
{},
{ Permissions: null },
{ Permissions: undefined, PermissionDependents: undefined },
{ Permissions: [] },
{ Permissions: [null as unknown as PermissionRowLike] },
{ Permissions: [{ PermissionName: "" } as PermissionRowLike] },
{
PermissionDependents: [
{ PermissionName: "A", DependentDetailName: "" } as DependentRowLike,
],
},
];
broken.forEach((item, i) => {
const index = buildPermissionIndex(item);
check(
`损坏档案[${i}]:不抛异常且判定为 false`,
can(index, "view", "A"),
false,
);
check(`损坏档案[${i}]:none 也为 false`, can(index, "none", "A"), false);
});
check(
"传 null 得到空索引",
can(buildPermissionIndex(null), "none", "A"),
false,
);
check(
"不传参得到空索引",
grantedPermissions(buildPermissionIndex(), "none").length,
0,
);
}
// ---------------------------------------------------------------- 7. canAny 批量判定
{
const index = buildPermissionIndex(
info([row("PreviewAudio", { view: "Y" })]),
);
check("数组任一命中", canAny(index, "view", ["Nope", "PreviewAudio"]), true);
check("数组全不命中", canAny(index, "view", ["Nope", "Nothing"]), false);
check("空数组为 false(而非 undefined)", canAny(index, "view", []), false);
check(
"数组里的空串被跳过",
canAny(index, "view", ["", "PreviewAudio"]),
true,
);
check("单串仍可用", canAny(index, "view", "PreviewAudio"), true);
check(
"批量判定带 dependent",
canAny(index, "view", ["PreviewAudio"], "F1"),
false,
);
}
// ---------------------------------------------------------------- 8. 未登记动作与未知权限
{
const index = buildPermissionIndex(
info([row("ActorEnquiry", { view: "Y" })]),
);
check("未授予的权限名:false", can(index, "view", "NeverGranted"), false);
const flags = actionsOf(index, "NeverGranted");
check(
"未授予权限的 actionsOf 返回全 false 而非 undefined",
`${flags.none}/${flags.view}`,
"false/false",
);
}
// ---------------------------------------------------------------- 9. 已授权清单
{
const index = buildPermissionIndex(
info([
row("A_Enquiry", { view: "Y" }),
row("B_Enquiry", { view: "N", create: "Y" }),
row("C_Enquiry", { view: "Y", delete: "Y" }),
]),
);
check(
"none 清单含全部挂过的名字",
grantedPermissions(index, "none").join(","),
"A_Enquiry,B_Enquiry,C_Enquiry",
);
check(
"view 清单只含 Y",
grantedPermissions(index, "view").join(","),
"A_Enquiry,C_Enquiry",
);
check(
"create 清单",
grantedPermissions(index, "create").join(","),
"B_Enquiry",
);
check(
"delete 清单",
grantedPermissions(index, "delete").join(","),
"C_Enquiry",
);
check("amend 清单为空", grantedPermissions(index, "amend").length, 0);
}
// ---------------------------------------------------------------- 10. 操作型判定(对齐后端 PermissionHandler 的 switch)
{
const index = buildPermissionIndex(
info(
[
row("PublishPhotoEnquiry", {
view: "Y",
create: "Y",
amend: "Y",
delete: "Y",
}),
],
// Submit/Approval 这类操作在后端是"数据级授权行",靠 DependentDetailName 命中
[depRow("PublishPhotoEnquiry", "Submit", { view: "N" })],
),
);
check("Find -> view", canOperate(index, "Find", "PublishPhotoEnquiry"), true);
check(
"Print -> view",
canOperate(index, "Print", "PublishPhotoEnquiry"),
true,
);
check(
"Insert -> create",
canOperate(index, "Insert", "PublishPhotoEnquiry"),
true,
);
check(
"Update -> amend",
canOperate(index, "Update", "PublishPhotoEnquiry"),
true,
);
check(
"Delete -> delete",
canOperate(index, "Delete", "PublishPhotoEnquiry"),
true,
);
check(
"None 只要求挂过",
canOperate(index, "None", "PublishPhotoEnquiry"),
true,
);
check(
"Submit 走数据级,命中",
canOperate(index, "Submit", "PublishPhotoEnquiry"),
true,
);
check(
"Approval 未挂 → false(即使四个 Indc 全 Y)",
canOperate(index, "Approval", "PublishPhotoEnquiry"),
false,
);
// view_indc='N' 时 Find 必须为 false:这条正是旧前端会误判成 true 的场景
const noView = buildPermissionIndex(
info([row("ActorEnquiry", { view: "N", create: "Y" })]),
);
check(
"view=N 时 Find 为 false",
canOperate(noView, "Find", "ActorEnquiry"),
false,
);
check(
"view=N 时 Insert 仍为 true",
canOperate(noView, "Insert", "ActorEnquiry"),
true,
);
}
console.warn(
failed === 0
? `permission-kernel: ${total} assertions passed`
: `permission-kernel: ${failed}/${total} assertions FAILED`,
);
if (failed > 0) {
// 让非零退出可被脚本捕获,避免"跑了但没人看输出"
throw new Error("permission-kernel assertions failed");
}