152 lines
5.0 KiB
Go
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)
|
|
}
|
|
}
|