deptTreeLogic_test.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. package dept
  2. import (
  3. "context"
  4. "testing"
  5. "perms-system-server/internal/consts"
  6. "perms-system-server/internal/loaders"
  7. "perms-system-server/internal/middleware"
  8. deptModel "perms-system-server/internal/model/dept"
  9. "perms-system-server/internal/testutil/mocks"
  10. "github.com/stretchr/testify/assert"
  11. "github.com/stretchr/testify/require"
  12. "go.uber.org/mock/gomock"
  13. )
  14. // ---------------------------------------------------------------------------
  15. // 覆盖目标:非超管/非 ADMIN 调 /api/dept/tree 时必须按
  16. // caller.DeptPath 前缀过滤部门,只返回以其为根的子树。避免:
  17. // * MEMBER 级账号枚举全公司组织结构;
  18. // * 定位 DEV 部门再针对性申请权限。
  19. //
  20. // ADMIN / SuperAdmin 保留完整树(运营使用场景)。
  21. //
  22. // 测试数据:一棵 "/100/" 根下挂 "/100/1/"、"/100/1/5/",以及一个平行分支 "/200/"。
  23. // 期望:caller DeptPath="/100/1/" 只能看到 "/100/1/" 和 "/100/1/5/"。
  24. // ---------------------------------------------------------------------------
  25. var allDepts = []*deptModel.SysDept{
  26. {Id: 100, ParentId: 0, Path: "/100/", Name: "root"},
  27. {Id: 1, ParentId: 100, Path: "/100/1/", Name: "childA"},
  28. {Id: 5, ParentId: 1, Path: "/100/1/5/", Name: "grandchild"},
  29. {Id: 2, ParentId: 100, Path: "/100/2/", Name: "childB"},
  30. {Id: 200, ParentId: 0, Path: "/200/", Name: "siblingRoot"},
  31. }
  32. func ctxWith(caller *loaders.UserDetails) context.Context {
  33. return middleware.WithUserDetails(context.Background(), caller)
  34. }
  35. // TC-0855: MEMBER DeptPath="/100/1/" 应只看到 "/100/1/" 和 "/100/1/5/",且局部根就是 "/100/1/"。
  36. func TestDeptTree_Member_PrunedToSubtree(t *testing.T) {
  37. ctrl := gomock.NewController(t)
  38. t.Cleanup(ctrl.Finish)
  39. deptMock := mocks.NewMockSysDeptModel(ctrl)
  40. deptMock.EXPECT().FindAll(gomock.Any()).Return(allDepts, nil)
  41. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Dept: deptMock})
  42. caller := &loaders.UserDetails{
  43. UserId: 42, IsSuperAdmin: false, MemberType: consts.MemberTypeMember,
  44. DeptId: 1, DeptPath: "/100/1/", ProductCode: "pA",
  45. }
  46. tree, err := NewDeptTreeLogic(ctxWith(caller), svcCtx).DeptTree()
  47. require.NoError(t, err)
  48. // 剪枝后只剩 2 个节点;根仍应只有一个(id=1,grandchild 挂在其下)。
  49. require.Len(t, tree, 1, "剪枝后根只剩 1 个(caller 所在部门)")
  50. assert.Equal(t, int64(1), tree[0].Id, "局部根必须是 /100/1/,不得把 /100/ 也暴露")
  51. assert.Equal(t, "/100/1/", tree[0].Path)
  52. require.Len(t, tree[0].Children, 1, "grandchild 必须挂在局部根下")
  53. assert.Equal(t, int64(5), tree[0].Children[0].Id)
  54. }
  55. // TC-0856: MEMBER DeptPath="" —— 返回空切片,即使 DB 有数据。
  56. func TestDeptTree_OrphanMember_ReturnsEmpty(t *testing.T) {
  57. ctrl := gomock.NewController(t)
  58. t.Cleanup(ctrl.Finish)
  59. deptMock := mocks.NewMockSysDeptModel(ctrl)
  60. // 注意:当前实现会先 FindAll 再剪枝,空 DeptPath 直接走空返回,但 FindAll 仍被调用一次(有些成本),
  61. // 这里设置 AnyTimes 适配 "就算调用一次也可以"。
  62. deptMock.EXPECT().FindAll(gomock.Any()).Return(allDepts, nil).AnyTimes()
  63. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Dept: deptMock})
  64. caller := &loaders.UserDetails{
  65. UserId: 43, IsSuperAdmin: false, MemberType: consts.MemberTypeMember,
  66. DeptId: 0, DeptPath: "", ProductCode: "pA",
  67. }
  68. tree, err := NewDeptTreeLogic(ctxWith(caller), svcCtx).DeptTree()
  69. require.NoError(t, err)
  70. assert.Len(t, tree, 0, "DeptPath 为空必须返回空树,不能泄露组织结构")
  71. }
  72. // TC-0857: 产品 ADMIN —— 视为 fullAccess,返回完整树(两个根)。
  73. func TestDeptTree_Admin_FullTree(t *testing.T) {
  74. ctrl := gomock.NewController(t)
  75. t.Cleanup(ctrl.Finish)
  76. deptMock := mocks.NewMockSysDeptModel(ctrl)
  77. deptMock.EXPECT().FindAll(gomock.Any()).Return(allDepts, nil)
  78. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Dept: deptMock})
  79. caller := &loaders.UserDetails{
  80. UserId: 2, IsSuperAdmin: false, MemberType: consts.MemberTypeAdmin,
  81. DeptId: 100, DeptPath: "/100/", ProductCode: "pA",
  82. }
  83. tree, err := NewDeptTreeLogic(ctxWith(caller), svcCtx).DeptTree()
  84. require.NoError(t, err)
  85. // 完整树:根有 2 个(id=100, id=200)。
  86. require.Len(t, tree, 2, "ADMIN 应看到完整部门树,包括兄弟分支")
  87. var rootIds []int64
  88. for _, r := range tree {
  89. rootIds = append(rootIds, r.Id)
  90. }
  91. assert.ElementsMatch(t, []int64{100, 200}, rootIds)
  92. }