1// Copyright 2013 Julien Schmidt. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be found
3// in the LICENSE file.
4
5package httprouter
6
7import (
8	"fmt"
9	"net/http"
10	"reflect"
11	"regexp"
12	"strings"
13	"testing"
14)
15
16func printChildren(n *node, prefix string) {
17	fmt.Printf(" %02d:%02d %s%s[%d] %v %t %d \r\n", n.priority, n.maxParams, prefix, n.path, len(n.children), n.handle, n.wildChild, n.nType)
18	for l := len(n.path); l > 0; l-- {
19		prefix += " "
20	}
21	for _, child := range n.children {
22		printChildren(child, prefix)
23	}
24}
25
26// Used as a workaround since we can't compare functions or their addresses
27var fakeHandlerValue string
28
29func fakeHandler(val string) Handle {
30	return func(http.ResponseWriter, *http.Request, Params) {
31		fakeHandlerValue = val
32	}
33}
34
35type testRequests []struct {
36	path       string
37	nilHandler bool
38	route      string
39	ps         Params
40}
41
42func checkRequests(t *testing.T, tree *node, requests testRequests) {
43	for _, request := range requests {
44		handler, ps, _ := tree.getValue(request.path)
45
46		if handler == nil {
47			if !request.nilHandler {
48				t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path)
49			}
50		} else if request.nilHandler {
51			t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path)
52		} else {
53			handler(nil, nil, nil)
54			if fakeHandlerValue != request.route {
55				t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, fakeHandlerValue, request.route)
56			}
57		}
58
59		if !reflect.DeepEqual(ps, request.ps) {
60			t.Errorf("Params mismatch for route '%s'", request.path)
61		}
62	}
63}
64
65func checkPriorities(t *testing.T, n *node) uint32 {
66	var prio uint32
67	for i := range n.children {
68		prio += checkPriorities(t, n.children[i])
69	}
70
71	if n.handle != nil {
72		prio++
73	}
74
75	if n.priority != prio {
76		t.Errorf(
77			"priority mismatch for node '%s': is %d, should be %d",
78			n.path, n.priority, prio,
79		)
80	}
81
82	return prio
83}
84
85func checkMaxParams(t *testing.T, n *node) uint8 {
86	var maxParams uint8
87	for i := range n.children {
88		params := checkMaxParams(t, n.children[i])
89		if params > maxParams {
90			maxParams = params
91		}
92	}
93	if n.nType > root && !n.wildChild {
94		maxParams++
95	}
96
97	if n.maxParams != maxParams {
98		t.Errorf(
99			"maxParams mismatch for node '%s': is %d, should be %d",
100			n.path, n.maxParams, maxParams,
101		)
102	}
103
104	return maxParams
105}
106
107func TestCountParams(t *testing.T) {
108	if countParams("/path/:param1/static/*catch-all") != 2 {
109		t.Fail()
110	}
111	if countParams(strings.Repeat("/:param", 256)) != 255 {
112		t.Fail()
113	}
114}
115
116func TestTreeAddAndGet(t *testing.T) {
117	tree := &node{}
118
119	routes := [...]string{
120		"/hi",
121		"/contact",
122		"/co",
123		"/c",
124		"/a",
125		"/ab",
126		"/doc/",
127		"/doc/go_faq.html",
128		"/doc/go1.html",
129		"/α",
130		"/β",
131	}
132	for _, route := range routes {
133		tree.addRoute(route, fakeHandler(route))
134	}
135
136	//printChildren(tree, "")
137
138	checkRequests(t, tree, testRequests{
139		{"/a", false, "/a", nil},
140		{"/", true, "", nil},
141		{"/hi", false, "/hi", nil},
142		{"/contact", false, "/contact", nil},
143		{"/co", false, "/co", nil},
144		{"/con", true, "", nil},  // key mismatch
145		{"/cona", true, "", nil}, // key mismatch
146		{"/no", true, "", nil},   // no matching child
147		{"/ab", false, "/ab", nil},
148		{"/α", false, "/α", nil},
149		{"/β", false, "/β", nil},
150	})
151
152	checkPriorities(t, tree)
153	checkMaxParams(t, tree)
154}
155
156func TestTreeWildcard(t *testing.T) {
157	tree := &node{}
158
159	routes := [...]string{
160		"/",
161		"/cmd/:tool/:sub",
162		"/cmd/:tool/",
163		"/src/*filepath",
164		"/search/",
165		"/search/:query",
166		"/user_:name",
167		"/user_:name/about",
168		"/files/:dir/*filepath",
169		"/doc/",
170		"/doc/go_faq.html",
171		"/doc/go1.html",
172		"/info/:user/public",
173		"/info/:user/project/:project",
174	}
175	for _, route := range routes {
176		tree.addRoute(route, fakeHandler(route))
177	}
178
179	//printChildren(tree, "")
180
181	checkRequests(t, tree, testRequests{
182		{"/", false, "/", nil},
183		{"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
184		{"/cmd/test", true, "", Params{Param{"tool", "test"}}},
185		{"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{"tool", "test"}, Param{"sub", "3"}}},
186		{"/src/", false, "/src/*filepath", Params{Param{"filepath", "/"}}},
187		{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
188		{"/search/", false, "/search/", nil},
189		{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
190		{"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
191		{"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}},
192		{"/user_gopher/about", false, "/user_:name/about", Params{Param{"name", "gopher"}}},
193		{"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{"dir", "js"}, Param{"filepath", "/inc/framework.js"}}},
194		{"/info/gordon/public", false, "/info/:user/public", Params{Param{"user", "gordon"}}},
195		{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}},
196	})
197
198	checkPriorities(t, tree)
199	checkMaxParams(t, tree)
200}
201
202func catchPanic(testFunc func()) (recv interface{}) {
203	defer func() {
204		recv = recover()
205	}()
206
207	testFunc()
208	return
209}
210
211type testRoute struct {
212	path     string
213	conflict bool
214}
215
216func testRoutes(t *testing.T, routes []testRoute) {
217	tree := &node{}
218
219	for _, route := range routes {
220		recv := catchPanic(func() {
221			tree.addRoute(route.path, nil)
222		})
223
224		if route.conflict {
225			if recv == nil {
226				t.Errorf("no panic for conflicting route '%s'", route.path)
227			}
228		} else if recv != nil {
229			t.Errorf("unexpected panic for route '%s': %v", route.path, recv)
230		}
231	}
232
233	//printChildren(tree, "")
234}
235
236func TestTreeWildcardConflict(t *testing.T) {
237	routes := []testRoute{
238		{"/cmd/:tool/:sub", false},
239		{"/cmd/vet", true},
240		{"/src/*filepath", false},
241		{"/src/*filepathx", true},
242		{"/src/", true},
243		{"/src1/", false},
244		{"/src1/*filepath", true},
245		{"/src2*filepath", true},
246		{"/search/:query", false},
247		{"/search/invalid", true},
248		{"/user_:name", false},
249		{"/user_x", true},
250		{"/user_:name", false},
251		{"/id:id", false},
252		{"/id/:id", true},
253	}
254	testRoutes(t, routes)
255}
256
257func TestTreeChildConflict(t *testing.T) {
258	routes := []testRoute{
259		{"/cmd/vet", false},
260		{"/cmd/:tool/:sub", true},
261		{"/src/AUTHORS", false},
262		{"/src/*filepath", true},
263		{"/user_x", false},
264		{"/user_:name", true},
265		{"/id/:id", false},
266		{"/id:id", true},
267		{"/:id", true},
268		{"/*filepath", true},
269	}
270	testRoutes(t, routes)
271}
272
273func TestTreeDupliatePath(t *testing.T) {
274	tree := &node{}
275
276	routes := [...]string{
277		"/",
278		"/doc/",
279		"/src/*filepath",
280		"/search/:query",
281		"/user_:name",
282	}
283	for _, route := range routes {
284		recv := catchPanic(func() {
285			tree.addRoute(route, fakeHandler(route))
286		})
287		if recv != nil {
288			t.Fatalf("panic inserting route '%s': %v", route, recv)
289		}
290
291		// Add again
292		recv = catchPanic(func() {
293			tree.addRoute(route, nil)
294		})
295		if recv == nil {
296			t.Fatalf("no panic while inserting duplicate route '%s", route)
297		}
298	}
299
300	//printChildren(tree, "")
301
302	checkRequests(t, tree, testRequests{
303		{"/", false, "/", nil},
304		{"/doc/", false, "/doc/", nil},
305		{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
306		{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
307		{"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}},
308	})
309}
310
311func TestEmptyWildcardName(t *testing.T) {
312	tree := &node{}
313
314	routes := [...]string{
315		"/user:",
316		"/user:/",
317		"/cmd/:/",
318		"/src/*",
319	}
320	for _, route := range routes {
321		recv := catchPanic(func() {
322			tree.addRoute(route, nil)
323		})
324		if recv == nil {
325			t.Fatalf("no panic while inserting route with empty wildcard name '%s", route)
326		}
327	}
328}
329
330func TestTreeCatchAllConflict(t *testing.T) {
331	routes := []testRoute{
332		{"/src/*filepath/x", true},
333		{"/src2/", false},
334		{"/src2/*filepath/x", true},
335	}
336	testRoutes(t, routes)
337}
338
339func TestTreeCatchAllConflictRoot(t *testing.T) {
340	routes := []testRoute{
341		{"/", false},
342		{"/*filepath", true},
343	}
344	testRoutes(t, routes)
345}
346
347func TestTreeDoubleWildcard(t *testing.T) {
348	const panicMsg = "only one wildcard per path segment is allowed"
349
350	routes := [...]string{
351		"/:foo:bar",
352		"/:foo:bar/",
353		"/:foo*bar",
354	}
355
356	for _, route := range routes {
357		tree := &node{}
358		recv := catchPanic(func() {
359			tree.addRoute(route, nil)
360		})
361
362		if rs, ok := recv.(string); !ok || !strings.HasPrefix(rs, panicMsg) {
363			t.Fatalf(`"Expected panic "%s" for route '%s', got "%v"`, panicMsg, route, recv)
364		}
365	}
366}
367
368/*func TestTreeDuplicateWildcard(t *testing.T) {
369	tree := &node{}
370
371	routes := [...]string{
372		"/:id/:name/:id",
373	}
374	for _, route := range routes {
375		...
376	}
377}*/
378
379func TestTreeTrailingSlashRedirect(t *testing.T) {
380	tree := &node{}
381
382	routes := [...]string{
383		"/hi",
384		"/b/",
385		"/search/:query",
386		"/cmd/:tool/",
387		"/src/*filepath",
388		"/x",
389		"/x/y",
390		"/y/",
391		"/y/z",
392		"/0/:id",
393		"/0/:id/1",
394		"/1/:id/",
395		"/1/:id/2",
396		"/aa",
397		"/a/",
398		"/admin",
399		"/admin/:category",
400		"/admin/:category/:page",
401		"/doc",
402		"/doc/go_faq.html",
403		"/doc/go1.html",
404		"/no/a",
405		"/no/b",
406		"/api/hello/:name",
407	}
408	for _, route := range routes {
409		recv := catchPanic(func() {
410			tree.addRoute(route, fakeHandler(route))
411		})
412		if recv != nil {
413			t.Fatalf("panic inserting route '%s': %v", route, recv)
414		}
415	}
416
417	//printChildren(tree, "")
418
419	tsrRoutes := [...]string{
420		"/hi/",
421		"/b",
422		"/search/gopher/",
423		"/cmd/vet",
424		"/src",
425		"/x/",
426		"/y",
427		"/0/go/",
428		"/1/go",
429		"/a",
430		"/admin/",
431		"/admin/config/",
432		"/admin/config/permissions/",
433		"/doc/",
434	}
435	for _, route := range tsrRoutes {
436		handler, _, tsr := tree.getValue(route)
437		if handler != nil {
438			t.Fatalf("non-nil handler for TSR route '%s", route)
439		} else if !tsr {
440			t.Errorf("expected TSR recommendation for route '%s'", route)
441		}
442	}
443
444	noTsrRoutes := [...]string{
445		"/",
446		"/no",
447		"/no/",
448		"/_",
449		"/_/",
450		"/api/world/abc",
451	}
452	for _, route := range noTsrRoutes {
453		handler, _, tsr := tree.getValue(route)
454		if handler != nil {
455			t.Fatalf("non-nil handler for No-TSR route '%s", route)
456		} else if tsr {
457			t.Errorf("expected no TSR recommendation for route '%s'", route)
458		}
459	}
460}
461
462func TestTreeRootTrailingSlashRedirect(t *testing.T) {
463	tree := &node{}
464
465	recv := catchPanic(func() {
466		tree.addRoute("/:test", fakeHandler("/:test"))
467	})
468	if recv != nil {
469		t.Fatalf("panic inserting test route: %v", recv)
470	}
471
472	handler, _, tsr := tree.getValue("/")
473	if handler != nil {
474		t.Fatalf("non-nil handler")
475	} else if tsr {
476		t.Errorf("expected no TSR recommendation")
477	}
478}
479
480func TestTreeFindCaseInsensitivePath(t *testing.T) {
481	tree := &node{}
482
483	routes := [...]string{
484		"/hi",
485		"/b/",
486		"/ABC/",
487		"/search/:query",
488		"/cmd/:tool/",
489		"/src/*filepath",
490		"/x",
491		"/x/y",
492		"/y/",
493		"/y/z",
494		"/0/:id",
495		"/0/:id/1",
496		"/1/:id/",
497		"/1/:id/2",
498		"/aa",
499		"/a/",
500		"/doc",
501		"/doc/go_faq.html",
502		"/doc/go1.html",
503		"/doc/go/away",
504		"/no/a",
505		"/no/b",
506		"/Π",
507		"/u/apfêl/",
508		"/u/äpfêl/",
509		"/u/öpfêl",
510		"/v/Äpfêl/",
511		"/v/Öpfêl",
512		"/w/♬",  // 3 byte
513		"/w/♭/", // 3 byte, last byte differs
514		"/w/��",  // 4 byte
515		"/w/��/", // 4 byte
516	}
517
518	for _, route := range routes {
519		recv := catchPanic(func() {
520			tree.addRoute(route, fakeHandler(route))
521		})
522		if recv != nil {
523			t.Fatalf("panic inserting route '%s': %v", route, recv)
524		}
525	}
526
527	// Check out == in for all registered routes
528	// With fixTrailingSlash = true
529	for _, route := range routes {
530		out, found := tree.findCaseInsensitivePath(route, true)
531		if !found {
532			t.Errorf("Route '%s' not found!", route)
533		} else if string(out) != route {
534			t.Errorf("Wrong result for route '%s': %s", route, string(out))
535		}
536	}
537	// With fixTrailingSlash = false
538	for _, route := range routes {
539		out, found := tree.findCaseInsensitivePath(route, false)
540		if !found {
541			t.Errorf("Route '%s' not found!", route)
542		} else if string(out) != route {
543			t.Errorf("Wrong result for route '%s': %s", route, string(out))
544		}
545	}
546
547	tests := []struct {
548		in    string
549		out   string
550		found bool
551		slash bool
552	}{
553		{"/HI", "/hi", true, false},
554		{"/HI/", "/hi", true, true},
555		{"/B", "/b/", true, true},
556		{"/B/", "/b/", true, false},
557		{"/abc", "/ABC/", true, true},
558		{"/abc/", "/ABC/", true, false},
559		{"/aBc", "/ABC/", true, true},
560		{"/aBc/", "/ABC/", true, false},
561		{"/abC", "/ABC/", true, true},
562		{"/abC/", "/ABC/", true, false},
563		{"/SEARCH/QUERY", "/search/QUERY", true, false},
564		{"/SEARCH/QUERY/", "/search/QUERY", true, true},
565		{"/CMD/TOOL/", "/cmd/TOOL/", true, false},
566		{"/CMD/TOOL", "/cmd/TOOL/", true, true},
567		{"/SRC/FILE/PATH", "/src/FILE/PATH", true, false},
568		{"/x/Y", "/x/y", true, false},
569		{"/x/Y/", "/x/y", true, true},
570		{"/X/y", "/x/y", true, false},
571		{"/X/y/", "/x/y", true, true},
572		{"/X/Y", "/x/y", true, false},
573		{"/X/Y/", "/x/y", true, true},
574		{"/Y/", "/y/", true, false},
575		{"/Y", "/y/", true, true},
576		{"/Y/z", "/y/z", true, false},
577		{"/Y/z/", "/y/z", true, true},
578		{"/Y/Z", "/y/z", true, false},
579		{"/Y/Z/", "/y/z", true, true},
580		{"/y/Z", "/y/z", true, false},
581		{"/y/Z/", "/y/z", true, true},
582		{"/Aa", "/aa", true, false},
583		{"/Aa/", "/aa", true, true},
584		{"/AA", "/aa", true, false},
585		{"/AA/", "/aa", true, true},
586		{"/aA", "/aa", true, false},
587		{"/aA/", "/aa", true, true},
588		{"/A/", "/a/", true, false},
589		{"/A", "/a/", true, true},
590		{"/DOC", "/doc", true, false},
591		{"/DOC/", "/doc", true, true},
592		{"/NO", "", false, true},
593		{"/DOC/GO", "", false, true},
594		{"/π", "/Π", true, false},
595		{"/π/", "/Π", true, true},
596		{"/u/ÄPFÊL/", "/u/äpfêl/", true, false},
597		{"/u/ÄPFÊL", "/u/äpfêl/", true, true},
598		{"/u/ÖPFÊL/", "/u/öpfêl", true, true},
599		{"/u/ÖPFÊL", "/u/öpfêl", true, false},
600		{"/v/äpfêL/", "/v/Äpfêl/", true, false},
601		{"/v/äpfêL", "/v/Äpfêl/", true, true},
602		{"/v/öpfêL/", "/v/Öpfêl", true, true},
603		{"/v/öpfêL", "/v/Öpfêl", true, false},
604		{"/w/♬/", "/w/♬", true, true},
605		{"/w/♭", "/w/♭/", true, true},
606		{"/w/��/", "/w/��", true, true},
607		{"/w/��", "/w/��/", true, true},
608	}
609	// With fixTrailingSlash = true
610	for _, test := range tests {
611		out, found := tree.findCaseInsensitivePath(test.in, true)
612		if found != test.found || (found && (string(out) != test.out)) {
613			t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t",
614				test.in, string(out), found, test.out, test.found)
615			return
616		}
617	}
618	// With fixTrailingSlash = false
619	for _, test := range tests {
620		out, found := tree.findCaseInsensitivePath(test.in, false)
621		if test.slash {
622			if found { // test needs a trailingSlash fix. It must not be found!
623				t.Errorf("Found without fixTrailingSlash: %s; got %s", test.in, string(out))
624			}
625		} else {
626			if found != test.found || (found && (string(out) != test.out)) {
627				t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t",
628					test.in, string(out), found, test.out, test.found)
629				return
630			}
631		}
632	}
633}
634
635func TestTreeInvalidNodeType(t *testing.T) {
636	const panicMsg = "invalid node type"
637
638	tree := &node{}
639	tree.addRoute("/", fakeHandler("/"))
640	tree.addRoute("/:page", fakeHandler("/:page"))
641
642	// set invalid node type
643	tree.children[0].nType = 42
644
645	// normal lookup
646	recv := catchPanic(func() {
647		tree.getValue("/test")
648	})
649	if rs, ok := recv.(string); !ok || rs != panicMsg {
650		t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv)
651	}
652
653	// case-insensitive lookup
654	recv = catchPanic(func() {
655		tree.findCaseInsensitivePath("/test", true)
656	})
657	if rs, ok := recv.(string); !ok || rs != panicMsg {
658		t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv)
659	}
660}
661
662func TestTreeWildcardConflictEx(t *testing.T) {
663	conflicts := [...]struct {
664		route        string
665		segPath      string
666		existPath    string
667		existSegPath string
668	}{
669		{"/who/are/foo", "/foo", `/who/are/\*you`, `/\*you`},
670		{"/who/are/foo/", "/foo/", `/who/are/\*you`, `/\*you`},
671		{"/who/are/foo/bar", "/foo/bar", `/who/are/\*you`, `/\*you`},
672		{"/conxxx", "xxx", `/con:tact`, `:tact`},
673		{"/conooo/xxx", "ooo", `/con:tact`, `:tact`},
674	}
675
676	for _, conflict := range conflicts {
677		// I have to re-create a 'tree', because the 'tree' will be
678		// in an inconsistent state when the loop recovers from the
679		// panic which threw by 'addRoute' function.
680		tree := &node{}
681		routes := [...]string{
682			"/con:tact",
683			"/who/are/*you",
684			"/who/foo/hello",
685		}
686
687		for _, route := range routes {
688			tree.addRoute(route, fakeHandler(route))
689		}
690
691		recv := catchPanic(func() {
692			tree.addRoute(conflict.route, fakeHandler(conflict.route))
693		})
694
695		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)) {
696			t.Fatalf("invalid wildcard conflict error (%v)", recv)
697		}
698	}
699}
700