turnstile_service_test.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. package repository
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "io"
  7. "net/http"
  8. "net/url"
  9. "strings"
  10. "testing"
  11. "github.com/Wei-Shaw/sub2api/internal/service"
  12. "github.com/stretchr/testify/require"
  13. "github.com/stretchr/testify/suite"
  14. )
  15. type TurnstileServiceSuite struct {
  16. suite.Suite
  17. ctx context.Context
  18. verifier *turnstileVerifier
  19. received chan url.Values
  20. }
  21. func (s *TurnstileServiceSuite) SetupTest() {
  22. s.ctx = context.Background()
  23. s.received = make(chan url.Values, 1)
  24. verifier, ok := NewTurnstileVerifier().(*turnstileVerifier)
  25. require.True(s.T(), ok, "type assertion failed")
  26. s.verifier = verifier
  27. }
  28. func (s *TurnstileServiceSuite) setupTransport(handler http.HandlerFunc) {
  29. s.verifier.verifyURL = "http://in-process/turnstile"
  30. s.verifier.httpClient = &http.Client{
  31. Transport: newInProcessTransport(handler, nil),
  32. }
  33. }
  34. func (s *TurnstileServiceSuite) TestVerifyToken_SendsFormAndDecodesJSON() {
  35. s.setupTransport(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  36. // Capture form data in main goroutine context later
  37. body, _ := io.ReadAll(r.Body)
  38. values, _ := url.ParseQuery(string(body))
  39. s.received <- values
  40. w.Header().Set("Content-Type", "application/json")
  41. _ = json.NewEncoder(w).Encode(service.TurnstileVerifyResponse{Success: true})
  42. }))
  43. resp, err := s.verifier.VerifyToken(s.ctx, "sk", "token", "1.1.1.1")
  44. require.NoError(s.T(), err, "VerifyToken")
  45. require.NotNil(s.T(), resp)
  46. require.True(s.T(), resp.Success, "expected success response")
  47. // Assert form fields in main goroutine
  48. select {
  49. case values := <-s.received:
  50. require.Equal(s.T(), "sk", values.Get("secret"))
  51. require.Equal(s.T(), "token", values.Get("response"))
  52. require.Equal(s.T(), "1.1.1.1", values.Get("remoteip"))
  53. default:
  54. require.Fail(s.T(), "expected server to receive request")
  55. }
  56. }
  57. func (s *TurnstileServiceSuite) TestVerifyToken_ContentType() {
  58. var contentType string
  59. s.setupTransport(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  60. contentType = r.Header.Get("Content-Type")
  61. w.Header().Set("Content-Type", "application/json")
  62. _ = json.NewEncoder(w).Encode(service.TurnstileVerifyResponse{Success: true})
  63. }))
  64. _, err := s.verifier.VerifyToken(s.ctx, "sk", "token", "1.1.1.1")
  65. require.NoError(s.T(), err)
  66. require.True(s.T(), strings.HasPrefix(contentType, "application/x-www-form-urlencoded"), "unexpected content-type: %s", contentType)
  67. }
  68. func (s *TurnstileServiceSuite) TestVerifyToken_EmptyRemoteIP_NotSent() {
  69. s.setupTransport(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  70. body, _ := io.ReadAll(r.Body)
  71. values, _ := url.ParseQuery(string(body))
  72. s.received <- values
  73. w.Header().Set("Content-Type", "application/json")
  74. _ = json.NewEncoder(w).Encode(service.TurnstileVerifyResponse{Success: true})
  75. }))
  76. _, err := s.verifier.VerifyToken(s.ctx, "sk", "token", "")
  77. require.NoError(s.T(), err)
  78. select {
  79. case values := <-s.received:
  80. require.Equal(s.T(), "", values.Get("remoteip"), "remoteip should be empty or not sent")
  81. default:
  82. require.Fail(s.T(), "expected server to receive request")
  83. }
  84. }
  85. func (s *TurnstileServiceSuite) TestVerifyToken_RequestError() {
  86. s.verifier.verifyURL = "http://in-process/turnstile"
  87. s.verifier.httpClient = &http.Client{
  88. Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
  89. return nil, errors.New("dial failed")
  90. }),
  91. }
  92. _, err := s.verifier.VerifyToken(s.ctx, "sk", "token", "1.1.1.1")
  93. require.Error(s.T(), err, "expected error when server is closed")
  94. }
  95. func (s *TurnstileServiceSuite) TestVerifyToken_InvalidJSON() {
  96. s.setupTransport(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  97. w.Header().Set("Content-Type", "application/json")
  98. _, _ = io.WriteString(w, "not-valid-json")
  99. }))
  100. _, err := s.verifier.VerifyToken(s.ctx, "sk", "token", "1.1.1.1")
  101. require.Error(s.T(), err, "expected error for invalid JSON response")
  102. }
  103. func (s *TurnstileServiceSuite) TestVerifyToken_SuccessFalse() {
  104. s.setupTransport(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  105. w.Header().Set("Content-Type", "application/json")
  106. _ = json.NewEncoder(w).Encode(service.TurnstileVerifyResponse{
  107. Success: false,
  108. ErrorCodes: []string{"invalid-input-response"},
  109. })
  110. }))
  111. resp, err := s.verifier.VerifyToken(s.ctx, "sk", "token", "1.1.1.1")
  112. require.NoError(s.T(), err, "VerifyToken should not error on success=false")
  113. require.NotNil(s.T(), resp)
  114. require.False(s.T(), resp.Success)
  115. require.Contains(s.T(), resp.ErrorCodes, "invalid-input-response")
  116. }
  117. func TestTurnstileServiceSuite(t *testing.T) {
  118. suite.Run(t, new(TurnstileServiceSuite))
  119. }