Files
Jyotisha/frontend/tests/admin-ui-permission-contract.test.ts
2026-08-11 11:59:03 +08:00

166 lines
7.1 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
const source = (path: string) => readFileSync(new URL(`../${path}`, import.meta.url), "utf8");
const highRiskMappings = [
{
name: "商品保存",
ui: "src/components/admin/product-management.tsx",
api: "src/app/api/admin/products/route.ts",
permission: "billing.products.write",
},
{
name: "商品发布",
ui: "src/components/admin/product-management.tsx",
api: "src/app/api/admin/products/route.ts",
permission: "billing.products.publish",
},
{
name: "订阅调整",
ui: "src/components/admin/billing-operations-resources.tsx",
api: "src/app/api/admin/subscriptions/route.ts",
permission: "billing.adjustments.write",
},
{
name: "订单调整",
ui: "src/components/admin/billing-operations-resources.tsx",
api: "src/app/api/admin/orders/route.ts",
permission: "billing.adjustments.write",
},
{
name: "兑换码创建",
ui: "src/components/admin/codes-resource.tsx",
api: "src/app/api/admin/codes/route.ts",
permission: "billing.adjustments.write",
},
{
name: "兑换码编辑与撤销",
ui: "src/components/admin/codes-resource.tsx",
api: "src/app/api/admin/codes/[id]/route.ts",
permission: "billing.adjustments.write",
},
{
name: "功能开关发布",
ui: "src/components/admin/feature-flags-management.tsx",
api: "src/app/api/admin/feature-flags/route.ts",
permission: "ops.flags.write",
},
{
name: "易支付设置保存",
ui: "src/components/admin/payment-management.tsx",
api: "src/app/api/admin/epay-settings/route.ts",
permission: "billing.adjustments.write",
},
{
name: "管理员角色变更",
ui: "src/components/admin/administrators-resource.tsx",
api: "src/app/api/admin/administrators/route.ts",
permission: "admin.users.manage_roles",
},
] as const;
test("high-risk UI reauth permissions match their API guards", async (t) => {
for (const mapping of highRiskMappings) {
await t.test(mapping.name, () => {
const ui = source(mapping.ui);
const api = source(mapping.api);
assert.match(ui, /ReasonActionModal/);
assert.match(ui, new RegExp(`reauthPermission=[\\s\\S]{0,80}["']${mapping.permission.replaceAll(".", "\\.")}["']|reauthPermission="${mapping.permission.replaceAll(".", "\\.")}"`));
assert.match(api, new RegExp(`requireHighRiskAdminMutation\\(\\s*request,\\s*(?:permission|["']${mapping.permission.replaceAll(".", "\\.")}["']),?\\s*\\)`));
assert.match(ui, new RegExp(`permissions\\.includes\\(["']${mapping.permission.replaceAll(".", "\\.")}["']\\)`));
});
}
});
test("product management localizes product, billing and entitlement enums", () => {
const ui = source("src/components/admin/product-management.tsx");
for (const label of [
"点数包",
"体验套餐",
"订阅套餐",
"一次性",
"按天",
"按月",
"按年",
"使用权限",
"不限量",
"限额",
"点数",
"不重置",
"每个计费周期",
]) {
assert.match(ui, new RegExp(label));
}
assert.doesNotMatch(ui, /<Tag>\{value\}<\/Tag>/);
assert.doesNotMatch(ui, /\$\{item\.intervalCount\} \$\{item\.billingPeriod\}/);
});
test("model management uses ordinary admin mutation guards without reauth", () => {
const ui = source("src/components/admin/model-management.tsx");
const route = source("src/app/api/admin/models/route.ts");
const discoverRoute = source("src/app/api/admin/models/discover/route.ts");
for (const permission of ["models.write", "models.test", "models.publish", "models.rollback"]) {
assert.ok(ui.includes(`permissions.includes("${permission}")`));
}
assert.match(route, /requireAdminMutation\(request, permission\)/);
assert.doesNotMatch(route, /requireHighRiskAdminMutation/);
assert.match(discoverRoute, /requireAdminMutation\(request,"models\.test"\)/);
assert.doesNotMatch(discoverRoute, /requireHighRiskAdminMutation/);
assert.doesNotMatch(ui, /reauthPermission=[^\n]*models\./);
assert.doesNotMatch(ui, /验证并(?:保存|获取)/);
});
test("admin request failures are real Error instances and reason modal keeps failures visible", () => {
const providers = source("src/lib/admin/providers.ts");
const modal = source("src/components/admin/reason-action-modal.tsx");
assert.match(
providers,
/Object\.assign\(new Error\(message\),\s*\{\s*statusCode: response\.status,/,
);
assert.doesNotMatch(providers, /throw \{ message, statusCode/);
assert.match(modal, /catch \(error\)[\s\S]*setReauthError\(error instanceof Error \? error\.message/);
assert.match(modal, /name="reason"[\s\S]*required: true, whitespace: true/);
assert.match(modal, /await onSubmit\(reason\.trim\(\)\)/);
});
test("customer list is always masked and a single explicit reveal is audited each time", () => {
const providers = source("src/lib/admin/providers.ts");
const route = source("src/app/api/admin/customers/route.ts");
const ui = source("src/components/admin/users-resource.tsx");
const migration = source("supabase/migrations/20260806010000_admin_rbac.sql");
assert.match(providers, /customers: \{ read: "admin\.customers\.read" \}/);
assert.match(route, /revealUserId[\s\S]*requirePermission\("admin\.customers\.birth_data\.read"\)/);
assert.match(route, /\[session\.user\.id, \[parsedUserId\.data\], requestId\(request\)\]/);
const listBranch = route.slice(route.indexOf('await requirePermission("admin.customers.read")'));
assert.doesNotMatch(listBranch, /admin_read_customer_birth_data/);
assert.match(listBranch, /birthDate: null[\s\S]*birthTimeStatus: null[\s\S]*birthPlace: null[\s\S]*birthDataMasked: true/);
assert.match(migration, /cardinality\(p_target_user_ids\) <> 1/);
assert.match(ui, /revealUserId=\$\{encodeURIComponent\(userId\)\}/);
assert.match(ui, /再次读取并审计/);
assert.match(ui, /admin\.customers\.birth_data\.read/);
});
test("administrator UI supports all six roles and exposes last-owner protection", () => {
const ui = source("src/components/admin/administrators-resource.tsx");
const route = source("src/app/api/admin/administrators/route.ts");
const roles = source("src/components/admin/roles-resource.tsx");
for (const role of ["owner", "model_admin", "billing_admin", "operations", "support", "auditor"]) {
assert.match(ui, new RegExp(`["']${role}["']`));
}
assert.match(ui, /method: pendingAction\.action === "assign" \? "POST" : "DELETE"/);
const mutationSection = route.slice(route.indexOf("async function mutate"));
assert.match(mutationSection, /last_owner_protected/);
assert.match(mutationSection, /不能撤销最后一位 Owner,请先分配另一位 Owner/);
assert.doesNotMatch(route.slice(0, route.indexOf("async function mutate")), /last_owner_protected/);
assert.match(roles, /系统预置只读矩阵/);
assert.match(roles, /不提供角色权限矩阵写 API/);
const providers = source("src/lib/admin/providers.ts");
assert.match(providers, /roles: \{ read: "admin\.users\.read" \}/);
assert.doesNotMatch(providers, /roles: \{[^}]*write:/);
});