1
0
Fork 1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-06-02 00:38:16 -04:00
pkg-proxy/internal/server/errors_test.go
Andrew Nesbitt 8d2740624f
Structured JSON error responses for API endpoints (#110)
* Structured JSON error responses for API endpoints

API handlers returned errors via http.Error (text/plain) with ad-hoc
strings, while the mirror API used a different {"error": "..."} shape
and leaked internal err.Error() text to clients.

Add ErrorResponse{Code, Message} with stable codes (BAD_REQUEST,
NOT_FOUND, UPSTREAM_ERROR, INTERNAL_ERROR) and writeError/badRequest/
notFound/internalError helpers. Convert all JSON API handlers in
api.go, browse.go, mirror_api.go and the /stats endpoint. Enrichment
failures now report 502 UPSTREAM_ERROR rather than 500.

Protocol handlers in internal/handler/ are deliberately unchanged
since npm/pip/cargo clients expect their own response formats, not
JSON. HTML page handlers in server.go also keep text/plain.

Swagger @Failure annotations updated and docs regenerated.

Fixes #76

* Convert validatePackagePath errors to JSON in API handlers
2026-05-03 09:42:03 +01:00

93 lines
2.3 KiB
Go

package server
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestWriteError(t *testing.T) {
tests := []struct {
name string
fn func(w http.ResponseWriter)
status int
code string
message string
}{
{
name: "badRequest",
fn: func(w http.ResponseWriter) { badRequest(w, "missing field") },
status: http.StatusBadRequest,
code: ErrCodeBadRequest,
message: "missing field",
},
{
name: "notFound",
fn: func(w http.ResponseWriter) { notFound(w, "package not found") },
status: http.StatusNotFound,
code: ErrCodeNotFound,
message: "package not found",
},
{
name: "internalError",
fn: func(w http.ResponseWriter) { internalError(w, "boom") },
status: http.StatusInternalServerError,
code: ErrCodeInternal,
message: "boom",
},
{
name: "upstream",
fn: func(w http.ResponseWriter) {
writeError(w, http.StatusBadGateway, ErrCodeUpstream, "registry unreachable")
},
status: http.StatusBadGateway,
code: ErrCodeUpstream,
message: "registry unreachable",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := httptest.NewRecorder()
tt.fn(w)
if w.Code != tt.status {
t.Errorf("status = %d, want %d", w.Code, tt.status)
}
if ct := w.Header().Get("Content-Type"); ct != "application/json" {
t.Errorf("Content-Type = %q, want application/json", ct)
}
var resp ErrorResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("response body is not valid JSON: %v (body: %q)", err, w.Body.String())
}
if resp.Code != tt.code {
t.Errorf("code = %q, want %q", resp.Code, tt.code)
}
if resp.Message != tt.message {
t.Errorf("message = %q, want %q", resp.Message, tt.message)
}
})
}
}
func TestAPIErrorResponseShape(t *testing.T) {
w := httptest.NewRecorder()
badRequest(w, "x")
var raw map[string]any
if err := json.Unmarshal(w.Body.Bytes(), &raw); err != nil {
t.Fatalf("invalid JSON: %v", err)
}
if _, ok := raw["code"]; !ok {
t.Error("response missing 'code' field")
}
if _, ok := raw["message"]; !ok {
t.Error("response missing 'message' field")
}
if len(raw) != 2 {
t.Errorf("response has unexpected fields: %v", raw)
}
}