import { resolveScope, scopeFor, scopeOf } from './acl'; import { Me } from './api/models'; /** * ACL 判级单测(SPEC FR-20 / 《API-契约》§五) * * 🔴 判级移植自 v1 `fmode-app/src/app/core/acl.ts:50-69`,重点回归: * ① **无 me → guest → `'mine'`**(安全默认,最不能错的一条) * ② 超管优先于公司角色(且不要求有公司) * ③ **管理员与成员必须区分**(company-admin vs company-member) * ④ **当前**两者都请求 `'company'`(用户裁决:同公司就看全公司) * * 本判定只选「请求哪个 scope」,**不做视图放行** —— 见 acl.ts 注释。 */ describe('acl 判级', () => { const me = (over: Partial): Me => ({ id: 'u1', name: 'U', ...over }); describe('🔴 安全默认:无 me 一律 mine', () => { it('me 为 null → guest → mine', () => { expect(resolveScope(null).level).toBe('guest'); expect(scopeFor(null)).toBe('mine'); }); it('me 为 undefined → mine', () => { expect(scopeFor(undefined)).toBe('mine'); }); }); describe('超管(不要求有公司)', () => { it('roles 含 masters → superadmin → all', () => { expect(resolveScope(me({ roles: ['masters'] })).level).toBe('superadmin'); expect(scopeFor(me({ roles: ['masters'] }))).toBe('all'); }); it('roles 含 FMODE_AGENT_SUPERADMIN → all', () => { expect(scopeFor(me({ roles: ['FMODE_AGENT_SUPERADMIN'] }))).toBe('all'); }); it('🔴 超管即使无 company 也是 superadmin → all', () => { expect(resolveScope(me({ roles: ['masters'] })).level).toBe('superadmin'); expect(scopeFor(me({ roles: ['masters'] }))).toBe('all'); }); it('超管优先于公司管理员', () => { expect(scopeFor(me({ roles: ['masters', 'C1_Admin'], companyId: 'C1' }))).toBe('all'); }); it('username 白名单 FMODE_AGENT_SUPERADMIN → superadmin', () => { expect(resolveScope(me({ name: 'FMODE_AGENT_SUPERADMIN' })).level).toBe('superadmin'); }); }); describe('🔥 有公司:管理员 vs 成员(必须区分)', () => { it('roles 含 _Admin → company-admin', () => { const r = resolveScope(me({ roles: ['C1_Admin'], companyId: 'C1' })); expect(r.level).toBe('company-admin'); expect(r.companyId).toBe('C1'); expect(scopeOf(r)).toBe('company'); }); it('有公司但无 _Admin 角色 → company-member,请求 company', () => { const r = resolveScope(me({ roles: [], companyId: 'C1' })); expect(r.level).toBe('company-member'); expect(r.companyId).toBe('C1'); /* * 2026-11-04 用户裁决变更:成员也请求 company。 * 通讯录需要按「同事 → 其名下容器」组织,看不到同事的容器就无法交流。 * 跨租户隔离仍由服务端按 company 字段强制过滤。 */ expect(scopeOf(r)).toBe('company'); }); it('🔴 2026-11-04 裁决:成员请求 company(可看到本公司全员容器)', () => { expect(scopeFor(me({ companyId: 'C1' }))).toBe('company'); expect(scopeFor(me({ roles: ['whatever'], companyId: 'C1' }))).toBe('company'); }); it('管理员仍然请求 company(与成员区分)', () => { expect(scopeFor(me({ roles: ['C1_Admin'], companyId: 'C1' }))).toBe('company'); }); it('角色前缀与 companyId 不匹配 → company-member(不是管理员)', () => { const r = resolveScope(me({ roles: ['C2_Admin'], companyId: 'C1' })); expect(r.level).toBe('company-member'); }); }); describe('无公司 → personal → mine', () => { it('无 companyId → personal → mine', () => { const r = resolveScope(me({ roles: ['whatever'] })); expect(r.level).toBe('personal'); expect(scopeOf(r)).toBe('mine'); }); it('无 companyId 但有无关角色 → mine', () => { expect(scopeFor(me({ roles: ['some_other_role'] }))).toBe('mine'); }); }); describe('标签(UI 展示用)', () => { it('各级别都有中文标签', () => { expect(resolveScope(null).label).toBe('未登录'); expect(resolveScope(me({ roles: ['masters'] })).label).toBe('系统超管'); expect(resolveScope(me({ roles: ['C1_Admin'], companyId: 'C1' })).label).toBe('公司管理员'); expect(resolveScope(me({ companyId: 'C1' })).label).toBe('公司成员'); expect(resolveScope(me({})).label).toBe('个人用户'); }); }); describe('返回类型受约束', () => { it('scopeFor 只返回 all / company / mine', () => { const cases: (Me | null)[] = [ null, me({}), me({ roles: ['masters'] }), me({ roles: ['C1_Admin'], companyId: 'C1' }), me({ companyId: 'C1' }), ]; for (const c of cases) expect(['all', 'company', 'mine']).toContain(scopeFor(c)); }); }); });