github.com/cloudwego/kitex@v0.9.0/pkg/generic/descriptor/tree_test.go (about) 1 /* 2 * Copyright 2013 Julien Schmidt. All rights reserved. 3 * Use of this source code is governed by a BSD-style license that can be found 4 * in the LICENSE file. 5 * 6 * This file may have been modified by CloudWeGo authors. All CloudWeGo 7 * Modifications are Copyright 2021 CloudWeGo Authors. 8 */ 9 10 package descriptor 11 12 import ( 13 "reflect" 14 "strings" 15 "testing" 16 ) 17 18 func fakeHandler(val string) *FunctionDescriptor { 19 return &FunctionDescriptor{Name: val} 20 } 21 22 type testRequests []struct { 23 path string 24 nilHandler bool 25 route string 26 ps *Params 27 } 28 29 func getParams() *Params { 30 return &Params{ 31 params: make([]Param, 0, 20), 32 } 33 } 34 35 func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ...bool) { 36 unescape := false 37 if len(unescapes) >= 1 { 38 unescape = unescapes[0] 39 } 40 for _, request := range requests { 41 handler, psp, _ := tree.getValue(request.path, getParams, unescape) 42 43 switch { 44 case handler == nil: 45 if !request.nilHandler { 46 t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path) 47 } 48 case request.nilHandler: 49 t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path) 50 default: 51 if handler.Name != request.route { 52 t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, handler.Name, request.route) 53 } 54 } 55 56 var ps *Params 57 if psp != nil { 58 ps = psp 59 } 60 61 if !reflect.DeepEqual(ps, request.ps) { 62 t.Errorf("Params mismatch for route '%s'", request.path) 63 } 64 } 65 } 66 67 func TestCountParams(t *testing.T) { 68 if countParams("/path/:param1/static/*catch-all") != 2 { 69 t.Fail() 70 } 71 if countParams(strings.Repeat("/:param", 256)) != 256 { 72 t.Fail() 73 } 74 } 75 76 func TestNoFunction(t *testing.T) { 77 tree := &node{} 78 79 route := "/hi" 80 recv := catchPanic(func() { 81 tree.addRoute(route, nil) 82 }) 83 if recv == nil { 84 t.Fatalf("no panic while inserting route with empty function '%s", route) 85 } 86 } 87 88 func TestEmptyPath(t *testing.T) { 89 tree := &node{} 90 91 routes := [...]string{ 92 "", 93 "user", 94 ":user", 95 "*user", 96 } 97 for _, route := range routes { 98 recv := catchPanic(func() { 99 tree.addRoute(route, nil) 100 }) 101 if recv == nil { 102 t.Fatalf("no panic while inserting route with empty path '%s", route) 103 } 104 } 105 } 106 107 func TestTreeAddAndGet(t *testing.T) { 108 tree := &node{} 109 110 routes := [...]string{ 111 "/hi", 112 "/contact", 113 "/co", 114 "/c", 115 "/a", 116 "/ab", 117 "/doc/", 118 "/doc/go_faq.html", 119 "/doc/go1.html", 120 "/α", 121 "/β", 122 } 123 for _, route := range routes { 124 tree.addRoute(route, fakeHandler(route)) 125 } 126 127 checkRequests(t, tree, testRequests{ 128 {"", true, "", nil}, 129 {"a", true, "", nil}, 130 {"/a", false, "/a", nil}, 131 {"/", true, "", nil}, 132 {"/hi", false, "/hi", nil}, 133 {"/contact", false, "/contact", nil}, 134 {"/co", false, "/co", nil}, 135 {"/con", true, "", nil}, // key mismatch 136 {"/cona", true, "", nil}, // key mismatch 137 {"/no", true, "", nil}, // no matching child 138 {"/ab", false, "/ab", nil}, 139 {"/α", false, "/α", nil}, 140 {"/β", false, "/β", nil}, 141 }) 142 } 143 144 func TestTreeWildcard(t *testing.T) { 145 tree := &node{} 146 147 routes := [...]string{ 148 "/", 149 "/cmd/:tool/:sub", 150 "/cmd/:tool/", 151 "/cmd/xxx/", 152 "/src/*filepath", 153 "/search/", 154 "/search/:query", 155 "/user_:name", 156 "/user_:name/about", 157 "/files/:dir/*filepath", 158 "/doc/", 159 "/doc/go_faq.html", 160 "/doc/go1.html", 161 "/info/:user/public", 162 "/info/:user/project/:project", 163 "/a/b/:c", 164 "/a/:b/c/d", 165 "/a/*b", 166 } 167 for _, route := range routes { 168 tree.addRoute(route, fakeHandler(route)) 169 } 170 171 checkRequests(t, tree, testRequests{ 172 {"/", false, "/", nil}, 173 {"/cmd/test/", false, "/cmd/:tool/", &Params{params: []Param{{"tool", "test"}}}}, 174 {"/cmd/test", true, "", &Params{params: []Param{}}}, 175 {"/cmd/test/3", false, "/cmd/:tool/:sub", &Params{params: []Param{{"tool", "test"}, {"sub", "3"}}}}, 176 {"/src/", false, "/src/*filepath", &Params{params: []Param{{"filepath", ""}}}}, 177 {"/src/some/file.png", false, "/src/*filepath", &Params{params: []Param{{"filepath", "some/file.png"}}}}, 178 {"/search/", false, "/search/", nil}, 179 {"/search/someth!ng+in+ünìcodé", false, "/search/:query", &Params{params: []Param{{"query", "someth!ng+in+ünìcodé"}}}}, 180 {"/search/someth!ng+in+ünìcodé/", true, "", &Params{params: []Param{}}}, 181 {"/user_gopher", false, "/user_:name", &Params{params: []Param{{"name", "gopher"}}}}, 182 {"/user_gopher/about", false, "/user_:name/about", &Params{params: []Param{{"name", "gopher"}}}}, 183 {"/files/js/inc/framework.js", false, "/files/:dir/*filepath", &Params{params: []Param{{"dir", "js"}, {"filepath", "inc/framework.js"}}}}, 184 {"/info/gordon/public", false, "/info/:user/public", &Params{params: []Param{{"user", "gordon"}}}}, 185 {"/info/gordon/project/go", false, "/info/:user/project/:project", &Params{params: []Param{{"user", "gordon"}, {"project", "go"}}}}, 186 {"/a/b/c", false, "/a/b/:c", &Params{params: []Param{{Key: "c", Value: "c"}}}}, 187 {"/a/b/c/d", false, "/a/:b/c/d", &Params{params: []Param{{Key: "b", Value: "b"}}}}, 188 {"/a/b", false, "/a/*b", &Params{params: []Param{{Key: "b", Value: "b"}}}}, 189 }) 190 } 191 192 func TestUnescapeParameters(t *testing.T) { 193 tree := &node{} 194 195 routes := [...]string{ 196 "/", 197 "/cmd/:tool/:sub", 198 "/cmd/:tool/", 199 "/src/*filepath", 200 "/search/:query", 201 "/files/:dir/*filepath", 202 "/info/:user/project/:project", 203 "/info/:user", 204 } 205 for _, route := range routes { 206 tree.addRoute(route, fakeHandler(route)) 207 } 208 209 unescape := true 210 checkRequests(t, tree, testRequests{ 211 {"/", false, "/", nil}, 212 {"/cmd/test/", false, "/cmd/:tool/", &Params{params: []Param{{"tool", "test"}}}}, 213 {"/cmd/test", true, "", &Params{params: []Param{}}}, 214 {"/src/some/file.png", false, "/src/*filepath", &Params{params: []Param{{"filepath", "some/file.png"}}}}, 215 {"/src/some/file+test.png", false, "/src/*filepath", &Params{params: []Param{{"filepath", "some/file test.png"}}}}, 216 {"/src/some/file++++%%%%test.png", false, "/src/*filepath", &Params{params: []Param{{"filepath", "some/file++++%%%%test.png"}}}}, 217 {"/src/some/file%2Ftest.png", false, "/src/*filepath", &Params{params: []Param{{"filepath", "some/file/test.png"}}}}, 218 {"/search/someth!ng+in+ünìcodé", false, "/search/:query", &Params{params: []Param{{"query", "someth!ng in ünìcodé"}}}}, 219 {"/info/gordon/project/go", false, "/info/:user/project/:project", &Params{params: []Param{{"user", "gordon"}, {"project", "go"}}}}, 220 {"/info/slash%2Fgordon", false, "/info/:user", &Params{params: []Param{{"user", "slash/gordon"}}}}, 221 {"/info/slash%2Fgordon/project/Project%20%231", false, "/info/:user/project/:project", &Params{params: []Param{{"user", "slash/gordon"}, {"project", "Project #1"}}}}, 222 {"/info/slash%%%%", false, "/info/:user", &Params{params: []Param{{"user", "slash%%%%"}}}}, 223 {"/info/slash%%%%2Fgordon/project/Project%%%%20%231", false, "/info/:user/project/:project", &Params{params: []Param{{"user", "slash%%%%2Fgordon"}, {"project", "Project%%%%20%231"}}}}, 224 }, unescape) 225 } 226 227 func catchPanic(testFunc func()) (recv interface{}) { 228 defer func() { 229 recv = recover() 230 }() 231 232 testFunc() 233 return 234 } 235 236 type testRoute struct { 237 path string 238 conflict bool 239 } 240 241 func testRoutes(t *testing.T, routes []testRoute) { 242 tree := &node{} 243 244 for i := range routes { 245 route := routes[i] 246 recv := catchPanic(func() { 247 tree.addRoute(route.path, fakeHandler(route.path)) 248 }) 249 250 if route.conflict { 251 if recv == nil { 252 t.Errorf("no panic for conflicting route '%s'", route.path) 253 } 254 } else if recv != nil { 255 t.Errorf("unexpected panic for route '%s': %v", route.path, recv) 256 } 257 } 258 } 259 260 func TestTreeWildcardConflict(t *testing.T) { 261 routes := []testRoute{ 262 {"/cmd/:tool/:sub", false}, 263 {"/cmd/vet", false}, 264 {"/src/*filepath", false}, 265 {"/src/*filepathx", true}, 266 {"/src/", false}, 267 {"/src1/", false}, 268 {"/src1/*filepath", false}, 269 {"/src2*filepath", true}, 270 {"/search/:query", false}, 271 {"/search/invalid", false}, 272 {"/user_:name", false}, 273 {"/user_x", false}, 274 {"/user_:name", true}, 275 {"/id:id", false}, 276 {"/id/:id", false}, 277 } 278 testRoutes(t, routes) 279 } 280 281 func TestTreeChildConflict(t *testing.T) { 282 routes := []testRoute{ 283 {"/cmd/vet", false}, 284 {"/cmd/:tool/:sub", false}, 285 {"/src/AUTHORS", false}, 286 {"/src/*filepath", false}, 287 {"/user_x", false}, 288 {"/user_:name", false}, 289 {"/id/:id", false}, 290 {"/id:id", false}, 291 {"/:id", false}, 292 {"/*filepath", false}, 293 } 294 testRoutes(t, routes) 295 } 296 297 func TestTreeDuplicatePath(t *testing.T) { 298 tree := &node{} 299 300 routes := [...]string{ 301 "/", 302 "/doc/", 303 "/src/*filepath", 304 "/search/:query", 305 "/user_:name", 306 } 307 for i := range routes { 308 route := routes[i] 309 recv := catchPanic(func() { 310 tree.addRoute(route, fakeHandler(route)) 311 }) 312 if recv != nil { 313 t.Fatalf("panic inserting route '%s': %v", route, recv) 314 } 315 316 // Add again 317 recv = catchPanic(func() { 318 tree.addRoute(route, fakeHandler(route)) 319 }) 320 if recv == nil { 321 t.Fatalf("no panic while inserting duplicate route '%s", route) 322 } 323 } 324 325 checkRequests(t, tree, testRequests{ 326 {"/", false, "/", nil}, 327 {"/doc/", false, "/doc/", nil}, 328 {"/src/some/file.png", false, "/src/*filepath", &Params{params: []Param{{"filepath", "some/file.png"}}}}, 329 {"/search/someth!ng+in+ünìcodé", false, "/search/:query", &Params{params: []Param{{"query", "someth!ng+in+ünìcodé"}}}}, 330 {"/user_gopher", false, "/user_:name", &Params{params: []Param{{"name", "gopher"}}}}, 331 }) 332 } 333 334 func TestEmptyWildcardName(t *testing.T) { 335 tree := &node{} 336 337 routes := [...]string{ 338 "/user:", 339 "/user:/", 340 "/cmd/:/", 341 "/src/*", 342 } 343 for i := range routes { 344 route := routes[i] 345 recv := catchPanic(func() { 346 tree.addRoute(route, nil) 347 }) 348 if recv == nil { 349 t.Fatalf("no panic while inserting route with empty wildcard name '%s", route) 350 } 351 } 352 } 353 354 func TestTreeCatchAllConflict(t *testing.T) { 355 routes := []testRoute{ 356 {"/src/*filepath/x", true}, 357 {"/src2/", false}, 358 {"/src2/*filepath/x", true}, 359 {"/src3/*filepath", false}, 360 {"/src3/*filepath/x", true}, 361 } 362 testRoutes(t, routes) 363 } 364 365 func TestTreeCatchMaxParams(t *testing.T) { 366 tree := &node{} 367 route := "/cmd/*filepath" 368 tree.addRoute(route, fakeHandler(route)) 369 } 370 371 func TestTreeDoubleWildcard(t *testing.T) { 372 const panicMsg = "only one wildcard per path segment is allowed" 373 374 routes := [...]string{ 375 "/:foo:bar", 376 "/:foo:bar/", 377 "/:foo*bar", 378 } 379 380 for i := range routes { 381 route := routes[i] 382 tree := &node{} 383 recv := catchPanic(func() { 384 tree.addRoute(route, nil) 385 }) 386 387 if rs, ok := recv.(string); !ok || !strings.HasPrefix(rs, panicMsg) { 388 t.Fatalf(`"Expected panic "%s" for route '%s', got "%v"`, panicMsg, route, recv) 389 } 390 } 391 } 392 393 func TestTreeTrailingSlashRedirect(t *testing.T) { 394 tree := &node{} 395 396 routes := [...]string{ 397 "/hi", 398 "/b/", 399 "/search/:query", 400 "/cmd/:tool/", 401 "/src/*filepath", 402 "/x", 403 "/x/y", 404 "/y/", 405 "/y/z", 406 "/0/:id", 407 "/0/:id/1", 408 "/1/:id/", 409 "/1/:id/2", 410 "/aa", 411 "/a/", 412 "/admin", 413 "/admin/:category", 414 "/admin/:category/:page", 415 "/doc", 416 "/doc/go_faq.html", 417 "/doc/go1.html", 418 "/no/a", 419 "/no/b", 420 "/api/hello/:name", 421 "/user/:name/*id", 422 "/resource", 423 "/r/*id", 424 "/book/biz/:name", 425 "/book/biz/abc", 426 "/book/biz/abc/bar", 427 "/book/:page/:name", 428 "/book/hello/:name/biz/", 429 } 430 for i := range routes { 431 route := routes[i] 432 recv := catchPanic(func() { 433 tree.addRoute(route, fakeHandler(route)) 434 }) 435 if recv != nil { 436 t.Fatalf("panic inserting route '%s': %v", route, recv) 437 } 438 } 439 440 tsrRoutes := [...]string{ 441 "/hi/", 442 "/b", 443 "/search/gopher/", 444 "/cmd/vet", 445 "/src", 446 "/x/", 447 "/y", 448 "/0/go/", 449 "/1/go", 450 "/a", 451 "/admin/", 452 "/admin/config/", 453 "/admin/config/permissions/", 454 "/doc/", 455 "/user/name", 456 "/r", 457 "/book/hello/a/biz", 458 "/book/biz/foo/", 459 "/book/biz/abc/bar/", 460 } 461 for _, route := range tsrRoutes { 462 handler, _, tsr := tree.getValue(route, getParams, false) 463 if handler != nil { 464 t.Fatalf("non-nil handler for TSR route '%s", route) 465 } else if !tsr { 466 t.Errorf("expected TSR recommendation for route '%s'", route) 467 } 468 } 469 470 noTsrRoutes := [...]string{ 471 "/", 472 "/no", 473 "/no/", 474 "/_", 475 "/_/", 476 "/api/world/abc", 477 "/book", 478 "/book/", 479 "/book/hello/a/abc", 480 "/book/biz/abc/biz", 481 } 482 for _, route := range noTsrRoutes { 483 handler, _, tsr := tree.getValue(route, getParams, false) 484 if handler != nil { 485 t.Fatalf("non-nil handler for No-TSR route '%s", route) 486 } else if tsr { 487 t.Errorf("expected no TSR recommendation for route '%s'", route) 488 } 489 } 490 } 491 492 func TestTreeTrailingSlashRedirect2(t *testing.T) { 493 tree := &node{} 494 495 routes := [...]string{ 496 "/api/:version/seller/locales/get", 497 "/api/v:version/seller/permissions/get", 498 "/api/v:version/seller/university/entrance_knowledge_list/get", 499 } 500 for _, route := range routes { 501 recv := catchPanic(func() { 502 tree.addRoute(route, fakeHandler(route)) 503 }) 504 if recv != nil { 505 t.Fatalf("panic inserting route '%s': %v", route, recv) 506 } 507 } 508 509 tsrRoutes := [...]string{ 510 "/api/v:version/seller/permissions/get/", 511 "/api/version/seller/permissions/get/", 512 } 513 514 for _, route := range tsrRoutes { 515 handler, _, tsr := tree.getValue(route, getParams, false) 516 if handler != nil { 517 t.Fatalf("non-nil handler for TSR route '%s", route) 518 } else if !tsr { 519 t.Errorf("expected TSR recommendation for route '%s'", route) 520 } 521 } 522 523 noTsrRoutes := [...]string{ 524 "/api/v:version/seller/permissions/get/a", 525 } 526 for _, route := range noTsrRoutes { 527 handler, _, tsr := tree.getValue(route, getParams, false) 528 if handler != nil { 529 t.Fatalf("non-nil handler for No-TSR route '%s", route) 530 } else if tsr { 531 t.Errorf("expected no TSR recommendation for route '%s'", route) 532 } 533 } 534 } 535 536 func TestTreeRootTrailingSlashRedirect(t *testing.T) { 537 tree := &node{} 538 539 recv := catchPanic(func() { 540 tree.addRoute("/:test", fakeHandler("/:test")) 541 }) 542 if recv != nil { 543 t.Fatalf("panic inserting test route: %v", recv) 544 } 545 546 handler, _, tsr := tree.getValue("/", nil, false) 547 if handler != nil { 548 t.Fatalf("non-nil handler") 549 } else if tsr { 550 t.Errorf("expected no TSR recommendation") 551 } 552 }