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