package problem import ( "encoding/json" "net/http" "net/http/httptest" "testing" ) func TestProblemDocument(t *testing.T) { tests := []struct { name string problem *Problem statusCode int hasType bool hasTitle bool hasStatus bool hasDetail bool }{ { name: "BadRequest", problem: BadRequest("missing field: model"), statusCode: http.StatusBadRequest, hasType: true, hasTitle: true, hasStatus: true, hasDetail: true, }, { name: "PayloadTooLarge", problem: PayloadTooLarge("request body 1001 bytes exceeds max 1000"), statusCode: http.StatusRequestEntityTooLarge, hasType: true, hasTitle: true, hasStatus: true, hasDetail: true, }, { name: "TooManyRequests", problem: TooManyRequests("rate limit exceeded", 60), statusCode: http.StatusTooManyRequests, hasType: true, hasTitle: true, hasStatus: true, hasDetail: true, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { w := httptest.NewRecorder() err := tc.problem.Write(w) if err != nil { t.Fatalf("Write failed: %v", err) } // Check status code if w.Code != tc.statusCode { t.Errorf("expected status %d, got %d", tc.statusCode, w.Code) } // Check Content-Type if ct := w.Header().Get("Content-Type"); ct != "application/problem+json" { t.Errorf("expected Content-Type: application/problem+json, got %s", ct) } // Parse response body var p Problem err = json.Unmarshal(w.Body.Bytes(), &p) if err != nil { t.Fatalf("failed to unmarshal response: %v", err) } // Verify required fields if tc.hasType && p.Type == "" { t.Errorf("expected 'type' field, got empty") } if tc.hasTitle && p.Title == "" { t.Errorf("expected 'title' field, got empty") } if tc.hasStatus && p.Status == 0 { t.Errorf("expected 'status' field, got 0") } if tc.hasDetail && p.Detail == "" { t.Errorf("expected 'detail' field, got empty") } // Verify status matches HTTP response code if p.Status != w.Code { t.Errorf("status field %d does not match HTTP status %d", p.Status, w.Code) } }) } } func TestProblemRetryAfter(t *testing.T) { p := TooManyRequests("rate limit", 120) w := httptest.NewRecorder() err := p.Write(w) if err != nil { t.Fatalf("Write failed: %v", err) } // Check Retry-After header is set if ra := w.Header().Get("Retry-After"); ra == "" { t.Errorf("expected Retry-After header, got empty") } var body Problem json.Unmarshal(w.Body.Bytes(), &body) if body.RetryAfter == nil || *body.RetryAfter != 120 { t.Errorf("expected RetryAfter=120, got %v", body.RetryAfter) } } func TestProblemWithExtra(t *testing.T) { p := BadRequest("invalid request") p.WithExtra("field", "model") p.WithExtra("reason", "unknown_model") w := httptest.NewRecorder() err := p.Write(w) if err != nil { t.Fatalf("Write failed: %v", err) } var body Problem json.Unmarshal(w.Body.Bytes(), &body) if body.Extra["field"] != "model" { t.Errorf("expected extra field 'model', got %v", body.Extra["field"]) } if body.Extra["reason"] != "unknown_model" { t.Errorf("expected extra reason 'unknown_model', got %v", body.Extra["reason"]) } } func TestNoSecretsInProblem(t *testing.T) { // Verify that secrets, tokens, bodies are never leaked p := Unauthorized("invalid bearer token"). WithExtra("attempted_route", "/v1/chat/completions") w := httptest.NewRecorder() p.Write(w) body := w.Body.String() // Should not contain any auth-related secrets if len(body) > 200 { t.Errorf("problem document too large for detail: %d bytes (check for leaked content)", len(body)) } // Parse and verify no sensitive fields are present var doc Problem json.Unmarshal(w.Body.Bytes(), &doc) // Detail should describe the problem, not echo the token if len(doc.Detail) > 100 { t.Errorf("detail too long: %s", doc.Detail) } }