blob: 833fe88bf6ab9833c7063456fe50bc0f33b7cfd3 [file] [log] [blame]
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package http
import (
"slices"
"strings"
"testing"
)
func TestParsePattern(t *testing.T) {
lit := func(name string) segment {
return segment{s: name}
}
wild := func(name string) segment {
return segment{s: name, wild: true}
}
multi := func(name string) segment {
s := wild(name)
s.multi = true
return s
}
for _, test := range []struct {
in string
want pattern
}{
{"/", pattern{segments: []segment{multi("")}}},
{"/a", pattern{segments: []segment{lit("a")}}},
{
"/a/",
pattern{segments: []segment{lit("a"), multi("")}},
},
{"/path/to/something", pattern{segments: []segment{
lit("path"), lit("to"), lit("something"),
}}},
{
"/{w1}/lit/{w2}",
pattern{
segments: []segment{wild("w1"), lit("lit"), wild("w2")},
},
},
{
"/{w1}/lit/{w2}/",
pattern{
segments: []segment{wild("w1"), lit("lit"), wild("w2"), multi("")},
},
},
{
"example.com/",
pattern{host: "example.com", segments: []segment{multi("")}},
},
{
"GET /",
pattern{method: "GET", segments: []segment{multi("")}},
},
{
"POST example.com/foo/{w}",
pattern{
method: "POST",
host: "example.com",
segments: []segment{lit("foo"), wild("w")},
},
},
{
"/{$}",
pattern{segments: []segment{lit("/")}},
},
{
"DELETE example.com/a/{foo12}/{$}",
pattern{method: "DELETE", host: "example.com", segments: []segment{lit("a"), wild("foo12"), lit("/")}},
},
{
"/foo/{$}",
pattern{segments: []segment{lit("foo"), lit("/")}},
},
{
"/{a}/foo/{rest...}",
pattern{segments: []segment{wild("a"), lit("foo"), multi("rest")}},
},
{
"//",
pattern{segments: []segment{lit(""), multi("")}},
},
{
"/foo///./../bar",
pattern{segments: []segment{lit("foo"), lit(""), lit(""), lit("."), lit(".."), lit("bar")}},
},
{
"a.com/foo//",
pattern{host: "a.com", segments: []segment{lit("foo"), lit(""), multi("")}},
},
{
"/%61%62/%7b/%",
pattern{segments: []segment{lit("ab"), lit("{"), lit("%")}},
},
// Allow multiple spaces matching regexp '[ \t]+' between method and path.
{
"GET\t /",
pattern{method: "GET", segments: []segment{multi("")}},
},
{
"POST \t example.com/foo/{w}",
pattern{
method: "POST",
host: "example.com",
segments: []segment{lit("foo"), wild("w")},
},
},
{
"DELETE \texample.com/a/{foo12}/{$}",
pattern{method: "DELETE", host: "example.com", segments: []segment{lit("a"), wild("foo12"), lit("/")}},
},
} {
got := mustParsePattern(t, test.in)
if !got.equal(&test.want) {
t.Errorf("%q:\ngot %#v\nwant %#v", test.in, got, &test.want)
}
}
}
func TestParsePatternError(t *testing.T) {
for _, test := range []struct {
in string
contains string
}{
{"", "empty pattern"},
{"A=B /", "at offset 0: invalid method"},
{" ", "at offset 1: host/path missing /"},
{"/{w}x", "at offset 1: bad wildcard segment"},
{"/x{w}", "at offset 1: bad wildcard segment"},
{"/{wx", "at offset 1: bad wildcard segment"},
{"/a/{/}/c", "at offset 3: bad wildcard segment"},
{"/a/{%61}/c", "at offset 3: bad wildcard name"}, // wildcard names aren't unescaped
{"/{a$}", "at offset 1: bad wildcard name"},
{"/{}", "at offset 1: empty wildcard"},
{"POST a.com/x/{}/y", "at offset 13: empty wildcard"},
{"/{...}", "at offset 1: empty wildcard"},
{"/{$...}", "at offset 1: bad wildcard"},
{"/{$}/", "at offset 1: {$} not at end"},
{"/{$}/x", "at offset 1: {$} not at end"},
{"/abc/{$}/x", "at offset 5: {$} not at end"},
{"/{a...}/", "at offset 1: {...} wildcard not at end"},
{"/{a...}/x", "at offset 1: {...} wildcard not at end"},
{"{a}/b", "at offset 0: host contains '{' (missing initial '/'?)"},
{"/a/{x}/b/{x...}", "at offset 9: duplicate wildcard name"},
{"GET //", "at offset 4: non-CONNECT pattern with unclean path"},
} {
_, err := parsePattern(test.in)
if err == nil || !strings.Contains(err.Error(), test.contains) {
t.Errorf("%q:\ngot %v, want error containing %q", test.in, err, test.contains)
}
}
}
func (p1 *pattern) equal(p2 *pattern) bool {
return p1.method == p2.method && p1.host == p2.host &&
slices.Equal(p1.segments, p2.segments)
}
func mustParsePattern(tb testing.TB, s string) *pattern {
tb.Helper()
p, err := parsePattern(s)
if err != nil {
tb.Fatal(err)
}
return p
}
func TestCompareMethods(t *testing.T) {
for _, test := range []struct {
p1, p2 string
want relationship
}{
{"/", "/", equivalent},
{"GET /", "GET /", equivalent},
{"HEAD /", "HEAD /", equivalent},
{"POST /", "POST /", equivalent},
{"GET /", "POST /", disjoint},
{"GET /", "/", moreSpecific},
{"HEAD /", "/", moreSpecific},
{"GET /", "HEAD /", moreGeneral},
} {
pat1 := mustParsePattern(t, test.p1)
pat2 := mustParsePattern(t, test.p2)
got := pat1.compareMethods(pat2)
if got != test.want {
t.Errorf("%s vs %s: got %s, want %s", test.p1, test.p2, got, test.want)
}
got2 := pat2.compareMethods(pat1)
want2 := inverseRelationship(test.want)
if got2 != want2 {
t.Errorf("%s vs %s: got %s, want %s", test.p2, test.p1, got2, want2)
}
}
}
func TestComparePaths(t *testing.T) {
for _, test := range []struct {
p1, p2 string
want relationship
}{
// A non-final pattern segment can have one of two values: literal or
// single wildcard. A final pattern segment can have one of 5: empty
// (trailing slash), literal, dollar, single wildcard, or multi
// wildcard. Trailing slash and multi wildcard are the same.
// A literal should be more specific than anything it overlaps, except itself.
{"/a", "/a", equivalent},
{"/a", "/b", disjoint},
{"/a", "/", moreSpecific},
{"/a", "/{$}", disjoint},
{"/a", "/{x}", moreSpecific},
{"/a", "/{x...}", moreSpecific},
// Adding a segment doesn't change that.
{"/b/a", "/b/a", equivalent},
{"/b/a", "/b/b", disjoint},
{"/b/a", "/b/", moreSpecific},
{"/b/a", "/b/{$}", disjoint},
{"/b/a", "/b/{x}", moreSpecific},
{"/b/a", "/b/{x...}", moreSpecific},
{"/{z}/a", "/{z}/a", equivalent},
{"/{z}/a", "/{z}/b", disjoint},
{"/{z}/a", "/{z}/", moreSpecific},
{"/{z}/a", "/{z}/{$}", disjoint},
{"/{z}/a", "/{z}/{x}", moreSpecific},
{"/{z}/a", "/{z}/{x...}", moreSpecific},
// Single wildcard on left.
{"/{z}", "/a", moreGeneral},
{"/{z}", "/a/b", disjoint},
{"/{z}", "/{$}", disjoint},
{"/{z}", "/{x}", equivalent},
{"/{z}", "/", moreSpecific},
{"/{z}", "/{x...}", moreSpecific},
{"/b/{z}", "/b/a", moreGeneral},
{"/b/{z}", "/b/a/b", disjoint},
{"/b/{z}", "/b/{$}", disjoint},
{"/b/{z}", "/b/{x}", equivalent},
{"/b/{z}", "/b/", moreSpecific},
{"/b/{z}", "/b/{x...}", moreSpecific},
// Trailing slash on left.
{"/", "/a", moreGeneral},
{"/", "/a/b", moreGeneral},
{"/", "/{$}", moreGeneral},
{"/", "/{x}", moreGeneral},
{"/", "/", equivalent},
{"/", "/{x...}", equivalent},
{"/b/", "/b/a", moreGeneral},
{"/b/", "/b/a/b", moreGeneral},
{"/b/", "/b/{$}", moreGeneral},
{"/b/", "/b/{x}", moreGeneral},
{"/b/", "/b/", equivalent},
{"/b/", "/b/{x...}", equivalent},
{"/{z}/", "/{z}/a", moreGeneral},
{"/{z}/", "/{z}/a/b", moreGeneral},
{"/{z}/", "/{z}/{$}", moreGeneral},
{"/{z}/", "/{z}/{x}", moreGeneral},
{"/{z}/", "/{z}/", equivalent},
{"/{z}/", "/a/", moreGeneral},
{"/{z}/", "/{z}/{x...}", equivalent},
{"/{z}/", "/a/{x...}", moreGeneral},
{"/a/{z}/", "/{z}/a/", overlaps},
{"/a/{z}/b/", "/{x}/c/{y...}", overlaps},
// Multi wildcard on left.
{"/{m...}", "/a", moreGeneral},
{"/{m...}", "/a/b", moreGeneral},
{"/{m...}", "/{$}", moreGeneral},
{"/{m...}", "/{x}", moreGeneral},
{"/{m...}", "/", equivalent},
{"/{m...}", "/{x...}", equivalent},
{"/b/{m...}", "/b/a", moreGeneral},
{"/b/{m...}", "/b/a/b", moreGeneral},
{"/b/{m...}", "/b/{$}", moreGeneral},
{"/b/{m...}", "/b/{x}", moreGeneral},
{"/b/{m...}", "/b/", equivalent},
{"/b/{m...}", "/b/{x...}", equivalent},
{"/b/{m...}", "/a/{x...}", disjoint},
{"/{z}/{m...}", "/{z}/a", moreGeneral},
{"/{z}/{m...}", "/{z}/a/b", moreGeneral},
{"/{z}/{m...}", "/{z}/{$}", moreGeneral},
{"/{z}/{m...}", "/{z}/{x}", moreGeneral},
{"/{z}/{m...}", "/{w}/", equivalent},
{"/{z}/{m...}", "/a/", moreGeneral},
{"/{z}/{m...}", "/{z}/{x...}", equivalent},
{"/{z}/{m...}", "/a/{x...}", moreGeneral},
{"/a/{m...}", "/a/b/{y...}", moreGeneral},
{"/a/{m...}", "/a/{x}/{y...}", moreGeneral},
{"/a/{z}/{m...}", "/a/b/{y...}", moreGeneral},
{"/a/{z}/{m...}", "/{z}/a/", overlaps},
{"/a/{z}/{m...}", "/{z}/b/{y...}", overlaps},
{"/a/{z}/b/{m...}", "/{x}/c/{y...}", overlaps},
{"/a/{z}/a/{m...}", "/{x}/b", disjoint},
// Dollar on left.
{"/{$}", "/a", disjoint},
{"/{$}", "/a/b", disjoint},
{"/{$}", "/{$}", equivalent},
{"/{$}", "/{x}", disjoint},
{"/{$}", "/", moreSpecific},
{"/{$}", "/{x...}", moreSpecific},
{"/b/{$}", "/b", disjoint},
{"/b/{$}", "/b/a", disjoint},
{"/b/{$}", "/b/a/b", disjoint},
{"/b/{$}", "/b/{$}", equivalent},
{"/b/{$}", "/b/{x}", disjoint},
{"/b/{$}", "/b/", moreSpecific},
{"/b/{$}", "/b/{x...}", moreSpecific},
{"/b/{$}", "/b/c/{x...}", disjoint},
{"/b/{x}/a/{$}", "/{x}/c/{y...}", overlaps},
{"/{x}/b/{$}", "/a/{x}/{y}", disjoint},
{"/{x}/b/{$}", "/a/{x}/c", disjoint},
{"/{z}/{$}", "/{z}/a", disjoint},
{"/{z}/{$}", "/{z}/a/b", disjoint},
{"/{z}/{$}", "/{z}/{$}", equivalent},
{"/{z}/{$}", "/{z}/{x}", disjoint},
{"/{z}/{$}", "/{z}/", moreSpecific},
{"/{z}/{$}", "/a/", overlaps},
{"/{z}/{$}", "/a/{x...}", overlaps},
{"/{z}/{$}", "/{z}/{x...}", moreSpecific},
{"/a/{z}/{$}", "/{z}/a/", overlaps},
} {
pat1 := mustParsePattern(t, test.p1)
pat2 := mustParsePattern(t, test.p2)
if g := pat1.comparePaths(pat1); g != equivalent {
t.Errorf("%s does not match itself; got %s", pat1, g)
}
if g := pat2.comparePaths(pat2); g != equivalent {
t.Errorf("%s does not match itself; got %s", pat2, g)
}
got := pat1.comparePaths(pat2)
if got != test.want {
t.Errorf("%s vs %s: got %s, want %s", test.p1, test.p2, got, test.want)
t.Logf("pat1: %+v\n", pat1.segments)
t.Logf("pat2: %+v\n", pat2.segments)
}
want2 := inverseRelationship(test.want)
got2 := pat2.comparePaths(pat1)
if got2 != want2 {
t.Errorf("%s vs %s: got %s, want %s", test.p2, test.p1, got2, want2)
}
}
}
func TestConflictsWith(t *testing.T) {
for _, test := range []struct {
p1, p2 string
want bool
}{
{"/a", "/a", true},
{"/a", "/ab", false},
{"/a/b/cd", "/a/b/cd", true},
{"/a/b/cd", "/a/b/c", false},
{"/a/b/c", "/a/c/c", false},
{"/{x}", "/{y}", true},
{"/{x}", "/a", false}, // more specific
{"/{x}/{y}", "/{x}/a", false},
{"/{x}/{y}", "/{x}/a/b", false},
{"/{x}", "/a/{y}", false},
{"/{x}/{y}", "/{x}/a/", false},
{"/{x}", "/a/{y...}", false}, // more specific
{"/{x}/a/{y}", "/{x}/a/{y...}", false}, // more specific
{"/{x}/{y}", "/{x}/a/{$}", false}, // more specific
{"/{x}/{y}/{$}", "/{x}/a/{$}", false},
{"/a/{x}", "/{x}/b", true},
{"/", "GET /", false},
{"/", "GET /foo", false},
{"GET /", "GET /foo", false},
{"GET /", "/foo", true},
{"GET /foo", "HEAD /", true},
} {
pat1 := mustParsePattern(t, test.p1)
pat2 := mustParsePattern(t, test.p2)
got := pat1.conflictsWith(pat2)
if got != test.want {
t.Errorf("%q.ConflictsWith(%q) = %t, want %t",
test.p1, test.p2, got, test.want)
}
// conflictsWith should be commutative.
got = pat2.conflictsWith(pat1)
if got != test.want {
t.Errorf("%q.ConflictsWith(%q) = %t, want %t",
test.p2, test.p1, got, test.want)
}
}
}
func TestRegisterConflict(t *testing.T) {
mux := NewServeMux()
pat1 := "/a/{x}/"
if err := mux.registerErr(pat1, NotFoundHandler()); err != nil {
t.Fatal(err)
}
pat2 := "/a/{y}/{z...}"
err := mux.registerErr(pat2, NotFoundHandler())
var got string
if err == nil {
got = "<nil>"
} else {
got = err.Error()
}
want := "matches the same requests as"
if !strings.Contains(got, want) {
t.Errorf("got\n%s\nwant\n%s", got, want)
}
}
func TestDescribeConflict(t *testing.T) {
for _, test := range []struct {
p1, p2 string
want string
}{
{"/a/{x}", "/a/{y}", "the same requests"},
{"/", "/{m...}", "the same requests"},
{"/a/{x}", "/{y}/b", "both match some paths"},
{"/a", "GET /{x}", "matches more methods than GET /{x}, but has a more specific path pattern"},
{"GET /a", "HEAD /", "matches more methods than HEAD /, but has a more specific path pattern"},
{"POST /", "/a", "matches fewer methods than /a, but has a more general path pattern"},
} {
got := describeConflict(mustParsePattern(t, test.p1), mustParsePattern(t, test.p2))
if !strings.Contains(got, test.want) {
t.Errorf("%s vs. %s:\ngot:\n%s\nwhich does not contain %q",
test.p1, test.p2, got, test.want)
}
}
}
func TestCommonPath(t *testing.T) {
for _, test := range []struct {
p1, p2 string
want string
}{
{"/a/{x}", "/{x}/a", "/a/a"},
{"/a/{z}/", "/{z}/a/", "/a/a/"},
{"/a/{z}/{m...}", "/{z}/a/", "/a/a/"},
{"/{z}/{$}", "/a/", "/a/"},
{"/{z}/{$}", "/a/{x...}", "/a/"},
{"/a/{z}/{$}", "/{z}/a/", "/a/a/"},
{"/a/{x}/b/{y...}", "/{x}/c/{y...}", "/a/c/b/"},
{"/a/{x}/b/", "/{x}/c/{y...}", "/a/c/b/"},
{"/a/{x}/b/{$}", "/{x}/c/{y...}", "/a/c/b/"},
{"/a/{z}/{x...}", "/{z}/b/{y...}", "/a/b/"},
} {
pat1 := mustParsePattern(t, test.p1)
pat2 := mustParsePattern(t, test.p2)
if pat1.comparePaths(pat2) != overlaps {
t.Fatalf("%s does not overlap %s", test.p1, test.p2)
}
got := commonPath(pat1, pat2)
if got != test.want {
t.Errorf("%s vs. %s: got %q, want %q", test.p1, test.p2, got, test.want)
}
}
}
func TestDifferencePath(t *testing.T) {
for _, test := range []struct {
p1, p2 string
want string
}{
{"/a/{x}", "/{x}/a", "/a/x"},
{"/{x}/a", "/a/{x}", "/x/a"},
{"/a/{z}/", "/{z}/a/", "/a/z/"},
{"/{z}/a/", "/a/{z}/", "/z/a/"},
{"/{a}/a/", "/a/{z}/", "/ax/a/"},
{"/a/{z}/{x...}", "/{z}/b/{y...}", "/a/z/"},
{"/{z}/b/{y...}", "/a/{z}/{x...}", "/z/b/"},
{"/a/b/", "/a/b/c", "/a/b/"},
{"/a/b/{x...}", "/a/b/c", "/a/b/"},
{"/a/b/{x...}", "/a/b/c/d", "/a/b/"},
{"/a/b/{x...}", "/a/b/c/d/", "/a/b/"},
{"/a/{z}/{m...}", "/{z}/a/", "/a/z/"},
{"/{z}/a/", "/a/{z}/{m...}", "/z/a/"},
{"/{z}/{$}", "/a/", "/z/"},
{"/a/", "/{z}/{$}", "/a/x"},
{"/{z}/{$}", "/a/{x...}", "/z/"},
{"/a/{foo...}", "/{z}/{$}", "/a/foo"},
{"/a/{z}/{$}", "/{z}/a/", "/a/z/"},
{"/{z}/a/", "/a/{z}/{$}", "/z/a/x"},
{"/a/{x}/b/{y...}", "/{x}/c/{y...}", "/a/x/b/"},
{"/{x}/c/{y...}", "/a/{x}/b/{y...}", "/x/c/"},
{"/a/{c}/b/", "/{x}/c/{y...}", "/a/cx/b/"},
{"/{x}/c/{y...}", "/a/{c}/b/", "/x/c/"},
{"/a/{x}/b/{$}", "/{x}/c/{y...}", "/a/x/b/"},
{"/{x}/c/{y...}", "/a/{x}/b/{$}", "/x/c/"},
} {
pat1 := mustParsePattern(t, test.p1)
pat2 := mustParsePattern(t, test.p2)
rel := pat1.comparePaths(pat2)
if rel != overlaps && rel != moreGeneral {
t.Fatalf("%s vs. %s are %s, need overlaps or moreGeneral", pat1, pat2, rel)
}
got := differencePath(pat1, pat2)
if got != test.want {
t.Errorf("%s vs. %s: got %q, want %q", test.p1, test.p2, got, test.want)
}
}
}