Files
summercms/modules/surf/overlap_edges_test.go
Jakub Zych 4ed45c1b03 test(13-06): cover the Phase 13 framework edges in surf, conga, lagoon and tide
- surf: a transitive three-route family, HEAD and sorted Allow on family
  paths, a family across two plugins with per-member middleware, refused
  trailing-slash and multi-segment shapes
- conga: refusal messages name kind and queue, a configured queue counts
  as served, a delayed unregistered dispatch waits scheduled
- lagoon: prohibited under a wildcard, on a dotted path and after bail
- tide: quoted, RFC 5987 and multi-date download names; a captured id
  beside a masked notification id
2026-10-03 11:01:58 +02:00

149 lines
6.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)
}
})
}
}