All checks were successful
CI / test (push) Successful in 30s
Cover edge cases: statusWriter multi-call/default/unwrap, UUID v4 format and uniqueness, non-string panics, recovery body and log attributes, 4xx log level, default status in logging, request ID propagation, server defaults/options/listen-error/multiple-hooks/logger, router groups with empty prefix/inherited middleware/ordering/path params/ isolation, mount trailing slash, health content-type and POST rejection. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
167 lines
4.2 KiB
Go
167 lines
4.2 KiB
Go
package server_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"git.codelab.vc/pkg/httpx/server"
|
|
)
|
|
|
|
func TestHealthHandler(t *testing.T) {
|
|
t.Run("liveness always returns 200", func(t *testing.T) {
|
|
h := server.HealthHandler()
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
|
h.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("got status %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
var resp map[string]any
|
|
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("decode failed: %v", err)
|
|
}
|
|
if resp["status"] != "ok" {
|
|
t.Fatalf("got status %q, want %q", resp["status"], "ok")
|
|
}
|
|
})
|
|
|
|
t.Run("readiness returns 200 when all checks pass", func(t *testing.T) {
|
|
h := server.HealthHandler(
|
|
func() error { return nil },
|
|
func() error { return nil },
|
|
)
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/readyz", nil)
|
|
h.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("got status %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
})
|
|
|
|
t.Run("readiness returns 503 when a check fails", func(t *testing.T) {
|
|
h := server.HealthHandler(
|
|
func() error { return nil },
|
|
func() error { return errors.New("db down") },
|
|
)
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/readyz", nil)
|
|
h.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusServiceUnavailable {
|
|
t.Fatalf("got status %d, want %d", w.Code, http.StatusServiceUnavailable)
|
|
}
|
|
|
|
var resp map[string]any
|
|
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("decode failed: %v", err)
|
|
}
|
|
if resp["status"] != "unavailable" {
|
|
t.Fatalf("got status %q, want %q", resp["status"], "unavailable")
|
|
}
|
|
errs, ok := resp["errors"].([]any)
|
|
if !ok || len(errs) != 1 {
|
|
t.Fatalf("expected 1 error, got %v", resp["errors"])
|
|
}
|
|
if errs[0] != "db down" {
|
|
t.Fatalf("got error %q, want %q", errs[0], "db down")
|
|
}
|
|
})
|
|
|
|
t.Run("readiness returns 200 with no checkers", func(t *testing.T) {
|
|
h := server.HealthHandler()
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/readyz", nil)
|
|
h.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("got status %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHealth_MultipleFailingCheckers(t *testing.T) {
|
|
h := server.HealthHandler(
|
|
func() error { return errors.New("db down") },
|
|
func() error { return errors.New("cache down") },
|
|
)
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/readyz", nil)
|
|
h.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusServiceUnavailable {
|
|
t.Fatalf("got status %d, want %d", w.Code, http.StatusServiceUnavailable)
|
|
}
|
|
|
|
var resp map[string]any
|
|
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("decode failed: %v", err)
|
|
}
|
|
|
|
errs, ok := resp["errors"].([]any)
|
|
if !ok || len(errs) != 2 {
|
|
t.Fatalf("expected 2 errors, got %v", resp["errors"])
|
|
}
|
|
|
|
errStrs := make(map[string]bool)
|
|
for _, e := range errs {
|
|
errStrs[e.(string)] = true
|
|
}
|
|
if !errStrs["db down"] || !errStrs["cache down"] {
|
|
t.Fatalf("expected 'db down' and 'cache down', got %v", errs)
|
|
}
|
|
}
|
|
|
|
func TestHealth_LivenessContentType(t *testing.T) {
|
|
h := server.HealthHandler()
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
|
h.ServeHTTP(w, req)
|
|
|
|
ct := w.Header().Get("Content-Type")
|
|
if ct != "application/json" {
|
|
t.Fatalf("got Content-Type %q, want %q", ct, "application/json")
|
|
}
|
|
}
|
|
|
|
func TestHealth_ReadinessContentType(t *testing.T) {
|
|
h := server.HealthHandler()
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/readyz", nil)
|
|
h.ServeHTTP(w, req)
|
|
|
|
ct := w.Header().Get("Content-Type")
|
|
if ct != "application/json" {
|
|
t.Fatalf("got Content-Type %q, want %q", ct, "application/json")
|
|
}
|
|
}
|
|
|
|
func TestHealth_PostMethodNotAllowed(t *testing.T) {
|
|
h := server.HealthHandler()
|
|
|
|
for _, path := range []string{"/healthz", "/readyz"} {
|
|
t.Run("POST "+path, func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, path, nil)
|
|
h.ServeHTTP(w, req)
|
|
|
|
// ServeMux with "GET /healthz" pattern should reject POST.
|
|
if w.Code == http.StatusOK {
|
|
t.Fatalf("POST %s should not return 200, got %d", path, w.Code)
|
|
}
|
|
})
|
|
}
|
|
}
|