loadCallerAssignableLevel_audit_test.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. package auth
  2. import (
  3. "context"
  4. "errors"
  5. "testing"
  6. "perms-system-server/internal/consts"
  7. "perms-system-server/internal/loaders"
  8. "perms-system-server/internal/response"
  9. "perms-system-server/internal/testutil/mocks"
  10. "github.com/stretchr/testify/assert"
  11. "github.com/stretchr/testify/require"
  12. "github.com/zeromicro/go-zero/core/stores/sqlx"
  13. "go.uber.org/mock/gomock"
  14. )
  15. // ---------------------------------------------------------------------------
  16. // 覆盖目标:审计 M-R10-3 —— LoadCallerAssignableLevel 在一次请求内对同一 caller 只做
  17. // 一次 DB 读;CheckRoleLevelAgainst 不再访问 DB,给 BindRoles 这种"批量覆盖"的接口把
  18. // N 次 loadFreshMinPermsLevel 合并为 1 次。
  19. //
  20. // 核心断言口径:
  21. // 1. SuperAdmin / ADMIN / DEVELOPER 等全权调用者不打 DB(HasFullPerms=true 短路);
  22. // 2. MEMBER caller 打 1 次 FindMinPermsLevelByUserIdAndProductCode;
  23. // 3. caller.ErrNotFound → NoRole=true(不打翻 500);
  24. // 4. caller 其他 DB 错误 → fail-close 500(保持与 loadFreshMinPermsLevel 一致的口径,
  25. // 避免降级为"无角色 = 最低级"放行)。
  26. // 5. CheckRoleLevelAgainst 是纯函数,不访问 svcCtx。
  27. // ---------------------------------------------------------------------------
  28. // TC-1017: M-R10-3 —— SuperAdmin / ADMIN / DEVELOPER 走 HasFullPerms 短路,不触碰 DB。
  29. func TestLoadCallerAssignableLevel_FullPermsShortCircuit_NoDB(t *testing.T) {
  30. ctrl := gomock.NewController(t)
  31. t.Cleanup(ctrl.Finish)
  32. mockRole := mocks.NewMockSysRoleModel(ctrl)
  33. // 关键:没有 EXPECT.FindMinPermsLevelByUserIdAndProductCode —— 一旦被调用会 fail。
  34. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Role: mockRole})
  35. cases := []struct {
  36. name string
  37. caller *loaders.UserDetails
  38. }{
  39. {
  40. name: "SuperAdmin",
  41. caller: &loaders.UserDetails{UserId: 1, IsSuperAdmin: true, ProductCode: "p"},
  42. },
  43. {
  44. name: "ADMIN",
  45. caller: &loaders.UserDetails{UserId: 2, MemberType: consts.MemberTypeAdmin, ProductCode: "p"},
  46. },
  47. {
  48. name: "DEVELOPER",
  49. caller: &loaders.UserDetails{UserId: 3, MemberType: consts.MemberTypeDeveloper, ProductCode: "p"},
  50. },
  51. }
  52. for _, c := range cases {
  53. t.Run(c.name, func(t *testing.T) {
  54. snap, err := LoadCallerAssignableLevel(context.Background(), svcCtx, c.caller)
  55. require.NoError(t, err)
  56. assert.True(t, snap.HasFullPerms, "全权调用者必须落 HasFullPerms 分支")
  57. assert.False(t, snap.NoRole)
  58. })
  59. }
  60. }
  61. // TC-1018: M-R10-3 —— MEMBER caller 仅打 1 次 FindMinPermsLevelByUserIdAndProductCode;
  62. // 循环内对 N 个角色走 CheckRoleLevelAgainst 不再打 DB。
  63. func TestLoadCallerAssignableLevel_Member_ReadsDBOnce_ThenConstantTime(t *testing.T) {
  64. ctrl := gomock.NewController(t)
  65. t.Cleanup(ctrl.Finish)
  66. const callerId = int64(1001)
  67. const productCode = "pc_m_r10_3"
  68. mockRole := mocks.NewMockSysRoleModel(ctrl)
  69. // 关键断言:Times(1) 保证 N 个角色场景不会退化为 N 次 DB 读。
  70. mockRole.EXPECT().
  71. FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), callerId, productCode).
  72. Return(int64(100), nil).
  73. Times(1)
  74. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Role: mockRole})
  75. caller := &loaders.UserDetails{
  76. UserId: callerId,
  77. MemberType: consts.MemberTypeMember,
  78. ProductCode: productCode,
  79. }
  80. snap, err := LoadCallerAssignableLevel(context.Background(), svcCtx, caller)
  81. require.NoError(t, err)
  82. assert.False(t, snap.HasFullPerms)
  83. assert.False(t, snap.NoRole)
  84. assert.Equal(t, int64(100), snap.Level)
  85. // 模拟 BindRoles 批量覆盖的循环:5 个角色,全部走 CheckRoleLevelAgainst 的纯比较,
  86. // 任何一个角色额外打 DB 都会命中 gomock 的 "unexpected call" 断言。
  87. roleLevels := []int64{200, 150, 300, 120, 999}
  88. for _, rl := range roleLevels {
  89. if err := CheckRoleLevelAgainst(snap, rl); err != nil {
  90. t.Fatalf("role level %d should be assignable against caller level %d: %v", rl, snap.Level, err)
  91. }
  92. }
  93. // 同级与更高级一律拒绝(与 GuardRoleLevelAssignable 对称):
  94. for _, rl := range []int64{100, 50, 1} {
  95. err := CheckRoleLevelAgainst(snap, rl)
  96. var codeErr *response.CodeError
  97. require.True(t, errors.As(err, &codeErr), "同级或更高级必须返回 CodeError")
  98. assert.Equal(t, 403, codeErr.Code())
  99. }
  100. }
  101. // TC-1019: M-R10-3 —— caller 在该产品下无角色 → NoRole=true,不回滚 500。
  102. func TestLoadCallerAssignableLevel_Member_ErrNotFound_MapsToNoRole(t *testing.T) {
  103. ctrl := gomock.NewController(t)
  104. t.Cleanup(ctrl.Finish)
  105. mockRole := mocks.NewMockSysRoleModel(ctrl)
  106. mockRole.EXPECT().
  107. FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), int64(42), "pc").
  108. Return(int64(0), sqlx.ErrNotFound).
  109. Times(1)
  110. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Role: mockRole})
  111. caller := &loaders.UserDetails{
  112. UserId: 42,
  113. MemberType: consts.MemberTypeMember,
  114. ProductCode: "pc",
  115. }
  116. snap, err := LoadCallerAssignableLevel(context.Background(), svcCtx, caller)
  117. require.NoError(t, err, "ErrNotFound 必须被归一为 NoRole=true,不得外泄为 500")
  118. assert.False(t, snap.HasFullPerms)
  119. assert.True(t, snap.NoRole)
  120. // 验证 NoRole 的 caller 连最低级角色也无法分配(与修复前保持的业务语义一致)。
  121. err = CheckRoleLevelAgainst(snap, 999)
  122. var codeErr *response.CodeError
  123. require.True(t, errors.As(err, &codeErr))
  124. assert.Equal(t, 403, codeErr.Code())
  125. assert.Contains(t, codeErr.Error(), "没有可分配的角色等级")
  126. }
  127. // TC-1020: M-R10-3 —— caller 其他 DB 错误必须 fail-close 500,不得降级为 NoRole 放行。
  128. // 保证修复没有把"DB 抖动"悄悄压成"无角色 → 最低级 → 403"这种语义欺骗。
  129. func TestLoadCallerAssignableLevel_Member_DBError_FailClose500(t *testing.T) {
  130. ctrl := gomock.NewController(t)
  131. t.Cleanup(ctrl.Finish)
  132. dbErr := errors.New("driver: bad connection")
  133. mockRole := mocks.NewMockSysRoleModel(ctrl)
  134. mockRole.EXPECT().
  135. FindMinPermsLevelByUserIdAndProductCode(gomock.Any(), int64(7), "pc").
  136. Return(int64(0), dbErr).
  137. Times(1)
  138. svcCtx := mocks.NewMockServiceContext(mocks.MockModels{Role: mockRole})
  139. caller := &loaders.UserDetails{
  140. UserId: 7,
  141. MemberType: consts.MemberTypeMember,
  142. ProductCode: "pc",
  143. }
  144. _, err := LoadCallerAssignableLevel(context.Background(), svcCtx, caller)
  145. var codeErr *response.CodeError
  146. require.True(t, errors.As(err, &codeErr), "DB 抖动必须 fail-close 为 CodeError 而非 nil")
  147. assert.Equal(t, 500, codeErr.Code())
  148. }