// rbac 包纯单测(T6.5-c 验收):Permissions/MenuTree 解析逻辑(fake 仓储,不连 DB)。 // // 覆盖: // - 超管 = 全量权限点 code(代码层放行,不查矩阵); // - 普通角色 = 仅矩阵内 code(空矩阵 → 空集); // - 菜单树:button 不进树 / 角色无权菜单不进树 / 父缺失提升顶级 / sort 序 / 空集返回非 nil []。 package rbac import ( "context" "testing" "yuansiwei/unified-backend/internal/org/model" ) // fakePermRepo 内存 fake(满足 Resolver 所需两方法;避免引接口变更)。 type fakePermRepo struct { all []model.Permission byRole map[string][]string } func (f *fakePermRepo) AllActive(ctx context.Context) ([]model.Permission, error) { if f.all == nil { return []model.Permission{}, nil } return f.all, nil } func (f *fakePermRepo) RolePermissionCodes(ctx context.Context, role string) ([]string, error) { v, ok := f.byRole[role] if !ok || v == nil { return []string{}, nil } return v, nil } // 编译期:Resolver 只依赖具体仓储两方法(此处显式声明依赖面,防误增方法)。 var _ = (*fakePermRepo)(nil) func s(p string) *string { return &p } // seedPerms 最小集权限点(org 2 菜单 + 4 按钮 + 1 顶级组菜单 + 1 其下子菜单)。 func seedPerms() []model.Permission { return []model.Permission{ {Code: "org.institution.list", Type: "menu", Name: "机构管理", Path: s("/org/institutions"), Sort: 1, Status: "active"}, {Code: "org.institution.get", Type: "button", Name: "机构详情", ParentCode: s("org.institution.list"), Sort: 2, Status: "active"}, {Code: "org.campus.list", Type: "menu", Name: "校区管理", Path: s("/org/campuses"), Sort: 3, Status: "active"}, {Code: "org.campus.get", Type: "button", Name: "校区详情", ParentCode: s("org.campus.list"), Sort: 4, Status: "active"}, {Code: "sys.group", Type: "menu", Name: "系统", Sort: 5, Status: "active"}, {Code: "sys.menu", Type: "menu", Name: "子项", ParentCode: s("sys.group"), Path: s("/sys/menu"), Sort: 6, Status: "active"}, } } // TestPermissions_SuperAdmin 超管 = 全量(6 个 code,含 button)。 func TestPermissions_SuperAdmin(t *testing.T) { r := NewResolver(&fakePermRepo{all: seedPerms()}, 0) codes, err := r.Permissions(context.Background(), model.RoleSuperAdmin) if err != nil { t.Fatalf("Permissions: %v", err) } if len(codes) != 6 { t.Fatalf("超管 code 数=%d(期望 6): %v", len(codes), codes) } } // TestPermissions_Academic 普通角色 = 仅矩阵内 code。 func TestPermissions_Academic(t *testing.T) { repo := &fakePermRepo{ all: seedPerms(), byRole: map[string][]string{ "academic": {"org.institution.list", "org.institution.get", "org.campus.list", "org.campus.get"}, }, } r := NewResolver(repo, 0) codes, err := r.Permissions(context.Background(), "academic") if err != nil { t.Fatalf("Permissions: %v", err) } if len(codes) != 4 { t.Fatalf("academic code 数=%d(期望 4): %v", len(codes), codes) } } // TestPermissions_Empty 空矩阵角色 → 空集(非 nil)。 func TestPermissions_Empty(t *testing.T) { r := NewResolver(&fakePermRepo{all: seedPerms()}, 0) codes, err := r.Permissions(context.Background(), "teacher") if err != nil { t.Fatalf("Permissions: %v", err) } if len(codes) != 0 { t.Fatalf("teacher code 数=%d(期望 0): %v", len(codes), codes) } } // TestMenuTree_Academic 菜单树:button 不进树、无权菜单不进树、顶级序稳定。 func TestMenuTree_Academic(t *testing.T) { repo := &fakePermRepo{ all: seedPerms(), byRole: map[string][]string{ "academic": {"org.institution.list", "org.institution.get", "org.campus.list", "org.campus.get"}, }, } r := NewResolver(repo, 0) tree, err := r.MenuTree(context.Background(), "academic") if err != nil { t.Fatalf("MenuTree: %v", err) } if len(tree) != 2 { t.Fatalf("顶级菜单数=%d(期望 2:机构/校区): %+v", len(tree), tree) } if tree[0].Code != "org.institution.list" || tree[1].Code != "org.campus.list" { t.Fatalf("顶级序错: %+v", tree) } // 叶子:button 绝不进树(无 Children) for _, n := range tree { if len(n.Children) != 0 { t.Fatalf("academic 菜单不应有子节点: %+v", n) } if n.Path == nil { t.Fatalf("叶子菜单缺 path: %+v", n) } } } // TestMenuTree_ParentMissing 父节点被过滤(未授权/禁用)→ 子菜单提升为顶级。 func TestMenuTree_ParentMissing(t *testing.T) { repo := &fakePermRepo{ all: seedPerms(), byRole: map[string][]string{ // 只有子菜单 sys.menu,父 sys.group 不在角色集 → sys.menu 提升顶级 "teacher": {"sys.menu"}, }, } r := NewResolver(repo, 0) tree, err := r.MenuTree(context.Background(), "teacher") if err != nil { t.Fatalf("MenuTree: %v", err) } if len(tree) != 1 || tree[0].Code != "sys.menu" { t.Fatalf("父缺失未提升顶级: %+v", tree) } } // TestMenuTree_Empty 无可见菜单 → 返回非 nil 空切片(JSON 为 [] 而非 null)。 func TestMenuTree_Empty(t *testing.T) { r := NewResolver(&fakePermRepo{all: seedPerms()}, 0) tree, err := r.MenuTree(context.Background(), "student") if err != nil { t.Fatalf("MenuTree: %v", err) } if tree == nil { t.Fatal("空菜单树返回 nil(应非 nil [])") } if len(tree) != 0 { t.Fatalf("student 菜单数=%d(期望 0): %+v", len(tree), tree) } } // TestMenuTree_SuperAdmin 超管 = 全量 menu(4 个顶级:2 org + sys.group;sys.menu 挂在 sys.group 下)。 func TestMenuTree_SuperAdmin(t *testing.T) { r := NewResolver(&fakePermRepo{all: seedPerms()}, 0) tree, err := r.MenuTree(context.Background(), model.RoleSuperAdmin) if err != nil { t.Fatalf("MenuTree: %v", err) } if len(tree) != 3 { // org.institution.list / org.campus.list / sys.group t.Fatalf("超管顶级菜单数=%d(期望 3): %+v", len(tree), tree) } var sys *MenuNode for i := range tree { if tree[i].Code == "sys.group" { sys = &tree[i] } } if sys == nil || len(sys.Children) != 1 || sys.Children[0].Code != "sys.menu" { t.Fatalf("sys.group 子菜单缺失: %+v", sys) } }