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") } }