package httpapi import ( "errors" "net/http" "net/http/httptest" "strings" "testing" ) func TestDecodeJSONAcceptsSingleStrictDocument(t *testing.T) { t.Parallel() type payload struct { Count int `json:"count"` } request := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"count":2}`)) request.Header.Set("Content-Type", "application/json; charset=utf-8") recorder := httptest.NewRecorder() var decoded payload if err := DecodeJSON(recorder, request, 64, &decoded); err != nil { t.Fatalf("DecodeJSON() error = %v", err) } if decoded.Count != 2 { t.Fatalf("decoded count = %d, want 2", decoded.Count) } } func TestDecodeJSONRejectsUnsafeInput(t *testing.T) { t.Parallel() tests := []struct { name string contentType string body string maxBytes int64 wantErr error }{ {name: "missing content type", body: `{}`, maxBytes: 64, wantErr: ErrUnsupportedMediaType}, {name: "wrong content type", contentType: "text/plain", body: `{}`, maxBytes: 64, wantErr: ErrUnsupportedMediaType}, {name: "unknown field", contentType: "application/json", body: `{"extra":true}`, maxBytes: 64, wantErr: ErrInvalidJSON}, {name: "multiple documents", contentType: "application/json", body: `{} {}`, maxBytes: 64, wantErr: ErrInvalidJSON}, {name: "oversized", contentType: "application/json", body: `{"value":"0123456789"}`, maxBytes: 8, wantErr: ErrBodyTooLarge}, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { t.Parallel() request := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(test.body)) request.Header.Set("Content-Type", test.contentType) recorder := httptest.NewRecorder() var decoded struct { Value string `json:"value"` } err := DecodeJSON(recorder, request, test.maxBytes, &decoded) if !errors.Is(err, test.wantErr) { t.Fatalf("DecodeJSON() error = %v, want %v", err, test.wantErr) } }) } } func TestResolveRequestID(t *testing.T) { t.Parallel() request := httptest.NewRequest(http.MethodGet, "/", nil) request.Header.Set(HeaderRequestID, "caller-request") requestID, err := ResolveRequestID(request) if err != nil { t.Fatalf("ResolveRequestID() error = %v", err) } if requestID != "caller-request" { t.Fatalf("request ID = %q, want caller-request", requestID) } generated, err := ResolveRequestID(httptest.NewRequest(http.MethodGet, "/", nil)) if err != nil { t.Fatalf("ResolveRequestID() generated error = %v", err) } if !strings.HasPrefix(generated, "req_") || len(generated) != 36 { t.Fatalf("generated request ID = %q", generated) } } func TestResolveRequestIDRejectsInvalidValues(t *testing.T) { t.Parallel() for _, value := range []string{" request", strings.Repeat("a", 129), "request\x7f"} { request := httptest.NewRequest(http.MethodGet, "/", nil) request.Header.Set(HeaderRequestID, value) requestID, err := ResolveRequestID(request) if !errors.Is(err, ErrInvalidRequestID) { t.Fatalf("ResolveRequestID(%q) error = %v, want %v", value, err, ErrInvalidRequestID) } if !strings.HasPrefix(requestID, "req_") { t.Fatalf("ResolveRequestID(%q) fallback = %q, want generated ID", value, requestID) } } } func TestResolveRequestIDRejectsDuplicateHeader(t *testing.T) { t.Parallel() request := httptest.NewRequest(http.MethodGet, "/", nil) request.Header.Add(HeaderRequestID, "req-one") request.Header.Add(HeaderRequestID, "req-two") requestID, err := ResolveRequestID(request) if !errors.Is(err, ErrInvalidRequestID) { t.Fatalf("ResolveRequestID() error = %v, want %v", err, ErrInvalidRequestID) } if !strings.HasPrefix(requestID, "req_") { t.Fatalf("ResolveRequestID() fallback = %q, want generated ID", requestID) } } func TestWriteProblemUsesStableContract(t *testing.T) { t.Parallel() recorder := httptest.NewRecorder() WriteProblem(recorder, Problem{ Type: "https://proxy-pool.local/problems/invalid-request", Title: "Invalid request", Status: http.StatusBadRequest, Code: "INVALID_REQUEST", Detail: "request payload is invalid", RequestID: "req-1", }) if recorder.Code != http.StatusBadRequest { t.Fatalf("status = %d, want %d", recorder.Code, http.StatusBadRequest) } if contentType := recorder.Header().Get("Content-Type"); contentType != ProblemContentType { t.Fatalf("Content-Type = %q, want %q", contentType, ProblemContentType) } if requestID := recorder.Header().Get(HeaderRequestID); requestID != "req-1" { t.Fatalf("X-Request-ID = %q, want req-1", requestID) } if body := recorder.Body.String(); !strings.Contains(body, `"code":"INVALID_REQUEST"`) || strings.Contains(body, "\n ") { t.Fatalf("unexpected problem body %q", body) } } func TestNewProblemBuildsCanonicalType(t *testing.T) { t.Parallel() problem := NewProblem(http.StatusConflict, "IDEMPOTENCY_CONFLICT", "Idempotency conflict", "request changed", "req-1") if problem.Type != "https://proxy-pool.local/problems/idempotency-conflict" || problem.Status != http.StatusConflict || problem.Code != "IDEMPOTENCY_CONFLICT" || problem.RequestID != "req-1" { t.Fatalf("NewProblem() = %+v", problem) } }