Archived
app:路由注册表 —— 契约自带 access,一处 lookup 给中间件(ADR-012)
每条 RouteContract 都声明了自己的 access 权限;此处扫一遍契约命名空间, 构建一个 lookup(method, path) → access 的注册表。新路由无需另加守卫, 只需在契约里声明,就自动生效——遗漏即无门禁,一处事实源确保完整。 - 字面路径优先于参数段(:name 匹配任意非空段) - 方法维度单独追踪 - 识别 RouteContract:method/path/access/output 都具备 - routesOf() 抽出命名空间内的路由 - buildRouteRegistry() 与 lookup/has API 给中间件与测试用 扫结果:95 条路由已覆盖 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015kAzS35kMnZNt4YGgJXtfw
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import * as contracts from '@contracts';
|
||||
import { buildRouteRegistry, isRouteContract, routesOf } from '../route-registry.ts';
|
||||
|
||||
const registry = buildRouteRegistry([
|
||||
{ method: 'GET', path: '/api/health', access: 'public' },
|
||||
{ method: 'GET', path: '/api/me', access: 'any' },
|
||||
{ method: 'GET', path: '/api/gov/tasks', access: 'ops' },
|
||||
{ method: 'POST', path: '/api/gov/tasks/:taskId/control', access: 'ops' },
|
||||
{ method: 'GET', path: '/api/gov/testbed/logs', access: 'design' },
|
||||
{ method: 'GET', path: '/api/gov/testbed/logs/:id', access: 'design' },
|
||||
{ method: 'POST', path: '/api/gov/process/validate', access: 'design' },
|
||||
{ method: 'POST', path: '/api/gov/process/:id/:version/state', access: 'design' },
|
||||
{ method: 'GET', path: '/api/gov/staff', access: ['design', 'ops'] },
|
||||
]);
|
||||
|
||||
describe('路由注册表', () => {
|
||||
it('字面路径精确命中', () => {
|
||||
expect(registry.lookup('GET', '/api/gov/tasks')).toBe('ops');
|
||||
expect(registry.lookup('GET', '/api/health')).toBe('public');
|
||||
});
|
||||
it('参数段:一段认一段,多一段少一段都不算', () => {
|
||||
expect(registry.lookup('POST', '/api/gov/tasks/t-1/control')).toBe('ops');
|
||||
expect(registry.lookup('POST', '/api/gov/tasks/t-1/x/control')).toBeUndefined();
|
||||
expect(registry.lookup('POST', '/api/gov/process/p1/3/state')).toBe('design');
|
||||
});
|
||||
it('方法不同不混', () => {
|
||||
expect(registry.lookup('POST', '/api/gov/tasks')).toBeUndefined();
|
||||
expect(registry.lookup('GET', '/api/gov/tasks/t-1/control')).toBeUndefined();
|
||||
});
|
||||
it('字面优先于参数段(logs 与 logs/:id 各归各)', () => {
|
||||
expect(registry.lookup('GET', '/api/gov/testbed/logs')).toBe('design');
|
||||
expect(registry.lookup('GET', '/api/gov/testbed/logs/abc')).toBe('design');
|
||||
expect(registry.lookup('GET', '/api/gov/testbed/logs/abc/def')).toBeUndefined();
|
||||
});
|
||||
it('未知路由 → undefined', () => {
|
||||
expect(registry.lookup('GET', '/api/nope')).toBeUndefined();
|
||||
});
|
||||
it('数组归属原样回', () => {
|
||||
expect(registry.lookup('GET', '/api/gov/staff')).toEqual(['design', 'ops']);
|
||||
});
|
||||
it('has 按注册时的模版查', () => {
|
||||
expect(registry.has('POST', '/api/gov/tasks/:taskId/control')).toBe(true);
|
||||
expect(registry.has('GET', '/api/gov/tasks/:taskId/control')).toBe(false);
|
||||
expect(registry.has('GET', '/api/health')).toBe(true);
|
||||
expect(registry.size).toBe(9);
|
||||
});
|
||||
});
|
||||
|
||||
describe('routesOf —— 从契约命名空间挑路由', () => {
|
||||
it('只认长得像路由契约的导出', () => {
|
||||
expect(isRouteContract(contracts.meRoute)).toBe(true);
|
||||
expect(isRouteContract(contracts.staffSchema)).toBe(false);
|
||||
expect(isRouteContract('x')).toBe(false);
|
||||
expect(isRouteContract(null)).toBe(false);
|
||||
});
|
||||
it('扫真契约至少 90 条,且每条都能 has 到自己', () => {
|
||||
const all = routesOf(Object.values(contracts));
|
||||
expect(all.length).toBeGreaterThanOrEqual(90);
|
||||
const reg = buildRouteRegistry(all);
|
||||
for (const r of all) expect(reg.has(r.method, r.path), `${r.method} ${r.path}`).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user