| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178 |
- // 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)
- }
- }
|