You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

238 lines
6.7 KiB

  1. package middleware
  2. import (
  3. "bytes"
  4. "net/http"
  5. "net/http/httptest"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. "testing"
  10. "github.com/gin-gonic/gin"
  11. "github.com/stretchr/testify/assert"
  12. "github.com/stretchr/testify/require"
  13. )
  14. func clearCaptureCache() {
  15. captureEnabledUsers.Range(func(key, value interface{}) bool {
  16. captureEnabledUsers.Delete(key)
  17. return true
  18. })
  19. }
  20. func TestCaptureEnabled_NotFound(t *testing.T) {
  21. clearCaptureCache()
  22. assert.False(t, isCaptureEnabled(1))
  23. }
  24. func TestSetCaptureEnabled_Enable(t *testing.T) {
  25. clearCaptureCache()
  26. SetCaptureEnabled(1, true)
  27. assert.True(t, isCaptureEnabled(1))
  28. SetCaptureEnabled(1, false)
  29. }
  30. func TestSetCaptureEnabled_Disable(t *testing.T) {
  31. clearCaptureCache()
  32. SetCaptureEnabled(1, true)
  33. SetCaptureEnabled(1, false)
  34. assert.False(t, isCaptureEnabled(1))
  35. }
  36. func TestSetCaptureEnabled_DifferentUser(t *testing.T) {
  37. clearCaptureCache()
  38. SetCaptureEnabled(1, true)
  39. assert.False(t, isCaptureEnabled(2))
  40. SetCaptureEnabled(1, false)
  41. }
  42. func TestGetCaptureEnabledUsersCount(t *testing.T) {
  43. clearCaptureCache()
  44. assert.Equal(t, 0, GetCaptureEnabledUsersCount())
  45. SetCaptureEnabled(1, true)
  46. SetCaptureEnabled(2, true)
  47. assert.Equal(t, 2, GetCaptureEnabledUsersCount())
  48. SetCaptureEnabled(1, false)
  49. assert.Equal(t, 1, GetCaptureEnabledUsersCount())
  50. SetCaptureEnabled(2, false)
  51. }
  52. func TestCaptureResponseWriter_Write(t *testing.T) {
  53. gin.SetMode(gin.TestMode)
  54. rec := httptest.NewRecorder()
  55. c, _ := gin.CreateTestContext(rec)
  56. collector := newResponseCaptureCollector()
  57. cw := &captureResponseWriter{
  58. ResponseWriter: c.Writer,
  59. collector: collector,
  60. }
  61. n, err := cw.Write([]byte("hello"))
  62. assert.NoError(t, err)
  63. assert.Equal(t, 5, n)
  64. assert.Equal(t, "hello", rec.Body.String())
  65. assert.Equal(t, "hello", collector.Body())
  66. assert.Equal(t, []string{"hello"}, collector.Chunks())
  67. }
  68. func TestCaptureResponseWriter_WriteString(t *testing.T) {
  69. gin.SetMode(gin.TestMode)
  70. rec := httptest.NewRecorder()
  71. c, _ := gin.CreateTestContext(rec)
  72. collector := newResponseCaptureCollector()
  73. cw := &captureResponseWriter{
  74. ResponseWriter: c.Writer,
  75. collector: collector,
  76. }
  77. n, err := cw.WriteString("test-string")
  78. assert.NoError(t, err)
  79. assert.Equal(t, len("test-string"), n)
  80. assert.Equal(t, "test-string", rec.Body.String())
  81. assert.Equal(t, "test-string", collector.Body())
  82. assert.Equal(t, []string{"test-string"}, collector.Chunks())
  83. }
  84. type partialResponseWriter struct {
  85. gin.ResponseWriter
  86. }
  87. func (w partialResponseWriter) Write(b []byte) (int, error) {
  88. _, err := w.ResponseWriter.Write(b[:2])
  89. return 2, err
  90. }
  91. func TestCaptureResponseWriter_RecordsOnlyWrittenBytes(t *testing.T) {
  92. gin.SetMode(gin.TestMode)
  93. rec := httptest.NewRecorder()
  94. c, _ := gin.CreateTestContext(rec)
  95. collector := newResponseCaptureCollector()
  96. cw := &captureResponseWriter{
  97. ResponseWriter: partialResponseWriter{ResponseWriter: c.Writer},
  98. collector: collector,
  99. }
  100. n, err := cw.Write([]byte("hello"))
  101. assert.NoError(t, err)
  102. assert.Equal(t, 2, n)
  103. assert.Equal(t, "he", rec.Body.String())
  104. assert.Equal(t, "he", collector.Body())
  105. assert.Equal(t, []string{"he"}, collector.Chunks())
  106. }
  107. func TestCreateCaptureFile(t *testing.T) {
  108. f, err := createCaptureFile("test-req-123")
  109. require.NoError(t, err)
  110. defer os.RemoveAll(filepath.Dir(f.Name()))
  111. defer f.Close()
  112. assert.Contains(t, f.Name(), "relay-capture")
  113. assert.Contains(t, f.Name(), "test-req-123.json")
  114. assert.NotContains(t, f.Name(), ".log")
  115. dir := filepath.Dir(f.Name())
  116. info, err := os.Stat(dir)
  117. require.NoError(t, err)
  118. assert.True(t, info.IsDir())
  119. }
  120. func TestNormalizeCaptureRequestID_UsesExistingID(t *testing.T) {
  121. requestID := normalizeCaptureRequestID("req-123")
  122. assert.Equal(t, "req-123", requestID)
  123. }
  124. func TestNormalizeCaptureRequestID_FallbackForEmptyID(t *testing.T) {
  125. requestID := normalizeCaptureRequestID("")
  126. assert.True(t, strings.HasPrefix(requestID, "capture-"))
  127. assert.False(t, strings.HasSuffix(requestID, ".json"))
  128. }
  129. func TestCopyHeadersForCapture_RedactsSensitiveRequestHeaders(t *testing.T) {
  130. headers := http.Header{
  131. "Authorization": []string{"Bearer sk-test"},
  132. "authorization": []string{"Bearer lower"},
  133. "X-Api-Key": []string{"x-key"},
  134. "Api-Key": []string{"api-key"},
  135. "Cookie": []string{"session=secret"},
  136. "Proxy-Authorization": []string{"Basic secret"},
  137. "Content-Type": []string{"application/json"},
  138. "X-Trace-ID": []string{"trace-1"},
  139. }
  140. copied := copyHeadersForCapture(headers, true)
  141. assert.Equal(t, []string{"[REDACTED]"}, copied["Authorization"])
  142. assert.Equal(t, []string{"[REDACTED]"}, copied["authorization"])
  143. assert.Equal(t, []string{"[REDACTED]"}, copied["X-Api-Key"])
  144. assert.Equal(t, []string{"[REDACTED]"}, copied["Api-Key"])
  145. assert.Equal(t, []string{"[REDACTED]"}, copied["Cookie"])
  146. assert.Equal(t, []string{"[REDACTED]"}, copied["Proxy-Authorization"])
  147. assert.Equal(t, []string{"application/json"}, copied["Content-Type"])
  148. assert.Equal(t, []string{"trace-1"}, copied["X-Trace-ID"])
  149. assert.Equal(t, []string{"Bearer sk-test"}, headers["Authorization"])
  150. }
  151. func TestCopyHeadersForCapture_ResponseHeadersNotRedacted(t *testing.T) {
  152. headers := http.Header{}
  153. headers.Add("Authorization", "response-token")
  154. headers.Add("Content-Type", "application/json")
  155. copied := copyHeadersForCapture(headers, false)
  156. assert.Equal(t, []string{"response-token"}, copied["Authorization"])
  157. assert.Equal(t, []string{"application/json"}, copied["Content-Type"])
  158. }
  159. func TestBuildCaptureRequestRecord_PostBody(t *testing.T) {
  160. gin.SetMode(gin.TestMode)
  161. w := httptest.NewRecorder()
  162. c, _ := gin.CreateTestContext(w)
  163. body := `{"model":"gpt-4"}`
  164. c.Request = httptest.NewRequest("POST", "/v1/chat/completions?stream=true", bytes.NewReader([]byte(body)))
  165. c.Request.Header.Set("Authorization", "Bearer sk-test")
  166. c.Request.Header.Set("Content-Type", "application/json")
  167. var captureErrors []string
  168. record := buildCaptureRequestRecord(c, &captureErrors)
  169. assert.Equal(t, "POST", record.Method)
  170. assert.Equal(t, "/v1/chat/completions", record.Path)
  171. assert.Equal(t, "stream=true", record.Query)
  172. assert.Equal(t, body, record.Body)
  173. assert.Equal(t, []string{"[REDACTED]"}, record.Headers["Authorization"])
  174. assert.Equal(t, []string{"application/json"}, record.Headers["Content-Type"])
  175. assert.Empty(t, captureErrors)
  176. }
  177. func TestBuildCaptureRequestRecord_GetNoBody(t *testing.T) {
  178. gin.SetMode(gin.TestMode)
  179. w := httptest.NewRecorder()
  180. c, _ := gin.CreateTestContext(w)
  181. c.Request = httptest.NewRequest("GET", "/v1/models", nil)
  182. var captureErrors []string
  183. record := buildCaptureRequestRecord(c, &captureErrors)
  184. assert.Equal(t, "GET", record.Method)
  185. assert.Equal(t, "/v1/models", record.Path)
  186. assert.Equal(t, "", record.Query)
  187. assert.Equal(t, "", record.Body)
  188. assert.Empty(t, captureErrors)
  189. }