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)) } }