// 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 = "" } 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) } } }