import { describe, expect, it } from 'vitest'; /** * 前端不做权限和状态判断,只读后端给的能力、按后端的错误码处理。规则只在后端一处,这里守住前端不要重新实现: * - 角色/权限:界面只通过 domain/capabilities 读后端给的 can_* 字段,不比较角色、不写 isOwner 判断; * - 选哪张令牌:只有 tokenRole 一处; * - 服务端为什么拒绝:只通过 hasCode(错误码),不比较 HTTP 状态码数字。 * 允许的例外写在 allowed 里,并说明理由。 */ const modules = import.meta.glob('/src/**/*.{ts,tsx}', { query: '?raw', import: 'default', eager: true }); const files = Object.entries(modules) .map(([path, text]) => ({ path: path.replace(/^\/src\//, ''), text })) .filter(({ path }) => !/\.test\.(ts|tsx)$/.test(path) && !path.startsWith('api/generated')); interface Rule { name: string; pattern: RegExp; allowed: string[] } const RULES: Rule[] = [ { name: '不用 session.can(...) 自己判断权限(选令牌的 tokenRole、必须持有所有者密钥才能做的密码学操作、分享的所有权释放除外)', pattern: /session\.can\('(owner|write)'\)/, allowed: ['worker/runtime/sessions.ts', 'worker/crypto/channelDataKeys.ts', 'worker/shares/shareState.ts', 'worker/shares/shareSeal.ts', 'worker/space/channelResources.ts', 'worker/space/channelTransport.ts'] }, { name: '不比较 HTTP 状态码数字(请用 hasCode / isRetryable)', pattern: /\.status\s*(===|!==|>=|<=|<|>)\s*\d{3}\b/, allowed: ['worker/runtime/http.ts'] }, { name: '不比较角色字符串(成员行的展示、授权包的密码学校验除外;分享的 mine 待后端提供)', pattern: /\.role\s*(===|!==)\s*'(owner|member|write|read)'/, allowed: ['features/channel/members/MembersPane.tsx', 'features/channel/members/RolesEditor.tsx', 'worker/account/accountChannels.ts', 'worker/crypto/channelGrant.ts', 'worker/runtime/wire.ts'] }, ]; describe('前端不重新实现后端的规则', () => { it('扫描到了源码', () => { expect(files.length).toBeGreaterThan(100); }); for (const rule of RULES) { it(rule.name, () => { const offenders = files .filter(({ path }) => !rule.allowed.includes(path)) .flatMap(({ path, text }) => text.split('\n') .map((line, index) => ({ path, line: index + 1, text: line.trim() })) .filter(({ text: line }) => !line.startsWith('//') && !line.startsWith('*') && rule.pattern.test(line)) .map(({ path: p, line, text: source }) => `${p}:${line} ${source}`)); expect(offenders, `这些地方重新实现了后端的规则:\n${offenders.join('\n')}`).toEqual([]); }); } });