Add production-ready HTTP server package with routing, health checks, and middleware
Introduces server/ sub-package as the server-side companion to the existing Client. Includes Router (over http.ServeMux with groups and mounting), graceful shutdown with signal handling, health endpoints (/healthz, /readyz), and built-in middlewares (RequestID, Recovery, Logging). Zero external dependencies. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
90
server/health_test.go
Normal file
90
server/health_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user