deleteDeptLogic_test.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. package dept
  2. import (
  3. "database/sql"
  4. "errors"
  5. "fmt"
  6. "testing"
  7. "time"
  8. userModel "perms-system-server/internal/model/user"
  9. "perms-system-server/internal/response"
  10. "perms-system-server/internal/svc"
  11. "perms-system-server/internal/testutil"
  12. "perms-system-server/internal/testutil/ctxhelper"
  13. "perms-system-server/internal/types"
  14. "github.com/stretchr/testify/assert"
  15. "github.com/stretchr/testify/require"
  16. "github.com/zeromicro/go-zero/core/stores/redis"
  17. )
  18. // TC-0106: 正常删除(无子部门)
  19. func TestDeleteDept_NoChildren(t *testing.T) {
  20. ctx := ctxhelper.SuperAdminCtx()
  21. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  22. conn := testutil.GetTestSqlConn()
  23. id, err := insertDeptRaw(ctx, svcCtx, 0, "del_"+testutil.UniqueId(), "/")
  24. require.NoError(t, err)
  25. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_dept`", id) })
  26. l := NewDeleteDeptLogic(ctx, svcCtx)
  27. err = l.DeleteDept(&types.DeleteDeptReq{Id: id})
  28. require.NoError(t, err)
  29. _, err = svcCtx.SysDeptModel.FindOne(ctx, id)
  30. assert.Error(t, err)
  31. }
  32. // TC-0108: 不存在的部门 (audit 修复后:放入事务 + SELECT ... FOR UPDATE,不存在时返回 404)
  33. func TestDeleteDept_NonExistentDept(t *testing.T) {
  34. ctx := ctxhelper.SuperAdminCtx()
  35. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  36. l := NewDeleteDeptLogic(ctx, svcCtx)
  37. err := l.DeleteDept(&types.DeleteDeptReq{Id: 999999999})
  38. require.Error(t, err, "删除不存在的部门应当显式返回 NotFound,而非静默成功")
  39. var ce *response.CodeError
  40. require.True(t, errors.As(err, &ce))
  41. assert.Equal(t, 404, ce.Code())
  42. assert.Contains(t, ce.Error(), "部门不存在")
  43. }
  44. // TC-0107: 有子部门
  45. func TestDeleteDept_HasChildren(t *testing.T) {
  46. ctx := ctxhelper.SuperAdminCtx()
  47. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  48. conn := testutil.GetTestSqlConn()
  49. parentId, err := insertDeptRaw(ctx, svcCtx, 0, "del_par_"+testutil.UniqueId(), "/")
  50. require.NoError(t, err)
  51. parent, _ := svcCtx.SysDeptModel.FindOne(ctx, parentId)
  52. childId, err := insertDeptRaw(ctx, svcCtx, parentId, "del_child_"+testutil.UniqueId(), parent.Path)
  53. require.NoError(t, err)
  54. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_dept`", childId, parentId) })
  55. l := NewDeleteDeptLogic(ctx, svcCtx)
  56. err = l.DeleteDept(&types.DeleteDeptReq{Id: parentId})
  57. require.Error(t, err)
  58. var ce *response.CodeError
  59. require.True(t, errors.As(err, &ce))
  60. assert.Equal(t, 400, ce.Code())
  61. assert.Contains(t, ce.Error(), "该部门下存在子部门,无法删除")
  62. _, err = svcCtx.SysDeptModel.FindOne(ctx, parentId)
  63. assert.NoError(t, err)
  64. }
  65. // TC-0109: 部门下有关联用户
  66. func TestDeleteDept_HasAssociatedUsers(t *testing.T) {
  67. ctx := ctxhelper.SuperAdminCtx()
  68. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  69. conn := testutil.GetTestSqlConn()
  70. deptId, err := insertDeptRaw(ctx, svcCtx, 0, "has_users_"+testutil.UniqueId(), "/")
  71. require.NoError(t, err)
  72. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_dept`", deptId) })
  73. now := time.Now().Unix()
  74. userRes, err := svcCtx.SysUserModel.Insert(ctx, &userModel.SysUser{
  75. Username: "dept_user_" + testutil.UniqueId(),
  76. Password: testutil.HashPassword("pass123456"),
  77. Nickname: "test",
  78. Avatar: sql.NullString{},
  79. DeptId: deptId,
  80. IsSuperAdmin: 2,
  81. MustChangePassword: 2,
  82. Status: 1,
  83. CreateTime: now,
  84. UpdateTime: now,
  85. })
  86. require.NoError(t, err)
  87. userId, _ := userRes.LastInsertId()
  88. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_user`", userId) })
  89. l := NewDeleteDeptLogic(ctx, svcCtx)
  90. err = l.DeleteDept(&types.DeleteDeptReq{Id: deptId})
  91. require.Error(t, err)
  92. var ce *response.CodeError
  93. require.True(t, errors.As(err, &ce))
  94. assert.Equal(t, 400, ce.Code())
  95. assert.Equal(t, "该部门下仍有关联用户,无法删除", ce.Error())
  96. _, err = svcCtx.SysDeptModel.FindOne(ctx, deptId)
  97. assert.NoError(t, err)
  98. }
  99. // TC-1200: H-R17-2 —— DeleteDept 成功后必须在 post-commit 上显式失效 sysDept 缓存。
  100. //
  101. // 攻击链:DeleteWithTx 内部走 sqlc.CachedConn.ExecCtx,其"exec → DelCache"钩子跑在
  102. // 外层 TransactCtx commit 之前。RR 隔离下其他事务在 commit 前按非锁读路径仍能看到旧行,
  103. // 并把它回填进 Redis;commit 之后 DB 没了该行,但 Redis 里挂着一张"幽灵快照"直到 TTL
  104. // 到期(5min)。叠加 H-R17-1 的 orphan user,幽灵 dept 会让该用户继续按 dept/Path 被
  105. // 授权。测试手工把 canary 写进 sysDeptIdKey,模拟上述"commit 后又被回填"的快照,
  106. // DeleteDept 在 post-commit 阶段必须把它删掉。
  107. func TestDeleteDept_H_R17_2_InvalidatesCacheAfterCommit(t *testing.T) {
  108. ctx := ctxhelper.SuperAdminCtx()
  109. cfg := testutil.GetTestConfig()
  110. svcCtx := svc.NewServiceContext(cfg)
  111. conn := testutil.GetTestSqlConn()
  112. rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf)
  113. id, err := insertDeptRaw(ctx, svcCtx, 0, "h_r17_2_"+testutil.UniqueId(), "/")
  114. require.NoError(t, err)
  115. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_dept`", id) })
  116. cacheKey := fmt.Sprintf("%s:cache:sysDept:id:%d", cfg.CacheRedis.KeyPrefix, id)
  117. t.Cleanup(func() { _, _ = rds.Del(cacheKey) })
  118. require.NoError(t, rds.Set(cacheKey, `{"Id":`+fmt.Sprint(id)+`,"Name":"ghost"}`),
  119. "预置 ghost snapshot 失败,Redis 不可用导致测试前置条件失败")
  120. pre, err := rds.Exists(cacheKey)
  121. require.NoError(t, err)
  122. require.True(t, pre, "canary 必须先出现在 Redis,作为 post-commit DEL 的观察对象")
  123. err = NewDeleteDeptLogic(ctx, svcCtx).DeleteDept(&types.DeleteDeptReq{Id: id})
  124. require.NoError(t, err)
  125. exists, err := rds.Exists(cacheKey)
  126. require.NoError(t, err)
  127. assert.False(t, exists,
  128. "H-R17-2:DeleteDept commit 之后 InvalidateDeptCache 必须把 sysDeptIdKey DEL 掉;"+
  129. "若 canary 残留 ≥1s,说明 post-commit 失效兜底被误撤,5min 内其他路径仍会读到 ghost dept,"+
  130. "叠加 orphan user 会产生跨部门授权泄漏")
  131. }
  132. // TC-0534: deleteDept非超管拒绝
  133. func TestDeleteDept_NonSuperAdminRejected(t *testing.T) {
  134. ctx := ctxhelper.AdminCtx("test_product")
  135. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  136. l := NewDeleteDeptLogic(ctx, svcCtx)
  137. err := l.DeleteDept(&types.DeleteDeptReq{Id: 1})
  138. require.Error(t, err)
  139. var ce *response.CodeError
  140. require.True(t, errors.As(err, &ce))
  141. assert.Equal(t, 403, ce.Code())
  142. }