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>
337 lines
8.9 KiB
Go
337 lines
8.9 KiB
Go
package server_test
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"git.codelab.vc/pkg/httpx/server"
|
|
)
|
|
|
|
func TestRouter(t *testing.T) {
|
|
t.Run("basic route", func(t *testing.T) {
|
|
r := server.NewRouter()
|
|
r.HandleFunc("GET /hello", func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte("world"))
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/hello", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("got status %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
if body := w.Body.String(); body != "world" {
|
|
t.Fatalf("got body %q, want %q", body, "world")
|
|
}
|
|
})
|
|
|
|
t.Run("Handle with http.Handler", func(t *testing.T) {
|
|
r := server.NewRouter()
|
|
r.Handle("GET /ping", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte("pong"))
|
|
}))
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if body := w.Body.String(); body != "pong" {
|
|
t.Fatalf("got body %q, want %q", body, "pong")
|
|
}
|
|
})
|
|
|
|
t.Run("path parameter", func(t *testing.T) {
|
|
r := server.NewRouter()
|
|
r.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, req *http.Request) {
|
|
_, _ = w.Write([]byte("user:" + req.PathValue("id")))
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/users/42", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if body := w.Body.String(); body != "user:42" {
|
|
t.Fatalf("got body %q, want %q", body, "user:42")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRouterGroup(t *testing.T) {
|
|
t.Run("prefix is applied", func(t *testing.T) {
|
|
r := server.NewRouter()
|
|
api := r.Group("/api/v1")
|
|
api.HandleFunc("GET /users", func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte("users"))
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/users", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("got status %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
if body := w.Body.String(); body != "users" {
|
|
t.Fatalf("got body %q, want %q", body, "users")
|
|
}
|
|
})
|
|
|
|
t.Run("nested groups", func(t *testing.T) {
|
|
r := server.NewRouter()
|
|
api := r.Group("/api")
|
|
v1 := api.Group("/v1")
|
|
v1.HandleFunc("GET /items", func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte("items"))
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/items", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if body := w.Body.String(); body != "items" {
|
|
t.Fatalf("got body %q, want %q", body, "items")
|
|
}
|
|
})
|
|
|
|
t.Run("group middleware", func(t *testing.T) {
|
|
var mwCalled bool
|
|
mw := func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
mwCalled = true
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
r := server.NewRouter()
|
|
g := r.Group("/admin", mw)
|
|
g.HandleFunc("GET /dashboard", func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte("ok"))
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/admin/dashboard", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if !mwCalled {
|
|
t.Fatal("group middleware was not called")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRouterMount(t *testing.T) {
|
|
t.Run("mounts sub-handler with prefix stripping", func(t *testing.T) {
|
|
sub := http.NewServeMux()
|
|
sub.HandleFunc("GET /info", func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte("info"))
|
|
})
|
|
|
|
r := server.NewRouter()
|
|
r.Mount("/sub", sub)
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/sub/info", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
body, _ := io.ReadAll(w.Body)
|
|
if string(body) != "info" {
|
|
t.Fatalf("got body %q, want %q", body, "info")
|
|
}
|
|
})
|
|
|
|
t.Run("mount with trailing slash", func(t *testing.T) {
|
|
sub := http.NewServeMux()
|
|
sub.HandleFunc("GET /data", func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte("data"))
|
|
})
|
|
|
|
r := server.NewRouter()
|
|
r.Mount("/sub/", sub)
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/sub/data", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
body, _ := io.ReadAll(w.Body)
|
|
if string(body) != "data" {
|
|
t.Fatalf("got body %q, want %q", body, "data")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRouter_PatternWithoutMethod(t *testing.T) {
|
|
r := server.NewRouter()
|
|
r.HandleFunc("/static/", func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte("static"))
|
|
})
|
|
|
|
for _, method := range []string{http.MethodGet, http.MethodPost} {
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(method, "/static/file.css", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("%s /static/file.css: got status %d, want %d", method, w.Code, http.StatusOK)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRouter_GroupEmptyPrefix(t *testing.T) {
|
|
r := server.NewRouter()
|
|
g := r.Group("")
|
|
g.HandleFunc("GET /hello", func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte("hello"))
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/hello", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("got status %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
if body := w.Body.String(); body != "hello" {
|
|
t.Fatalf("got body %q, want %q", body, "hello")
|
|
}
|
|
}
|
|
|
|
func TestRouter_GroupInheritsMiddleware(t *testing.T) {
|
|
var order []string
|
|
|
|
parentMW := func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
order = append(order, "parent")
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
childMW := func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
order = append(order, "child")
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
r := server.NewRouter()
|
|
parent := r.Group("/api", parentMW)
|
|
child := parent.Group("/v1", childMW)
|
|
child.HandleFunc("GET /items", func(w http.ResponseWriter, _ *http.Request) {
|
|
order = append(order, "handler")
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/items", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("got status %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
expected := []string{"parent", "child", "handler"}
|
|
if len(order) != len(expected) {
|
|
t.Fatalf("got %v, want %v", order, expected)
|
|
}
|
|
for i, v := range expected {
|
|
if order[i] != v {
|
|
t.Fatalf("order[%d] = %q, want %q", i, order[i], v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRouter_GroupMiddlewareOrder(t *testing.T) {
|
|
var order []string
|
|
|
|
mwA := func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
order = append(order, "A")
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
mwB := func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
order = append(order, "B")
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
r := server.NewRouter()
|
|
g := r.Group("/api", mwA)
|
|
sub := g.Group("/v1", mwB)
|
|
sub.HandleFunc("GET /test", func(w http.ResponseWriter, _ *http.Request) {
|
|
order = append(order, "handler")
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/test", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
// Parent MW (A) should run before child MW (B), then handler.
|
|
expected := []string{"A", "B", "handler"}
|
|
if len(order) != len(expected) {
|
|
t.Fatalf("got %v, want %v", order, expected)
|
|
}
|
|
for i, v := range expected {
|
|
if order[i] != v {
|
|
t.Fatalf("order[%d] = %q, want %q", i, order[i], v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRouter_PathParamWithGroup(t *testing.T) {
|
|
r := server.NewRouter()
|
|
api := r.Group("/api")
|
|
api.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, req *http.Request) {
|
|
_, _ = w.Write([]byte("id=" + req.PathValue("id")))
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/users/42", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("got status %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
if body := w.Body.String(); body != "id=42" {
|
|
t.Fatalf("got body %q, want %q", body, "id=42")
|
|
}
|
|
}
|
|
|
|
func TestRouter_MiddlewareNotAppliedToOtherRoutes(t *testing.T) {
|
|
var mwCalled bool
|
|
mw := func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
mwCalled = true
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
r := server.NewRouter()
|
|
|
|
// Add middleware only to /admin group.
|
|
admin := r.Group("/admin", mw)
|
|
admin.HandleFunc("GET /dashboard", func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte("admin"))
|
|
})
|
|
|
|
// Route outside the group.
|
|
r.HandleFunc("GET /public", func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte("public"))
|
|
})
|
|
|
|
// Request to /public should NOT trigger group middleware.
|
|
mwCalled = false
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/public", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if mwCalled {
|
|
t.Fatal("group middleware should not be called for routes outside the group")
|
|
}
|
|
if w.Body.String() != "public" {
|
|
t.Fatalf("got body %q, want %q", w.Body.String(), "public")
|
|
}
|
|
}
|