2017-05-05 15:42:38 +00:00
// Copyright 2013 Julien Schmidt. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be found
// in the LICENSE file.
package httprouter
import (
"fmt"
"net/http"
"reflect"
2018-10-23 03:00:09 +00:00
"regexp"
2017-05-05 15:42:38 +00:00
"strings"
"testing"
)
func printChildren ( n * node , prefix string ) {
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 )
for l := len ( n . path ) ; l > 0 ; l -- {
prefix += " "
}
for _ , child := range n . children {
printChildren ( child , prefix )
}
}
2018-10-23 03:00:09 +00:00
// Used as a workaround since we can't compare functions or their addresses
2017-05-05 15:42:38 +00:00
var fakeHandlerValue string
func fakeHandler ( val string ) Handle {
return func ( http . ResponseWriter , * http . Request , Params ) {
fakeHandlerValue = val
}
}
type testRequests [ ] struct {
path string
nilHandler bool
route string
ps Params
}
func checkRequests ( t * testing . T , tree * node , requests testRequests ) {
for _ , request := range requests {
handler , ps , _ := tree . getValue ( request . path )
if handler == nil {
if ! request . nilHandler {
t . Errorf ( "handle mismatch for route '%s': Expected non-nil handle" , request . path )
}
} else if request . nilHandler {
t . Errorf ( "handle mismatch for route '%s': Expected nil handle" , request . path )
} else {
handler ( nil , nil , nil )
if fakeHandlerValue != request . route {
t . Errorf ( "handle mismatch for route '%s': Wrong handle (%s != %s)" , request . path , fakeHandlerValue , request . route )
}
}
if ! reflect . DeepEqual ( ps , request . ps ) {
t . Errorf ( "Params mismatch for route '%s'" , request . path )
}
}
}
func checkPriorities ( t * testing . T , n * node ) uint32 {
var prio uint32
for i := range n . children {
prio += checkPriorities ( t , n . children [ i ] )
}
if n . handle != nil {
prio ++
}
if n . priority != prio {
t . Errorf (
"priority mismatch for node '%s': is %d, should be %d" ,
n . path , n . priority , prio ,
)
}
return prio
}
func checkMaxParams ( t * testing . T , n * node ) uint8 {
var maxParams uint8
for i := range n . children {
params := checkMaxParams ( t , n . children [ i ] )
if params > maxParams {
maxParams = params
}
}
2018-10-23 03:00:09 +00:00
if n . nType > root && ! n . wildChild {
2017-05-05 15:42:38 +00:00
maxParams ++
}
if n . maxParams != maxParams {
t . Errorf (
"maxParams mismatch for node '%s': is %d, should be %d" ,
n . path , n . maxParams , maxParams ,
)
}
return maxParams
}
func TestCountParams ( t * testing . T ) {
if countParams ( "/path/:param1/static/*catch-all" ) != 2 {
t . Fail ( )
}
if countParams ( strings . Repeat ( "/:param" , 256 ) ) != 255 {
t . Fail ( )
}
}
func TestTreeAddAndGet ( t * testing . T ) {
tree := & node { }
routes := [ ... ] string {
"/hi" ,
"/contact" ,
"/co" ,
"/c" ,
"/a" ,
"/ab" ,
"/doc/" ,
"/doc/go_faq.html" ,
"/doc/go1.html" ,
"/α " ,
"/β" ,
}
for _ , route := range routes {
tree . addRoute ( route , fakeHandler ( route ) )
}
//printChildren(tree, "")
checkRequests ( t , tree , testRequests {
{ "/a" , false , "/a" , nil } ,
{ "/" , true , "" , nil } ,
{ "/hi" , false , "/hi" , nil } ,
{ "/contact" , false , "/contact" , nil } ,
{ "/co" , false , "/co" , nil } ,
{ "/con" , true , "" , nil } , // key mismatch
{ "/cona" , true , "" , nil } , // key mismatch
{ "/no" , true , "" , nil } , // no matching child
{ "/ab" , false , "/ab" , nil } ,
{ "/α " , false , "/α " , nil } ,
{ "/β" , false , "/β" , nil } ,
} )
checkPriorities ( t , tree )
checkMaxParams ( t , tree )
}
func TestTreeWildcard ( t * testing . T ) {
tree := & node { }
routes := [ ... ] string {
"/" ,
"/cmd/:tool/:sub" ,
"/cmd/:tool/" ,
"/src/*filepath" ,
"/search/" ,
"/search/:query" ,
"/user_:name" ,
"/user_:name/about" ,
"/files/:dir/*filepath" ,
"/doc/" ,
"/doc/go_faq.html" ,
"/doc/go1.html" ,
"/info/:user/public" ,
"/info/:user/project/:project" ,
}
for _ , route := range routes {
tree . addRoute ( route , fakeHandler ( route ) )
}
//printChildren(tree, "")
checkRequests ( t , tree , testRequests {
{ "/" , false , "/" , nil } ,
{ "/cmd/test/" , false , "/cmd/:tool/" , Params { Param { "tool" , "test" } } } ,
{ "/cmd/test" , true , "" , Params { Param { "tool" , "test" } } } ,
{ "/cmd/test/3" , false , "/cmd/:tool/:sub" , Params { Param { "tool" , "test" } , Param { "sub" , "3" } } } ,
{ "/src/" , false , "/src/*filepath" , Params { Param { "filepath" , "/" } } } ,
{ "/src/some/file.png" , false , "/src/*filepath" , Params { Param { "filepath" , "/some/file.png" } } } ,
{ "/search/" , false , "/search/" , nil } ,
{ "/search/someth!ng+in+ünìcodé" , false , "/search/:query" , Params { Param { "query" , "someth!ng+in+ünìcodé" } } } ,
{ "/search/someth!ng+in+ünìcodé/" , true , "" , Params { Param { "query" , "someth!ng+in+ünìcodé" } } } ,
{ "/user_gopher" , false , "/user_:name" , Params { Param { "name" , "gopher" } } } ,
{ "/user_gopher/about" , false , "/user_:name/about" , Params { Param { "name" , "gopher" } } } ,
{ "/files/js/inc/framework.js" , false , "/files/:dir/*filepath" , Params { Param { "dir" , "js" } , Param { "filepath" , "/inc/framework.js" } } } ,
{ "/info/gordon/public" , false , "/info/:user/public" , Params { Param { "user" , "gordon" } } } ,
{ "/info/gordon/project/go" , false , "/info/:user/project/:project" , Params { Param { "user" , "gordon" } , Param { "project" , "go" } } } ,
} )
checkPriorities ( t , tree )
checkMaxParams ( t , tree )
}
func catchPanic ( testFunc func ( ) ) ( recv interface { } ) {
defer func ( ) {
recv = recover ( )
} ( )
testFunc ( )
return
}
type testRoute struct {
path string
conflict bool
}
func testRoutes ( t * testing . T , routes [ ] testRoute ) {
tree := & node { }
for _ , route := range routes {
recv := catchPanic ( func ( ) {
tree . addRoute ( route . path , nil )
} )
if route . conflict {
if recv == nil {
t . Errorf ( "no panic for conflicting route '%s'" , route . path )
}
} else if recv != nil {
t . Errorf ( "unexpected panic for route '%s': %v" , route . path , recv )
}
}
//printChildren(tree, "")
}
func TestTreeWildcardConflict ( t * testing . T ) {
routes := [ ] testRoute {
{ "/cmd/:tool/:sub" , false } ,
{ "/cmd/vet" , true } ,
{ "/src/*filepath" , false } ,
{ "/src/*filepathx" , true } ,
{ "/src/" , true } ,
{ "/src1/" , false } ,
{ "/src1/*filepath" , true } ,
{ "/src2*filepath" , true } ,
{ "/search/:query" , false } ,
{ "/search/invalid" , true } ,
{ "/user_:name" , false } ,
{ "/user_x" , true } ,
{ "/user_:name" , false } ,
{ "/id:id" , false } ,
{ "/id/:id" , true } ,
}
testRoutes ( t , routes )
}
func TestTreeChildConflict ( t * testing . T ) {
routes := [ ] testRoute {
{ "/cmd/vet" , false } ,
{ "/cmd/:tool/:sub" , true } ,
{ "/src/AUTHORS" , false } ,
{ "/src/*filepath" , true } ,
{ "/user_x" , false } ,
{ "/user_:name" , true } ,
{ "/id/:id" , false } ,
{ "/id:id" , true } ,
{ "/:id" , true } ,
{ "/*filepath" , true } ,
}
testRoutes ( t , routes )
}
func TestTreeDupliatePath ( t * testing . T ) {
tree := & node { }
routes := [ ... ] string {
"/" ,
"/doc/" ,
"/src/*filepath" ,
"/search/:query" ,
"/user_:name" ,
}
for _ , route := range routes {
recv := catchPanic ( func ( ) {
tree . addRoute ( route , fakeHandler ( route ) )
} )
if recv != nil {
t . Fatalf ( "panic inserting route '%s': %v" , route , recv )
}
// Add again
recv = catchPanic ( func ( ) {
tree . addRoute ( route , nil )
} )
if recv == nil {
t . Fatalf ( "no panic while inserting duplicate route '%s" , route )
}
}
//printChildren(tree, "")
checkRequests ( t , tree , testRequests {
{ "/" , false , "/" , nil } ,
{ "/doc/" , false , "/doc/" , nil } ,
{ "/src/some/file.png" , false , "/src/*filepath" , Params { Param { "filepath" , "/some/file.png" } } } ,
{ "/search/someth!ng+in+ünìcodé" , false , "/search/:query" , Params { Param { "query" , "someth!ng+in+ünìcodé" } } } ,
{ "/user_gopher" , false , "/user_:name" , Params { Param { "name" , "gopher" } } } ,
} )
}
func TestEmptyWildcardName ( t * testing . T ) {
tree := & node { }
routes := [ ... ] string {
"/user:" ,
"/user:/" ,
"/cmd/:/" ,
"/src/*" ,
}
for _ , route := range routes {
recv := catchPanic ( func ( ) {
tree . addRoute ( route , nil )
} )
if recv == nil {
t . Fatalf ( "no panic while inserting route with empty wildcard name '%s" , route )
}
}
}
func TestTreeCatchAllConflict ( t * testing . T ) {
routes := [ ] testRoute {
{ "/src/*filepath/x" , true } ,
{ "/src2/" , false } ,
{ "/src2/*filepath/x" , true } ,
}
testRoutes ( t , routes )
}
func TestTreeCatchAllConflictRoot ( t * testing . T ) {
routes := [ ] testRoute {
{ "/" , false } ,
{ "/*filepath" , true } ,
}
testRoutes ( t , routes )
}
func TestTreeDoubleWildcard ( t * testing . T ) {
const panicMsg = "only one wildcard per path segment is allowed"
routes := [ ... ] string {
"/:foo:bar" ,
"/:foo:bar/" ,
"/:foo*bar" ,
}
for _ , route := range routes {
tree := & node { }
recv := catchPanic ( func ( ) {
tree . addRoute ( route , nil )
} )
if rs , ok := recv . ( string ) ; ! ok || ! strings . HasPrefix ( rs , panicMsg ) {
t . Fatalf ( ` "Expected panic "%s" for route '%s', got "%v" ` , panicMsg , route , recv )
}
}
}
/ * func TestTreeDuplicateWildcard ( t * testing . T ) {
tree := & node { }
routes := [ ... ] string {
"/:id/:name/:id" ,
}
for _ , route := range routes {
...
}
} * /
func TestTreeTrailingSlashRedirect ( t * testing . T ) {
tree := & node { }
routes := [ ... ] string {
"/hi" ,
"/b/" ,
"/search/:query" ,
"/cmd/:tool/" ,
"/src/*filepath" ,
"/x" ,
"/x/y" ,
"/y/" ,
"/y/z" ,
"/0/:id" ,
"/0/:id/1" ,
"/1/:id/" ,
"/1/:id/2" ,
"/aa" ,
"/a/" ,
2018-10-23 03:00:09 +00:00
"/admin" ,
"/admin/:category" ,
"/admin/:category/:page" ,
2017-05-05 15:42:38 +00:00
"/doc" ,
"/doc/go_faq.html" ,
"/doc/go1.html" ,
"/no/a" ,
"/no/b" ,
"/api/hello/:name" ,
}
for _ , route := range routes {
recv := catchPanic ( func ( ) {
tree . addRoute ( route , fakeHandler ( route ) )
} )
if recv != nil {
t . Fatalf ( "panic inserting route '%s': %v" , route , recv )
}
}
//printChildren(tree, "")
tsrRoutes := [ ... ] string {
"/hi/" ,
"/b" ,
"/search/gopher/" ,
"/cmd/vet" ,
"/src" ,
"/x/" ,
"/y" ,
"/0/go/" ,
"/1/go" ,
"/a" ,
2018-10-23 03:00:09 +00:00
"/admin/" ,
"/admin/config/" ,
"/admin/config/permissions/" ,
2017-05-05 15:42:38 +00:00
"/doc/" ,
}
for _ , route := range tsrRoutes {
handler , _ , tsr := tree . getValue ( route )
if handler != nil {
t . Fatalf ( "non-nil handler for TSR route '%s" , route )
} else if ! tsr {
t . Errorf ( "expected TSR recommendation for route '%s'" , route )
}
}
noTsrRoutes := [ ... ] string {
"/" ,
"/no" ,
"/no/" ,
"/_" ,
"/_/" ,
"/api/world/abc" ,
}
for _ , route := range noTsrRoutes {
handler , _ , tsr := tree . getValue ( route )
if handler != nil {
t . Fatalf ( "non-nil handler for No-TSR route '%s" , route )
} else if tsr {
t . Errorf ( "expected no TSR recommendation for route '%s'" , route )
}
}
}
2018-10-23 03:00:09 +00:00
func TestTreeRootTrailingSlashRedirect ( t * testing . T ) {
tree := & node { }
recv := catchPanic ( func ( ) {
tree . addRoute ( "/:test" , fakeHandler ( "/:test" ) )
} )
if recv != nil {
t . Fatalf ( "panic inserting test route: %v" , recv )
}
handler , _ , tsr := tree . getValue ( "/" )
if handler != nil {
t . Fatalf ( "non-nil handler" )
} else if tsr {
t . Errorf ( "expected no TSR recommendation" )
}
}
2017-05-05 15:42:38 +00:00
func TestTreeFindCaseInsensitivePath ( t * testing . T ) {
tree := & node { }
routes := [ ... ] string {
"/hi" ,
"/b/" ,
"/ABC/" ,
"/search/:query" ,
"/cmd/:tool/" ,
"/src/*filepath" ,
"/x" ,
"/x/y" ,
"/y/" ,
"/y/z" ,
"/0/:id" ,
"/0/:id/1" ,
"/1/:id/" ,
"/1/:id/2" ,
"/aa" ,
"/a/" ,
"/doc" ,
"/doc/go_faq.html" ,
"/doc/go1.html" ,
"/doc/go/away" ,
"/no/a" ,
"/no/b" ,
2018-10-23 03:00:09 +00:00
"/Π" ,
"/u/apfêl/" ,
"/u/äpfêl/" ,
"/u/öpfêl" ,
"/v/Äpfêl/" ,
"/v/Öpfêl" ,
"/w/♬" , // 3 byte
"/w/♭/" , // 3 byte, last byte differs
"/w/𠜎" , // 4 byte
"/w/𠜏/" , // 4 byte
2017-05-05 15:42:38 +00:00
}
for _ , route := range routes {
recv := catchPanic ( func ( ) {
tree . addRoute ( route , fakeHandler ( route ) )
} )
if recv != nil {
t . Fatalf ( "panic inserting route '%s': %v" , route , recv )
}
}
// Check out == in for all registered routes
// With fixTrailingSlash = true
for _ , route := range routes {
out , found := tree . findCaseInsensitivePath ( route , true )
if ! found {
t . Errorf ( "Route '%s' not found!" , route )
} else if string ( out ) != route {
t . Errorf ( "Wrong result for route '%s': %s" , route , string ( out ) )
}
}
// With fixTrailingSlash = false
for _ , route := range routes {
out , found := tree . findCaseInsensitivePath ( route , false )
if ! found {
t . Errorf ( "Route '%s' not found!" , route )
} else if string ( out ) != route {
t . Errorf ( "Wrong result for route '%s': %s" , route , string ( out ) )
}
}
tests := [ ] struct {
in string
out string
found bool
slash bool
} {
{ "/HI" , "/hi" , true , false } ,
{ "/HI/" , "/hi" , true , true } ,
{ "/B" , "/b/" , true , true } ,
{ "/B/" , "/b/" , true , false } ,
{ "/abc" , "/ABC/" , true , true } ,
{ "/abc/" , "/ABC/" , true , false } ,
{ "/aBc" , "/ABC/" , true , true } ,
{ "/aBc/" , "/ABC/" , true , false } ,
{ "/abC" , "/ABC/" , true , true } ,
{ "/abC/" , "/ABC/" , true , false } ,
{ "/SEARCH/QUERY" , "/search/QUERY" , true , false } ,
{ "/SEARCH/QUERY/" , "/search/QUERY" , true , true } ,
{ "/CMD/TOOL/" , "/cmd/TOOL/" , true , false } ,
{ "/CMD/TOOL" , "/cmd/TOOL/" , true , true } ,
{ "/SRC/FILE/PATH" , "/src/FILE/PATH" , true , false } ,
{ "/x/Y" , "/x/y" , true , false } ,
{ "/x/Y/" , "/x/y" , true , true } ,
{ "/X/y" , "/x/y" , true , false } ,
{ "/X/y/" , "/x/y" , true , true } ,
{ "/X/Y" , "/x/y" , true , false } ,
{ "/X/Y/" , "/x/y" , true , true } ,
{ "/Y/" , "/y/" , true , false } ,
{ "/Y" , "/y/" , true , true } ,
{ "/Y/z" , "/y/z" , true , false } ,
{ "/Y/z/" , "/y/z" , true , true } ,
{ "/Y/Z" , "/y/z" , true , false } ,
{ "/Y/Z/" , "/y/z" , true , true } ,
{ "/y/Z" , "/y/z" , true , false } ,
{ "/y/Z/" , "/y/z" , true , true } ,
{ "/Aa" , "/aa" , true , false } ,
{ "/Aa/" , "/aa" , true , true } ,
{ "/AA" , "/aa" , true , false } ,
{ "/AA/" , "/aa" , true , true } ,
{ "/aA" , "/aa" , true , false } ,
{ "/aA/" , "/aa" , true , true } ,
{ "/A/" , "/a/" , true , false } ,
{ "/A" , "/a/" , true , true } ,
{ "/DOC" , "/doc" , true , false } ,
{ "/DOC/" , "/doc" , true , true } ,
{ "/NO" , "" , false , true } ,
{ "/DOC/GO" , "" , false , true } ,
2018-10-23 03:00:09 +00:00
{ "/π" , "/Π" , true , false } ,
{ "/π/" , "/Π" , true , true } ,
{ "/u/ÄPFÊL/" , "/u/äpfêl/" , true , false } ,
{ "/u/ÄPFÊL" , "/u/äpfêl/" , true , true } ,
{ "/u/ÖPFÊL/" , "/u/öpfêl" , true , true } ,
{ "/u/ÖPFÊL" , "/u/öpfêl" , true , false } ,
{ "/v/äpfêL/" , "/v/Äpfêl/" , true , false } ,
{ "/v/äpfêL" , "/v/Äpfêl/" , true , true } ,
{ "/v/öpfêL/" , "/v/Öpfêl" , true , true } ,
{ "/v/öpfêL" , "/v/Öpfêl" , true , false } ,
{ "/w/♬/" , "/w/♬" , true , true } ,
{ "/w/♭" , "/w/♭/" , true , true } ,
{ "/w/𠜎/" , "/w/𠜎" , true , true } ,
{ "/w/𠜏" , "/w/𠜏/" , true , true } ,
2017-05-05 15:42:38 +00:00
}
// With fixTrailingSlash = true
for _ , test := range tests {
out , found := tree . findCaseInsensitivePath ( test . in , true )
if found != test . found || ( found && ( string ( out ) != test . out ) ) {
t . Errorf ( "Wrong result for '%s': got %s, %t; want %s, %t" ,
test . in , string ( out ) , found , test . out , test . found )
return
}
}
// With fixTrailingSlash = false
for _ , test := range tests {
out , found := tree . findCaseInsensitivePath ( test . in , false )
if test . slash {
if found { // test needs a trailingSlash fix. It must not be found!
t . Errorf ( "Found without fixTrailingSlash: %s; got %s" , test . in , string ( out ) )
}
} else {
if found != test . found || ( found && ( string ( out ) != test . out ) ) {
t . Errorf ( "Wrong result for '%s': got %s, %t; want %s, %t" ,
test . in , string ( out ) , found , test . out , test . found )
return
}
}
}
}
func TestTreeInvalidNodeType ( t * testing . T ) {
const panicMsg = "invalid node type"
tree := & node { }
tree . addRoute ( "/" , fakeHandler ( "/" ) )
tree . addRoute ( "/:page" , fakeHandler ( "/:page" ) )
// set invalid node type
tree . children [ 0 ] . nType = 42
// normal lookup
recv := catchPanic ( func ( ) {
tree . getValue ( "/test" )
} )
if rs , ok := recv . ( string ) ; ! ok || rs != panicMsg {
t . Fatalf ( "Expected panic '" + panicMsg + "', got '%v'" , recv )
}
// case-insensitive lookup
recv = catchPanic ( func ( ) {
tree . findCaseInsensitivePath ( "/test" , true )
} )
if rs , ok := recv . ( string ) ; ! ok || rs != panicMsg {
t . Fatalf ( "Expected panic '" + panicMsg + "', got '%v'" , recv )
}
}
2018-10-23 03:00:09 +00:00
func TestTreeWildcardConflictEx ( t * testing . T ) {
conflicts := [ ... ] struct {
route string
segPath string
existPath string
existSegPath string
} {
{ "/who/are/foo" , "/foo" , ` /who/are/\*you ` , ` /\*you ` } ,
{ "/who/are/foo/" , "/foo/" , ` /who/are/\*you ` , ` /\*you ` } ,
{ "/who/are/foo/bar" , "/foo/bar" , ` /who/are/\*you ` , ` /\*you ` } ,
{ "/conxxx" , "xxx" , ` /con:tact ` , ` :tact ` } ,
{ "/conooo/xxx" , "ooo" , ` /con:tact ` , ` :tact ` } ,
}
for _ , conflict := range conflicts {
// I have to re-create a 'tree', because the 'tree' will be
// in an inconsistent state when the loop recovers from the
// panic which threw by 'addRoute' function.
tree := & node { }
routes := [ ... ] string {
"/con:tact" ,
"/who/are/*you" ,
"/who/foo/hello" ,
}
for _ , route := range routes {
tree . addRoute ( route , fakeHandler ( route ) )
}
recv := catchPanic ( func ( ) {
tree . addRoute ( conflict . route , fakeHandler ( conflict . route ) )
} )
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 ) ) {
t . Fatalf ( "invalid wildcard conflict error (%v)" , recv )
}
}
}