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