proxy-pool/internal/platform/httpapi/httpapi_test.go
2026-07-29 10:14:56 +08:00

152 lines
5.0 KiB
Go

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