- TestOverlapConstraintFallsThrough: an earlier member that matches the literals but not its Where constraint must let a later member answer; the routes.php pairs cannot show this, since each member's own handler re-checks its constraints and the pairs differ in their literals (T-13-23)
173 lines
7.3 KiB
Go
173 lines
7.3 KiB
Go
package surf
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// rackTable is a three-member family joined transitively: token/{token}
|
|
// conflicts with {rackId}/items, which conflicts with items/{id}, while
|
|
// token/{token} and items/{id} differ in a literal and never conflict.
|
|
var rackTable = []overlapRoute{
|
|
{name: "rack-token", method: "GET", path: "/racks/token/{token}"},
|
|
{name: "rack-items", method: "GET", path: "/racks/{rackId}/items", where: map[string]string{"rackId": "[0-9]+"}},
|
|
{name: "rack-items-delete", method: "DELETE", path: "/racks/{rackId}/items", where: map[string]string{"rackId": "[0-9]+"}},
|
|
{name: "item-show", method: "GET", path: "/racks/items/{id}", where: map[string]string{"id": "[0-9]+"}},
|
|
{name: "item-put", method: "PUT", path: "/racks/items/{id}", where: map[string]string{"id": "[0-9]+"}},
|
|
{name: "rack-create", method: "POST", path: "/racks/{kind}/new"},
|
|
}
|
|
|
|
func TestOverlapFamilyOfThree(t *testing.T) {
|
|
// The conflict closure holds the three GET routes; the other methods
|
|
// join the family at compile time through its generated pattern.
|
|
r := buildOverlapRouter(t, rackTable)
|
|
fams, err := overlapFamilies(r.routes)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(fams) != 1 || fams[0].pattern != "/racks/{surfOverlap1}/{surfOverlap2}" {
|
|
t.Fatalf("families = %+v, want one family on one pattern", fams)
|
|
}
|
|
var members []string
|
|
for _, idx := range fams[0].members {
|
|
members = append(members, r.routes[idx].method+" "+r.routes[idx].path)
|
|
}
|
|
if strings.Join(members, ",") != "GET /racks/token/{token},GET /racks/{rackId}/items,GET /racks/items/{id}" {
|
|
t.Fatalf("family members = %v", members)
|
|
}
|
|
h := compileOverlap(t, rackTable)
|
|
for _, c := range []struct{ method, path, want string }{
|
|
{"GET", "/racks/token/abc", "rack-token token=abc"},
|
|
{"GET", "/racks/token/7", "rack-token token=7"},
|
|
{"GET", "/racks/7/items", "rack-items rackId=7"},
|
|
{"DELETE", "/racks/7/items", "rack-items-delete rackId=7"},
|
|
{"GET", "/racks/items/9", "item-show id=9"},
|
|
{"PUT", "/racks/items/9", "item-put id=9"},
|
|
{"POST", "/racks/steel/new", "rack-create kind=steel"},
|
|
} {
|
|
rec := serve(h, c.method, c.path)
|
|
if rec.Code != http.StatusOK || rec.Body.String() != c.want {
|
|
t.Errorf("%s %s = %d %q, want 200 %q", c.method, c.path, rec.Code, rec.Body.String(), c.want)
|
|
}
|
|
}
|
|
for _, path := range []string{"/racks/items/items", "/racks/x7/items", "/racks/items/9x"} {
|
|
if rec := serve(h, "GET", path); rec.Code != http.StatusNotFound || rec.Body.String() != "404 page not found\n" {
|
|
t.Errorf("GET %s = %d %q, want the bare 404", path, rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOverlapHeadAndAllow(t *testing.T) {
|
|
h := compileOverlap(t, rackTable)
|
|
// A GET member answers HEAD, with its own handler.
|
|
for _, path := range []string{"/racks/token/abc", "/racks/7/items", "/racks/items/9"} {
|
|
if rec := serve(h, "HEAD", path); rec.Code != http.StatusOK {
|
|
t.Errorf("HEAD %s = %d, want 200", path, rec.Code)
|
|
}
|
|
}
|
|
// Allow lists the methods of every route that matches the path, sorted,
|
|
// with HEAD beside GET, as ServeMux does for unrelated routes.
|
|
for _, c := range []struct{ method, path, allow string }{
|
|
{"POST", "/racks/items/9", "GET, HEAD, PUT"},
|
|
{"PATCH", "/racks/7/items", "DELETE, GET, HEAD"},
|
|
{"PUT", "/racks/token/abc", "GET, HEAD"},
|
|
{"GET", "/racks/steel/new", "POST"},
|
|
} {
|
|
rec := serve(h, c.method, c.path)
|
|
if rec.Code != http.StatusMethodNotAllowed || rec.Header().Get("Allow") != c.allow {
|
|
t.Errorf("%s %s = %d Allow %q, want 405 %q", c.method, c.path, rec.Code, rec.Header().Get("Allow"), c.allow)
|
|
}
|
|
}
|
|
// A method a member serves, whose constraint fails, is the bare 404,
|
|
// never a 405 naming the member.
|
|
if rec := serve(h, "DELETE", "/racks/abc/items"); rec.Code != http.StatusNotFound || rec.Header().Get("Allow") != "" {
|
|
t.Errorf("DELETE /racks/abc/items = %d Allow %q, want the bare 404", rec.Code, rec.Header().Get("Allow"))
|
|
}
|
|
// A path no member and no route matches stays the router's 404.
|
|
if rec := serve(h, "GET", "/racks/a/b/c"); rec.Code != http.StatusNotFound {
|
|
t.Errorf("GET /racks/a/b/c = %d, want 404", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestOverlapFamilyAcrossPlugins(t *testing.T) {
|
|
r := New(nil)
|
|
register := func(plugin string, rt overlapRoute) {
|
|
r.BindPlugin(plugin)
|
|
mw := "mw." + rt.name
|
|
if err := r.RegisterMiddleware(plugin, mw, markMiddleware(rt.name)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r.Get(rt.path, overlapHandler(rt), mw)
|
|
for p, re := range rt.where {
|
|
r.Where(p, re)
|
|
}
|
|
}
|
|
register("acme.alpha", overlapRoute{name: "alpha-token", path: "/bins/token/{token}"})
|
|
register("acme.beta", overlapRoute{name: "beta-items", path: "/bins/{binId}/items", where: map[string]string{"binId": "[0-9]+"}})
|
|
h, err := r.compile()
|
|
if err != nil {
|
|
t.Fatalf("a family across plugins must compile: %v", err)
|
|
}
|
|
for _, c := range []struct{ path, body, mw string }{
|
|
{"/bins/token/q", "alpha-token token=q", "alpha-token"},
|
|
{"/bins/3/items", "beta-items binId=3", "beta-items"},
|
|
} {
|
|
rec := serve(h, "GET", c.path)
|
|
if rec.Code != http.StatusOK || rec.Body.String() != c.body || strings.Join(rec.Header().Values("X-Mw"), ",") != c.mw {
|
|
t.Errorf("GET %s = %d %q mw %v, want %q with only %s's middleware", c.path, rec.Code, rec.Body.String(), rec.Header().Values("X-Mw"), c.body, c.mw)
|
|
}
|
|
}
|
|
plugins := map[string]string{}
|
|
for _, info := range r.Routes() {
|
|
plugins[info.Pattern] = info.PluginID
|
|
}
|
|
if plugins["/bins/token/{token}"] != "acme.alpha" || plugins["/bins/{binId}/items"] != "acme.beta" {
|
|
t.Fatalf("route table plugins = %v", plugins)
|
|
}
|
|
}
|
|
|
|
func TestOverlapUnsupportedShapes(t *testing.T) {
|
|
for name, table := range map[string][]overlapRoute{
|
|
"trailing-slash": {
|
|
{name: "dir", method: "GET", path: "/bins/token/{token}/"},
|
|
{name: "items", method: "GET", path: "/bins/{binId}/items/", where: map[string]string{"binId": "[0-9]+"}},
|
|
},
|
|
"multi-segment": {
|
|
{name: "rest", method: "GET", path: "/bins/token/{rest...}"},
|
|
{name: "items", method: "GET", path: "/bins/{binId}/items", where: map[string]string{"binId": "[0-9]+"}},
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
_, err := buildOverlapRouter(t, table).compile()
|
|
if err == nil || !strings.Contains(err.Error(), "route conflict") || !strings.Contains(err.Error(), `plugin "acme.shelves"`) {
|
|
t.Fatalf("err = %v, want a route conflict naming both routes and their plugin", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestOverlapConstraintFallsThrough: when an earlier member matches the
|
|
// literals but not its constraint, the family tries the next member
|
|
// instead of answering for the first one, as Laravel's router walks its
|
|
// routes in order.
|
|
func TestOverlapConstraintFallsThrough(t *testing.T) {
|
|
h := compileOverlap(t, []overlapRoute{
|
|
{name: "by-number", method: "GET", path: "/drawers/{id}/open", where: map[string]string{"id": "[0-9]+"}},
|
|
{name: "by-name", method: "GET", path: "/drawers/top/{action}"},
|
|
})
|
|
for _, c := range []struct{ path, want string }{
|
|
{"/drawers/top/open", "by-name action=open"},
|
|
{"/drawers/7/open", "by-number id=7"},
|
|
{"/drawers/top/close", "by-name action=close"},
|
|
} {
|
|
rec := serve(h, "GET", c.path)
|
|
if rec.Code != http.StatusOK || rec.Body.String() != c.want {
|
|
t.Errorf("GET %s = %d %q, want 200 %q", c.path, rec.Code, rec.Body.String(), c.want)
|
|
}
|
|
}
|
|
if rec := serve(h, "GET", "/drawers/x/open"); rec.Code != http.StatusNotFound {
|
|
t.Errorf("GET /drawers/x/open = %d, want 404", rec.Code)
|
|
}
|
|
}
|