Files
Jyotisha/frontend/tests/admin-ui-permission-contract.test.ts
T
Jesse_Chen f4a2ba86ae
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled
fix: restore mobile report scrolling and published product edits
Report pages now scroll inside the chat shell lock, and admin product save forks a draft or retires a published plan instead of rejecting with a generic constraint error.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-18 17:28:22 +08:00

188 lines
7.7 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 mutationMappings = [
{
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/product-management.tsx",
api: "src/app/api/admin/products/route.ts",
permission: "billing.products.write",
},
{
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: "admin.access",
},
{
name: "兑换码编辑与撤销",
ui: "src/components/admin/codes-resource.tsx",
api: "src/app/api/admin/codes/[id]/route.ts",
permission: "admin.access",
},
{
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("admin mutation UI permissions match ordinary API guards without operation reauth", async (t) => {
for (const mapping of mutationMappings) {
await t.test(mapping.name, () => {
const ui = source(mapping.ui);
const api = source(mapping.api);
assert.doesNotMatch(ui, /ConfirmActionModal|Popconfirm|Modal\.confirm/);
assert.doesNotMatch(ui, /reauthPermission|邮箱验证码|\/api\/admin\/reauth/);
assert.match(api, new RegExp(String.raw`requireAdminMutation\(\s*request,\s*(?:permission|["']${mapping.permission.replaceAll(".", "\\.")}["']),?\s*\)`));
assert.doesNotMatch(api, /requireHighRiskAdminMutation/);
assert.match(ui, new RegExp(String.raw`permissions\.includes\(["']${mapping.permission.replaceAll(".", "\\.")}["']\)`));
});
}
});
test("redemption-code list and mutations are available to every authenticated admin role", () => {
const providers = source("src/lib/admin/providers.ts");
const route = source("src/app/api/admin/codes/route.ts");
assert.match(
providers,
/codes:\s*\{\s*read:\s*"admin\.access",\s*write:\s*"admin\.access"\s*\}/,
);
assert.match(route, /await requirePermission\("admin\.access"\)/);
});
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\}/);
assert.match(ui, /action: "delete"/);
assert.match(ui, /下架/);
assert.match(ui, /草稿已删除/);
assert.match(ui, /item\.status === "draft"/);
});
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 operation confirmation UI is absent", () => {
const providers = source("src/lib/admin/providers.ts");
assert.match(
providers,
/Object\.assign\(new Error\(message\),\s*\{\s*statusCode: response\.status,/,
);
assert.doesNotMatch(providers, /throw \{ message, statusCode/);
for (const path of mutationMappings.map((mapping) => mapping.ui)) {
assert.doesNotMatch(source(path), /ConfirmActionModal|Popconfirm|Modal\.confirm/);
}
});
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: action\.action === "assign" \? "POST" : "DELETE"/);
assert.doesNotMatch(ui, /pendingAction|ConfirmActionModal/);
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:/);
});