updateRoleLogic_test.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. package role
  2. import (
  3. "errors"
  4. "testing"
  5. "time"
  6. "perms-system-server/internal/consts"
  7. roleModel "perms-system-server/internal/model/role"
  8. "perms-system-server/internal/response"
  9. "perms-system-server/internal/svc"
  10. "perms-system-server/internal/testutil"
  11. "perms-system-server/internal/testutil/ctxhelper"
  12. "perms-system-server/internal/testutil/mocks"
  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/sqlx"
  17. "go.uber.org/mock/gomock"
  18. )
  19. // TC-0120: 正常更新
  20. func TestUpdateRole_Normal(t *testing.T) {
  21. ctx := ctxhelper.SuperAdminCtx()
  22. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  23. conn := testutil.GetTestSqlConn()
  24. now := time.Now().Unix()
  25. pc := testutil.UniqueId()
  26. res, err := svcCtx.SysRoleModel.Insert(ctx, &roleModel.SysRole{
  27. ProductCode: pc, Name: testutil.UniqueId(), Status: 1, PermsLevel: 1,
  28. CreateTime: now, UpdateTime: now,
  29. })
  30. require.NoError(t, err)
  31. roleId, _ := res.LastInsertId()
  32. t.Cleanup(func() {
  33. testutil.CleanTable(ctx, conn, "`sys_role`", roleId)
  34. })
  35. newName := testutil.UniqueId()
  36. logic := NewUpdateRoleLogic(ctx, svcCtx)
  37. err = logic.UpdateRole(&types.UpdateRoleReq{
  38. Id: roleId,
  39. Name: newName,
  40. Remark: "updated remark",
  41. PermsLevel: 2,
  42. Status: 2,
  43. })
  44. require.NoError(t, err)
  45. updated, err := svcCtx.SysRoleModel.FindOne(ctx, roleId)
  46. require.NoError(t, err)
  47. assert.Equal(t, newName, updated.Name)
  48. assert.Equal(t, "updated remark", updated.Remark)
  49. assert.Equal(t, int64(2), updated.PermsLevel)
  50. assert.Equal(t, int64(2), updated.Status)
  51. }
  52. // TC-0121: 不存在
  53. func TestUpdateRole_NotFound(t *testing.T) {
  54. ctx := ctxhelper.SuperAdminCtx()
  55. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  56. logic := NewUpdateRoleLogic(ctx, svcCtx)
  57. err := logic.UpdateRole(&types.UpdateRoleReq{
  58. Id: 999999999,
  59. Name: "whatever",
  60. PermsLevel: 1,
  61. })
  62. require.Error(t, err)
  63. var ce *response.CodeError
  64. require.True(t, errors.As(err, &ce))
  65. assert.Equal(t, 404, ce.Code())
  66. assert.Equal(t, "角色不存在", ce.Error())
  67. }
  68. // TC-0539: updateRole非管理员拒绝
  69. func TestUpdateRole_MemberRejected(t *testing.T) {
  70. pc := "test_product"
  71. ctx := ctxhelper.MemberCtx(pc)
  72. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  73. conn := testutil.GetTestSqlConn()
  74. now := time.Now().Unix()
  75. roleRes, err := svcCtx.SysRoleModel.Insert(ctx, &roleModel.SysRole{
  76. ProductCode: pc, Name: testutil.UniqueId(), Status: 1, PermsLevel: 1,
  77. CreateTime: now, UpdateTime: now,
  78. })
  79. require.NoError(t, err)
  80. roleId, _ := roleRes.LastInsertId()
  81. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_role`", roleId) })
  82. logic := NewUpdateRoleLogic(ctx, svcCtx)
  83. err = logic.UpdateRole(&types.UpdateRoleReq{Id: roleId, Name: "test", PermsLevel: 1})
  84. require.Error(t, err)
  85. var ce *response.CodeError
  86. require.True(t, errors.As(err, &ce))
  87. assert.Equal(t, 403, ce.Code())
  88. }
  89. // 覆盖目标:事务已 COMMIT 成功后,
  90. // 任何缓存清理路径的失败只应记 Errorf,不得把 degraded 成功映射成 5xx 让客户端误触发重试。
  91. // adminCtx helper 定义于 bindRolePermsLogic_test.go (同 package role)。
  92. // TC-1119: L-R14-1 非超管 UpdateRole 访问别产品的 roleId 必须返回 404 "角色不存在",
  93. // 与 RoleDetail 的 M-N3 口径一致,消除 404 vs 403 的跨产品 roleId 枚举 oracle。
  94. func TestUpdateRole_L_R14_1_CrossProductReturns404(t *testing.T) {
  95. ctx := ctxhelper.SuperAdminCtx()
  96. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  97. conn := testutil.GetTestSqlConn()
  98. now := time.Now().Unix()
  99. otherProduct := "l_r14_1_upd_" + testutil.UniqueId()
  100. res, err := svcCtx.SysRoleModel.Insert(ctx, &roleModel.SysRole{
  101. ProductCode: otherProduct, Name: testutil.UniqueId(), Status: 1, PermsLevel: 1,
  102. CreateTime: now, UpdateTime: now,
  103. })
  104. require.NoError(t, err)
  105. roleId, _ := res.LastInsertId()
  106. t.Cleanup(func() { testutil.CleanTable(ctx, conn, "`sys_role`", roleId) })
  107. adminCtx := ctxhelper.AdminCtx("test_product")
  108. err = NewUpdateRoleLogic(adminCtx, svcCtx).UpdateRole(&types.UpdateRoleReq{
  109. Id: roleId, Name: "should_not_update", PermsLevel: 1,
  110. })
  111. require.Error(t, err)
  112. var ce *response.CodeError
  113. require.True(t, errors.As(err, &ce))
  114. assert.Equal(t, 404, ce.Code(),
  115. "L-R14-1:跨产品 roleId 必须 404,不得以 403 暴露存在性")
  116. assert.Equal(t, "角色不存在", ce.Error(),
  117. "L-R14-1:文案必须与 'id 不存在' 完全一致,彻底消除枚举 oracle")
  118. // DB 不得被污染
  119. after, err := svcCtx.SysRoleModel.FindOne(ctx, roleId)
  120. require.NoError(t, err)
  121. assert.NotEqual(t, "should_not_update", after.Name,
  122. "跨产品被拒绝的请求不得对 DB 产生任何副作用")
  123. }
  124. // TC-0859: UpdateRole —— UpdateWithOptLock 成功,FindUserIdsByRoleId 失败,handler 返回 nil。
  125. func TestUpdateRole_PostCommitUserIdsError_StaysSuccess(t *testing.T) {
  126. ctrl := gomock.NewController(t)
  127. t.Cleanup(ctrl.Finish)
  128. roleMock := mocks.NewMockSysRoleModel(ctrl)
  129. urMock := mocks.NewMockSysUserRoleModel(ctrl)
  130. roleMock.EXPECT().FindOne(gomock.Any(), int64(9)).
  131. Return(&roleModel.SysRole{
  132. Id: 9, ProductCode: "pc_m4u", Name: "before",
  133. PermsLevel: 50, Status: consts.StatusEnabled, UpdateTime: 100,
  134. }, nil)
  135. // UpdateWithOptLock 成功;签名:UpdateWithOptLock(ctx, role, prevUpdateTime)。
  136. roleMock.EXPECT().UpdateWithOptLock(gomock.Any(), gomock.Any(), int64(100)).Return(nil)
  137. // 审计 H-R18-1:rename(before→after) 触发 InvalidateRoleCache(prevName),属于 post-commit 尽力而为,
  138. // 无返回值,mock 仅注册期望、不校验结果——符合"缓存失效失败只记日志"的语义。
  139. roleMock.EXPECT().InvalidateRoleCache(gomock.Any(), int64(9), "pc_m4u", "before")
  140. // 关键断言:post-commit transient err 不应导致 handler 失败。
  141. urMock.EXPECT().FindUserIdsByRoleId(gomock.Any(), int64(9)).
  142. Return(nil, errors.New("boom"))
  143. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{
  144. Role: roleMock, UserRole: urMock,
  145. })
  146. err := NewUpdateRoleLogic(adminCtx("pc_m4u"), svcCtx).UpdateRole(&types.UpdateRoleReq{
  147. Id: 9, Name: "after", Remark: "r", PermsLevel: 60, Status: 0,
  148. })
  149. assert.NoError(t, err,
  150. "UpdateRole 已提交成功,post-commit 缓存失败只记日志,handler 必须返回 nil")
  151. }
  152. // TC-1204: UpdateRole 重命名后旧 name 索引缓存必须失效。
  153. // UpdateWithOptLock 内部只失效新 name 键;rename 路径的旧 name 键必须在 post-commit 由
  154. // InvalidateRoleCache(prevName) 显式清除,否则 Redis TTL 窗口内同名并发创建会命中幽灵快照。
  155. func TestUpdateRole_RenameInvalidatesOldNameCache(t *testing.T) {
  156. ctx := ctxhelper.SuperAdminCtx()
  157. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  158. conn := testutil.GetTestSqlConn()
  159. now := time.Now().Unix()
  160. pc := testutil.UniqueId()
  161. pid := mustInsertEnabledProduct(t, ctx, svcCtx, pc)
  162. oldName := "old_" + testutil.UniqueId()
  163. roleRes, err := svcCtx.SysRoleModel.Insert(ctx, &roleModel.SysRole{
  164. ProductCode: pc, Name: oldName,
  165. Status: consts.StatusEnabled, PermsLevel: 10, CreateTime: now, UpdateTime: now,
  166. })
  167. require.NoError(t, err)
  168. roleId, _ := roleRes.LastInsertId()
  169. t.Cleanup(func() {
  170. testutil.CleanTable(ctx, conn, "`sys_role`", roleId)
  171. testutil.CleanTable(ctx, conn, "`sys_product`", pid)
  172. })
  173. // 第一次查询,让 sysRole:productCode:name 索引键写入 Redis 缓存。
  174. _, err = svcCtx.SysRoleModel.FindOneByProductCodeName(ctx, pc, oldName)
  175. require.NoError(t, err)
  176. newName := "new_" + testutil.UniqueId()
  177. require.NoError(t, NewUpdateRoleLogic(ctx, svcCtx).UpdateRole(&types.UpdateRoleReq{
  178. Id: roleId, Name: newName, Remark: "renamed", PermsLevel: 10,
  179. }))
  180. // 重命名后,旧 name 对应的 Redis 缓存键必须失效,否则 FindOneByProductCodeName 仍会返回旧行。
  181. _, err = svcCtx.SysRoleModel.FindOneByProductCodeName(ctx, pc, oldName)
  182. require.Error(t, err,
  183. "rename 后旧 name 索引缓存键必须被 InvalidateRoleCache(prevName) 清除;"+
  184. "若残留则 FindOneByProductCodeName 会返回已改名的旧行,形成幽灵快照")
  185. require.True(t, errors.Is(err, sqlx.ErrNotFound),
  186. "旧 name 缓存失效后,FindOneByProductCodeName 应返回 ErrNotFound 而非旧行")
  187. // 新 name 应能正常查询到。
  188. found, err := svcCtx.SysRoleModel.FindOneByProductCodeName(ctx, pc, newName)
  189. require.NoError(t, err)
  190. assert.Equal(t, roleId, found.Id)
  191. assert.Equal(t, newName, found.Name)
  192. }