| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122 |
- 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>): 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 含 <companyId>_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));
- });
- });
- });
|