Files
zhanlu_proxy/internal/server/recorder_test.go
T
m1saka 22d78c4576
build / build (push) Successful in 2m31s
Add recent API request recorder with admin tab and /api/recent endpoint
Record the last 10 /v1/ API requests (including errors) in an in-memory
ring buffer. Each entry captures request headers (Authorization masked),
request body, response status, and response body — all up to 1 MB.

- recorder.go: RecentRecorder ring buffer, recordingResponseWriter,
  withRecording middleware, getRecent handler
- server.go: add recorder to Server, wrap /v1/ routes, add /api/recent
- admin.html: new 最近请求 tab with lazy-load and expandable cards
- recorder_test.go: 5 tests (ring buffer, ordering, capture, errors, API)
2026-08-23 14:52:33 +08:00

149 lines
4.7 KiB
Go

package server
import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// TestRecentRecorderRingBuffer verifies that the recorder keeps at most
// maxRecentEntries and evicts the oldest when full.
func TestRecentRecorderRingBuffer(t *testing.T) {
rec := NewRecentRecorder()
for i := 0; i < maxRecentEntries+5; i++ {
rec.Record(RecentEntry{Method: "POST", Path: "/v1/test", Status: 200})
}
entries := rec.Entries()
if len(entries) != maxRecentEntries {
t.Fatalf("got %d entries, want %d", len(entries), maxRecentEntries)
}
}
// TestRecentRecorderNewestFirst verifies entries are returned newest-first.
func TestRecentRecorderNewestFirst(t *testing.T) {
rec := NewRecentRecorder()
rec.Record(RecentEntry{Path: "/first"})
rec.Record(RecentEntry{Path: "/second"})
rec.Record(RecentEntry{Path: "/third"})
entries := rec.Entries()
if len(entries) != 3 {
t.Fatalf("got %d entries", len(entries))
}
if entries[0].Path != "/third" {
t.Fatalf("first entry = %q, want /third", entries[0].Path)
}
if entries[2].Path != "/first" {
t.Fatalf("last entry = %q, want /first", entries[2].Path)
}
}
// TestWithRecordingCapturesRequestResponse verifies the middleware captures
// request headers, request body, response status, and response body.
func TestWithRecordingCapturesRequestResponse(t *testing.T) {
rec := NewRecentRecorder()
s := &Server{recorder: rec}
handler := s.withRecording(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"result":"ok"}`))
})
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{"model":"GLM-4.7","messages":[]}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer sk-secret-key-12345")
req.Header.Set("User-Agent", "test-client/1.0")
w := httptest.NewRecorder()
handler(w, req)
entries := rec.Entries()
if len(entries) != 1 {
t.Fatalf("got %d entries, want 1", len(entries))
}
e := entries[0]
if e.Method != "POST" {
t.Fatalf("method = %q", e.Method)
}
if e.Path != "/v1/chat/completions" {
t.Fatalf("path = %q", e.Path)
}
if e.Status != 200 {
t.Fatalf("status = %d", e.Status)
}
if e.RequestBody != `{"model":"GLM-4.7","messages":[]}` {
t.Fatalf("request body = %q", e.RequestBody)
}
if e.ResponseBody != `{"result":"ok"}` {
t.Fatalf("response body = %q", e.ResponseBody)
}
// Authorization must be masked
auth, ok := e.RequestHeaders["Authorization"]
if !ok || !strings.Contains(auth, "****") {
t.Fatalf("authorization not masked: %q", auth)
}
if strings.Contains(auth, "sk-secret-key-12345") {
t.Fatal("authorization leaked raw key")
}
if e.RequestHeaders["Content-Type"] != "application/json" {
t.Fatalf("content-type = %v", e.RequestHeaders["Content-Type"])
}
if e.RequestHeaders["User-Agent"] != "test-client/1.0" {
t.Fatalf("user-agent = %v", e.RequestHeaders["User-Agent"])
}
}
// TestWithRecordingCapturesErrors verifies error responses are recorded.
func TestWithRecordingCapturesErrors(t *testing.T) {
rec := NewRecentRecorder()
s := &Server{recorder: rec}
handler := s.withRecording(func(w http.ResponseWriter, r *http.Request) {
writeOpenAIError(w, http.StatusUnauthorized, "invalid api key", "auth_error", "invalid_api_key")
})
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{"input":"hi"}`))
req.Header.Set("Authorization", "Bearer wrong-key")
w := httptest.NewRecorder()
handler(w, req)
entries := rec.Entries()
if len(entries) != 1 {
t.Fatalf("got %d entries, want 1", len(entries))
}
e := entries[0]
if e.Status != 401 {
t.Fatalf("status = %d, want 401", e.Status)
}
if !strings.Contains(e.ResponseBody, "invalid api key") {
t.Fatalf("response body = %q", e.ResponseBody)
}
}
// TestGetRecentAPI verifies GET /api/recent returns recorded entries as JSON.
func TestGetRecentAPI(t *testing.T) {
rec := NewRecentRecorder()
rec.Record(RecentEntry{Method: "POST", Path: "/v1/chat/completions", Status: 200})
rec.Record(RecentEntry{Method: "POST", Path: "/v1/responses", Status: 500})
s := &Server{recorder: rec}
req := httptest.NewRequest(http.MethodGet, "/api/recent", nil)
w := httptest.NewRecorder()
s.getRecent(w, req)
body, _ := io.ReadAll(w.Body)
if !strings.Contains(string(body), `"ok":true`) {
t.Fatalf("response missing ok:true: %s", string(body))
}
if !strings.Contains(string(body), "/v1/responses") {
t.Fatalf("response missing /v1/responses: %s", string(body))
}
// newest first
idxResponses := strings.Index(string(body), "/v1/responses")
idxChat := strings.Index(string(body), "/v1/chat/completions")
if idxResponses < 0 || idxChat < 0 || idxResponses > idxChat {
t.Fatalf("entries not newest-first: %s", string(body))
}
}