rbac_test.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. // rbac 包纯单测(T6.5-c 验收):Permissions/MenuTree 解析逻辑(fake 仓储,不连 DB)。
  2. //
  3. // 覆盖:
  4. // - 超管 = 全量权限点 code(代码层放行,不查矩阵);
  5. // - 普通角色 = 仅矩阵内 code(空矩阵 → 空集);
  6. // - 菜单树:button 不进树 / 角色无权菜单不进树 / 父缺失提升顶级 / sort 序 / 空集返回非 nil []。
  7. package rbac
  8. import (
  9. "context"
  10. "testing"
  11. "yuansiwei/unified-backend/internal/org/model"
  12. )
  13. // fakePermRepo 内存 fake(满足 Resolver 所需两方法;避免引接口变更)。
  14. type fakePermRepo struct {
  15. all []model.Permission
  16. byRole map[string][]string
  17. }
  18. func (f *fakePermRepo) AllActive(ctx context.Context) ([]model.Permission, error) {
  19. if f.all == nil {
  20. return []model.Permission{}, nil
  21. }
  22. return f.all, nil
  23. }
  24. func (f *fakePermRepo) RolePermissionCodes(ctx context.Context, role string) ([]string, error) {
  25. v, ok := f.byRole[role]
  26. if !ok || v == nil {
  27. return []string{}, nil
  28. }
  29. return v, nil
  30. }
  31. // 编译期:Resolver 只依赖具体仓储两方法(此处显式声明依赖面,防误增方法)。
  32. var _ = (*fakePermRepo)(nil)
  33. func s(p string) *string { return &p }
  34. // seedPerms 最小集权限点(org 2 菜单 + 4 按钮 + 1 顶级组菜单 + 1 其下子菜单)。
  35. func seedPerms() []model.Permission {
  36. return []model.Permission{
  37. {Code: "org.institution.list", Type: "menu", Name: "机构管理", Path: s("/org/institutions"), Sort: 1, Status: "active"},
  38. {Code: "org.institution.get", Type: "button", Name: "机构详情", ParentCode: s("org.institution.list"), Sort: 2, Status: "active"},
  39. {Code: "org.campus.list", Type: "menu", Name: "校区管理", Path: s("/org/campuses"), Sort: 3, Status: "active"},
  40. {Code: "org.campus.get", Type: "button", Name: "校区详情", ParentCode: s("org.campus.list"), Sort: 4, Status: "active"},
  41. {Code: "sys.group", Type: "menu", Name: "系统", Sort: 5, Status: "active"},
  42. {Code: "sys.menu", Type: "menu", Name: "子项", ParentCode: s("sys.group"), Path: s("/sys/menu"), Sort: 6, Status: "active"},
  43. }
  44. }
  45. // TestPermissions_SuperAdmin 超管 = 全量(6 个 code,含 button)。
  46. func TestPermissions_SuperAdmin(t *testing.T) {
  47. r := NewResolver(&fakePermRepo{all: seedPerms()}, 0)
  48. codes, err := r.Permissions(context.Background(), model.RoleSuperAdmin)
  49. if err != nil {
  50. t.Fatalf("Permissions: %v", err)
  51. }
  52. if len(codes) != 6 {
  53. t.Fatalf("超管 code 数=%d(期望 6): %v", len(codes), codes)
  54. }
  55. }
  56. // TestPermissions_Academic 普通角色 = 仅矩阵内 code。
  57. func TestPermissions_Academic(t *testing.T) {
  58. repo := &fakePermRepo{
  59. all: seedPerms(),
  60. byRole: map[string][]string{
  61. "academic": {"org.institution.list", "org.institution.get", "org.campus.list", "org.campus.get"},
  62. },
  63. }
  64. r := NewResolver(repo, 0)
  65. codes, err := r.Permissions(context.Background(), "academic")
  66. if err != nil {
  67. t.Fatalf("Permissions: %v", err)
  68. }
  69. if len(codes) != 4 {
  70. t.Fatalf("academic code 数=%d(期望 4): %v", len(codes), codes)
  71. }
  72. }
  73. // TestPermissions_Empty 空矩阵角色 → 空集(非 nil)。
  74. func TestPermissions_Empty(t *testing.T) {
  75. r := NewResolver(&fakePermRepo{all: seedPerms()}, 0)
  76. codes, err := r.Permissions(context.Background(), "teacher")
  77. if err != nil {
  78. t.Fatalf("Permissions: %v", err)
  79. }
  80. if len(codes) != 0 {
  81. t.Fatalf("teacher code 数=%d(期望 0): %v", len(codes), codes)
  82. }
  83. }
  84. // TestMenuTree_Academic 菜单树:button 不进树、无权菜单不进树、顶级序稳定。
  85. func TestMenuTree_Academic(t *testing.T) {
  86. repo := &fakePermRepo{
  87. all: seedPerms(),
  88. byRole: map[string][]string{
  89. "academic": {"org.institution.list", "org.institution.get", "org.campus.list", "org.campus.get"},
  90. },
  91. }
  92. r := NewResolver(repo, 0)
  93. tree, err := r.MenuTree(context.Background(), "academic")
  94. if err != nil {
  95. t.Fatalf("MenuTree: %v", err)
  96. }
  97. if len(tree) != 2 {
  98. t.Fatalf("顶级菜单数=%d(期望 2:机构/校区): %+v", len(tree), tree)
  99. }
  100. if tree[0].Code != "org.institution.list" || tree[1].Code != "org.campus.list" {
  101. t.Fatalf("顶级序错: %+v", tree)
  102. }
  103. // 叶子:button 绝不进树(无 Children)
  104. for _, n := range tree {
  105. if len(n.Children) != 0 {
  106. t.Fatalf("academic 菜单不应有子节点: %+v", n)
  107. }
  108. if n.Path == nil {
  109. t.Fatalf("叶子菜单缺 path: %+v", n)
  110. }
  111. }
  112. }
  113. // TestMenuTree_ParentMissing 父节点被过滤(未授权/禁用)→ 子菜单提升为顶级。
  114. func TestMenuTree_ParentMissing(t *testing.T) {
  115. repo := &fakePermRepo{
  116. all: seedPerms(),
  117. byRole: map[string][]string{
  118. // 只有子菜单 sys.menu,父 sys.group 不在角色集 → sys.menu 提升顶级
  119. "teacher": {"sys.menu"},
  120. },
  121. }
  122. r := NewResolver(repo, 0)
  123. tree, err := r.MenuTree(context.Background(), "teacher")
  124. if err != nil {
  125. t.Fatalf("MenuTree: %v", err)
  126. }
  127. if len(tree) != 1 || tree[0].Code != "sys.menu" {
  128. t.Fatalf("父缺失未提升顶级: %+v", tree)
  129. }
  130. }
  131. // TestMenuTree_Empty 无可见菜单 → 返回非 nil 空切片(JSON 为 [] 而非 null)。
  132. func TestMenuTree_Empty(t *testing.T) {
  133. r := NewResolver(&fakePermRepo{all: seedPerms()}, 0)
  134. tree, err := r.MenuTree(context.Background(), "student")
  135. if err != nil {
  136. t.Fatalf("MenuTree: %v", err)
  137. }
  138. if tree == nil {
  139. t.Fatal("空菜单树返回 nil(应非 nil [])")
  140. }
  141. if len(tree) != 0 {
  142. t.Fatalf("student 菜单数=%d(期望 0): %+v", len(tree), tree)
  143. }
  144. }
  145. // TestMenuTree_SuperAdmin 超管 = 全量 menu(4 个顶级:2 org + sys.group;sys.menu 挂在 sys.group 下)。
  146. func TestMenuTree_SuperAdmin(t *testing.T) {
  147. r := NewResolver(&fakePermRepo{all: seedPerms()}, 0)
  148. tree, err := r.MenuTree(context.Background(), model.RoleSuperAdmin)
  149. if err != nil {
  150. t.Fatalf("MenuTree: %v", err)
  151. }
  152. if len(tree) != 3 { // org.institution.list / org.campus.list / sys.group
  153. t.Fatalf("超管顶级菜单数=%d(期望 3): %+v", len(tree), tree)
  154. }
  155. var sys *MenuNode
  156. for i := range tree {
  157. if tree[i].Code == "sys.group" {
  158. sys = &tree[i]
  159. }
  160. }
  161. if sys == nil || len(sys.Children) != 1 || sys.Children[0].Code != "sys.menu" {
  162. t.Fatalf("sys.group 子菜单缺失: %+v", sys)
  163. }
  164. }