aliyun_captcha_service_test.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. //go:build unit
  2. package service
  3. import (
  4. "context"
  5. "errors"
  6. "testing"
  7. "github.com/Wei-Shaw/sub2api/internal/config"
  8. "github.com/stretchr/testify/require"
  9. )
  10. type aliyunVerifierSpy struct {
  11. called int
  12. lastCred AliyunCaptchaCredentials
  13. lastParam string
  14. result *AliyunCaptchaVerifyResult
  15. err error
  16. }
  17. func (s *aliyunVerifierSpy) VerifyCaptcha(_ context.Context, cred AliyunCaptchaCredentials, param string) (*AliyunCaptchaVerifyResult, error) {
  18. s.called++
  19. s.lastCred = cred
  20. s.lastParam = param
  21. if s.err != nil {
  22. return nil, s.err
  23. }
  24. if s.result != nil {
  25. return s.result, nil
  26. }
  27. return &AliyunCaptchaVerifyResult{VerifyResult: true}, nil
  28. }
  29. func aliyunEnabledSettings() map[string]string {
  30. return map[string]string{
  31. SettingKeyAliyunCaptchaEnabled: "true",
  32. SettingKeyAliyunCaptchaAccessKeyID: "ak-id",
  33. SettingKeyAliyunCaptchaAccessKeySecret: "ak-secret",
  34. SettingKeyAliyunCaptchaSceneID: "scene-1",
  35. SettingKeyAliyunCaptchaPrefix: "prefix-1",
  36. }
  37. }
  38. func aliyunTestConfig() AliyunCaptchaConfig {
  39. return AliyunCaptchaConfig{
  40. Enabled: true,
  41. AccessKeyID: "ak-id",
  42. AccessKeySecret: "ak-secret",
  43. SceneID: "scene-1",
  44. Region: AliyunCaptchaRegionCN,
  45. }
  46. }
  47. func newAliyunAuthServiceForTest(cfg *config.Config, settings map[string]string, aliyunSpy *aliyunVerifierSpy) *AuthService {
  48. settingService := NewSettingService(&settingPublicRepoStub{values: settings}, cfg)
  49. authService := NewAuthService(
  50. nil, // entClient
  51. nil, // userRepo
  52. nil, // redeemRepo
  53. nil, // refreshTokenCache
  54. cfg,
  55. settingService,
  56. nil, // emailService
  57. NewTurnstileService(settingService, &turnstileVerifierSpy{}),
  58. nil, // emailQueueService
  59. nil, // promoService
  60. nil, // defaultSubAssigner
  61. nil, // affiliateService
  62. nil, // userPlatformQuotaRepo
  63. )
  64. authService.SetAliyunCaptchaService(NewAliyunCaptchaService(settingService, aliyunSpy))
  65. return authService
  66. }
  67. func TestAliyunCaptchaServiceVerifyParamDispatch(t *testing.T) {
  68. spy := &aliyunVerifierSpy{}
  69. svc := NewAliyunCaptchaService(nil, spy)
  70. err := svc.VerifyParamWithConfig(context.Background(), aliyunTestConfig(), "captcha-verify-param")
  71. require.NoError(t, err)
  72. require.Equal(t, 1, spy.called)
  73. require.Equal(t, "captcha-verify-param", spy.lastParam)
  74. require.Equal(t, "ak-id", spy.lastCred.AccessKeyID)
  75. require.Equal(t, "scene-1", spy.lastCred.SceneID)
  76. require.Equal(t, "captcha.cn-shanghai.aliyuncs.com", spy.lastCred.Endpoint)
  77. }
  78. func TestAliyunCaptchaServiceSgpEndpoint(t *testing.T) {
  79. spy := &aliyunVerifierSpy{}
  80. svc := NewAliyunCaptchaService(nil, spy)
  81. cfg := aliyunTestConfig()
  82. cfg.Region = AliyunCaptchaRegionSGP
  83. err := svc.VerifyParamWithConfig(context.Background(), cfg, "captcha-verify-param")
  84. require.NoError(t, err)
  85. require.Equal(t, "captcha.ap-southeast-1.aliyuncs.com", spy.lastCred.Endpoint)
  86. }
  87. func TestAliyunCaptchaServiceFailsClosedOnVerifierError(t *testing.T) {
  88. spy := &aliyunVerifierSpy{err: errors.New("network down")}
  89. svc := NewAliyunCaptchaService(nil, spy)
  90. err := svc.VerifyParamWithConfig(context.Background(), aliyunTestConfig(), "captcha-verify-param")
  91. require.ErrorIs(t, err, ErrAliyunCaptchaVerificationFailed)
  92. }
  93. func TestAliyunCaptchaServiceRejectsVerifyResultFalse(t *testing.T) {
  94. spy := &aliyunVerifierSpy{result: &AliyunCaptchaVerifyResult{VerifyResult: false, VerifyCode: "F001"}}
  95. svc := NewAliyunCaptchaService(nil, spy)
  96. err := svc.VerifyParamWithConfig(context.Background(), aliyunTestConfig(), "captcha-verify-param")
  97. require.ErrorIs(t, err, ErrAliyunCaptchaVerificationFailed)
  98. }
  99. func TestAliyunCaptchaServiceRejectsIncompleteCredentials(t *testing.T) {
  100. spy := &aliyunVerifierSpy{}
  101. svc := NewAliyunCaptchaService(nil, spy)
  102. cfg := aliyunTestConfig()
  103. cfg.AccessKeySecret = ""
  104. err := svc.VerifyParamWithConfig(context.Background(), cfg, "captcha-verify-param")
  105. require.ErrorIs(t, err, ErrAliyunCaptchaNotConfigured)
  106. require.Zero(t, spy.called)
  107. }
  108. func TestAliyunCaptchaServiceRejectsEmptyParam(t *testing.T) {
  109. spy := &aliyunVerifierSpy{}
  110. svc := NewAliyunCaptchaService(nil, spy)
  111. err := svc.VerifyParamWithConfig(context.Background(), aliyunTestConfig(), "")
  112. require.ErrorIs(t, err, ErrAliyunCaptchaVerificationFailed)
  113. require.Zero(t, spy.called)
  114. }
  115. func TestAliyunCaptchaServiceValidateCredentials(t *testing.T) {
  116. t.Run("invalid credential code", func(t *testing.T) {
  117. spy := &aliyunVerifierSpy{err: &AliyunCaptchaAPIError{Code: "SignatureDoesNotMatch", Message: "bad sk"}}
  118. svc := NewAliyunCaptchaService(nil, spy)
  119. err := svc.ValidateCredentials(context.Background(), "id", "sk", "scene", "cn")
  120. require.ErrorIs(t, err, ErrCaptchaInvalidCredentials)
  121. })
  122. t.Run("network error surfaces", func(t *testing.T) {
  123. spy := &aliyunVerifierSpy{err: errors.New("timeout")}
  124. svc := NewAliyunCaptchaService(nil, spy)
  125. err := svc.ValidateCredentials(context.Background(), "id", "sk", "scene", "cn")
  126. require.Error(t, err)
  127. require.NotErrorIs(t, err, ErrCaptchaInvalidCredentials)
  128. })
  129. t.Run("verify result false means credentials valid", func(t *testing.T) {
  130. spy := &aliyunVerifierSpy{result: &AliyunCaptchaVerifyResult{VerifyResult: false}}
  131. svc := NewAliyunCaptchaService(nil, spy)
  132. err := svc.ValidateCredentials(context.Background(), "id", "sk", "scene", "sgp")
  133. require.NoError(t, err)
  134. require.Equal(t, "captcha.ap-southeast-1.aliyuncs.com", spy.lastCred.Endpoint)
  135. })
  136. }
  137. func TestAuthServiceVerifyCaptchaDispatchesAliyun(t *testing.T) {
  138. spy := &aliyunVerifierSpy{}
  139. authService := newAliyunAuthServiceForTest(&config.Config{}, aliyunEnabledSettings(), spy)
  140. // 阿里云 captchaVerifyParam 复用 turnstile_token 请求字段
  141. err := authService.VerifyCaptcha(context.Background(), CaptchaProof{TurnstileToken: "captcha-verify-param"}, "127.0.0.1")
  142. require.NoError(t, err)
  143. require.Equal(t, 1, spy.called)
  144. require.Equal(t, "captcha-verify-param", spy.lastParam)
  145. }
  146. func TestAuthServiceVerifyCaptchaRejectsProviderConflict(t *testing.T) {
  147. settings := aliyunEnabledSettings()
  148. settings[SettingKeyTurnstileEnabled] = "true"
  149. settings[SettingKeyTurnstileSecretKey] = "secret"
  150. spy := &aliyunVerifierSpy{}
  151. authService := newAliyunAuthServiceForTest(&config.Config{}, settings, spy)
  152. err := authService.VerifyCaptcha(context.Background(), CaptchaProof{TurnstileToken: "param"}, "127.0.0.1")
  153. require.ErrorIs(t, err, ErrCaptchaProviderConflict)
  154. require.Zero(t, spy.called)
  155. }
  156. func TestAuthServiceVerifyCaptchaRequiredModeWithAliyun(t *testing.T) {
  157. cfg := &config.Config{
  158. Server: config.ServerConfig{Mode: "release"},
  159. Turnstile: config.TurnstileConfig{Required: true},
  160. }
  161. spy := &aliyunVerifierSpy{}
  162. authService := newAliyunAuthServiceForTest(cfg, aliyunEnabledSettings(), spy)
  163. // required 模式 + 阿里云启用且凭证齐全:不误报 NOT_CONFIGURED,正常走阿里云校验
  164. err := authService.VerifyCaptcha(context.Background(), CaptchaProof{TurnstileToken: "captcha-verify-param"}, "127.0.0.1")
  165. require.NoError(t, err)
  166. require.Equal(t, 1, spy.called)
  167. }
  168. func TestAuthServiceVerifyActionCaptchaIfEnabledDispatchesAliyun(t *testing.T) {
  169. spy := &aliyunVerifierSpy{}
  170. authService := newAliyunAuthServiceForTest(&config.Config{}, aliyunEnabledSettings(), spy)
  171. err := authService.VerifyActionCaptchaIfEnabled(context.Background(), CaptchaProof{TurnstileToken: "captcha-verify-param"}, "127.0.0.1")
  172. require.NoError(t, err)
  173. require.Equal(t, 1, spy.called)
  174. require.Equal(t, "captcha-verify-param", spy.lastParam)
  175. }
  176. func TestAuthServiceVerifyActionCaptchaIfEnabledSkipsWhenOnlyTurnstile(t *testing.T) {
  177. spy := &aliyunVerifierSpy{}
  178. authService := newAliyunAuthServiceForTest(&config.Config{}, map[string]string{
  179. SettingKeyTurnstileEnabled: "true",
  180. SettingKeyTurnstileSecretKey: "secret",
  181. }, spy)
  182. // Turnstile 不扩大既有覆盖:扩展入口不拦截
  183. err := authService.VerifyActionCaptchaIfEnabled(context.Background(), CaptchaProof{}, "127.0.0.1")
  184. require.NoError(t, err)
  185. require.Zero(t, spy.called)
  186. }