loginService_test.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. package pub
  2. import (
  3. "context"
  4. "errors"
  5. "github.com/stretchr/testify/assert"
  6. "github.com/stretchr/testify/require"
  7. "github.com/zeromicro/go-zero/core/limit"
  8. "github.com/zeromicro/go-zero/core/stores/redis"
  9. "perms-system-server/internal/testutil"
  10. "testing"
  11. )
  12. func TestValidateProductLogin_FrozenWrongPassword_Return401(t *testing.T) {
  13. ctx := context.Background()
  14. svcCtx := newTestSvcCtx()
  15. svcCtx.UsernameLoginLimit = nil // 隔离本测试变量
  16. username := "h2_frozen_wrong_" + testutil.UniqueId()
  17. // status=2(冻结),isSuperAdmin=2(非超管)
  18. _, clean := insertRefreshTestUser(t, ctx, username, "CorrectPass123", 2, 2)
  19. t.Cleanup(clean)
  20. _, err := ValidateProductLogin(ctx, svcCtx, username, "WrongPass", "test_product", "127.0.0.1")
  21. require.Error(t, err)
  22. var le *LoginError
  23. require.True(t, errors.As(err, &le))
  24. assert.Equal(t, 401, le.Code,
  25. "冻结用户 + 错误密码不得返回 403;必须 401 与'用户不存在/密码错'三合一")
  26. assert.Equal(t, "用户名或密码错误", le.Message,
  27. "文案不得泄露冻结态")
  28. }
  29. // TC-0839: 冻结用户 + 正确密码 —— 此时才允许披露"账号已被冻结"(攻击者已经猜中密码,继续隐藏已无意义)。
  30. func TestValidateProductLogin_FrozenCorrectPassword_Return403(t *testing.T) {
  31. ctx := context.Background()
  32. svcCtx := newTestSvcCtx()
  33. svcCtx.UsernameLoginLimit = nil
  34. username := "h2_frozen_right_" + testutil.UniqueId()
  35. _, clean := insertRefreshTestUser(t, ctx, username, "RightPass123", 2, 2)
  36. t.Cleanup(clean)
  37. _, err := ValidateProductLogin(ctx, svcCtx, username, "RightPass123", "test_product", "127.0.0.1")
  38. require.Error(t, err)
  39. var le *LoginError
  40. require.True(t, errors.As(err, &le))
  41. assert.Equal(t, 403, le.Code, "密码正确后的冻结分支仍走 403 披露")
  42. assert.Equal(t, "账号已被冻结", le.Message)
  43. }
  44. // TC-0840: 超管走产品端 + 错误密码 —— 不得提前暴露"该账号是超管"。
  45. func TestValidateProductLogin_SuperAdminWrongPassword_Return401(t *testing.T) {
  46. ctx := context.Background()
  47. svcCtx := newTestSvcCtx()
  48. svcCtx.UsernameLoginLimit = nil
  49. username := "h2_sa_wrong_" + testutil.UniqueId()
  50. // status=1(启用),isSuperAdmin=1(超管)
  51. _, clean := insertRefreshTestUser(t, ctx, username, "RightPass123", 1, 1)
  52. t.Cleanup(clean)
  53. _, err := ValidateProductLogin(ctx, svcCtx, username, "WrongPass", "test_product", "127.0.0.1")
  54. require.Error(t, err)
  55. var le *LoginError
  56. require.True(t, errors.As(err, &le))
  57. assert.Equal(t, 401, le.Code,
  58. "超管 + 错误密码必须归一到 401'用户名或密码错误',不得提前披露超管身份")
  59. assert.Equal(t, "用户名或密码错误", le.Message)
  60. }
  61. // TC-0841: 超管走产品端 + 正确密码 —— 密码正确后才披露"超管请走管理后台"。
  62. func TestValidateProductLogin_SuperAdminCorrectPassword_Return403(t *testing.T) {
  63. ctx := context.Background()
  64. svcCtx := newTestSvcCtx()
  65. svcCtx.UsernameLoginLimit = nil
  66. username := "h2_sa_right_" + testutil.UniqueId()
  67. _, clean := insertRefreshTestUser(t, ctx, username, "RightPass123", 1, 1)
  68. t.Cleanup(clean)
  69. _, err := ValidateProductLogin(ctx, svcCtx, username, "RightPass123", "test_product", "127.0.0.1")
  70. require.Error(t, err)
  71. var le *LoginError
  72. require.True(t, errors.As(err, &le))
  73. assert.Equal(t, 403, le.Code)
  74. assert.Equal(t, "超级管理员不允许通过产品端登录,请使用管理后台", le.Message)
  75. }
  76. // TC-0842: 用户名不存在 —— 沿用 dummy bcrypt 恒时对齐,文案必须与 TC-0838 完全一致。
  77. // 这条是三重 oracle 消除的"对照组",缺了它单看 0838/0840 还不能证明"三路归一"。
  78. func TestValidateProductLogin_UnknownUserSame401(t *testing.T) {
  79. ctx := context.Background()
  80. svcCtx := newTestSvcCtx()
  81. svcCtx.UsernameLoginLimit = nil
  82. _, err := ValidateProductLogin(ctx, svcCtx, "h2_noexist_"+testutil.UniqueId(), "anypwd", "test_product", "127.0.0.1")
  83. require.Error(t, err)
  84. var le *LoginError
  85. require.True(t, errors.As(err, &le))
  86. assert.Equal(t, 401, le.Code)
  87. assert.Equal(t, "用户名或密码错误", le.Message,
  88. "未知用户 / 冻结+错密 / 存在+错密 三路必须归一为同一 401 + 文案")
  89. }
  90. func TestValidateProductLogin_UnknownUserSameError(t *testing.T) {
  91. ctx := context.Background()
  92. svcCtx := newTestSvcCtx()
  93. username := "enum_unknown_" + testutil.UniqueId()
  94. _, err := ValidateProductLogin(ctx, svcCtx, username, "random-pw", "test_product", "127.0.0.1")
  95. require.Error(t, err)
  96. var le *LoginError
  97. require.True(t, errors.As(err, &le))
  98. assert.Equal(t, 401, le.Code)
  99. assert.Equal(t, "用户名或密码错误", le.Message,
  100. "M-C:不存在用户名不得暴露差异化文案")
  101. }
  102. // TC-0752: -C 修复回归 —— 存在用户名但密码错,返回相同文案相同 code,供与 TC-0751 做对照。
  103. func TestValidateProductLogin_KnownUserWrongPwd(t *testing.T) {
  104. ctx := context.Background()
  105. svcCtx := newTestSvcCtx()
  106. username := "enum_known_" + testutil.UniqueId()
  107. userId, cleanUser := insertRefreshTestUser(t, ctx, username, "RightPass123", 1, 2)
  108. t.Cleanup(cleanUser)
  109. _ = userId
  110. _, err := ValidateProductLogin(ctx, svcCtx, username, "wrong-pw", "test_product", "127.0.0.1")
  111. require.Error(t, err)
  112. var le *LoginError
  113. require.True(t, errors.As(err, &le))
  114. assert.Equal(t, 401, le.Code, "M-C:Code 必须与未知用户完全一致")
  115. assert.Equal(t, "用户名或密码错误", le.Message, "M-C:文案必须与未知用户完全一致")
  116. }
  117. // TC-0753: -C 修复回归 —— UsernameLoginLimit 的 key 必须按 ip:username 构造。
  118. // 同一 username 不同 IP 的配额互不共用,防止攻击者"用任意 IP 打爆某账号"导致账号 DoS。
  119. func TestValidateProductLogin_RateLimitKeyedByIPAndUsername(t *testing.T) {
  120. ctx := context.Background()
  121. svcCtx := newTestSvcCtx()
  122. // 使用独立的 quota=1 limiter
  123. cfg := testutil.GetTestConfig()
  124. rds := redis.MustNewRedis(cfg.CacheRedis.Nodes[0].RedisConf)
  125. svcCtx.UsernameLoginLimit = limit.NewPeriodLimit(300, 1, rds,
  126. cfg.CacheRedis.KeyPrefix+":rl:userlogin:ut:"+testutil.UniqueId())
  127. username := "enum_rl_" + testutil.UniqueId()
  128. // IP-A 第 1 次:"用户名或密码错误"
  129. _, err := ValidateProductLogin(ctx, svcCtx, username, "x", "test_product", "1.1.1.1")
  130. require.Error(t, err)
  131. var le *LoginError
  132. require.True(t, errors.As(err, &le))
  133. assert.Equal(t, 401, le.Code)
  134. // IP-A 第 2 次:超限 429
  135. _, err = ValidateProductLogin(ctx, svcCtx, username, "x", "test_product", "1.1.1.1")
  136. require.Error(t, err)
  137. require.True(t, errors.As(err, &le))
  138. assert.Equal(t, 429, le.Code, "M-C:同 IP 同 username 第 2 次必须触发 429")
  139. // IP-B 第 1 次:独立桶,仍应走到密码校验(不是 429)
  140. _, err = ValidateProductLogin(ctx, svcCtx, username, "x", "test_product", "2.2.2.2")
  141. require.Error(t, err)
  142. require.True(t, errors.As(err, &le))
  143. assert.Equal(t, 401, le.Code,
  144. "M-C:不同 IP 的同 username 必须走独立限流桶(不是 429)")
  145. }