package httpx import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" ) func TestWriteJSON(t *testing.T) { rec := httptest.NewRecorder() WriteJSON(rec, http.StatusCreated, map[string]any{"id": "x", "n": 1}) if rec.Code != http.StatusCreated { t.Errorf("status = %d; want 201", rec.Code) } ct := rec.Header().Get("Content-Type") if ct != "application/json; charset=utf-8" { t.Errorf("Content-Type = %q", ct) } if body := rec.Body.String(); body != `{"id":"x","n":1}`+"\n" { t.Errorf("body = %q", body) } } // A nil value, or 204 No Content, writes the status and headers but no body. func TestWriteJSONNoBody(t *testing.T) { tests := []struct { name string status int v any }{ {"nil value", http.StatusOK, nil}, {"no content", http.StatusNoContent, map[string]string{"ignored": "yes"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { rec := httptest.NewRecorder() WriteJSON(rec, tt.status, tt.v) if rec.Code != tt.status || rec.Body.Len() != 0 { t.Errorf( "got %d %q; want %d and no body", rec.Code, rec.Body, tt.status, ) } ct := rec.Header().Get("Content-Type") if ct != "application/json; charset=utf-8" { t.Errorf("Content-Type = %q", ct) } }) } } // A value that can't be encoded still gets its status; the body is empty. func TestWriteJSONUnencodable(t *testing.T) { rec := httptest.NewRecorder() WriteJSON(rec, http.StatusOK, func() { }) if rec.Code != http.StatusOK || rec.Body.Len() != 0 { t.Errorf("got %d %q; want 200 and no body", rec.Code, rec.Body) } } func TestWriteProblem(t *testing.T) { req := httptest.NewRequest("GET", "/api/items/1", nil) req = req.WithContext(WithRequestID(req.Context(), "req-123")) rec := httptest.NewRecorder() WriteProblem(rec, req, http.StatusNotFound, "no such item") if rec.Code != http.StatusNotFound { t.Errorf("status = %d; want 404", rec.Code) } if ct := rec.Header().Get("Content-Type"); ct != "application/problem+json" { t.Errorf("Content-Type = %q", ct) } nosniff := rec.Header().Get("X-Content-Type-Options") if nosniff != "nosniff" { t.Errorf("X-Content-Type-Options = %q", nosniff) } var p Problem if err := json.Unmarshal(rec.Body.Bytes(), &p); err != nil { t.Fatalf("body %q: %v", rec.Body, err) } want := Problem{ Type: "about:blank", Status: http.StatusNotFound, Title: "Not Found", Detail: "no such item", RequestID: "req-123", } if p != want { t.Errorf("problem = %+v; want %+v", p, want) } } // Without a request (or a request ID) the problem has no requestId, and an // empty detail is left out. func TestWriteProblemMinimal(t *testing.T) { for _, req := range []*http.Request{ nil, httptest.NewRequest("GET", "/", nil), } { rec := httptest.NewRecorder() WriteProblem(rec, req, http.StatusTooManyRequests, "") var body map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatalf("body %q: %v", rec.Body, err) } want := map[string]any{ "type": "about:blank", "status": float64(http.StatusTooManyRequests), "title": "Too Many Requests", } if len(body) != len(want) { t.Errorf("request %v: body = %v; want %v", req != nil, body, want) } for k, v := range want { if body[k] != v { t.Errorf("request %v: %s = %v; want %v", req != nil, k, body[k], v) } } } } func TestRequestID(t *testing.T) { ctx := context.Background() if id := RequestID(ctx); id != "" { t.Errorf("RequestID(empty) = %q; want empty", id) } ctx = WithRequestID(ctx, "abc") if id := RequestID(ctx); id != "abc" { t.Errorf("RequestID = %q; want abc", id) } // The latest ID wins; a value of another type under the key is ignored. if id := RequestID(WithRequestID(ctx, "def")); id != "def" { t.Errorf("RequestID after replacing = %q; want def", id) } if id := RequestID(context.WithValue(ctx, requestIDKey, 42)); id != "" { t.Errorf("RequestID(int) = %q; want empty", id) } }