feat: standardize transport contracts across Gin and nethttp
This commit is contained in:
198
transport/transporttest/router.go
Normal file
198
transport/transporttest/router.go
Normal file
@@ -0,0 +1,198 @@
|
||||
// Package transporttest provides the reusable acceptance suite for GoOmni
|
||||
// router adapters. Import it only from tests and pass a fresh router factory.
|
||||
// Register routes before serving; the suite exercises concurrent requests.
|
||||
package transporttest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
||||
)
|
||||
|
||||
// Run verifies routing and the same business handlers on a supplied adapter.
|
||||
// Factories are called serially and must return a fresh, empty router.
|
||||
func Run(t *testing.T, newRouter func() httpx.Router) {
|
||||
t.Helper()
|
||||
t.Run("routing", func(t *testing.T) { routing(t, newRouter) })
|
||||
t.Run("registration", func(t *testing.T) { registration(t, newRouter) })
|
||||
t.Run("application", func(t *testing.T) { application(t, newRouter) })
|
||||
}
|
||||
|
||||
type contextKey struct{}
|
||||
|
||||
type routeResult struct {
|
||||
Pattern string `json:"pattern"`
|
||||
Params map[string]string `json:"params"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
func routing(t *testing.T, newRouter func() httpx.Router) {
|
||||
r := newRouter()
|
||||
echo := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
value, _ := req.Context().Value(contextKey{}).(string)
|
||||
_ = json.NewEncoder(w).Encode(routeResult{
|
||||
Pattern: httpx.RoutePattern(req.Context()), Params: httpx.RouteParams(req.Context()), Value: value,
|
||||
})
|
||||
})
|
||||
for _, path := range []string{
|
||||
"/", "/users/:id", "/users/me", "/files/*path", "/slash/",
|
||||
"/choice/:first/fixed/final", "/choice/static/:second/:third",
|
||||
"/back/static", "/back/:id/details",
|
||||
} {
|
||||
r.Handle(http.MethodGet, path, echo)
|
||||
}
|
||||
// Registration order must not determine static-vs-parameter precedence.
|
||||
r.Handle(http.MethodPost, "/users/me", echo)
|
||||
r.Handle(http.MethodPost, "/users/:id", echo)
|
||||
r.Handle(http.MethodHead, "/head", echo)
|
||||
r.Handle(http.MethodOptions, "/head", echo)
|
||||
r.Handle("REPORT", "/report", echo)
|
||||
group := r.Group("/api/").Group("v1")
|
||||
group.Handle(http.MethodGet, "/users/:id", echo)
|
||||
group.Handle(http.MethodGet, "", echo)
|
||||
group.Handle(http.MethodGet, "/", echo)
|
||||
group.Any("/any", echo)
|
||||
r.Any("/any", echo)
|
||||
|
||||
cases := []struct {
|
||||
method, path, pattern, param, value string
|
||||
status int
|
||||
allow string
|
||||
}{
|
||||
{"GET", "/", "/", "", "", 200, ""},
|
||||
{"GET", "/users/42?x=y", "/users/:id", "id", "42", 200, ""},
|
||||
{"GET", "/users/me", "/users/me", "", "", 200, ""},
|
||||
{"POST", "/users/me", "/users/me", "", "", 200, ""},
|
||||
{"GET", "/users/%E4%B8%AD%E6%96%87", "/users/:id", "id", "中文", 200, ""},
|
||||
{"GET", "/users/a%2Fb", "", "", "", 404, ""},
|
||||
{"GET", "/users/a+b", "/users/:id", "id", "a+b", 200, ""},
|
||||
{"GET", "/users/", "", "", "", 404, ""},
|
||||
{"GET", "/users/42/", "", "", "", 404, ""},
|
||||
{"GET", "/users//42", "", "", "", 404, ""},
|
||||
{"GET", "/Users/42", "", "", "", 404, ""},
|
||||
{"GET", "/slash", "", "", "", 404, ""},
|
||||
{"GET", "/slash/", "/slash/", "", "", 200, ""},
|
||||
{"GET", "/files", "", "", "", 404, ""},
|
||||
{"GET", "/files/", "/files/*path", "path", "/", 200, ""},
|
||||
{"GET", "/files/a%2Fb/c", "/files/*path", "path", "/a/b/c", 200, ""},
|
||||
{"GET", "/files//a", "/files/*path", "path", "//a", 200, ""},
|
||||
{"GET", "/choice/static/fixed/final", "/choice/static/:second/:third", "second", "fixed", 200, ""},
|
||||
{"GET", "/back/static/details", "/back/:id/details", "id", "static", 200, ""},
|
||||
{"GET", "/api/v1/users/42", "/api/v1/users/:id", "id", "42", 200, ""},
|
||||
{"GET", "/api/v1", "/api/v1", "", "", 200, ""},
|
||||
{"GET", "/api/v1/", "/api/v1/", "", "", 200, ""},
|
||||
{"REPORT", "/report", "/report", "", "", 200, ""},
|
||||
{"HEAD", "/head", "/head", "", "", 200, ""},
|
||||
{"OPTIONS", "/head", "/head", "", "", 200, ""},
|
||||
{"HEAD", "/users/42", "", "", "", 405, "GET, POST"},
|
||||
{"OPTIONS", "/users/42", "", "", "", 405, "GET, POST"},
|
||||
{"DELETE", "/users/me", "", "", "", 405, "GET, POST"},
|
||||
{"GET", "/head", "", "", "", 405, "HEAD, OPTIONS"},
|
||||
{"GET", "/missing", "", "", "", 404, ""},
|
||||
{"REPORT", "/any", "", "", "", 405, "CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE"},
|
||||
}
|
||||
for _, method := range []string{"GET", "POST", "PUT", "PATCH", "HEAD", "OPTIONS", "DELETE", "CONNECT", "TRACE"} {
|
||||
for _, path := range []string{"/any", "/api/v1/any"} {
|
||||
cases = append(cases, struct {
|
||||
method, path, pattern, param, value string
|
||||
status int
|
||||
allow string
|
||||
}{method, path, path, "", "", 200, ""})
|
||||
}
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.method+" "+tc.path, func(t *testing.T) {
|
||||
req := httptest.NewRequest(tc.method, tc.path, nil)
|
||||
// CONNECT has authority-form handling in net/http's parser; the
|
||||
// adapter contract consumes an already parsed URL.Path.
|
||||
if tc.method == "CONNECT" {
|
||||
req.URL.Path = tc.path
|
||||
}
|
||||
req = req.WithContext(context.WithValue(req.Context(), contextKey{}, "preserved"))
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != tc.status || w.Header().Get("Allow") != tc.allow || w.Header().Get("Location") != "" {
|
||||
t.Fatalf("status=%d allow=%q location=%q body=%s", w.Code, w.Header().Get("Allow"), w.Header().Get("Location"), w.Body)
|
||||
}
|
||||
if tc.status != 200 {
|
||||
want := "404 page not found\n"
|
||||
if tc.status == 405 {
|
||||
want = "Method Not Allowed\n"
|
||||
}
|
||||
if w.Body.String() != want || !strings.HasPrefix(w.Header().Get("Content-Type"), "text/plain") {
|
||||
t.Fatalf("unexpected fallback response: %s", w.Body)
|
||||
}
|
||||
return
|
||||
}
|
||||
var got routeResult
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantParams := map[string]string{}
|
||||
if tc.param != "" {
|
||||
wantParams[tc.param] = tc.value
|
||||
}
|
||||
// Routes with several parameters are checked in full.
|
||||
if tc.pattern == "/choice/static/:second/:third" {
|
||||
wantParams["third"] = "final"
|
||||
}
|
||||
if got.Pattern != tc.pattern || !reflect.DeepEqual(got.Params, wantParams) || got.Value != "preserved" {
|
||||
t.Fatalf("route metadata=%+v, want %s %v with original context", got, tc.pattern, wantParams)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func registration(t *testing.T, newRouter func() httpx.Router) {
|
||||
handler := http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})
|
||||
cases := []struct {
|
||||
name string
|
||||
register func(httpx.Router)
|
||||
}{
|
||||
{"nil handler", func(r httpx.Router) { r.Handle("GET", "/", nil) }},
|
||||
{"empty method", func(r httpx.Router) { r.Handle("", "/", handler) }},
|
||||
{"lowercase method", func(r httpx.Router) { r.Handle("get", "/", handler) }},
|
||||
}
|
||||
for _, path := range []string{"", "relative", "/:", "/*", "/a/*rest/b", "/a/:id/:id", "/a//b", "/a/../b", "/a?b", "/prefix:id", "/:a:b"} {
|
||||
cases = append(cases, struct {
|
||||
name string
|
||||
register func(httpx.Router)
|
||||
}{"invalid " + path, func(r httpx.Router) { r.Handle("GET", path, handler) }})
|
||||
}
|
||||
for _, pair := range [][2]string{
|
||||
{"/users", "/users"}, {"/users/:id", "/users/:name/more"},
|
||||
{"/files/*path", "/files/static"}, {"/files/static", "/files/*path"},
|
||||
{"/", "/*path"}, {"/*path", "/"},
|
||||
} {
|
||||
cases = append(cases, struct {
|
||||
name string
|
||||
register func(httpx.Router)
|
||||
}{"conflict " + pair[0] + " " + pair[1], func(r httpx.Router) {
|
||||
r.Handle("GET", pair[0], handler)
|
||||
r.Handle("GET", pair[1], handler)
|
||||
}})
|
||||
}
|
||||
cases = append(cases, struct {
|
||||
name string
|
||||
register func(httpx.Router)
|
||||
}{"Any conflicts with explicit method", func(r httpx.Router) {
|
||||
r.Any("/any", handler)
|
||||
r.Handle("GET", "/any", handler)
|
||||
}})
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
t.Error("registration must panic")
|
||||
}
|
||||
}()
|
||||
tc.register(newRouter())
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user