199 lines
7.5 KiB
Go
199 lines
7.5 KiB
Go
// 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())
|
|
})
|
|
}
|
|
}
|