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