1
2
3
4
5 package gin
6
7 import (
8 "fmt"
9 "reflect"
10 "regexp"
11 "strings"
12 "testing"
13 )
14
15
16 var fakeHandlerValue string
17
18 func fakeHandler(val string) HandlersChain {
19 return HandlersChain{func(c *Context) {
20 fakeHandlerValue = val
21 }}
22 }
23
24 type testRequests []struct {
25 path string
26 nilHandler bool
27 route string
28 ps Params
29 }
30
31 func getParams() *Params {
32 ps := make(Params, 0, 20)
33 return &ps
34 }
35
36 func getSkippedNodes() *[]skippedNode {
37 ps := make([]skippedNode, 0, 20)
38 return &ps
39 }
40
41 func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ...bool) {
42 unescape := false
43 if len(unescapes) >= 1 {
44 unescape = unescapes[0]
45 }
46
47 for _, request := range requests {
48 value := tree.getValue(request.path, getParams(), getSkippedNodes(), unescape)
49
50 if value.handlers == nil {
51 if !request.nilHandler {
52 t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path)
53 }
54 } else if request.nilHandler {
55 t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path)
56 } else {
57 value.handlers[0](nil)
58 if fakeHandlerValue != request.route {
59 t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, fakeHandlerValue, request.route)
60 }
61 }
62
63 if value.params != nil {
64 if !reflect.DeepEqual(*value.params, request.ps) {
65 t.Errorf("Params mismatch for route '%s'", request.path)
66 }
67 }
68
69 }
70 }
71
72 func checkPriorities(t *testing.T, n *node) uint32 {
73 var prio uint32
74 for i := range n.children {
75 prio += checkPriorities(t, n.children[i])
76 }
77
78 if n.handlers != nil {
79 prio++
80 }
81
82 if n.priority != prio {
83 t.Errorf(
84 "priority mismatch for node '%s': is %d, should be %d",
85 n.path, n.priority, prio,
86 )
87 }
88
89 return prio
90 }
91
92 func TestCountParams(t *testing.T) {
93 if countParams("/path/:param1/static/*catch-all") != 2 {
94 t.Fail()
95 }
96 if countParams(strings.Repeat("/:param", 256)) != 256 {
97 t.Fail()
98 }
99 }
100
101 func TestTreeAddAndGet(t *testing.T) {
102 tree := &node{}
103
104 routes := [...]string{
105 "/hi",
106 "/contact",
107 "/co",
108 "/c",
109 "/a",
110 "/ab",
111 "/doc/",
112 "/doc/go_faq.html",
113 "/doc/go1.html",
114 "/α",
115 "/β",
116 }
117 for _, route := range routes {
118 tree.addRoute(route, fakeHandler(route))
119 }
120
121 checkRequests(t, tree, testRequests{
122 {"/a", false, "/a", nil},
123 {"/", true, "", nil},
124 {"/hi", false, "/hi", nil},
125 {"/contact", false, "/contact", nil},
126 {"/co", false, "/co", nil},
127 {"/con", true, "", nil},
128 {"/cona", true, "", nil},
129 {"/no", true, "", nil},
130 {"/ab", false, "/ab", nil},
131 {"/α", false, "/α", nil},
132 {"/β", false, "/β", nil},
133 })
134
135 checkPriorities(t, tree)
136 }
137
138 func TestTreeWildcard(t *testing.T) {
139 tree := &node{}
140
141 routes := [...]string{
142 "/",
143 "/cmd/:tool/",
144 "/cmd/:tool/:sub",
145 "/cmd/whoami",
146 "/cmd/whoami/root",
147 "/cmd/whoami/root/",
148 "/src/*filepath",
149 "/search/",
150 "/search/:query",
151 "/search/gin-gonic",
152 "/search/google",
153 "/user_:name",
154 "/user_:name/about",
155 "/files/:dir/*filepath",
156 "/doc/",
157 "/doc/go_faq.html",
158 "/doc/go1.html",
159 "/info/:user/public",
160 "/info/:user/project/:project",
161 "/info/:user/project/golang",
162 "/aa/*xx",
163 "/ab/*xx",
164 "/:cc",
165 "/c1/:dd/e",
166 "/c1/:dd/e1",
167 "/:cc/cc",
168 "/:cc/:dd/ee",
169 "/:cc/:dd/:ee/ff",
170 "/:cc/:dd/:ee/:ff/gg",
171 "/:cc/:dd/:ee/:ff/:gg/hh",
172 "/get/test/abc/",
173 "/get/:param/abc/",
174 "/something/:paramname/thirdthing",
175 "/something/secondthing/test",
176 "/get/abc",
177 "/get/:param",
178 "/get/abc/123abc",
179 "/get/abc/:param",
180 "/get/abc/123abc/xxx8",
181 "/get/abc/123abc/:param",
182 "/get/abc/123abc/xxx8/1234",
183 "/get/abc/123abc/xxx8/:param",
184 "/get/abc/123abc/xxx8/1234/ffas",
185 "/get/abc/123abc/xxx8/1234/:param",
186 "/get/abc/123abc/xxx8/1234/kkdd/12c",
187 "/get/abc/123abc/xxx8/1234/kkdd/:param",
188 "/get/abc/:param/test",
189 "/get/abc/123abd/:param",
190 "/get/abc/123abddd/:param",
191 "/get/abc/123/:param",
192 "/get/abc/123abg/:param",
193 "/get/abc/123abf/:param",
194 "/get/abc/123abfff/:param",
195 }
196 for _, route := range routes {
197 tree.addRoute(route, fakeHandler(route))
198 }
199
200 checkRequests(t, tree, testRequests{
201 {"/", false, "/", nil},
202 {"/cmd/test", true, "/cmd/:tool/", Params{Param{"tool", "test"}}},
203 {"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
204 {"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "test"}, Param{Key: "sub", Value: "3"}}},
205 {"/cmd/who", true, "/cmd/:tool/", Params{Param{"tool", "who"}}},
206 {"/cmd/who/", false, "/cmd/:tool/", Params{Param{"tool", "who"}}},
207 {"/cmd/whoami", false, "/cmd/whoami", nil},
208 {"/cmd/whoami/", true, "/cmd/whoami", nil},
209 {"/cmd/whoami/r", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "whoami"}, Param{Key: "sub", Value: "r"}}},
210 {"/cmd/whoami/r/", true, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "whoami"}, Param{Key: "sub", Value: "r"}}},
211 {"/cmd/whoami/root", false, "/cmd/whoami/root", nil},
212 {"/cmd/whoami/root/", false, "/cmd/whoami/root/", nil},
213 {"/src/", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/"}}},
214 {"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
215 {"/search/", false, "/search/", nil},
216 {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
217 {"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
218 {"/search/gin", false, "/search/:query", Params{Param{"query", "gin"}}},
219 {"/search/gin-gonic", false, "/search/gin-gonic", nil},
220 {"/search/google", false, "/search/google", nil},
221 {"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}},
222 {"/user_gopher/about", false, "/user_:name/about", Params{Param{Key: "name", Value: "gopher"}}},
223 {"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{Key: "dir", Value: "js"}, Param{Key: "filepath", Value: "/inc/framework.js"}}},
224 {"/info/gordon/public", false, "/info/:user/public", Params{Param{Key: "user", Value: "gordon"}}},
225 {"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}},
226 {"/info/gordon/project/golang", false, "/info/:user/project/golang", Params{Param{Key: "user", Value: "gordon"}}},
227 {"/aa/aa", false, "/aa/*xx", Params{Param{Key: "xx", Value: "/aa"}}},
228 {"/ab/ab", false, "/ab/*xx", Params{Param{Key: "xx", Value: "/ab"}}},
229 {"/a", false, "/:cc", Params{Param{Key: "cc", Value: "a"}}},
230
231
232
233 {"/all", false, "/:cc", Params{Param{Key: "cc", Value: "all"}}},
234 {"/d", false, "/:cc", Params{Param{Key: "cc", Value: "d"}}},
235 {"/ad", false, "/:cc", Params{Param{Key: "cc", Value: "ad"}}},
236 {"/dd", false, "/:cc", Params{Param{Key: "cc", Value: "dd"}}},
237 {"/dddaa", false, "/:cc", Params{Param{Key: "cc", Value: "dddaa"}}},
238 {"/aa", false, "/:cc", Params{Param{Key: "cc", Value: "aa"}}},
239 {"/aaa", false, "/:cc", Params{Param{Key: "cc", Value: "aaa"}}},
240 {"/aaa/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "aaa"}}},
241 {"/ab", false, "/:cc", Params{Param{Key: "cc", Value: "ab"}}},
242 {"/abb", false, "/:cc", Params{Param{Key: "cc", Value: "abb"}}},
243 {"/abb/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "abb"}}},
244 {"/allxxxx", false, "/:cc", Params{Param{Key: "cc", Value: "allxxxx"}}},
245 {"/alldd", false, "/:cc", Params{Param{Key: "cc", Value: "alldd"}}},
246 {"/all/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "all"}}},
247 {"/a/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "a"}}},
248 {"/c1/d/e", false, "/c1/:dd/e", Params{Param{Key: "dd", Value: "d"}}},
249 {"/c1/d/e1", false, "/c1/:dd/e1", Params{Param{Key: "dd", Value: "d"}}},
250 {"/c1/d/ee", false, "/:cc/:dd/ee", Params{Param{Key: "cc", Value: "c1"}, Param{Key: "dd", Value: "d"}}},
251 {"/cc/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "cc"}}},
252 {"/ccc/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "ccc"}}},
253 {"/deedwjfs/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "deedwjfs"}}},
254 {"/acllcc/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "acllcc"}}},
255 {"/get/test/abc/", false, "/get/test/abc/", nil},
256 {"/get/te/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "te"}}},
257 {"/get/testaa/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "testaa"}}},
258 {"/get/xx/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "xx"}}},
259 {"/get/tt/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "tt"}}},
260 {"/get/a/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "a"}}},
261 {"/get/t/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "t"}}},
262 {"/get/aa/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "aa"}}},
263 {"/get/abas/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "abas"}}},
264 {"/something/secondthing/test", false, "/something/secondthing/test", nil},
265 {"/something/abcdad/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "abcdad"}}},
266 {"/something/secondthingaaaa/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "secondthingaaaa"}}},
267 {"/something/se/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "se"}}},
268 {"/something/s/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "s"}}},
269 {"/c/d/ee", false, "/:cc/:dd/ee", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}}},
270 {"/c/d/e/ff", false, "/:cc/:dd/:ee/ff", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}, Param{Key: "ee", Value: "e"}}},
271 {"/c/d/e/f/gg", false, "/:cc/:dd/:ee/:ff/gg", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}, Param{Key: "ee", Value: "e"}, Param{Key: "ff", Value: "f"}}},
272 {"/c/d/e/f/g/hh", false, "/:cc/:dd/:ee/:ff/:gg/hh", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}, Param{Key: "ee", Value: "e"}, Param{Key: "ff", Value: "f"}, Param{Key: "gg", Value: "g"}}},
273 {"/cc/dd/ee/ff/gg/hh", false, "/:cc/:dd/:ee/:ff/:gg/hh", Params{Param{Key: "cc", Value: "cc"}, Param{Key: "dd", Value: "dd"}, Param{Key: "ee", Value: "ee"}, Param{Key: "ff", Value: "ff"}, Param{Key: "gg", Value: "gg"}}},
274 {"/get/abc", false, "/get/abc", nil},
275 {"/get/a", false, "/get/:param", Params{Param{Key: "param", Value: "a"}}},
276 {"/get/abz", false, "/get/:param", Params{Param{Key: "param", Value: "abz"}}},
277 {"/get/12a", false, "/get/:param", Params{Param{Key: "param", Value: "12a"}}},
278 {"/get/abcd", false, "/get/:param", Params{Param{Key: "param", Value: "abcd"}}},
279 {"/get/abc/123abc", false, "/get/abc/123abc", nil},
280 {"/get/abc/12", false, "/get/abc/:param", Params{Param{Key: "param", Value: "12"}}},
281 {"/get/abc/123ab", false, "/get/abc/:param", Params{Param{Key: "param", Value: "123ab"}}},
282 {"/get/abc/xyz", false, "/get/abc/:param", Params{Param{Key: "param", Value: "xyz"}}},
283 {"/get/abc/123abcddxx", false, "/get/abc/:param", Params{Param{Key: "param", Value: "123abcddxx"}}},
284 {"/get/abc/123abc/xxx8", false, "/get/abc/123abc/xxx8", nil},
285 {"/get/abc/123abc/x", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "x"}}},
286 {"/get/abc/123abc/xxx", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "xxx"}}},
287 {"/get/abc/123abc/abc", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "abc"}}},
288 {"/get/abc/123abc/xxx8xxas", false, "/get/abc/123abc/:param", Params{Param{Key: "param", Value: "xxx8xxas"}}},
289 {"/get/abc/123abc/xxx8/1234", false, "/get/abc/123abc/xxx8/1234", nil},
290 {"/get/abc/123abc/xxx8/1", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "1"}}},
291 {"/get/abc/123abc/xxx8/123", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "123"}}},
292 {"/get/abc/123abc/xxx8/78k", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "78k"}}},
293 {"/get/abc/123abc/xxx8/1234xxxd", false, "/get/abc/123abc/xxx8/:param", Params{Param{Key: "param", Value: "1234xxxd"}}},
294 {"/get/abc/123abc/xxx8/1234/ffas", false, "/get/abc/123abc/xxx8/1234/ffas", nil},
295 {"/get/abc/123abc/xxx8/1234/f", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "f"}}},
296 {"/get/abc/123abc/xxx8/1234/ffa", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "ffa"}}},
297 {"/get/abc/123abc/xxx8/1234/kka", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "kka"}}},
298 {"/get/abc/123abc/xxx8/1234/ffas321", false, "/get/abc/123abc/xxx8/1234/:param", Params{Param{Key: "param", Value: "ffas321"}}},
299 {"/get/abc/123abc/xxx8/1234/kkdd/12c", false, "/get/abc/123abc/xxx8/1234/kkdd/12c", nil},
300 {"/get/abc/123abc/xxx8/1234/kkdd/1", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "1"}}},
301 {"/get/abc/123abc/xxx8/1234/kkdd/12", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "12"}}},
302 {"/get/abc/123abc/xxx8/1234/kkdd/12b", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "12b"}}},
303 {"/get/abc/123abc/xxx8/1234/kkdd/34", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "34"}}},
304 {"/get/abc/123abc/xxx8/1234/kkdd/12c2e3", false, "/get/abc/123abc/xxx8/1234/kkdd/:param", Params{Param{Key: "param", Value: "12c2e3"}}},
305 {"/get/abc/12/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "12"}}},
306 {"/get/abc/123abdd/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abdd"}}},
307 {"/get/abc/123abdddf/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abdddf"}}},
308 {"/get/abc/123ab/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123ab"}}},
309 {"/get/abc/123abgg/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abgg"}}},
310 {"/get/abc/123abff/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abff"}}},
311 {"/get/abc/123abffff/test", false, "/get/abc/:param/test", Params{Param{Key: "param", Value: "123abffff"}}},
312 {"/get/abc/123abd/test", false, "/get/abc/123abd/:param", Params{Param{Key: "param", Value: "test"}}},
313 {"/get/abc/123abddd/test", false, "/get/abc/123abddd/:param", Params{Param{Key: "param", Value: "test"}}},
314 {"/get/abc/123/test22", false, "/get/abc/123/:param", Params{Param{Key: "param", Value: "test22"}}},
315 {"/get/abc/123abg/test", false, "/get/abc/123abg/:param", Params{Param{Key: "param", Value: "test"}}},
316 {"/get/abc/123abf/testss", false, "/get/abc/123abf/:param", Params{Param{Key: "param", Value: "testss"}}},
317 {"/get/abc/123abfff/te", false, "/get/abc/123abfff/:param", Params{Param{Key: "param", Value: "te"}}},
318 })
319
320 checkPriorities(t, tree)
321 }
322
323 func TestUnescapeParameters(t *testing.T) {
324 tree := &node{}
325
326 routes := [...]string{
327 "/",
328 "/cmd/:tool/:sub",
329 "/cmd/:tool/",
330 "/src/*filepath",
331 "/search/:query",
332 "/files/:dir/*filepath",
333 "/info/:user/project/:project",
334 "/info/:user",
335 }
336 for _, route := range routes {
337 tree.addRoute(route, fakeHandler(route))
338 }
339
340 unescape := true
341 checkRequests(t, tree, testRequests{
342 {"/", false, "/", nil},
343 {"/cmd/test/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}},
344 {"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}},
345 {"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
346 {"/src/some/file+test.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file test.png"}}},
347 {"/src/some/file++++%%%%test.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file++++%%%%test.png"}}},
348 {"/src/some/file%2Ftest.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file/test.png"}}},
349 {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng in ünìcodé"}}},
350 {"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}},
351 {"/info/slash%2Fgordon", false, "/info/:user", Params{Param{Key: "user", Value: "slash/gordon"}}},
352 {"/info/slash%2Fgordon/project/Project%20%231", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "slash/gordon"}, Param{Key: "project", Value: "Project #1"}}},
353 {"/info/slash%%%%", false, "/info/:user", Params{Param{Key: "user", Value: "slash%%%%"}}},
354 {"/info/slash%%%%2Fgordon/project/Project%%%%20%231", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "slash%%%%2Fgordon"}, Param{Key: "project", Value: "Project%%%%20%231"}}},
355 }, unescape)
356
357 checkPriorities(t, tree)
358 }
359
360 func catchPanic(testFunc func()) (recv any) {
361 defer func() {
362 recv = recover()
363 }()
364
365 testFunc()
366 return
367 }
368
369 type testRoute struct {
370 path string
371 conflict bool
372 }
373
374 func testRoutes(t *testing.T, routes []testRoute) {
375 tree := &node{}
376
377 for _, route := range routes {
378 recv := catchPanic(func() {
379 tree.addRoute(route.path, nil)
380 })
381
382 if route.conflict {
383 if recv == nil {
384 t.Errorf("no panic for conflicting route '%s'", route.path)
385 }
386 } else if recv != nil {
387 t.Errorf("unexpected panic for route '%s': %v", route.path, recv)
388 }
389 }
390 }
391
392 func TestTreeWildcardConflict(t *testing.T) {
393 routes := []testRoute{
394 {"/cmd/:tool/:sub", false},
395 {"/cmd/vet", false},
396 {"/foo/bar", false},
397 {"/foo/:name", false},
398 {"/foo/:names", true},
399 {"/cmd/*path", true},
400 {"/cmd/:badvar", true},
401 {"/cmd/:tool/names", false},
402 {"/cmd/:tool/:badsub/details", true},
403 {"/src/*filepath", false},
404 {"/src/:file", true},
405 {"/src/static.json", true},
406 {"/src/*filepathx", true},
407 {"/src/", true},
408 {"/src/foo/bar", true},
409 {"/src1/", false},
410 {"/src1/*filepath", true},
411 {"/src2*filepath", true},
412 {"/src2/*filepath", false},
413 {"/search/:query", false},
414 {"/search/valid", false},
415 {"/user_:name", false},
416 {"/user_x", false},
417 {"/user_:name", false},
418 {"/id:id", false},
419 {"/id/:id", false},
420 }
421 testRoutes(t, routes)
422 }
423
424 func TestCatchAllAfterSlash(t *testing.T) {
425 routes := []testRoute{
426 {"/non-leading-*catchall", true},
427 }
428 testRoutes(t, routes)
429 }
430
431 func TestTreeChildConflict(t *testing.T) {
432 routes := []testRoute{
433 {"/cmd/vet", false},
434 {"/cmd/:tool", false},
435 {"/cmd/:tool/:sub", false},
436 {"/cmd/:tool/misc", false},
437 {"/cmd/:tool/:othersub", true},
438 {"/src/AUTHORS", false},
439 {"/src/*filepath", true},
440 {"/user_x", false},
441 {"/user_:name", false},
442 {"/id/:id", false},
443 {"/id:id", false},
444 {"/:id", false},
445 {"/*filepath", true},
446 }
447 testRoutes(t, routes)
448 }
449
450 func TestTreeDuplicatePath(t *testing.T) {
451 tree := &node{}
452
453 routes := [...]string{
454 "/",
455 "/doc/",
456 "/src/*filepath",
457 "/search/:query",
458 "/user_:name",
459 }
460 for _, route := range routes {
461 recv := catchPanic(func() {
462 tree.addRoute(route, fakeHandler(route))
463 })
464 if recv != nil {
465 t.Fatalf("panic inserting route '%s': %v", route, recv)
466 }
467
468
469 recv = catchPanic(func() {
470 tree.addRoute(route, nil)
471 })
472 if recv == nil {
473 t.Fatalf("no panic while inserting duplicate route '%s", route)
474 }
475 }
476
477
478
479 checkRequests(t, tree, testRequests{
480 {"/", false, "/", nil},
481 {"/doc/", false, "/doc/", nil},
482 {"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
483 {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
484 {"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}},
485 })
486 }
487
488 func TestEmptyWildcardName(t *testing.T) {
489 tree := &node{}
490
491 routes := [...]string{
492 "/user:",
493 "/user:/",
494 "/cmd/:/",
495 "/src/*",
496 }
497 for _, route := range routes {
498 recv := catchPanic(func() {
499 tree.addRoute(route, nil)
500 })
501 if recv == nil {
502 t.Fatalf("no panic while inserting route with empty wildcard name '%s", route)
503 }
504 }
505 }
506
507 func TestTreeCatchAllConflict(t *testing.T) {
508 routes := []testRoute{
509 {"/src/*filepath/x", true},
510 {"/src2/", false},
511 {"/src2/*filepath/x", true},
512 {"/src3/*filepath", false},
513 {"/src3/*filepath/x", true},
514 }
515 testRoutes(t, routes)
516 }
517
518 func TestTreeCatchAllConflictRoot(t *testing.T) {
519 routes := []testRoute{
520 {"/", false},
521 {"/*filepath", true},
522 }
523 testRoutes(t, routes)
524 }
525
526 func TestTreeCatchMaxParams(t *testing.T) {
527 tree := &node{}
528 var route = "/cmd/*filepath"
529 tree.addRoute(route, fakeHandler(route))
530 }
531
532 func TestTreeDoubleWildcard(t *testing.T) {
533 const panicMsg = "only one wildcard per path segment is allowed"
534
535 routes := [...]string{
536 "/:foo:bar",
537 "/:foo:bar/",
538 "/:foo*bar",
539 }
540
541 for _, route := range routes {
542 tree := &node{}
543 recv := catchPanic(func() {
544 tree.addRoute(route, nil)
545 })
546
547 if rs, ok := recv.(string); !ok || !strings.HasPrefix(rs, panicMsg) {
548 t.Fatalf(`"Expected panic "%s" for route '%s', got "%v"`, panicMsg, route, recv)
549 }
550 }
551 }
552
553
562
563 func TestTreeTrailingSlashRedirect(t *testing.T) {
564 tree := &node{}
565
566 routes := [...]string{
567 "/hi",
568 "/b/",
569 "/search/:query",
570 "/cmd/:tool/",
571 "/src/*filepath",
572 "/x",
573 "/x/y",
574 "/y/",
575 "/y/z",
576 "/0/:id",
577 "/0/:id/1",
578 "/1/:id/",
579 "/1/:id/2",
580 "/aa",
581 "/a/",
582 "/admin",
583 "/admin/:category",
584 "/admin/:category/:page",
585 "/doc",
586 "/doc/go_faq.html",
587 "/doc/go1.html",
588 "/no/a",
589 "/no/b",
590 "/api/:page/:name",
591 "/api/hello/:name/bar/",
592 "/api/bar/:name",
593 "/api/baz/foo",
594 "/api/baz/foo/bar",
595 "/blog/:p",
596 "/posts/:b/:c",
597 "/posts/b/:c/d/",
598 "/vendor/:x/*y",
599 }
600 for _, route := range routes {
601 recv := catchPanic(func() {
602 tree.addRoute(route, fakeHandler(route))
603 })
604 if recv != nil {
605 t.Fatalf("panic inserting route '%s': %v", route, recv)
606 }
607 }
608
609 tsrRoutes := [...]string{
610 "/hi/",
611 "/b",
612 "/search/gopher/",
613 "/cmd/vet",
614 "/src",
615 "/x/",
616 "/y",
617 "/0/go/",
618 "/1/go",
619 "/a",
620 "/admin/",
621 "/admin/config/",
622 "/admin/config/permissions/",
623 "/doc/",
624 "/admin/static/",
625 "/admin/cfg/",
626 "/admin/cfg/users/",
627 "/api/hello/x/bar",
628 "/api/baz/foo/",
629 "/api/baz/bax/",
630 "/api/bar/huh/",
631 "/api/baz/foo/bar/",
632 "/api/world/abc/",
633 "/blog/pp/",
634 "/posts/b/c/d",
635 "/vendor/x",
636 }
637
638 for _, route := range tsrRoutes {
639 value := tree.getValue(route, nil, getSkippedNodes(), false)
640 if value.handlers != nil {
641 t.Fatalf("non-nil handler for TSR route '%s", route)
642 } else if !value.tsr {
643 t.Errorf("expected TSR recommendation for route '%s'", route)
644 }
645 }
646
647 noTsrRoutes := [...]string{
648 "/",
649 "/no",
650 "/no/",
651 "/_",
652 "/_/",
653 "/api",
654 "/api/",
655 "/api/hello/x/foo",
656 "/api/baz/foo/bad",
657 "/foo/p/p",
658 }
659 for _, route := range noTsrRoutes {
660 value := tree.getValue(route, nil, getSkippedNodes(), false)
661 if value.handlers != nil {
662 t.Fatalf("non-nil handler for No-TSR route '%s", route)
663 } else if value.tsr {
664 t.Errorf("expected no TSR recommendation for route '%s'", route)
665 }
666 }
667 }
668
669 func TestTreeRootTrailingSlashRedirect(t *testing.T) {
670 tree := &node{}
671
672 recv := catchPanic(func() {
673 tree.addRoute("/:test", fakeHandler("/:test"))
674 })
675 if recv != nil {
676 t.Fatalf("panic inserting test route: %v", recv)
677 }
678
679 value := tree.getValue("/", nil, getSkippedNodes(), false)
680 if value.handlers != nil {
681 t.Fatalf("non-nil handler")
682 } else if value.tsr {
683 t.Errorf("expected no TSR recommendation")
684 }
685 }
686
687 func TestRedirectTrailingSlash(t *testing.T) {
688 var data = []struct {
689 path string
690 }{
691 {"/hello/:name"},
692 {"/hello/:name/123"},
693 {"/hello/:name/234"},
694 }
695
696 node := &node{}
697 for _, item := range data {
698 node.addRoute(item.path, fakeHandler("test"))
699 }
700
701 value := node.getValue("/hello/abx/", nil, getSkippedNodes(), false)
702 if value.tsr != true {
703 t.Fatalf("want true, is false")
704 }
705 }
706
707 func TestTreeFindCaseInsensitivePath(t *testing.T) {
708 tree := &node{}
709
710 longPath := "/l" + strings.Repeat("o", 128) + "ng"
711 lOngPath := "/l" + strings.Repeat("O", 128) + "ng/"
712
713 routes := [...]string{
714 "/hi",
715 "/b/",
716 "/ABC/",
717 "/search/:query",
718 "/cmd/:tool/",
719 "/src/*filepath",
720 "/x",
721 "/x/y",
722 "/y/",
723 "/y/z",
724 "/0/:id",
725 "/0/:id/1",
726 "/1/:id/",
727 "/1/:id/2",
728 "/aa",
729 "/a/",
730 "/doc",
731 "/doc/go_faq.html",
732 "/doc/go1.html",
733 "/doc/go/away",
734 "/no/a",
735 "/no/b",
736 "/Π",
737 "/u/apfêl/",
738 "/u/äpfêl/",
739 "/u/öpfêl",
740 "/v/Äpfêl/",
741 "/v/Öpfêl",
742 "/w/♬",
743 "/w/♭/",
744 "/w/𠜎",
745 "/w/𠜏/",
746 longPath,
747 }
748
749 for _, route := range routes {
750 recv := catchPanic(func() {
751 tree.addRoute(route, fakeHandler(route))
752 })
753 if recv != nil {
754 t.Fatalf("panic inserting route '%s': %v", route, recv)
755 }
756 }
757
758
759
760 for _, route := range routes {
761 out, found := tree.findCaseInsensitivePath(route, true)
762 if !found {
763 t.Errorf("Route '%s' not found!", route)
764 } else if string(out) != route {
765 t.Errorf("Wrong result for route '%s': %s", route, string(out))
766 }
767 }
768
769 for _, route := range routes {
770 out, found := tree.findCaseInsensitivePath(route, false)
771 if !found {
772 t.Errorf("Route '%s' not found!", route)
773 } else if string(out) != route {
774 t.Errorf("Wrong result for route '%s': %s", route, string(out))
775 }
776 }
777
778 tests := []struct {
779 in string
780 out string
781 found bool
782 slash bool
783 }{
784 {"/HI", "/hi", true, false},
785 {"/HI/", "/hi", true, true},
786 {"/B", "/b/", true, true},
787 {"/B/", "/b/", true, false},
788 {"/abc", "/ABC/", true, true},
789 {"/abc/", "/ABC/", true, false},
790 {"/aBc", "/ABC/", true, true},
791 {"/aBc/", "/ABC/", true, false},
792 {"/abC", "/ABC/", true, true},
793 {"/abC/", "/ABC/", true, false},
794 {"/SEARCH/QUERY", "/search/QUERY", true, false},
795 {"/SEARCH/QUERY/", "/search/QUERY", true, true},
796 {"/CMD/TOOL/", "/cmd/TOOL/", true, false},
797 {"/CMD/TOOL", "/cmd/TOOL/", true, true},
798 {"/SRC/FILE/PATH", "/src/FILE/PATH", true, false},
799 {"/x/Y", "/x/y", true, false},
800 {"/x/Y/", "/x/y", true, true},
801 {"/X/y", "/x/y", true, false},
802 {"/X/y/", "/x/y", true, true},
803 {"/X/Y", "/x/y", true, false},
804 {"/X/Y/", "/x/y", true, true},
805 {"/Y/", "/y/", true, false},
806 {"/Y", "/y/", true, true},
807 {"/Y/z", "/y/z", true, false},
808 {"/Y/z/", "/y/z", true, true},
809 {"/Y/Z", "/y/z", true, false},
810 {"/Y/Z/", "/y/z", true, true},
811 {"/y/Z", "/y/z", true, false},
812 {"/y/Z/", "/y/z", true, true},
813 {"/Aa", "/aa", true, false},
814 {"/Aa/", "/aa", true, true},
815 {"/AA", "/aa", true, false},
816 {"/AA/", "/aa", true, true},
817 {"/aA", "/aa", true, false},
818 {"/aA/", "/aa", true, true},
819 {"/A/", "/a/", true, false},
820 {"/A", "/a/", true, true},
821 {"/DOC", "/doc", true, false},
822 {"/DOC/", "/doc", true, true},
823 {"/NO", "", false, true},
824 {"/DOC/GO", "", false, true},
825 {"/π", "/Π", true, false},
826 {"/π/", "/Π", true, true},
827 {"/u/ÄPFÊL/", "/u/äpfêl/", true, false},
828 {"/u/ÄPFÊL", "/u/äpfêl/", true, true},
829 {"/u/ÖPFÊL/", "/u/öpfêl", true, true},
830 {"/u/ÖPFÊL", "/u/öpfêl", true, false},
831 {"/v/äpfêL/", "/v/Äpfêl/", true, false},
832 {"/v/äpfêL", "/v/Äpfêl/", true, true},
833 {"/v/öpfêL/", "/v/Öpfêl", true, true},
834 {"/v/öpfêL", "/v/Öpfêl", true, false},
835 {"/w/♬/", "/w/♬", true, true},
836 {"/w/♭", "/w/♭/", true, true},
837 {"/w/𠜎/", "/w/𠜎", true, true},
838 {"/w/𠜏", "/w/𠜏/", true, true},
839 {lOngPath, longPath, true, true},
840 }
841
842 for _, test := range tests {
843 out, found := tree.findCaseInsensitivePath(test.in, true)
844 if found != test.found || (found && (string(out) != test.out)) {
845 t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t",
846 test.in, string(out), found, test.out, test.found)
847 return
848 }
849 }
850
851 for _, test := range tests {
852 out, found := tree.findCaseInsensitivePath(test.in, false)
853 if test.slash {
854 if found {
855 t.Errorf("Found without fixTrailingSlash: %s; got %s", test.in, string(out))
856 }
857 } else {
858 if found != test.found || (found && (string(out) != test.out)) {
859 t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t",
860 test.in, string(out), found, test.out, test.found)
861 return
862 }
863 }
864 }
865 }
866
867 func TestTreeInvalidNodeType(t *testing.T) {
868 const panicMsg = "invalid node type"
869
870 tree := &node{}
871 tree.addRoute("/", fakeHandler("/"))
872 tree.addRoute("/:page", fakeHandler("/:page"))
873
874
875 tree.children[0].nType = 42
876
877
878 recv := catchPanic(func() {
879 tree.getValue("/test", nil, getSkippedNodes(), false)
880 })
881 if rs, ok := recv.(string); !ok || rs != panicMsg {
882 t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv)
883 }
884
885
886 recv = catchPanic(func() {
887 tree.findCaseInsensitivePath("/test", true)
888 })
889 if rs, ok := recv.(string); !ok || rs != panicMsg {
890 t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv)
891 }
892 }
893
894 func TestTreeInvalidParamsType(t *testing.T) {
895 tree := &node{}
896 tree.wildChild = true
897 tree.children = append(tree.children, &node{})
898 tree.children[0].nType = 2
899
900
901 params := make(Params, 0)
902
903
904 tree.getValue("/test", ¶ms, getSkippedNodes(), false)
905 }
906
907 func TestTreeWildcardConflictEx(t *testing.T) {
908 conflicts := [...]struct {
909 route string
910 segPath string
911 existPath string
912 existSegPath string
913 }{
914 {"/who/are/foo", "/foo", `/who/are/\*you`, `/\*you`},
915 {"/who/are/foo/", "/foo/", `/who/are/\*you`, `/\*you`},
916 {"/who/are/foo/bar", "/foo/bar", `/who/are/\*you`, `/\*you`},
917 {"/con:nection", ":nection", `/con:tact`, `:tact`},
918 }
919
920 for _, conflict := range conflicts {
921
922
923
924 tree := &node{}
925 routes := [...]string{
926 "/con:tact",
927 "/who/are/*you",
928 "/who/foo/hello",
929 }
930
931 for _, route := range routes {
932 tree.addRoute(route, fakeHandler(route))
933 }
934
935 recv := catchPanic(func() {
936 tree.addRoute(conflict.route, fakeHandler(conflict.route))
937 })
938
939 if !regexp.MustCompile(fmt.Sprintf("'%s' in new path .* conflicts with existing wildcard '%s' in existing prefix '%s'", conflict.segPath, conflict.existSegPath, conflict.existPath)).MatchString(fmt.Sprint(recv)) {
940 t.Fatalf("invalid wildcard conflict error (%v)", recv)
941 }
942 }
943 }
944
View as plain text