Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 

494 строки
15 KiB

  1. package middleware
  2. import (
  3. "bytes"
  4. "fmt"
  5. "net/http"
  6. "net/http/httptest"
  7. "os"
  8. "path/filepath"
  9. "strings"
  10. "sync/atomic"
  11. "testing"
  12. "time"
  13. "github.com/QuantumNous/new-api/common"
  14. "github.com/gin-gonic/gin"
  15. "github.com/stretchr/testify/assert"
  16. "github.com/stretchr/testify/require"
  17. )
  18. func setupCaptureTestRouter() *gin.Engine {
  19. gin.SetMode(gin.TestMode)
  20. return gin.New()
  21. }
  22. const captureTestDir = "./data/relay-capture"
  23. type relayCaptureTestRecord struct {
  24. RequestID string `json:"request_id"`
  25. CapturedAt string `json:"captured_at"`
  26. DurationMs int64 `json:"duration_ms"`
  27. UserID int64 `json:"user_id"`
  28. Request struct {
  29. Method string `json:"method"`
  30. Path string `json:"path"`
  31. Query string `json:"query"`
  32. Headers map[string][]string `json:"headers"`
  33. Body string `json:"body"`
  34. } `json:"request"`
  35. Response struct {
  36. StatusCode int `json:"status_code"`
  37. Headers map[string][]string `json:"headers"`
  38. Body string `json:"body"`
  39. BodyChunks []string `json:"body_chunks"`
  40. } `json:"response"`
  41. CaptureErrors []string `json:"capture_errors"`
  42. }
  43. func readCaptureRecord(t *testing.T, requestID string) relayCaptureTestRecord {
  44. t.Helper()
  45. today := time.Now().Format("2006-01-02")
  46. path := filepath.Join(captureTestDir, today, requestID+".json")
  47. content, err := os.ReadFile(path)
  48. require.NoError(t, err)
  49. require.NotContains(t, string(content), "=== REQUEST")
  50. require.NotContains(t, string(content), "=== RESPONSE")
  51. require.NotContains(t, string(content), "=== END")
  52. var record relayCaptureTestRecord
  53. require.NoError(t, common.Unmarshal(content, &record))
  54. return record
  55. }
  56. func readLatestCaptureRecord(t *testing.T) relayCaptureTestRecord {
  57. t.Helper()
  58. today := time.Now().Format("2006-01-02")
  59. dateDir := filepath.Join(captureTestDir, today)
  60. files, err := os.ReadDir(dateDir)
  61. require.NoError(t, err)
  62. require.NotEmpty(t, files, "should have capture json files")
  63. latest := filepath.Join(dateDir, files[len(files)-1].Name())
  64. require.True(t, strings.HasSuffix(latest, ".json"))
  65. content, err := os.ReadFile(latest)
  66. require.NoError(t, err)
  67. var record relayCaptureTestRecord
  68. require.NoError(t, common.Unmarshal(content, &record))
  69. return record
  70. }
  71. func TestRelayCapture_NonStreaming_EnabledUser(t *testing.T) {
  72. clearCaptureCache()
  73. t.Cleanup(func() { os.RemoveAll(captureTestDir) })
  74. SetCaptureEnabled(42, true)
  75. defer SetCaptureEnabled(42, false)
  76. requestID := fmt.Sprintf("test-non-stream-%d", time.Now().UnixNano())
  77. router := setupCaptureTestRouter()
  78. router.Use(func(c *gin.Context) {
  79. c.Set("id", 42)
  80. c.Set(common.RequestIdKey, requestID)
  81. c.Next()
  82. })
  83. router.Use(RelayCaptureMiddleware())
  84. router.POST("/v1/chat/completions", func(c *gin.Context) {
  85. c.JSON(200, gin.H{
  86. "id": "chatcmpl-test",
  87. "object": "chat.completion",
  88. "choices": []gin.H{{"message": gin.H{"role": "assistant", "content": "Hello!"}}},
  89. })
  90. })
  91. body := `{"model":"gpt-4","messages":[{"role":"user","content":"hi"}]}`
  92. req, _ := http.NewRequest("POST", "/v1/chat/completions", bytes.NewReader([]byte(body)))
  93. req.Header.Set("Authorization", "Bearer sk-test")
  94. req.Header.Set("Content-Type", "application/json")
  95. rec := httptest.NewRecorder()
  96. router.ServeHTTP(rec, req)
  97. assert.Equal(t, 200, rec.Code)
  98. record := readCaptureRecord(t, requestID)
  99. assert.Equal(t, requestID, record.RequestID)
  100. assert.NotEmpty(t, record.CapturedAt)
  101. assert.GreaterOrEqual(t, record.DurationMs, int64(0))
  102. assert.Equal(t, int64(42), record.UserID)
  103. assert.Equal(t, "POST", record.Request.Method)
  104. assert.Equal(t, "/v1/chat/completions", record.Request.Path)
  105. assert.Equal(t, body, record.Request.Body)
  106. assert.Equal(t, []string{"[REDACTED]"}, record.Request.Headers["Authorization"])
  107. assert.Equal(t, []string{"application/json"}, record.Request.Headers["Content-Type"])
  108. assert.Equal(t, 200, record.Response.StatusCode)
  109. assert.Contains(t, record.Response.Headers["Content-Type"][0], "application/json")
  110. assert.Contains(t, record.Response.Body, "chatcmpl-test")
  111. assert.NotEmpty(t, record.Response.BodyChunks)
  112. assert.Empty(t, record.CaptureErrors)
  113. }
  114. func TestRelayCapture_Streaming_EnabledUser(t *testing.T) {
  115. clearCaptureCache()
  116. t.Cleanup(func() { os.RemoveAll(captureTestDir) })
  117. SetCaptureEnabled(42, true)
  118. defer SetCaptureEnabled(42, false)
  119. requestID := fmt.Sprintf("test-stream-%d", time.Now().UnixNano())
  120. router := setupCaptureTestRouter()
  121. router.Use(func(c *gin.Context) {
  122. c.Set("id", 42)
  123. c.Set(common.RequestIdKey, requestID)
  124. c.Next()
  125. })
  126. router.Use(RelayCaptureMiddleware())
  127. router.POST("/v1/chat/completions", func(c *gin.Context) {
  128. c.Header("Content-Type", "text/event-stream")
  129. c.Header("Cache-Control", "no-cache")
  130. c.Header("Connection", "keep-alive")
  131. flusher := c.Writer.(http.Flusher)
  132. chunks := []string{
  133. "data: {\"id\":\"1\",\"choices\":[{\"delta\":{\"content\":\"Hello \"}}]}\n\n",
  134. "data: {\"id\":\"1\",\"choices\":[{\"delta\":{\"content\":\"world!\"}}]}\n\n",
  135. "data: [DONE]\n\n",
  136. }
  137. for _, chunk := range chunks {
  138. _, _ = c.Writer.Write([]byte(chunk))
  139. flusher.Flush()
  140. time.Sleep(5 * time.Millisecond)
  141. }
  142. })
  143. body := `{"model":"gpt-4","messages":[{"role":"user","content":"hi"}],"stream":true}`
  144. req, _ := http.NewRequest("POST", "/v1/chat/completions", bytes.NewReader([]byte(body)))
  145. req.Header.Set("Authorization", "Bearer sk-test")
  146. req.Header.Set("Content-Type", "application/json")
  147. rec := httptest.NewRecorder()
  148. router.ServeHTTP(rec, req)
  149. assert.Equal(t, 200, rec.Code)
  150. record := readCaptureRecord(t, requestID)
  151. assert.Equal(t, requestID, record.RequestID)
  152. assert.Equal(t, "POST", record.Request.Method)
  153. assert.Equal(t, "/v1/chat/completions", record.Request.Path)
  154. assert.Equal(t, body, record.Request.Body)
  155. assert.Equal(t, 200, record.Response.StatusCode)
  156. assert.Contains(t, record.Response.Body, "Hello ")
  157. assert.Contains(t, record.Response.Body, "world!")
  158. assert.Contains(t, record.Response.Body, "[DONE]")
  159. require.Len(t, record.Response.BodyChunks, 3)
  160. assert.Contains(t, record.Response.BodyChunks[0], "Hello ")
  161. assert.Contains(t, record.Response.BodyChunks[1], "world!")
  162. assert.Contains(t, record.Response.BodyChunks[2], "[DONE]")
  163. assert.Empty(t, record.CaptureErrors)
  164. }
  165. func TestRelayCapture_RedactsSensitiveRequestHeaders(t *testing.T) {
  166. clearCaptureCache()
  167. t.Cleanup(func() { os.RemoveAll(captureTestDir) })
  168. SetCaptureEnabled(42, true)
  169. defer SetCaptureEnabled(42, false)
  170. requestID := fmt.Sprintf("test-redaction-%d", time.Now().UnixNano())
  171. router := setupCaptureTestRouter()
  172. router.Use(func(c *gin.Context) {
  173. c.Set("id", 42)
  174. c.Set(common.RequestIdKey, requestID)
  175. c.Next()
  176. })
  177. router.Use(RelayCaptureMiddleware())
  178. router.POST("/v1/chat/completions", func(c *gin.Context) {
  179. c.JSON(200, gin.H{"result": "ok"})
  180. })
  181. req, _ := http.NewRequest("POST", "/v1/chat/completions", bytes.NewReader([]byte(`{"model":"gpt-4"}`)))
  182. req.Header.Set("Authorization", "Bearer sk-test")
  183. req.Header.Set("X-Api-Key", "x-secret")
  184. req.Header.Set("Cookie", "session=secret")
  185. rec := httptest.NewRecorder()
  186. router.ServeHTTP(rec, req)
  187. assert.Equal(t, 200, rec.Code)
  188. today := time.Now().Format("2006-01-02")
  189. path := filepath.Join(captureTestDir, today, requestID+".json")
  190. content, err := os.ReadFile(path)
  191. require.NoError(t, err)
  192. contentStr := string(content)
  193. assert.Contains(t, contentStr, "[REDACTED]")
  194. assert.NotContains(t, contentStr, "Bearer sk-test")
  195. assert.NotContains(t, contentStr, "x-secret")
  196. assert.NotContains(t, contentStr, "session=secret")
  197. }
  198. func TestRelayCapture_DisabledUser(t *testing.T) {
  199. clearCaptureCache()
  200. router := setupCaptureTestRouter()
  201. router.Use(func(c *gin.Context) {
  202. c.Set("id", 99)
  203. c.Set(common.RequestIdKey, fmt.Sprintf("test-disabled-%d", time.Now().UnixNano()))
  204. c.Next()
  205. })
  206. router.Use(RelayCaptureMiddleware())
  207. router.POST("/v1/chat/completions", func(c *gin.Context) {
  208. c.JSON(200, gin.H{"result": "ok"})
  209. })
  210. req, _ := http.NewRequest("POST", "/v1/chat/completions", bytes.NewReader([]byte(`{}`)))
  211. req.Header.Set("Authorization", "Bearer sk-test")
  212. rec := httptest.NewRecorder()
  213. router.ServeHTTP(rec, req)
  214. assert.Equal(t, 200, rec.Code)
  215. today := time.Now().Format("2006-01-02")
  216. dateDir := filepath.Join(captureTestDir, today)
  217. if _, err := os.Stat(dateDir); err == nil {
  218. files, _ := os.ReadDir(dateDir)
  219. for _, f := range files {
  220. assert.NotContains(t, f.Name(), "test-disabled", "disabled user should not generate capture files")
  221. }
  222. }
  223. }
  224. func TestRelayCapture_LargeStream(t *testing.T) {
  225. clearCaptureCache()
  226. t.Cleanup(func() { os.RemoveAll(captureTestDir) })
  227. SetCaptureEnabled(42, true)
  228. defer SetCaptureEnabled(42, false)
  229. requestID := fmt.Sprintf("test-large-%d", time.Now().UnixNano())
  230. router := setupCaptureTestRouter()
  231. router.Use(func(c *gin.Context) {
  232. c.Set("id", 42)
  233. c.Set(common.RequestIdKey, requestID)
  234. c.Next()
  235. })
  236. router.Use(RelayCaptureMiddleware())
  237. router.POST("/v1/chat/completions", func(c *gin.Context) {
  238. c.Header("Content-Type", "text/event-stream")
  239. flusher := c.Writer.(http.Flusher)
  240. for i := 0; i < 50; i++ {
  241. chunk := fmt.Sprintf("data: {\"id\":\"%d\",\"choices\":[{\"delta\":{\"content\":\"chunk%d \"}}]}\n\n", i, i)
  242. _, _ = c.Writer.Write([]byte(chunk))
  243. flusher.Flush()
  244. }
  245. _, _ = c.Writer.Write([]byte("data: [DONE]\n\n"))
  246. flusher.Flush()
  247. })
  248. req, _ := http.NewRequest("POST", "/v1/chat/completions", bytes.NewReader([]byte(`{"model":"gpt-4","stream":true}`)))
  249. req.Header.Set("Authorization", "Bearer sk-test")
  250. rec := httptest.NewRecorder()
  251. router.ServeHTTP(rec, req)
  252. assert.Equal(t, 200, rec.Code)
  253. record := readCaptureRecord(t, requestID)
  254. assert.Contains(t, record.Response.Body, "chunk0")
  255. assert.Contains(t, record.Response.Body, "chunk49")
  256. assert.Contains(t, record.Response.Body, "[DONE]")
  257. assert.Len(t, record.Response.BodyChunks, 51)
  258. }
  259. func TestRelayCapture_GetRequest(t *testing.T) {
  260. clearCaptureCache()
  261. t.Cleanup(func() { os.RemoveAll(captureTestDir) })
  262. SetCaptureEnabled(42, true)
  263. defer SetCaptureEnabled(42, false)
  264. requestID := fmt.Sprintf("test-get-%d", time.Now().UnixNano())
  265. router := setupCaptureTestRouter()
  266. router.Use(func(c *gin.Context) {
  267. c.Set("id", 42)
  268. c.Set(common.RequestIdKey, requestID)
  269. c.Next()
  270. })
  271. router.Use(RelayCaptureMiddleware())
  272. router.GET("/v1/models", func(c *gin.Context) {
  273. c.JSON(200, gin.H{
  274. "data": []gin.H{{"id": "gpt-4", "object": "model"}},
  275. })
  276. })
  277. req, _ := http.NewRequest("GET", "/v1/models", nil)
  278. req.Header.Set("Authorization", "Bearer sk-test")
  279. rec := httptest.NewRecorder()
  280. router.ServeHTTP(rec, req)
  281. assert.Equal(t, 200, rec.Code)
  282. record := readCaptureRecord(t, requestID)
  283. assert.Equal(t, "GET", record.Request.Method)
  284. assert.Equal(t, "/v1/models", record.Request.Path)
  285. assert.Equal(t, "", record.Request.Body)
  286. assert.Equal(t, 200, record.Response.StatusCode)
  287. assert.Contains(t, record.Response.Body, "gpt-4")
  288. }
  289. func TestRelayCapture_ZeroUserID(t *testing.T) {
  290. clearCaptureCache()
  291. router := setupCaptureTestRouter()
  292. router.Use(func(c *gin.Context) {
  293. c.Set(common.RequestIdKey, fmt.Sprintf("test-zero-%d", time.Now().UnixNano()))
  294. c.Next()
  295. })
  296. router.Use(RelayCaptureMiddleware())
  297. router.GET("/v1/models", func(c *gin.Context) {
  298. c.JSON(200, gin.H{"data": []gin.H{}})
  299. })
  300. req, _ := http.NewRequest("GET", "/v1/models", nil)
  301. rec := httptest.NewRecorder()
  302. router.ServeHTTP(rec, req)
  303. assert.Equal(t, 200, rec.Code)
  304. }
  305. func TestRelayCapture_EmptyRequestIDUsesFallbackFilename(t *testing.T) {
  306. clearCaptureCache()
  307. t.Cleanup(func() { os.RemoveAll(captureTestDir) })
  308. SetCaptureEnabled(42, true)
  309. defer SetCaptureEnabled(42, false)
  310. router := setupCaptureTestRouter()
  311. router.Use(func(c *gin.Context) {
  312. c.Set("id", 42)
  313. c.Next()
  314. })
  315. router.Use(RelayCaptureMiddleware())
  316. router.GET("/v1/models", func(c *gin.Context) {
  317. c.JSON(200, gin.H{"data": []gin.H{}})
  318. })
  319. req, _ := http.NewRequest("GET", "/v1/models", nil)
  320. rec := httptest.NewRecorder()
  321. router.ServeHTTP(rec, req)
  322. assert.Equal(t, 200, rec.Code)
  323. today := time.Now().Format("2006-01-02")
  324. dateDir := filepath.Join(captureTestDir, today)
  325. files, err := os.ReadDir(dateDir)
  326. require.NoError(t, err)
  327. require.Len(t, files, 1)
  328. assert.True(t, strings.HasPrefix(files[0].Name(), "capture-"))
  329. assert.True(t, strings.HasSuffix(files[0].Name(), ".json"))
  330. record := readLatestCaptureRecord(t)
  331. assert.True(t, strings.HasPrefix(record.RequestID, "capture-"))
  332. assert.Equal(t, "GET", record.Request.Method)
  333. assert.Equal(t, "/v1/models", record.Request.Path)
  334. }
  335. func TestRelayCapture_ResponseUnchanged(t *testing.T) {
  336. clearCaptureCache()
  337. t.Cleanup(func() { os.RemoveAll(captureTestDir) })
  338. SetCaptureEnabled(42, true)
  339. defer SetCaptureEnabled(42, false)
  340. router := setupCaptureTestRouter()
  341. router.Use(func(c *gin.Context) {
  342. c.Set("id", 42)
  343. c.Set(common.RequestIdKey, fmt.Sprintf("test-unchanged-%d", time.Now().UnixNano()))
  344. c.Next()
  345. })
  346. router.Use(RelayCaptureMiddleware())
  347. router.POST("/v1/chat/completions", func(c *gin.Context) {
  348. c.JSON(200, gin.H{
  349. "id": "chatcmpl-123",
  350. "choices": []gin.H{{"message": gin.H{"content": "exact response"}}},
  351. })
  352. })
  353. req, _ := http.NewRequest("POST", "/v1/chat/completions", bytes.NewReader([]byte(`{"model":"gpt-4"}`)))
  354. req.Header.Set("Content-Type", "application/json")
  355. rec := httptest.NewRecorder()
  356. router.ServeHTTP(rec, req)
  357. assert.Equal(t, 200, rec.Code)
  358. assert.Contains(t, rec.Body.String(), "exact response")
  359. assert.Contains(t, rec.Body.String(), "chatcmpl-123")
  360. }
  361. func TestRelayCapture_ConcurrentRequests(t *testing.T) {
  362. clearCaptureCache()
  363. t.Cleanup(func() { os.RemoveAll(captureTestDir) })
  364. SetCaptureEnabled(42, true)
  365. defer SetCaptureEnabled(42, false)
  366. router := setupCaptureTestRouter()
  367. var reqCounter int64
  368. router.Use(func(c *gin.Context) {
  369. c.Set("id", 42)
  370. n := atomic.AddInt64(&reqCounter, 1)
  371. c.Set(common.RequestIdKey, fmt.Sprintf("concurrent-%d-%d", n, time.Now().UnixNano()))
  372. c.Next()
  373. })
  374. router.Use(RelayCaptureMiddleware())
  375. router.POST("/v1/chat/completions", func(c *gin.Context) {
  376. c.JSON(200, gin.H{"status": "ok"})
  377. })
  378. done := make(chan bool, 10)
  379. for i := 0; i < 10; i++ {
  380. go func(idx int) {
  381. req, _ := http.NewRequest("POST", "/v1/chat/completions", bytes.NewReader([]byte(fmt.Sprintf(`{"req":%d}`, idx))))
  382. req.Header.Set("Content-Type", "application/json")
  383. rec := httptest.NewRecorder()
  384. router.ServeHTTP(rec, req)
  385. assert.Equal(t, 200, rec.Code)
  386. done <- true
  387. }(i)
  388. }
  389. for i := 0; i < 10; i++ {
  390. select {
  391. case <-done:
  392. case <-time.After(10 * time.Second):
  393. t.Fatal("timeout waiting for concurrent request")
  394. }
  395. }
  396. time.Sleep(100 * time.Millisecond)
  397. today := time.Now().Format("2006-01-02")
  398. dateDir := filepath.Join(captureTestDir, today)
  399. files, err := os.ReadDir(dateDir)
  400. require.NoError(t, err)
  401. assert.Equal(t, 10, len(files), "should have 10 capture json files")
  402. for _, f := range files {
  403. require.True(t, strings.HasSuffix(f.Name(), ".json"))
  404. content, err := os.ReadFile(filepath.Join(dateDir, f.Name()))
  405. require.NoError(t, err)
  406. var record relayCaptureTestRecord
  407. require.NoError(t, common.Unmarshal(content, &record))
  408. assert.Equal(t, "POST", record.Request.Method)
  409. assert.Equal(t, "/v1/chat/completions", record.Request.Path)
  410. assert.Equal(t, 200, record.Response.StatusCode)
  411. assert.Contains(t, record.Response.Body, "ok")
  412. }
  413. }