gitee.com/ks-custle/core-gm@v0.0.0-20230922171213-b83bdd97b62c/mux/mux_test.go (about) 1 // Copyright 2012 The Gorilla Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package mux 6 7 import ( 8 "bufio" 9 "bytes" 10 "context" 11 "errors" 12 "fmt" 13 "io/ioutil" 14 "net/url" 15 "reflect" 16 "strings" 17 "testing" 18 "time" 19 20 http "gitee.com/ks-custle/core-gm/gmhttp" 21 "gitee.com/ks-custle/core-gm/gmhttp/httptest" 22 ) 23 24 func (r *Route) GoString() string { 25 matchers := make([]string, len(r.matchers)) 26 for i, m := range r.matchers { 27 matchers[i] = fmt.Sprintf("%#v", m) 28 } 29 return fmt.Sprintf("&Route{matchers:[]matcher{%s}}", strings.Join(matchers, ", ")) 30 } 31 32 func (r *routeRegexp) GoString() string { 33 return fmt.Sprintf("&routeRegexp{template: %q, regexpType: %v, options: %v, regexp: regexp.MustCompile(%q), reverse: %q, varsN: %v, varsR: %v", r.template, r.regexpType, r.options, r.regexp.String(), r.reverse, r.varsN, r.varsR) 34 } 35 36 type routeTest struct { 37 title string // title of the test 38 route *Route // the route being tested 39 request *http.Request // a request to test the route 40 vars map[string]string // the expected vars of the match 41 scheme string // the expected scheme of the built URL 42 host string // the expected host of the built URL 43 path string // the expected path of the built URL 44 query string // the expected query string of the built URL 45 pathTemplate string // the expected path template of the route 46 hostTemplate string // the expected host template of the route 47 queriesTemplate string // the expected query template of the route 48 methods []string // the expected route methods 49 pathRegexp string // the expected path regexp 50 queriesRegexp string // the expected query regexp 51 shouldMatch bool // whether the request is expected to match the route at all 52 shouldRedirect bool // whether the request should result in a redirect 53 } 54 55 func TestHost(t *testing.T) { 56 57 tests := []routeTest{ 58 { 59 title: "Host route match", 60 route: new(Route).Host("aaa.bbb.ccc"), 61 request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), 62 vars: map[string]string{}, 63 host: "aaa.bbb.ccc", 64 path: "", 65 shouldMatch: true, 66 }, 67 { 68 title: "Host route, wrong host in request URL", 69 route: new(Route).Host("aaa.bbb.ccc"), 70 request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), 71 vars: map[string]string{}, 72 host: "aaa.bbb.ccc", 73 path: "", 74 shouldMatch: false, 75 }, 76 { 77 title: "Host route with port, match", 78 route: new(Route).Host("aaa.bbb.ccc:1234"), 79 request: newRequest("GET", "http://aaa.bbb.ccc:1234/111/222/333"), 80 vars: map[string]string{}, 81 host: "aaa.bbb.ccc:1234", 82 path: "", 83 shouldMatch: true, 84 }, 85 { 86 title: "Host route with port, wrong port in request URL", 87 route: new(Route).Host("aaa.bbb.ccc:1234"), 88 request: newRequest("GET", "http://aaa.bbb.ccc:9999/111/222/333"), 89 vars: map[string]string{}, 90 host: "aaa.bbb.ccc:1234", 91 path: "", 92 shouldMatch: false, 93 }, 94 { 95 title: "Host route, match with host in request header", 96 route: new(Route).Host("aaa.bbb.ccc"), 97 request: newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc"), 98 vars: map[string]string{}, 99 host: "aaa.bbb.ccc", 100 path: "", 101 shouldMatch: true, 102 }, 103 { 104 title: "Host route, wrong host in request header", 105 route: new(Route).Host("aaa.bbb.ccc"), 106 request: newRequestHost("GET", "/111/222/333", "aaa.222.ccc"), 107 vars: map[string]string{}, 108 host: "aaa.bbb.ccc", 109 path: "", 110 shouldMatch: false, 111 }, 112 { 113 title: "Host route with port, match with request header", 114 route: new(Route).Host("aaa.bbb.ccc:1234"), 115 request: newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc:1234"), 116 vars: map[string]string{}, 117 host: "aaa.bbb.ccc:1234", 118 path: "", 119 shouldMatch: true, 120 }, 121 { 122 title: "Host route with port, wrong host in request header", 123 route: new(Route).Host("aaa.bbb.ccc:1234"), 124 request: newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc:9999"), 125 vars: map[string]string{}, 126 host: "aaa.bbb.ccc:1234", 127 path: "", 128 shouldMatch: false, 129 }, 130 { 131 title: "Host route with pattern, match with request header", 132 route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc:1{v2:(?:23|4)}"), 133 request: newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc:123"), 134 vars: map[string]string{"v1": "bbb", "v2": "23"}, 135 host: "aaa.bbb.ccc:123", 136 path: "", 137 hostTemplate: `aaa.{v1:[a-z]{3}}.ccc:1{v2:(?:23|4)}`, 138 shouldMatch: true, 139 }, 140 { 141 title: "Host route with pattern, match", 142 route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"), 143 request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), 144 vars: map[string]string{"v1": "bbb"}, 145 host: "aaa.bbb.ccc", 146 path: "", 147 hostTemplate: `aaa.{v1:[a-z]{3}}.ccc`, 148 shouldMatch: true, 149 }, 150 { 151 title: "Host route with pattern, additional capturing group, match", 152 route: new(Route).Host("aaa.{v1:[a-z]{2}(?:b|c)}.ccc"), 153 request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), 154 vars: map[string]string{"v1": "bbb"}, 155 host: "aaa.bbb.ccc", 156 path: "", 157 hostTemplate: `aaa.{v1:[a-z]{2}(?:b|c)}.ccc`, 158 shouldMatch: true, 159 }, 160 { 161 title: "Host route with pattern, wrong host in request URL", 162 route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"), 163 request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), 164 vars: map[string]string{"v1": "bbb"}, 165 host: "aaa.bbb.ccc", 166 path: "", 167 hostTemplate: `aaa.{v1:[a-z]{3}}.ccc`, 168 shouldMatch: false, 169 }, 170 { 171 title: "Host route with multiple patterns, match", 172 route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}"), 173 request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), 174 vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"}, 175 host: "aaa.bbb.ccc", 176 path: "", 177 hostTemplate: `{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}`, 178 shouldMatch: true, 179 }, 180 { 181 title: "Host route with multiple patterns, wrong host in request URL", 182 route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}"), 183 request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), 184 vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"}, 185 host: "aaa.bbb.ccc", 186 path: "", 187 hostTemplate: `{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}`, 188 shouldMatch: false, 189 }, 190 { 191 title: "Host route with hyphenated name and pattern, match", 192 route: new(Route).Host("aaa.{v-1:[a-z]{3}}.ccc"), 193 request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), 194 vars: map[string]string{"v-1": "bbb"}, 195 host: "aaa.bbb.ccc", 196 path: "", 197 hostTemplate: `aaa.{v-1:[a-z]{3}}.ccc`, 198 shouldMatch: true, 199 }, 200 { 201 title: "Host route with hyphenated name and pattern, additional capturing group, match", 202 route: new(Route).Host("aaa.{v-1:[a-z]{2}(?:b|c)}.ccc"), 203 request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), 204 vars: map[string]string{"v-1": "bbb"}, 205 host: "aaa.bbb.ccc", 206 path: "", 207 hostTemplate: `aaa.{v-1:[a-z]{2}(?:b|c)}.ccc`, 208 shouldMatch: true, 209 }, 210 { 211 title: "Host route with multiple hyphenated names and patterns, match", 212 route: new(Route).Host("{v-1:[a-z]{3}}.{v-2:[a-z]{3}}.{v-3:[a-z]{3}}"), 213 request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), 214 vars: map[string]string{"v-1": "aaa", "v-2": "bbb", "v-3": "ccc"}, 215 host: "aaa.bbb.ccc", 216 path: "", 217 hostTemplate: `{v-1:[a-z]{3}}.{v-2:[a-z]{3}}.{v-3:[a-z]{3}}`, 218 shouldMatch: true, 219 }, 220 } 221 for _, test := range tests { 222 t.Run(test.title, func(t *testing.T) { 223 testRoute(t, test) 224 testTemplate(t, test) 225 }) 226 } 227 } 228 229 func TestPath(t *testing.T) { 230 tests := []routeTest{ 231 { 232 title: "Path route, match", 233 route: new(Route).Path("/111/222/333"), 234 request: newRequest("GET", "http://localhost/111/222/333"), 235 vars: map[string]string{}, 236 host: "", 237 path: "/111/222/333", 238 shouldMatch: true, 239 }, 240 { 241 title: "Path route, match with trailing slash in request and path", 242 route: new(Route).Path("/111/"), 243 request: newRequest("GET", "http://localhost/111/"), 244 vars: map[string]string{}, 245 host: "", 246 path: "/111/", 247 shouldMatch: true, 248 }, 249 { 250 title: "Path route, do not match with trailing slash in path", 251 route: new(Route).Path("/111/"), 252 request: newRequest("GET", "http://localhost/111"), 253 vars: map[string]string{}, 254 host: "", 255 path: "/111", 256 pathTemplate: `/111/`, 257 pathRegexp: `^/111/$`, 258 shouldMatch: false, 259 }, 260 { 261 title: "Path route, do not match with trailing slash in request", 262 route: new(Route).Path("/111"), 263 request: newRequest("GET", "http://localhost/111/"), 264 vars: map[string]string{}, 265 host: "", 266 path: "/111/", 267 pathTemplate: `/111`, 268 shouldMatch: false, 269 }, 270 { 271 title: "Path route, match root with no host", 272 route: new(Route).Path("/"), 273 request: newRequest("GET", "/"), 274 vars: map[string]string{}, 275 host: "", 276 path: "/", 277 pathTemplate: `/`, 278 pathRegexp: `^/$`, 279 shouldMatch: true, 280 }, 281 { 282 title: "Path route, match root with no host, App Engine format", 283 route: new(Route).Path("/"), 284 request: func() *http.Request { 285 r := newRequest("GET", "http://localhost/") 286 r.RequestURI = "/" 287 return r 288 }(), 289 vars: map[string]string{}, 290 host: "", 291 path: "/", 292 pathTemplate: `/`, 293 shouldMatch: true, 294 }, 295 { 296 title: "Path route, wrong path in request in request URL", 297 route: new(Route).Path("/111/222/333"), 298 request: newRequest("GET", "http://localhost/1/2/3"), 299 vars: map[string]string{}, 300 host: "", 301 path: "/111/222/333", 302 shouldMatch: false, 303 }, 304 { 305 title: "Path route with pattern, match", 306 route: new(Route).Path("/111/{v1:[0-9]{3}}/333"), 307 request: newRequest("GET", "http://localhost/111/222/333"), 308 vars: map[string]string{"v1": "222"}, 309 host: "", 310 path: "/111/222/333", 311 pathTemplate: `/111/{v1:[0-9]{3}}/333`, 312 shouldMatch: true, 313 }, 314 { 315 title: "Path route with pattern, URL in request does not match", 316 route: new(Route).Path("/111/{v1:[0-9]{3}}/333"), 317 request: newRequest("GET", "http://localhost/111/aaa/333"), 318 vars: map[string]string{"v1": "222"}, 319 host: "", 320 path: "/111/222/333", 321 pathTemplate: `/111/{v1:[0-9]{3}}/333`, 322 pathRegexp: `^/111/(?P<v0>[0-9]{3})/333$`, 323 shouldMatch: false, 324 }, 325 { 326 title: "Path route with multiple patterns, match", 327 route: new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}"), 328 request: newRequest("GET", "http://localhost/111/222/333"), 329 vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"}, 330 host: "", 331 path: "/111/222/333", 332 pathTemplate: `/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}`, 333 pathRegexp: `^/(?P<v0>[0-9]{3})/(?P<v1>[0-9]{3})/(?P<v2>[0-9]{3})$`, 334 shouldMatch: true, 335 }, 336 { 337 title: "Path route with multiple patterns, URL in request does not match", 338 route: new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}"), 339 request: newRequest("GET", "http://localhost/111/aaa/333"), 340 vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"}, 341 host: "", 342 path: "/111/222/333", 343 pathTemplate: `/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}`, 344 pathRegexp: `^/(?P<v0>[0-9]{3})/(?P<v1>[0-9]{3})/(?P<v2>[0-9]{3})$`, 345 shouldMatch: false, 346 }, 347 { 348 title: "Path route with multiple patterns with pipe, match", 349 route: new(Route).Path("/{category:a|(?:b/c)}/{product}/{id:[0-9]+}"), 350 request: newRequest("GET", "http://localhost/a/product_name/1"), 351 vars: map[string]string{"category": "a", "product": "product_name", "id": "1"}, 352 host: "", 353 path: "/a/product_name/1", 354 pathTemplate: `/{category:a|(?:b/c)}/{product}/{id:[0-9]+}`, 355 pathRegexp: `^/(?P<v0>a|(?:b/c))/(?P<v1>[^/]+)/(?P<v2>[0-9]+)$`, 356 shouldMatch: true, 357 }, 358 { 359 title: "Path route with hyphenated name and pattern, match", 360 route: new(Route).Path("/111/{v-1:[0-9]{3}}/333"), 361 request: newRequest("GET", "http://localhost/111/222/333"), 362 vars: map[string]string{"v-1": "222"}, 363 host: "", 364 path: "/111/222/333", 365 pathTemplate: `/111/{v-1:[0-9]{3}}/333`, 366 pathRegexp: `^/111/(?P<v0>[0-9]{3})/333$`, 367 shouldMatch: true, 368 }, 369 { 370 title: "Path route with multiple hyphenated names and patterns, match", 371 route: new(Route).Path("/{v-1:[0-9]{3}}/{v-2:[0-9]{3}}/{v-3:[0-9]{3}}"), 372 request: newRequest("GET", "http://localhost/111/222/333"), 373 vars: map[string]string{"v-1": "111", "v-2": "222", "v-3": "333"}, 374 host: "", 375 path: "/111/222/333", 376 pathTemplate: `/{v-1:[0-9]{3}}/{v-2:[0-9]{3}}/{v-3:[0-9]{3}}`, 377 pathRegexp: `^/(?P<v0>[0-9]{3})/(?P<v1>[0-9]{3})/(?P<v2>[0-9]{3})$`, 378 shouldMatch: true, 379 }, 380 { 381 title: "Path route with multiple hyphenated names and patterns with pipe, match", 382 route: new(Route).Path("/{product-category:a|(?:b/c)}/{product-name}/{product-id:[0-9]+}"), 383 request: newRequest("GET", "http://localhost/a/product_name/1"), 384 vars: map[string]string{"product-category": "a", "product-name": "product_name", "product-id": "1"}, 385 host: "", 386 path: "/a/product_name/1", 387 pathTemplate: `/{product-category:a|(?:b/c)}/{product-name}/{product-id:[0-9]+}`, 388 pathRegexp: `^/(?P<v0>a|(?:b/c))/(?P<v1>[^/]+)/(?P<v2>[0-9]+)$`, 389 shouldMatch: true, 390 }, 391 { 392 title: "Path route with multiple hyphenated names and patterns with pipe and case insensitive, match", 393 route: new(Route).Path("/{type:(?i:daily|mini|variety)}-{date:\\d{4,4}-\\d{2,2}-\\d{2,2}}"), 394 request: newRequest("GET", "http://localhost/daily-2016-01-01"), 395 vars: map[string]string{"type": "daily", "date": "2016-01-01"}, 396 host: "", 397 path: "/daily-2016-01-01", 398 pathTemplate: `/{type:(?i:daily|mini|variety)}-{date:\d{4,4}-\d{2,2}-\d{2,2}}`, 399 pathRegexp: `^/(?P<v0>(?i:daily|mini|variety))-(?P<v1>\d{4,4}-\d{2,2}-\d{2,2})$`, 400 shouldMatch: true, 401 }, 402 { 403 title: "Path route with empty match right after other match", 404 route: new(Route).Path(`/{v1:[0-9]*}{v2:[a-z]*}/{v3:[0-9]*}`), 405 request: newRequest("GET", "http://localhost/111/222"), 406 vars: map[string]string{"v1": "111", "v2": "", "v3": "222"}, 407 host: "", 408 path: "/111/222", 409 pathTemplate: `/{v1:[0-9]*}{v2:[a-z]*}/{v3:[0-9]*}`, 410 pathRegexp: `^/(?P<v0>[0-9]*)(?P<v1>[a-z]*)/(?P<v2>[0-9]*)$`, 411 shouldMatch: true, 412 }, 413 { 414 title: "Path route with single pattern with pipe, match", 415 route: new(Route).Path("/{category:a|b/c}"), 416 request: newRequest("GET", "http://localhost/a"), 417 vars: map[string]string{"category": "a"}, 418 host: "", 419 path: "/a", 420 pathTemplate: `/{category:a|b/c}`, 421 shouldMatch: true, 422 }, 423 { 424 title: "Path route with single pattern with pipe, match", 425 route: new(Route).Path("/{category:a|b/c}"), 426 request: newRequest("GET", "http://localhost/b/c"), 427 vars: map[string]string{"category": "b/c"}, 428 host: "", 429 path: "/b/c", 430 pathTemplate: `/{category:a|b/c}`, 431 shouldMatch: true, 432 }, 433 { 434 title: "Path route with multiple patterns with pipe, match", 435 route: new(Route).Path("/{category:a|b/c}/{product}/{id:[0-9]+}"), 436 request: newRequest("GET", "http://localhost/a/product_name/1"), 437 vars: map[string]string{"category": "a", "product": "product_name", "id": "1"}, 438 host: "", 439 path: "/a/product_name/1", 440 pathTemplate: `/{category:a|b/c}/{product}/{id:[0-9]+}`, 441 shouldMatch: true, 442 }, 443 { 444 title: "Path route with multiple patterns with pipe, match", 445 route: new(Route).Path("/{category:a|b/c}/{product}/{id:[0-9]+}"), 446 request: newRequest("GET", "http://localhost/b/c/product_name/1"), 447 vars: map[string]string{"category": "b/c", "product": "product_name", "id": "1"}, 448 host: "", 449 path: "/b/c/product_name/1", 450 pathTemplate: `/{category:a|b/c}/{product}/{id:[0-9]+}`, 451 shouldMatch: true, 452 }, 453 } 454 455 for _, test := range tests { 456 t.Run(test.title, func(t *testing.T) { 457 testRoute(t, test) 458 testTemplate(t, test) 459 testUseEscapedRoute(t, test) 460 testRegexp(t, test) 461 }) 462 } 463 } 464 465 func TestPathPrefix(t *testing.T) { 466 tests := []routeTest{ 467 { 468 title: "PathPrefix route, match", 469 route: new(Route).PathPrefix("/111"), 470 request: newRequest("GET", "http://localhost/111/222/333"), 471 vars: map[string]string{}, 472 host: "", 473 path: "/111", 474 shouldMatch: true, 475 }, 476 { 477 title: "PathPrefix route, match substring", 478 route: new(Route).PathPrefix("/1"), 479 request: newRequest("GET", "http://localhost/111/222/333"), 480 vars: map[string]string{}, 481 host: "", 482 path: "/1", 483 shouldMatch: true, 484 }, 485 { 486 title: "PathPrefix route, URL prefix in request does not match", 487 route: new(Route).PathPrefix("/111"), 488 request: newRequest("GET", "http://localhost/1/2/3"), 489 vars: map[string]string{}, 490 host: "", 491 path: "/111", 492 shouldMatch: false, 493 }, 494 { 495 title: "PathPrefix route with pattern, match", 496 route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"), 497 request: newRequest("GET", "http://localhost/111/222/333"), 498 vars: map[string]string{"v1": "222"}, 499 host: "", 500 path: "/111/222", 501 pathTemplate: `/111/{v1:[0-9]{3}}`, 502 shouldMatch: true, 503 }, 504 { 505 title: "PathPrefix route with pattern, URL prefix in request does not match", 506 route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"), 507 request: newRequest("GET", "http://localhost/111/aaa/333"), 508 vars: map[string]string{"v1": "222"}, 509 host: "", 510 path: "/111/222", 511 pathTemplate: `/111/{v1:[0-9]{3}}`, 512 shouldMatch: false, 513 }, 514 { 515 title: "PathPrefix route with multiple patterns, match", 516 route: new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}"), 517 request: newRequest("GET", "http://localhost/111/222/333"), 518 vars: map[string]string{"v1": "111", "v2": "222"}, 519 host: "", 520 path: "/111/222", 521 pathTemplate: `/{v1:[0-9]{3}}/{v2:[0-9]{3}}`, 522 shouldMatch: true, 523 }, 524 { 525 title: "PathPrefix route with multiple patterns, URL prefix in request does not match", 526 route: new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}"), 527 request: newRequest("GET", "http://localhost/111/aaa/333"), 528 vars: map[string]string{"v1": "111", "v2": "222"}, 529 host: "", 530 path: "/111/222", 531 pathTemplate: `/{v1:[0-9]{3}}/{v2:[0-9]{3}}`, 532 shouldMatch: false, 533 }, 534 } 535 536 for _, test := range tests { 537 t.Run(test.title, func(t *testing.T) { 538 testRoute(t, test) 539 testTemplate(t, test) 540 testUseEscapedRoute(t, test) 541 }) 542 } 543 } 544 545 func TestSchemeHostPath(t *testing.T) { 546 tests := []routeTest{ 547 { 548 title: "Host and Path route, match", 549 route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"), 550 request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), 551 vars: map[string]string{}, 552 scheme: "http", 553 host: "aaa.bbb.ccc", 554 path: "/111/222/333", 555 pathTemplate: `/111/222/333`, 556 hostTemplate: `aaa.bbb.ccc`, 557 shouldMatch: true, 558 }, 559 { 560 title: "Scheme, Host, and Path route, match", 561 route: new(Route).Schemes("https").Host("aaa.bbb.ccc").Path("/111/222/333"), 562 request: newRequest("GET", "https://aaa.bbb.ccc/111/222/333"), 563 vars: map[string]string{}, 564 scheme: "https", 565 host: "aaa.bbb.ccc", 566 path: "/111/222/333", 567 pathTemplate: `/111/222/333`, 568 hostTemplate: `aaa.bbb.ccc`, 569 shouldMatch: true, 570 }, 571 { 572 title: "Host and Path route, wrong host in request URL", 573 route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"), 574 request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), 575 vars: map[string]string{}, 576 scheme: "http", 577 host: "aaa.bbb.ccc", 578 path: "/111/222/333", 579 pathTemplate: `/111/222/333`, 580 hostTemplate: `aaa.bbb.ccc`, 581 shouldMatch: false, 582 }, 583 { 584 title: "Host and Path route with pattern, match", 585 route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), 586 request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), 587 vars: map[string]string{"v1": "bbb", "v2": "222"}, 588 scheme: "http", 589 host: "aaa.bbb.ccc", 590 path: "/111/222/333", 591 pathTemplate: `/111/{v2:[0-9]{3}}/333`, 592 hostTemplate: `aaa.{v1:[a-z]{3}}.ccc`, 593 shouldMatch: true, 594 }, 595 { 596 title: "Scheme, Host, and Path route with host and path patterns, match", 597 route: new(Route).Schemes("ftp", "ssss").Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), 598 request: newRequest("GET", "ssss://aaa.bbb.ccc/111/222/333"), 599 vars: map[string]string{"v1": "bbb", "v2": "222"}, 600 scheme: "ftp", 601 host: "aaa.bbb.ccc", 602 path: "/111/222/333", 603 pathTemplate: `/111/{v2:[0-9]{3}}/333`, 604 hostTemplate: `aaa.{v1:[a-z]{3}}.ccc`, 605 shouldMatch: true, 606 }, 607 { 608 title: "Host and Path route with pattern, URL in request does not match", 609 route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), 610 request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), 611 vars: map[string]string{"v1": "bbb", "v2": "222"}, 612 scheme: "http", 613 host: "aaa.bbb.ccc", 614 path: "/111/222/333", 615 pathTemplate: `/111/{v2:[0-9]{3}}/333`, 616 hostTemplate: `aaa.{v1:[a-z]{3}}.ccc`, 617 shouldMatch: false, 618 }, 619 { 620 title: "Host and Path route with multiple patterns, match", 621 route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}"), 622 request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), 623 vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"}, 624 scheme: "http", 625 host: "aaa.bbb.ccc", 626 path: "/111/222/333", 627 pathTemplate: `/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}`, 628 hostTemplate: `{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}`, 629 shouldMatch: true, 630 }, 631 { 632 title: "Host and Path route with multiple patterns, URL in request does not match", 633 route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}"), 634 request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), 635 vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"}, 636 scheme: "http", 637 host: "aaa.bbb.ccc", 638 path: "/111/222/333", 639 pathTemplate: `/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}`, 640 hostTemplate: `{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}`, 641 shouldMatch: false, 642 }, 643 } 644 645 for _, test := range tests { 646 t.Run(test.title, func(t *testing.T) { 647 testRoute(t, test) 648 testTemplate(t, test) 649 testUseEscapedRoute(t, test) 650 }) 651 } 652 } 653 654 func TestHeaders(t *testing.T) { 655 // newRequestHeaders creates a new request with a method, url, and headers 656 newRequestHeaders := func(method, url string, headers map[string]string) *http.Request { 657 req, err := http.NewRequest(method, url, nil) 658 if err != nil { 659 panic(err) 660 } 661 for k, v := range headers { 662 req.Header.Add(k, v) 663 } 664 return req 665 } 666 667 tests := []routeTest{ 668 { 669 title: "Headers route, match", 670 route: new(Route).Headers("foo", "bar", "baz", "ding"), 671 request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "bar", "baz": "ding"}), 672 vars: map[string]string{}, 673 host: "", 674 path: "", 675 shouldMatch: true, 676 }, 677 { 678 title: "Headers route, bad header values", 679 route: new(Route).Headers("foo", "bar", "baz", "ding"), 680 request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "bar", "baz": "dong"}), 681 vars: map[string]string{}, 682 host: "", 683 path: "", 684 shouldMatch: false, 685 }, 686 { 687 title: "Headers route, regex header values to match", 688 route: new(Route).HeadersRegexp("foo", "ba[zr]"), 689 request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "baw"}), 690 vars: map[string]string{}, 691 host: "", 692 path: "", 693 shouldMatch: false, 694 }, 695 { 696 title: "Headers route, regex header values to match", 697 route: new(Route).HeadersRegexp("foo", "ba[zr]"), 698 request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "baz"}), 699 vars: map[string]string{}, 700 host: "", 701 path: "", 702 shouldMatch: true, 703 }, 704 } 705 706 for _, test := range tests { 707 t.Run(test.title, func(t *testing.T) { 708 testRoute(t, test) 709 testTemplate(t, test) 710 }) 711 } 712 } 713 714 func TestMethods(t *testing.T) { 715 tests := []routeTest{ 716 { 717 title: "Methods route, match GET", 718 route: new(Route).Methods("GET", "POST"), 719 request: newRequest("GET", "http://localhost"), 720 vars: map[string]string{}, 721 host: "", 722 path: "", 723 methods: []string{"GET", "POST"}, 724 shouldMatch: true, 725 }, 726 { 727 title: "Methods route, match POST", 728 route: new(Route).Methods("GET", "POST"), 729 request: newRequest("POST", "http://localhost"), 730 vars: map[string]string{}, 731 host: "", 732 path: "", 733 methods: []string{"GET", "POST"}, 734 shouldMatch: true, 735 }, 736 { 737 title: "Methods route, bad method", 738 route: new(Route).Methods("GET", "POST"), 739 request: newRequest("PUT", "http://localhost"), 740 vars: map[string]string{}, 741 host: "", 742 path: "", 743 methods: []string{"GET", "POST"}, 744 shouldMatch: false, 745 }, 746 { 747 title: "Route without methods", 748 route: new(Route), 749 request: newRequest("PUT", "http://localhost"), 750 vars: map[string]string{}, 751 host: "", 752 path: "", 753 methods: []string{}, 754 shouldMatch: true, 755 }, 756 } 757 758 for _, test := range tests { 759 t.Run(test.title, func(t *testing.T) { 760 testRoute(t, test) 761 testTemplate(t, test) 762 testMethods(t, test) 763 }) 764 } 765 } 766 767 func TestQueries(t *testing.T) { 768 tests := []routeTest{ 769 { 770 title: "Queries route, match", 771 route: new(Route).Queries("foo", "bar", "baz", "ding"), 772 request: newRequest("GET", "http://localhost?foo=bar&baz=ding"), 773 vars: map[string]string{}, 774 host: "", 775 path: "", 776 query: "foo=bar&baz=ding", 777 queriesTemplate: "foo=bar,baz=ding", 778 queriesRegexp: "^foo=bar$,^baz=ding$", 779 shouldMatch: true, 780 }, 781 { 782 title: "Queries route, match with a query string", 783 route: new(Route).Host("www.example.com").Path("/api").Queries("foo", "bar", "baz", "ding"), 784 request: newRequest("GET", "http://www.example.com/api?foo=bar&baz=ding"), 785 vars: map[string]string{}, 786 host: "", 787 path: "", 788 query: "foo=bar&baz=ding", 789 pathTemplate: `/api`, 790 hostTemplate: `www.example.com`, 791 queriesTemplate: "foo=bar,baz=ding", 792 queriesRegexp: "^foo=bar$,^baz=ding$", 793 shouldMatch: true, 794 }, 795 { 796 title: "Queries route, match with a query string out of order", 797 route: new(Route).Host("www.example.com").Path("/api").Queries("foo", "bar", "baz", "ding"), 798 request: newRequest("GET", "http://www.example.com/api?baz=ding&foo=bar"), 799 vars: map[string]string{}, 800 host: "", 801 path: "", 802 query: "foo=bar&baz=ding", 803 pathTemplate: `/api`, 804 hostTemplate: `www.example.com`, 805 queriesTemplate: "foo=bar,baz=ding", 806 queriesRegexp: "^foo=bar$,^baz=ding$", 807 shouldMatch: true, 808 }, 809 { 810 title: "Queries route, bad query", 811 route: new(Route).Queries("foo", "bar", "baz", "ding"), 812 request: newRequest("GET", "http://localhost?foo=bar&baz=dong"), 813 vars: map[string]string{}, 814 host: "", 815 path: "", 816 queriesTemplate: "foo=bar,baz=ding", 817 queriesRegexp: "^foo=bar$,^baz=ding$", 818 shouldMatch: false, 819 }, 820 { 821 title: "Queries route with pattern, match", 822 route: new(Route).Queries("foo", "{v1}"), 823 request: newRequest("GET", "http://localhost?foo=bar"), 824 vars: map[string]string{"v1": "bar"}, 825 host: "", 826 path: "", 827 query: "foo=bar", 828 queriesTemplate: "foo={v1}", 829 queriesRegexp: "^foo=(?P<v0>.*)$", 830 shouldMatch: true, 831 }, 832 { 833 title: "Queries route with multiple patterns, match", 834 route: new(Route).Queries("foo", "{v1}", "baz", "{v2}"), 835 request: newRequest("GET", "http://localhost?foo=bar&baz=ding"), 836 vars: map[string]string{"v1": "bar", "v2": "ding"}, 837 host: "", 838 path: "", 839 query: "foo=bar&baz=ding", 840 queriesTemplate: "foo={v1},baz={v2}", 841 queriesRegexp: "^foo=(?P<v0>.*)$,^baz=(?P<v0>.*)$", 842 shouldMatch: true, 843 }, 844 { 845 title: "Queries route with regexp pattern, match", 846 route: new(Route).Queries("foo", "{v1:[0-9]+}"), 847 request: newRequest("GET", "http://localhost?foo=10"), 848 vars: map[string]string{"v1": "10"}, 849 host: "", 850 path: "", 851 query: "foo=10", 852 queriesTemplate: "foo={v1:[0-9]+}", 853 queriesRegexp: "^foo=(?P<v0>[0-9]+)$", 854 shouldMatch: true, 855 }, 856 { 857 title: "Queries route with regexp pattern, regexp does not match", 858 route: new(Route).Queries("foo", "{v1:[0-9]+}"), 859 request: newRequest("GET", "http://localhost?foo=a"), 860 vars: map[string]string{}, 861 host: "", 862 path: "", 863 queriesTemplate: "foo={v1:[0-9]+}", 864 queriesRegexp: "^foo=(?P<v0>[0-9]+)$", 865 shouldMatch: false, 866 }, 867 { 868 title: "Queries route with regexp pattern with quantifier, match", 869 route: new(Route).Queries("foo", "{v1:[0-9]{1}}"), 870 request: newRequest("GET", "http://localhost?foo=1"), 871 vars: map[string]string{"v1": "1"}, 872 host: "", 873 path: "", 874 query: "foo=1", 875 queriesTemplate: "foo={v1:[0-9]{1}}", 876 queriesRegexp: "^foo=(?P<v0>[0-9]{1})$", 877 shouldMatch: true, 878 }, 879 { 880 title: "Queries route with regexp pattern with quantifier, additional variable in query string, match", 881 route: new(Route).Queries("foo", "{v1:[0-9]{1}}"), 882 request: newRequest("GET", "http://localhost?bar=2&foo=1"), 883 vars: map[string]string{"v1": "1"}, 884 host: "", 885 path: "", 886 query: "foo=1", 887 queriesTemplate: "foo={v1:[0-9]{1}}", 888 queriesRegexp: "^foo=(?P<v0>[0-9]{1})$", 889 shouldMatch: true, 890 }, 891 { 892 title: "Queries route with regexp pattern with quantifier, regexp does not match", 893 route: new(Route).Queries("foo", "{v1:[0-9]{1}}"), 894 request: newRequest("GET", "http://localhost?foo=12"), 895 vars: map[string]string{}, 896 host: "", 897 path: "", 898 queriesTemplate: "foo={v1:[0-9]{1}}", 899 queriesRegexp: "^foo=(?P<v0>[0-9]{1})$", 900 shouldMatch: false, 901 }, 902 { 903 title: "Queries route with regexp pattern with quantifier, additional capturing group", 904 route: new(Route).Queries("foo", "{v1:[0-9]{1}(?:a|b)}"), 905 request: newRequest("GET", "http://localhost?foo=1a"), 906 vars: map[string]string{"v1": "1a"}, 907 host: "", 908 path: "", 909 query: "foo=1a", 910 queriesTemplate: "foo={v1:[0-9]{1}(?:a|b)}", 911 queriesRegexp: "^foo=(?P<v0>[0-9]{1}(?:a|b))$", 912 shouldMatch: true, 913 }, 914 { 915 title: "Queries route with regexp pattern with quantifier, additional variable in query string, regexp does not match", 916 route: new(Route).Queries("foo", "{v1:[0-9]{1}}"), 917 request: newRequest("GET", "http://localhost?foo=12"), 918 vars: map[string]string{}, 919 host: "", 920 path: "", 921 queriesTemplate: "foo={v1:[0-9]{1}}", 922 queriesRegexp: "^foo=(?P<v0>[0-9]{1})$", 923 shouldMatch: false, 924 }, 925 { 926 title: "Queries route with hyphenated name, match", 927 route: new(Route).Queries("foo", "{v-1}"), 928 request: newRequest("GET", "http://localhost?foo=bar"), 929 vars: map[string]string{"v-1": "bar"}, 930 host: "", 931 path: "", 932 query: "foo=bar", 933 queriesTemplate: "foo={v-1}", 934 queriesRegexp: "^foo=(?P<v0>.*)$", 935 shouldMatch: true, 936 }, 937 { 938 title: "Queries route with multiple hyphenated names, match", 939 route: new(Route).Queries("foo", "{v-1}", "baz", "{v-2}"), 940 request: newRequest("GET", "http://localhost?foo=bar&baz=ding"), 941 vars: map[string]string{"v-1": "bar", "v-2": "ding"}, 942 host: "", 943 path: "", 944 query: "foo=bar&baz=ding", 945 queriesTemplate: "foo={v-1},baz={v-2}", 946 queriesRegexp: "^foo=(?P<v0>.*)$,^baz=(?P<v0>.*)$", 947 shouldMatch: true, 948 }, 949 { 950 title: "Queries route with hyphenate name and pattern, match", 951 route: new(Route).Queries("foo", "{v-1:[0-9]+}"), 952 request: newRequest("GET", "http://localhost?foo=10"), 953 vars: map[string]string{"v-1": "10"}, 954 host: "", 955 path: "", 956 query: "foo=10", 957 queriesTemplate: "foo={v-1:[0-9]+}", 958 queriesRegexp: "^foo=(?P<v0>[0-9]+)$", 959 shouldMatch: true, 960 }, 961 { 962 title: "Queries route with hyphenated name and pattern with quantifier, additional capturing group", 963 route: new(Route).Queries("foo", "{v-1:[0-9]{1}(?:a|b)}"), 964 request: newRequest("GET", "http://localhost?foo=1a"), 965 vars: map[string]string{"v-1": "1a"}, 966 host: "", 967 path: "", 968 query: "foo=1a", 969 queriesTemplate: "foo={v-1:[0-9]{1}(?:a|b)}", 970 queriesRegexp: "^foo=(?P<v0>[0-9]{1}(?:a|b))$", 971 shouldMatch: true, 972 }, 973 { 974 title: "Queries route with empty value, should match", 975 route: new(Route).Queries("foo", ""), 976 request: newRequest("GET", "http://localhost?foo=bar"), 977 vars: map[string]string{}, 978 host: "", 979 path: "", 980 query: "foo=", 981 queriesTemplate: "foo=", 982 queriesRegexp: "^foo=.*$", 983 shouldMatch: true, 984 }, 985 { 986 title: "Queries route with empty value and no parameter in request, should not match", 987 route: new(Route).Queries("foo", ""), 988 request: newRequest("GET", "http://localhost"), 989 vars: map[string]string{}, 990 host: "", 991 path: "", 992 queriesTemplate: "foo=", 993 queriesRegexp: "^foo=.*$", 994 shouldMatch: false, 995 }, 996 { 997 title: "Queries route with empty value and empty parameter in request, should match", 998 route: new(Route).Queries("foo", ""), 999 request: newRequest("GET", "http://localhost?foo="), 1000 vars: map[string]string{}, 1001 host: "", 1002 path: "", 1003 query: "foo=", 1004 queriesTemplate: "foo=", 1005 queriesRegexp: "^foo=.*$", 1006 shouldMatch: true, 1007 }, 1008 { 1009 title: "Queries route with overlapping value, should not match", 1010 route: new(Route).Queries("foo", "bar"), 1011 request: newRequest("GET", "http://localhost?foo=barfoo"), 1012 vars: map[string]string{}, 1013 host: "", 1014 path: "", 1015 queriesTemplate: "foo=bar", 1016 queriesRegexp: "^foo=bar$", 1017 shouldMatch: false, 1018 }, 1019 { 1020 title: "Queries route with no parameter in request, should not match", 1021 route: new(Route).Queries("foo", "{bar}"), 1022 request: newRequest("GET", "http://localhost"), 1023 vars: map[string]string{}, 1024 host: "", 1025 path: "", 1026 queriesTemplate: "foo={bar}", 1027 queriesRegexp: "^foo=(?P<v0>.*)$", 1028 shouldMatch: false, 1029 }, 1030 { 1031 title: "Queries route with empty parameter in request, should match", 1032 route: new(Route).Queries("foo", "{bar}"), 1033 request: newRequest("GET", "http://localhost?foo="), 1034 vars: map[string]string{"foo": ""}, 1035 host: "", 1036 path: "", 1037 query: "foo=", 1038 queriesTemplate: "foo={bar}", 1039 queriesRegexp: "^foo=(?P<v0>.*)$", 1040 shouldMatch: true, 1041 }, 1042 { 1043 title: "Queries route, bad submatch", 1044 route: new(Route).Queries("foo", "bar", "baz", "ding"), 1045 request: newRequest("GET", "http://localhost?fffoo=bar&baz=dingggg"), 1046 vars: map[string]string{}, 1047 host: "", 1048 path: "", 1049 queriesTemplate: "foo=bar,baz=ding", 1050 queriesRegexp: "^foo=bar$,^baz=ding$", 1051 shouldMatch: false, 1052 }, 1053 { 1054 title: "Queries route with pattern, match, escaped value", 1055 route: new(Route).Queries("foo", "{v1}"), 1056 request: newRequest("GET", "http://localhost?foo=%25bar%26%20%2F%3D%3F"), 1057 vars: map[string]string{"v1": "%bar& /=?"}, 1058 host: "", 1059 path: "", 1060 query: "foo=%25bar%26+%2F%3D%3F", 1061 queriesTemplate: "foo={v1}", 1062 queriesRegexp: "^foo=(?P<v0>.*)$", 1063 shouldMatch: true, 1064 }, 1065 } 1066 1067 for _, test := range tests { 1068 t.Run(test.title, func(t *testing.T) { 1069 testTemplate(t, test) 1070 testQueriesTemplates(t, test) 1071 testUseEscapedRoute(t, test) 1072 testQueriesRegexp(t, test) 1073 }) 1074 } 1075 } 1076 1077 func TestSchemes(t *testing.T) { 1078 tests := []routeTest{ 1079 // Schemes 1080 { 1081 title: "Schemes route, default scheme, match http, build http", 1082 route: new(Route).Host("localhost"), 1083 request: newRequest("GET", "http://localhost"), 1084 scheme: "http", 1085 host: "localhost", 1086 shouldMatch: true, 1087 }, 1088 { 1089 title: "Schemes route, match https, build https", 1090 route: new(Route).Schemes("https", "ftp").Host("localhost"), 1091 request: newRequest("GET", "https://localhost"), 1092 scheme: "https", 1093 host: "localhost", 1094 shouldMatch: true, 1095 }, 1096 { 1097 title: "Schemes route, match ftp, build https", 1098 route: new(Route).Schemes("https", "ftp").Host("localhost"), 1099 request: newRequest("GET", "ftp://localhost"), 1100 scheme: "https", 1101 host: "localhost", 1102 shouldMatch: true, 1103 }, 1104 { 1105 title: "Schemes route, match ftp, build ftp", 1106 route: new(Route).Schemes("ftp", "https").Host("localhost"), 1107 request: newRequest("GET", "ftp://localhost"), 1108 scheme: "ftp", 1109 host: "localhost", 1110 shouldMatch: true, 1111 }, 1112 { 1113 title: "Schemes route, bad scheme", 1114 route: new(Route).Schemes("https", "ftp").Host("localhost"), 1115 request: newRequest("GET", "http://localhost"), 1116 scheme: "https", 1117 host: "localhost", 1118 shouldMatch: false, 1119 }, 1120 } 1121 for _, test := range tests { 1122 t.Run(test.title, func(t *testing.T) { 1123 testRoute(t, test) 1124 testTemplate(t, test) 1125 }) 1126 } 1127 } 1128 1129 func TestMatcherFunc(t *testing.T) { 1130 m := func(r *http.Request, m *RouteMatch) bool { 1131 return r.URL.Host == "aaa.bbb.ccc" 1132 } 1133 1134 tests := []routeTest{ 1135 { 1136 title: "MatchFunc route, match", 1137 route: new(Route).MatcherFunc(m), 1138 request: newRequest("GET", "http://aaa.bbb.ccc"), 1139 vars: map[string]string{}, 1140 host: "", 1141 path: "", 1142 shouldMatch: true, 1143 }, 1144 { 1145 title: "MatchFunc route, non-match", 1146 route: new(Route).MatcherFunc(m), 1147 request: newRequest("GET", "http://aaa.222.ccc"), 1148 vars: map[string]string{}, 1149 host: "", 1150 path: "", 1151 shouldMatch: false, 1152 }, 1153 } 1154 1155 for _, test := range tests { 1156 t.Run(test.title, func(t *testing.T) { 1157 testRoute(t, test) 1158 testTemplate(t, test) 1159 }) 1160 } 1161 } 1162 1163 func TestBuildVarsFunc(t *testing.T) { 1164 tests := []routeTest{ 1165 { 1166 title: "BuildVarsFunc set on route", 1167 route: new(Route).Path(`/111/{v1:\d}{v2:.*}`).BuildVarsFunc(func(vars map[string]string) map[string]string { 1168 vars["v1"] = "3" 1169 vars["v2"] = "a" 1170 return vars 1171 }), 1172 request: newRequest("GET", "http://localhost/111/2"), 1173 path: "/111/3a", 1174 pathTemplate: `/111/{v1:\d}{v2:.*}`, 1175 shouldMatch: true, 1176 }, 1177 { 1178 title: "BuildVarsFunc set on route and parent route", 1179 route: new(Route).PathPrefix(`/{v1:\d}`).BuildVarsFunc(func(vars map[string]string) map[string]string { 1180 vars["v1"] = "2" 1181 return vars 1182 }).Subrouter().Path(`/{v2:\w}`).BuildVarsFunc(func(vars map[string]string) map[string]string { 1183 vars["v2"] = "b" 1184 return vars 1185 }), 1186 request: newRequest("GET", "http://localhost/1/a"), 1187 path: "/2/b", 1188 pathTemplate: `/{v1:\d}/{v2:\w}`, 1189 shouldMatch: true, 1190 }, 1191 } 1192 1193 for _, test := range tests { 1194 t.Run(test.title, func(t *testing.T) { 1195 testRoute(t, test) 1196 testTemplate(t, test) 1197 }) 1198 } 1199 } 1200 1201 func TestSubRouter(t *testing.T) { 1202 subrouter1 := new(Route).Host("{v1:[a-z]+}.google.com").Subrouter() 1203 subrouter2 := new(Route).PathPrefix("/foo/{v1}").Subrouter() 1204 subrouter3 := new(Route).PathPrefix("/foo").Subrouter() 1205 subrouter4 := new(Route).PathPrefix("/foo/bar").Subrouter() 1206 subrouter5 := new(Route).PathPrefix("/{category}").Subrouter() 1207 tests := []routeTest{ 1208 { 1209 route: subrouter1.Path("/{v2:[a-z]+}"), 1210 request: newRequest("GET", "http://aaa.google.com/bbb"), 1211 vars: map[string]string{"v1": "aaa", "v2": "bbb"}, 1212 host: "aaa.google.com", 1213 path: "/bbb", 1214 pathTemplate: `/{v2:[a-z]+}`, 1215 hostTemplate: `{v1:[a-z]+}.google.com`, 1216 shouldMatch: true, 1217 }, 1218 { 1219 route: subrouter1.Path("/{v2:[a-z]+}"), 1220 request: newRequest("GET", "http://111.google.com/111"), 1221 vars: map[string]string{"v1": "aaa", "v2": "bbb"}, 1222 host: "aaa.google.com", 1223 path: "/bbb", 1224 pathTemplate: `/{v2:[a-z]+}`, 1225 hostTemplate: `{v1:[a-z]+}.google.com`, 1226 shouldMatch: false, 1227 }, 1228 { 1229 route: subrouter2.Path("/baz/{v2}"), 1230 request: newRequest("GET", "http://localhost/foo/bar/baz/ding"), 1231 vars: map[string]string{"v1": "bar", "v2": "ding"}, 1232 host: "", 1233 path: "/foo/bar/baz/ding", 1234 pathTemplate: `/foo/{v1}/baz/{v2}`, 1235 shouldMatch: true, 1236 }, 1237 { 1238 route: subrouter2.Path("/baz/{v2}"), 1239 request: newRequest("GET", "http://localhost/foo/bar"), 1240 vars: map[string]string{"v1": "bar", "v2": "ding"}, 1241 host: "", 1242 path: "/foo/bar/baz/ding", 1243 pathTemplate: `/foo/{v1}/baz/{v2}`, 1244 shouldMatch: false, 1245 }, 1246 { 1247 route: subrouter3.Path("/"), 1248 request: newRequest("GET", "http://localhost/foo/"), 1249 vars: map[string]string{}, 1250 host: "", 1251 path: "/foo/", 1252 pathTemplate: `/foo/`, 1253 shouldMatch: true, 1254 }, 1255 { 1256 route: subrouter3.Path(""), 1257 request: newRequest("GET", "http://localhost/foo"), 1258 vars: map[string]string{}, 1259 host: "", 1260 path: "/foo", 1261 pathTemplate: `/foo`, 1262 shouldMatch: true, 1263 }, 1264 1265 { 1266 route: subrouter4.Path("/"), 1267 request: newRequest("GET", "http://localhost/foo/bar/"), 1268 vars: map[string]string{}, 1269 host: "", 1270 path: "/foo/bar/", 1271 pathTemplate: `/foo/bar/`, 1272 shouldMatch: true, 1273 }, 1274 { 1275 route: subrouter4.Path(""), 1276 request: newRequest("GET", "http://localhost/foo/bar"), 1277 vars: map[string]string{}, 1278 host: "", 1279 path: "/foo/bar", 1280 pathTemplate: `/foo/bar`, 1281 shouldMatch: true, 1282 }, 1283 { 1284 route: subrouter5.Path("/"), 1285 request: newRequest("GET", "http://localhost/baz/"), 1286 vars: map[string]string{"category": "baz"}, 1287 host: "", 1288 path: "/baz/", 1289 pathTemplate: `/{category}/`, 1290 shouldMatch: true, 1291 }, 1292 { 1293 route: subrouter5.Path(""), 1294 request: newRequest("GET", "http://localhost/baz"), 1295 vars: map[string]string{"category": "baz"}, 1296 host: "", 1297 path: "/baz", 1298 pathTemplate: `/{category}`, 1299 shouldMatch: true, 1300 }, 1301 { 1302 title: "Mismatch method specified on parent route", 1303 route: new(Route).Methods("POST").PathPrefix("/foo").Subrouter().Path("/"), 1304 request: newRequest("GET", "http://localhost/foo/"), 1305 vars: map[string]string{}, 1306 host: "", 1307 path: "/foo/", 1308 pathTemplate: `/foo/`, 1309 shouldMatch: false, 1310 }, 1311 { 1312 title: "Match method specified on parent route", 1313 route: new(Route).Methods("POST").PathPrefix("/foo").Subrouter().Path("/"), 1314 request: newRequest("POST", "http://localhost/foo/"), 1315 vars: map[string]string{}, 1316 host: "", 1317 path: "/foo/", 1318 pathTemplate: `/foo/`, 1319 shouldMatch: true, 1320 }, 1321 { 1322 title: "Mismatch scheme specified on parent route", 1323 route: new(Route).Schemes("https").Subrouter().PathPrefix("/"), 1324 request: newRequest("GET", "http://localhost/"), 1325 vars: map[string]string{}, 1326 host: "", 1327 path: "/", 1328 pathTemplate: `/`, 1329 shouldMatch: false, 1330 }, 1331 { 1332 title: "Match scheme specified on parent route", 1333 route: new(Route).Schemes("http").Subrouter().PathPrefix("/"), 1334 request: newRequest("GET", "http://localhost/"), 1335 vars: map[string]string{}, 1336 host: "", 1337 path: "/", 1338 pathTemplate: `/`, 1339 shouldMatch: true, 1340 }, 1341 { 1342 title: "No match header specified on parent route", 1343 route: new(Route).Headers("X-Forwarded-Proto", "https").Subrouter().PathPrefix("/"), 1344 request: newRequest("GET", "http://localhost/"), 1345 vars: map[string]string{}, 1346 host: "", 1347 path: "/", 1348 pathTemplate: `/`, 1349 shouldMatch: false, 1350 }, 1351 { 1352 title: "Header mismatch value specified on parent route", 1353 route: new(Route).Headers("X-Forwarded-Proto", "https").Subrouter().PathPrefix("/"), 1354 request: newRequestWithHeaders("GET", "http://localhost/", "X-Forwarded-Proto", "http"), 1355 vars: map[string]string{}, 1356 host: "", 1357 path: "/", 1358 pathTemplate: `/`, 1359 shouldMatch: false, 1360 }, 1361 { 1362 title: "Header match value specified on parent route", 1363 route: new(Route).Headers("X-Forwarded-Proto", "https").Subrouter().PathPrefix("/"), 1364 request: newRequestWithHeaders("GET", "http://localhost/", "X-Forwarded-Proto", "https"), 1365 vars: map[string]string{}, 1366 host: "", 1367 path: "/", 1368 pathTemplate: `/`, 1369 shouldMatch: true, 1370 }, 1371 { 1372 title: "Query specified on parent route not present", 1373 route: new(Route).Headers("key", "foobar").Subrouter().PathPrefix("/"), 1374 request: newRequest("GET", "http://localhost/"), 1375 vars: map[string]string{}, 1376 host: "", 1377 path: "/", 1378 pathTemplate: `/`, 1379 shouldMatch: false, 1380 }, 1381 { 1382 title: "Query mismatch value specified on parent route", 1383 route: new(Route).Queries("key", "foobar").Subrouter().PathPrefix("/"), 1384 request: newRequest("GET", "http://localhost/?key=notfoobar"), 1385 vars: map[string]string{}, 1386 host: "", 1387 path: "/", 1388 pathTemplate: `/`, 1389 shouldMatch: false, 1390 }, 1391 { 1392 title: "Query match value specified on subroute", 1393 route: new(Route).Queries("key", "foobar").Subrouter().PathPrefix("/"), 1394 request: newRequest("GET", "http://localhost/?key=foobar"), 1395 vars: map[string]string{}, 1396 host: "", 1397 path: "/", 1398 pathTemplate: `/`, 1399 shouldMatch: true, 1400 }, 1401 { 1402 title: "Build with scheme on parent router", 1403 route: new(Route).Schemes("ftp").Host("google.com").Subrouter().Path("/"), 1404 request: newRequest("GET", "ftp://google.com/"), 1405 scheme: "ftp", 1406 host: "google.com", 1407 path: "/", 1408 pathTemplate: `/`, 1409 hostTemplate: `google.com`, 1410 shouldMatch: true, 1411 }, 1412 { 1413 title: "Prefer scheme on child route when building URLs", 1414 route: new(Route).Schemes("https", "ftp").Host("google.com").Subrouter().Schemes("ftp").Path("/"), 1415 request: newRequest("GET", "ftp://google.com/"), 1416 scheme: "ftp", 1417 host: "google.com", 1418 path: "/", 1419 pathTemplate: `/`, 1420 hostTemplate: `google.com`, 1421 shouldMatch: true, 1422 }, 1423 } 1424 1425 for _, test := range tests { 1426 t.Run(test.title, func(t *testing.T) { 1427 testRoute(t, test) 1428 testTemplate(t, test) 1429 testUseEscapedRoute(t, test) 1430 }) 1431 } 1432 } 1433 1434 func TestNamedRoutes(t *testing.T) { 1435 r1 := NewRouter() 1436 r1.NewRoute().Name("a") 1437 r1.NewRoute().Name("b") 1438 r1.NewRoute().Name("c") 1439 1440 r2 := r1.NewRoute().Subrouter() 1441 r2.NewRoute().Name("d") 1442 r2.NewRoute().Name("e") 1443 r2.NewRoute().Name("f") 1444 1445 r3 := r2.NewRoute().Subrouter() 1446 r3.NewRoute().Name("g") 1447 r3.NewRoute().Name("h") 1448 r3.NewRoute().Name("i") 1449 r3.Name("j") 1450 1451 if r1.namedRoutes == nil || len(r1.namedRoutes) != 10 { 1452 t.Errorf("Expected 10 named routes, got %v", r1.namedRoutes) 1453 } else if r1.Get("j") == nil { 1454 t.Errorf("Subroute name not registered") 1455 } 1456 } 1457 1458 func TestNameMultipleCalls(t *testing.T) { 1459 r1 := NewRouter() 1460 rt := r1.NewRoute().Name("foo").Name("bar") 1461 err := rt.GetError() 1462 if err == nil { 1463 t.Errorf("Expected an error") 1464 } 1465 } 1466 1467 func TestStrictSlash(t *testing.T) { 1468 r := NewRouter() 1469 r.StrictSlash(true) 1470 1471 tests := []routeTest{ 1472 { 1473 title: "Redirect path without slash", 1474 route: r.NewRoute().Path("/111/"), 1475 request: newRequest("GET", "http://localhost/111"), 1476 vars: map[string]string{}, 1477 host: "", 1478 path: "/111/", 1479 shouldMatch: true, 1480 shouldRedirect: true, 1481 }, 1482 { 1483 title: "Do not redirect path with slash", 1484 route: r.NewRoute().Path("/111/"), 1485 request: newRequest("GET", "http://localhost/111/"), 1486 vars: map[string]string{}, 1487 host: "", 1488 path: "/111/", 1489 shouldMatch: true, 1490 shouldRedirect: false, 1491 }, 1492 { 1493 title: "Redirect path with slash", 1494 route: r.NewRoute().Path("/111"), 1495 request: newRequest("GET", "http://localhost/111/"), 1496 vars: map[string]string{}, 1497 host: "", 1498 path: "/111", 1499 shouldMatch: true, 1500 shouldRedirect: true, 1501 }, 1502 { 1503 title: "Do not redirect path without slash", 1504 route: r.NewRoute().Path("/111"), 1505 request: newRequest("GET", "http://localhost/111"), 1506 vars: map[string]string{}, 1507 host: "", 1508 path: "/111", 1509 shouldMatch: true, 1510 shouldRedirect: false, 1511 }, 1512 { 1513 title: "Propagate StrictSlash to subrouters", 1514 route: r.NewRoute().PathPrefix("/static/").Subrouter().Path("/images/"), 1515 request: newRequest("GET", "http://localhost/static/images"), 1516 vars: map[string]string{}, 1517 host: "", 1518 path: "/static/images/", 1519 shouldMatch: true, 1520 shouldRedirect: true, 1521 }, 1522 { 1523 title: "Ignore StrictSlash for path prefix", 1524 route: r.NewRoute().PathPrefix("/static/"), 1525 request: newRequest("GET", "http://localhost/static/logo.png"), 1526 vars: map[string]string{}, 1527 host: "", 1528 path: "/static/", 1529 shouldMatch: true, 1530 shouldRedirect: false, 1531 }, 1532 } 1533 1534 for _, test := range tests { 1535 t.Run(test.title, func(t *testing.T) { 1536 testRoute(t, test) 1537 testTemplate(t, test) 1538 testUseEscapedRoute(t, test) 1539 }) 1540 } 1541 } 1542 1543 func TestUseEncodedPath(t *testing.T) { 1544 r := NewRouter() 1545 r.UseEncodedPath() 1546 1547 tests := []routeTest{ 1548 { 1549 title: "Router with useEncodedPath, URL with encoded slash does match", 1550 route: r.NewRoute().Path("/v1/{v1}/v2"), 1551 request: newRequest("GET", "http://localhost/v1/1%2F2/v2"), 1552 vars: map[string]string{"v1": "1%2F2"}, 1553 host: "", 1554 path: "/v1/1%2F2/v2", 1555 pathTemplate: `/v1/{v1}/v2`, 1556 shouldMatch: true, 1557 }, 1558 { 1559 title: "Router with useEncodedPath, URL with encoded slash doesn't match", 1560 route: r.NewRoute().Path("/v1/1/2/v2"), 1561 request: newRequest("GET", "http://localhost/v1/1%2F2/v2"), 1562 vars: map[string]string{"v1": "1%2F2"}, 1563 host: "", 1564 path: "/v1/1%2F2/v2", 1565 pathTemplate: `/v1/1/2/v2`, 1566 shouldMatch: false, 1567 }, 1568 } 1569 1570 for _, test := range tests { 1571 t.Run(test.title, func(t *testing.T) { 1572 testRoute(t, test) 1573 testTemplate(t, test) 1574 }) 1575 } 1576 } 1577 1578 func TestWalkSingleDepth(t *testing.T) { 1579 r0 := NewRouter() 1580 r1 := NewRouter() 1581 r2 := NewRouter() 1582 1583 r0.Path("/g") 1584 r0.Path("/o") 1585 r0.Path("/d").Handler(r1) 1586 r0.Path("/r").Handler(r2) 1587 r0.Path("/a") 1588 1589 r1.Path("/z") 1590 r1.Path("/i") 1591 r1.Path("/l") 1592 r1.Path("/l") 1593 1594 r2.Path("/i") 1595 r2.Path("/l") 1596 r2.Path("/l") 1597 1598 paths := []string{"g", "o", "r", "i", "l", "l", "a"} 1599 depths := []int{0, 0, 0, 1, 1, 1, 0} 1600 i := 0 1601 err := r0.Walk(func(route *Route, router *Router, ancestors []*Route) error { 1602 matcher := route.matchers[0].(*routeRegexp) 1603 if matcher.template == "/d" { 1604 return SkipRouter 1605 } 1606 if len(ancestors) != depths[i] { 1607 t.Errorf(`Expected depth of %d at i = %d; got "%d"`, depths[i], i, len(ancestors)) 1608 } 1609 if matcher.template != "/"+paths[i] { 1610 t.Errorf(`Expected "/%s" at i = %d; got "%s"`, paths[i], i, matcher.template) 1611 } 1612 i++ 1613 return nil 1614 }) 1615 if err != nil { 1616 panic(err) 1617 } 1618 if i != len(paths) { 1619 t.Errorf("Expected %d routes, found %d", len(paths), i) 1620 } 1621 } 1622 1623 func TestWalkNested(t *testing.T) { 1624 router := NewRouter() 1625 1626 routeSubrouter := func(r *Route) (*Route, *Router) { 1627 return r, r.Subrouter() 1628 } 1629 1630 gRoute, g := routeSubrouter(router.Path("/g")) 1631 oRoute, o := routeSubrouter(g.PathPrefix("/o")) 1632 rRoute, r := routeSubrouter(o.PathPrefix("/r")) 1633 iRoute, i := routeSubrouter(r.PathPrefix("/i")) 1634 l1Route, l1 := routeSubrouter(i.PathPrefix("/l")) 1635 l2Route, l2 := routeSubrouter(l1.PathPrefix("/l")) 1636 l2.Path("/a") 1637 1638 testCases := []struct { 1639 path string 1640 ancestors []*Route 1641 }{ 1642 {"/g", []*Route{}}, 1643 {"/g/o", []*Route{gRoute}}, 1644 {"/g/o/r", []*Route{gRoute, oRoute}}, 1645 {"/g/o/r/i", []*Route{gRoute, oRoute, rRoute}}, 1646 {"/g/o/r/i/l", []*Route{gRoute, oRoute, rRoute, iRoute}}, 1647 {"/g/o/r/i/l/l", []*Route{gRoute, oRoute, rRoute, iRoute, l1Route}}, 1648 {"/g/o/r/i/l/l/a", []*Route{gRoute, oRoute, rRoute, iRoute, l1Route, l2Route}}, 1649 } 1650 1651 idx := 0 1652 err := router.Walk(func(route *Route, router *Router, ancestors []*Route) error { 1653 path := testCases[idx].path 1654 tpl := route.regexp.path.template 1655 if tpl != path { 1656 t.Errorf(`Expected %s got %s`, path, tpl) 1657 } 1658 currWantAncestors := testCases[idx].ancestors 1659 if !reflect.DeepEqual(currWantAncestors, ancestors) { 1660 t.Errorf(`Expected %+v got %+v`, currWantAncestors, ancestors) 1661 } 1662 idx++ 1663 return nil 1664 }) 1665 if err != nil { 1666 panic(err) 1667 } 1668 if idx != len(testCases) { 1669 t.Errorf("Expected %d routes, found %d", len(testCases), idx) 1670 } 1671 } 1672 1673 func TestWalkSubrouters(t *testing.T) { 1674 router := NewRouter() 1675 1676 g := router.Path("/g").Subrouter() 1677 o := g.PathPrefix("/o").Subrouter() 1678 o.Methods("GET") 1679 o.Methods("PUT") 1680 1681 // all 4 routes should be matched 1682 paths := []string{"/g", "/g/o", "/g/o", "/g/o"} 1683 idx := 0 1684 err := router.Walk(func(route *Route, router *Router, ancestors []*Route) error { 1685 path := paths[idx] 1686 tpl, _ := route.GetPathTemplate() 1687 if tpl != path { 1688 t.Errorf(`Expected %s got %s`, path, tpl) 1689 } 1690 idx++ 1691 return nil 1692 }) 1693 if err != nil { 1694 panic(err) 1695 } 1696 if idx != len(paths) { 1697 t.Errorf("Expected %d routes, found %d", len(paths), idx) 1698 } 1699 } 1700 1701 func TestWalkErrorRoute(t *testing.T) { 1702 router := NewRouter() 1703 router.Path("/g") 1704 expectedError := errors.New("error") 1705 err := router.Walk(func(route *Route, router *Router, ancestors []*Route) error { 1706 return expectedError 1707 }) 1708 if err != expectedError { 1709 t.Errorf("Expected %v routes, found %v", expectedError, err) 1710 } 1711 } 1712 1713 func TestWalkErrorMatcher(t *testing.T) { 1714 router := NewRouter() 1715 expectedError := router.Path("/g").Subrouter().Path("").GetError() 1716 err := router.Walk(func(route *Route, router *Router, ancestors []*Route) error { 1717 return route.GetError() 1718 }) 1719 if err != expectedError { 1720 t.Errorf("Expected %v routes, found %v", expectedError, err) 1721 } 1722 } 1723 1724 func TestWalkErrorHandler(t *testing.T) { 1725 handler := NewRouter() 1726 expectedError := handler.Path("/path").Subrouter().Path("").GetError() 1727 router := NewRouter() 1728 router.Path("/g").Handler(handler) 1729 err := router.Walk(func(route *Route, router *Router, ancestors []*Route) error { 1730 return route.GetError() 1731 }) 1732 if err != expectedError { 1733 t.Errorf("Expected %v routes, found %v", expectedError, err) 1734 } 1735 } 1736 1737 func TestSubrouterErrorHandling(t *testing.T) { 1738 superRouterCalled := false 1739 subRouterCalled := false 1740 1741 router := NewRouter() 1742 router.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1743 superRouterCalled = true 1744 }) 1745 subRouter := router.PathPrefix("/bign8").Subrouter() 1746 subRouter.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1747 subRouterCalled = true 1748 }) 1749 1750 req, _ := http.NewRequest("GET", "http://localhost/bign8/was/here", nil) 1751 router.ServeHTTP(NewRecorder(), req) 1752 1753 if superRouterCalled { 1754 t.Error("Super router 404 handler called when sub-router 404 handler is available.") 1755 } 1756 if !subRouterCalled { 1757 t.Error("Sub-router 404 handler was not called.") 1758 } 1759 } 1760 1761 // See: https://github.com/gorilla/mux/issues/200 1762 func TestPanicOnCapturingGroups(t *testing.T) { 1763 defer func() { 1764 if recover() == nil { 1765 t.Errorf("(Test that capturing groups now fail fast) Expected panic, however test completed successfully.\n") 1766 } 1767 }() 1768 NewRouter().NewRoute().Path("/{type:(promo|special)}/{promoId}.json") 1769 } 1770 1771 // ---------------------------------------------------------------------------- 1772 // Helpers 1773 // ---------------------------------------------------------------------------- 1774 1775 func getRouteTemplate(route *Route) string { 1776 host, err := route.GetHostTemplate() 1777 if err != nil { 1778 host = "none" 1779 } 1780 path, err := route.GetPathTemplate() 1781 if err != nil { 1782 path = "none" 1783 } 1784 return fmt.Sprintf("Host: %v, Path: %v", host, path) 1785 } 1786 1787 func testRoute(t *testing.T, test routeTest) { 1788 request := test.request 1789 route := test.route 1790 vars := test.vars 1791 shouldMatch := test.shouldMatch 1792 query := test.query 1793 shouldRedirect := test.shouldRedirect 1794 uri := url.URL{ 1795 Scheme: test.scheme, 1796 Host: test.host, 1797 Path: test.path, 1798 } 1799 if uri.Scheme == "" { 1800 uri.Scheme = "http" 1801 } 1802 1803 var match RouteMatch 1804 ok := route.Match(request, &match) 1805 if ok != shouldMatch { 1806 msg := "Should match" 1807 if !shouldMatch { 1808 msg = "Should not match" 1809 } 1810 t.Errorf("(%v) %v:\nRoute: %#v\nRequest: %#v\nVars: %v\n", test.title, msg, route, request, vars) 1811 return 1812 } 1813 if shouldMatch { 1814 if vars != nil && !stringMapEqual(vars, match.Vars) { 1815 t.Errorf("(%v) Vars not equal: expected %v, got %v", test.title, vars, match.Vars) 1816 return 1817 } 1818 if test.scheme != "" { 1819 u, err := route.URL(mapToPairs(match.Vars)...) 1820 if err != nil { 1821 t.Fatalf("(%v) URL error: %v -- %v", test.title, err, getRouteTemplate(route)) 1822 } 1823 if uri.Scheme != u.Scheme { 1824 t.Errorf("(%v) URLScheme not equal: expected %v, got %v", test.title, uri.Scheme, u.Scheme) 1825 return 1826 } 1827 } 1828 if test.host != "" { 1829 u, err := test.route.URLHost(mapToPairs(match.Vars)...) 1830 if err != nil { 1831 t.Fatalf("(%v) URLHost error: %v -- %v", test.title, err, getRouteTemplate(route)) 1832 } 1833 if uri.Scheme != u.Scheme { 1834 t.Errorf("(%v) URLHost scheme not equal: expected %v, got %v -- %v", test.title, uri.Scheme, u.Scheme, getRouteTemplate(route)) 1835 return 1836 } 1837 if uri.Host != u.Host { 1838 t.Errorf("(%v) URLHost host not equal: expected %v, got %v -- %v", test.title, uri.Host, u.Host, getRouteTemplate(route)) 1839 return 1840 } 1841 } 1842 if test.path != "" { 1843 u, err := route.URLPath(mapToPairs(match.Vars)...) 1844 if err != nil { 1845 t.Fatalf("(%v) URLPath error: %v -- %v", test.title, err, getRouteTemplate(route)) 1846 } 1847 if uri.Path != u.Path { 1848 t.Errorf("(%v) URLPath not equal: expected %v, got %v -- %v", test.title, uri.Path, u.Path, getRouteTemplate(route)) 1849 return 1850 } 1851 } 1852 if test.host != "" && test.path != "" { 1853 u, err := route.URL(mapToPairs(match.Vars)...) 1854 if err != nil { 1855 t.Fatalf("(%v) URL error: %v -- %v", test.title, err, getRouteTemplate(route)) 1856 } 1857 if expected, got := uri.String(), u.String(); expected != got { 1858 t.Errorf("(%v) URL not equal: expected %v, got %v -- %v", test.title, expected, got, getRouteTemplate(route)) 1859 return 1860 } 1861 } 1862 if query != "" { 1863 u, err := route.URL(mapToPairs(match.Vars)...) 1864 if err != nil { 1865 t.Errorf("(%v) erred while creating url: %v", test.title, err) 1866 return 1867 } 1868 if query != u.RawQuery { 1869 t.Errorf("(%v) URL query not equal: expected %v, got %v", test.title, query, u.RawQuery) 1870 return 1871 } 1872 } 1873 if shouldRedirect && match.Handler == nil { 1874 t.Errorf("(%v) Did not redirect", test.title) 1875 return 1876 } 1877 if !shouldRedirect && match.Handler != nil { 1878 t.Errorf("(%v) Unexpected redirect", test.title) 1879 return 1880 } 1881 } 1882 } 1883 1884 func testUseEscapedRoute(t *testing.T, test routeTest) { 1885 test.route.useEncodedPath = true 1886 testRoute(t, test) 1887 } 1888 1889 func testTemplate(t *testing.T, test routeTest) { 1890 route := test.route 1891 pathTemplate := test.pathTemplate 1892 if len(pathTemplate) == 0 { 1893 pathTemplate = test.path 1894 } 1895 hostTemplate := test.hostTemplate 1896 if len(hostTemplate) == 0 { 1897 hostTemplate = test.host 1898 } 1899 1900 routePathTemplate, pathErr := route.GetPathTemplate() 1901 if pathErr == nil && routePathTemplate != pathTemplate { 1902 t.Errorf("(%v) GetPathTemplate not equal: expected %v, got %v", test.title, pathTemplate, routePathTemplate) 1903 } 1904 1905 routeHostTemplate, hostErr := route.GetHostTemplate() 1906 if hostErr == nil && routeHostTemplate != hostTemplate { 1907 t.Errorf("(%v) GetHostTemplate not equal: expected %v, got %v", test.title, hostTemplate, routeHostTemplate) 1908 } 1909 } 1910 1911 func testMethods(t *testing.T, test routeTest) { 1912 route := test.route 1913 methods, _ := route.GetMethods() 1914 if strings.Join(methods, ",") != strings.Join(test.methods, ",") { 1915 t.Errorf("(%v) GetMethods not equal: expected %v, got %v", test.title, test.methods, methods) 1916 } 1917 } 1918 1919 func testRegexp(t *testing.T, test routeTest) { 1920 route := test.route 1921 routePathRegexp, regexpErr := route.GetPathRegexp() 1922 if test.pathRegexp != "" && regexpErr == nil && routePathRegexp != test.pathRegexp { 1923 t.Errorf("(%v) GetPathRegexp not equal: expected %v, got %v", test.title, test.pathRegexp, routePathRegexp) 1924 } 1925 } 1926 1927 func testQueriesRegexp(t *testing.T, test routeTest) { 1928 route := test.route 1929 queries, queriesErr := route.GetQueriesRegexp() 1930 gotQueries := strings.Join(queries, ",") 1931 if test.queriesRegexp != "" && queriesErr == nil && gotQueries != test.queriesRegexp { 1932 t.Errorf("(%v) GetQueriesRegexp not equal: expected %v, got %v", test.title, test.queriesRegexp, gotQueries) 1933 } 1934 } 1935 1936 func testQueriesTemplates(t *testing.T, test routeTest) { 1937 route := test.route 1938 queries, queriesErr := route.GetQueriesTemplates() 1939 gotQueries := strings.Join(queries, ",") 1940 if test.queriesTemplate != "" && queriesErr == nil && gotQueries != test.queriesTemplate { 1941 t.Errorf("(%v) GetQueriesTemplates not equal: expected %v, got %v", test.title, test.queriesTemplate, gotQueries) 1942 } 1943 } 1944 1945 type TestA301ResponseWriter struct { 1946 hh http.Header 1947 status int 1948 } 1949 1950 func (ho *TestA301ResponseWriter) Header() http.Header { 1951 return ho.hh 1952 } 1953 1954 func (ho *TestA301ResponseWriter) Write(b []byte) (int, error) { 1955 return 0, nil 1956 } 1957 1958 func (ho *TestA301ResponseWriter) WriteHeader(code int) { 1959 ho.status = code 1960 } 1961 1962 func Test301Redirect(t *testing.T) { 1963 m := make(http.Header) 1964 1965 func1 := func(w http.ResponseWriter, r *http.Request) {} 1966 func2 := func(w http.ResponseWriter, r *http.Request) {} 1967 1968 r := NewRouter() 1969 r.HandleFunc("/api/", func2).Name("func2") 1970 r.HandleFunc("/", func1).Name("func1") 1971 1972 req, _ := http.NewRequest("GET", "http://localhost//api/?abc=def", nil) 1973 1974 res := TestA301ResponseWriter{ 1975 hh: m, 1976 status: 0, 1977 } 1978 r.ServeHTTP(&res, req) 1979 1980 if "http://localhost/api/?abc=def" != res.hh["Location"][0] { 1981 t.Errorf("Should have complete URL with query string") 1982 } 1983 } 1984 1985 func TestSkipClean(t *testing.T) { 1986 func1 := func(w http.ResponseWriter, r *http.Request) {} 1987 func2 := func(w http.ResponseWriter, r *http.Request) {} 1988 1989 r := NewRouter() 1990 r.SkipClean(true) 1991 r.HandleFunc("/api/", func2).Name("func2") 1992 r.HandleFunc("/", func1).Name("func1") 1993 1994 req, _ := http.NewRequest("GET", "http://localhost//api/?abc=def", nil) 1995 res := NewRecorder() 1996 r.ServeHTTP(res, req) 1997 1998 if len(res.HeaderMap["Location"]) != 0 { 1999 t.Errorf("Shouldn't redirect since skip clean is disabled") 2000 } 2001 } 2002 2003 // https://plus.google.com/101022900381697718949/posts/eWy6DjFJ6uW 2004 func TestSubrouterHeader(t *testing.T) { 2005 expected := "func1 response" 2006 func1 := func(w http.ResponseWriter, r *http.Request) { 2007 fmt.Fprint(w, expected) 2008 } 2009 func2 := func(http.ResponseWriter, *http.Request) {} 2010 2011 r := NewRouter() 2012 s := r.Headers("SomeSpecialHeader", "").Subrouter() 2013 s.HandleFunc("/", func1).Name("func1") 2014 r.HandleFunc("/", func2).Name("func2") 2015 2016 req, _ := http.NewRequest("GET", "http://localhost/", nil) 2017 req.Header.Add("SomeSpecialHeader", "foo") 2018 match := new(RouteMatch) 2019 matched := r.Match(req, match) 2020 if !matched { 2021 t.Errorf("Should match request") 2022 } 2023 if match.Route.GetName() != "func1" { 2024 t.Errorf("Expecting func1 handler, got %s", match.Route.GetName()) 2025 } 2026 resp := NewRecorder() 2027 match.Handler.ServeHTTP(resp, req) 2028 if resp.Body.String() != expected { 2029 t.Errorf("Expecting %q", expected) 2030 } 2031 } 2032 2033 func TestNoMatchMethodErrorHandler(t *testing.T) { 2034 func1 := func(w http.ResponseWriter, r *http.Request) {} 2035 2036 r := NewRouter() 2037 r.HandleFunc("/", func1).Methods("GET", "POST") 2038 2039 req, _ := http.NewRequest("PUT", "http://localhost/", nil) 2040 match := new(RouteMatch) 2041 matched := r.Match(req, match) 2042 2043 if matched { 2044 t.Error("Should not have matched route for methods") 2045 } 2046 2047 if match.MatchErr != ErrMethodMismatch { 2048 t.Error("Should get ErrMethodMismatch error") 2049 } 2050 2051 resp := NewRecorder() 2052 r.ServeHTTP(resp, req) 2053 if resp.Code != http.StatusMethodNotAllowed { 2054 t.Errorf("Expecting code %v", 405) 2055 } 2056 2057 // Add matching route 2058 r.HandleFunc("/", func1).Methods("PUT") 2059 2060 match = new(RouteMatch) 2061 matched = r.Match(req, match) 2062 2063 if !matched { 2064 t.Error("Should have matched route for methods") 2065 } 2066 2067 if match.MatchErr != nil { 2068 t.Error("Should not have any matching error. Found:", match.MatchErr) 2069 } 2070 } 2071 2072 func TestErrMatchNotFound(t *testing.T) { 2073 emptyHandler := func(w http.ResponseWriter, r *http.Request) {} 2074 2075 r := NewRouter() 2076 r.HandleFunc("/", emptyHandler) 2077 s := r.PathPrefix("/sub/").Subrouter() 2078 s.HandleFunc("/", emptyHandler) 2079 2080 // Regular 404 not found 2081 req, _ := http.NewRequest("GET", "/sub/whatever", nil) 2082 match := new(RouteMatch) 2083 matched := r.Match(req, match) 2084 2085 if matched { 2086 t.Errorf("Subrouter should not have matched that, got %v", match.Route) 2087 } 2088 // Even without a custom handler, MatchErr is set to ErrNotFound 2089 if match.MatchErr != ErrNotFound { 2090 t.Errorf("Expected ErrNotFound MatchErr, but was %v", match.MatchErr) 2091 } 2092 2093 // Now lets add a 404 handler to subrouter 2094 s.NotFoundHandler = http.NotFoundHandler() 2095 req, _ = http.NewRequest("GET", "/sub/whatever", nil) 2096 2097 // Test the subrouter first 2098 match = new(RouteMatch) 2099 matched = s.Match(req, match) 2100 // Now we should get a match 2101 if !matched { 2102 t.Errorf("Subrouter should have matched %s", req.RequestURI) 2103 } 2104 // But MatchErr should be set to ErrNotFound anyway 2105 if match.MatchErr != ErrNotFound { 2106 t.Errorf("Expected ErrNotFound MatchErr, but was %v", match.MatchErr) 2107 } 2108 2109 // Now test the parent (MatchErr should propagate) 2110 match = new(RouteMatch) 2111 matched = r.Match(req, match) 2112 2113 // Now we should get a match 2114 if !matched { 2115 t.Errorf("Router should have matched %s via subrouter", req.RequestURI) 2116 } 2117 // But MatchErr should be set to ErrNotFound anyway 2118 if match.MatchErr != ErrNotFound { 2119 t.Errorf("Expected ErrNotFound MatchErr, but was %v", match.MatchErr) 2120 } 2121 } 2122 2123 // methodsSubrouterTest models the data necessary for testing handler 2124 // matching for subrouters created after HTTP methods matcher registration. 2125 type methodsSubrouterTest struct { 2126 title string 2127 wantCode int 2128 router *Router 2129 // method is the input into the request and expected response 2130 method string 2131 // input request path 2132 path string 2133 // redirectTo is the expected location path for strict-slash matches 2134 redirectTo string 2135 } 2136 2137 // methodHandler writes the method string in response. 2138 func methodHandler(method string) http.HandlerFunc { 2139 return func(w http.ResponseWriter, r *http.Request) { 2140 w.Write([]byte(method)) 2141 } 2142 } 2143 2144 // TestMethodsSubrouterCatchall matches handlers for subrouters where a 2145 // catchall handler is set for a mis-matching method. 2146 func TestMethodsSubrouterCatchall(t *testing.T) { 2147 t.Parallel() 2148 2149 router := NewRouter() 2150 router.Methods("PATCH").Subrouter().PathPrefix("/").HandlerFunc(methodHandler("PUT")) 2151 router.Methods("GET").Subrouter().HandleFunc("/foo", methodHandler("GET")) 2152 router.Methods("POST").Subrouter().HandleFunc("/foo", methodHandler("POST")) 2153 router.Methods("DELETE").Subrouter().HandleFunc("/foo", methodHandler("DELETE")) 2154 2155 tests := []methodsSubrouterTest{ 2156 { 2157 title: "match GET handler", 2158 router: router, 2159 path: "http://localhost/foo", 2160 method: "GET", 2161 wantCode: http.StatusOK, 2162 }, 2163 { 2164 title: "match POST handler", 2165 router: router, 2166 method: "POST", 2167 path: "http://localhost/foo", 2168 wantCode: http.StatusOK, 2169 }, 2170 { 2171 title: "match DELETE handler", 2172 router: router, 2173 method: "DELETE", 2174 path: "http://localhost/foo", 2175 wantCode: http.StatusOK, 2176 }, 2177 { 2178 title: "disallow PUT method", 2179 router: router, 2180 method: "PUT", 2181 path: "http://localhost/foo", 2182 wantCode: http.StatusMethodNotAllowed, 2183 }, 2184 } 2185 2186 for _, test := range tests { 2187 t.Run(test.title, func(t *testing.T) { 2188 testMethodsSubrouter(t, test) 2189 }) 2190 } 2191 } 2192 2193 // TestMethodsSubrouterStrictSlash matches handlers on subrouters with 2194 // strict-slash matchers. 2195 func TestMethodsSubrouterStrictSlash(t *testing.T) { 2196 t.Parallel() 2197 2198 router := NewRouter() 2199 sub := router.PathPrefix("/").Subrouter() 2200 sub.StrictSlash(true).Path("/foo").Methods("GET").Subrouter().HandleFunc("", methodHandler("GET")) 2201 sub.StrictSlash(true).Path("/foo/").Methods("PUT").Subrouter().HandleFunc("/", methodHandler("PUT")) 2202 sub.StrictSlash(true).Path("/foo/").Methods("POST").Subrouter().HandleFunc("/", methodHandler("POST")) 2203 2204 tests := []methodsSubrouterTest{ 2205 { 2206 title: "match POST handler", 2207 router: router, 2208 method: "POST", 2209 path: "http://localhost/foo/", 2210 wantCode: http.StatusOK, 2211 }, 2212 { 2213 title: "match GET handler", 2214 router: router, 2215 method: "GET", 2216 path: "http://localhost/foo", 2217 wantCode: http.StatusOK, 2218 }, 2219 { 2220 title: "match POST handler, redirect strict-slash", 2221 router: router, 2222 method: "POST", 2223 path: "http://localhost/foo", 2224 redirectTo: "http://localhost/foo/", 2225 wantCode: http.StatusMovedPermanently, 2226 }, 2227 { 2228 title: "match GET handler, redirect strict-slash", 2229 router: router, 2230 method: "GET", 2231 path: "http://localhost/foo/", 2232 redirectTo: "http://localhost/foo", 2233 wantCode: http.StatusMovedPermanently, 2234 }, 2235 { 2236 title: "disallow DELETE method", 2237 router: router, 2238 method: "DELETE", 2239 path: "http://localhost/foo", 2240 wantCode: http.StatusMethodNotAllowed, 2241 }, 2242 } 2243 2244 for _, test := range tests { 2245 t.Run(test.title, func(t *testing.T) { 2246 testMethodsSubrouter(t, test) 2247 }) 2248 } 2249 } 2250 2251 // TestMethodsSubrouterPathPrefix matches handlers on subrouters created 2252 // on a router with a path prefix matcher and method matcher. 2253 func TestMethodsSubrouterPathPrefix(t *testing.T) { 2254 t.Parallel() 2255 2256 router := NewRouter() 2257 router.PathPrefix("/1").Methods("POST").Subrouter().HandleFunc("/2", methodHandler("POST")) 2258 router.PathPrefix("/1").Methods("DELETE").Subrouter().HandleFunc("/2", methodHandler("DELETE")) 2259 router.PathPrefix("/1").Methods("PUT").Subrouter().HandleFunc("/2", methodHandler("PUT")) 2260 router.PathPrefix("/1").Methods("POST").Subrouter().HandleFunc("/2", methodHandler("POST2")) 2261 2262 tests := []methodsSubrouterTest{ 2263 { 2264 title: "match first POST handler", 2265 router: router, 2266 method: "POST", 2267 path: "http://localhost/1/2", 2268 wantCode: http.StatusOK, 2269 }, 2270 { 2271 title: "match DELETE handler", 2272 router: router, 2273 method: "DELETE", 2274 path: "http://localhost/1/2", 2275 wantCode: http.StatusOK, 2276 }, 2277 { 2278 title: "match PUT handler", 2279 router: router, 2280 method: "PUT", 2281 path: "http://localhost/1/2", 2282 wantCode: http.StatusOK, 2283 }, 2284 { 2285 title: "disallow PATCH method", 2286 router: router, 2287 method: "PATCH", 2288 path: "http://localhost/1/2", 2289 wantCode: http.StatusMethodNotAllowed, 2290 }, 2291 } 2292 2293 for _, test := range tests { 2294 t.Run(test.title, func(t *testing.T) { 2295 testMethodsSubrouter(t, test) 2296 }) 2297 } 2298 } 2299 2300 // TestMethodsSubrouterSubrouter matches handlers on subrouters produced 2301 // from method matchers registered on a root subrouter. 2302 func TestMethodsSubrouterSubrouter(t *testing.T) { 2303 t.Parallel() 2304 2305 router := NewRouter() 2306 sub := router.PathPrefix("/1").Subrouter() 2307 sub.Methods("POST").Subrouter().HandleFunc("/2", methodHandler("POST")) 2308 sub.Methods("GET").Subrouter().HandleFunc("/2", methodHandler("GET")) 2309 sub.Methods("PATCH").Subrouter().HandleFunc("/2", methodHandler("PATCH")) 2310 sub.HandleFunc("/2", methodHandler("PUT")).Subrouter().Methods("PUT") 2311 sub.HandleFunc("/2", methodHandler("POST2")).Subrouter().Methods("POST") 2312 2313 tests := []methodsSubrouterTest{ 2314 { 2315 title: "match first POST handler", 2316 router: router, 2317 method: "POST", 2318 path: "http://localhost/1/2", 2319 wantCode: http.StatusOK, 2320 }, 2321 { 2322 title: "match GET handler", 2323 router: router, 2324 method: "GET", 2325 path: "http://localhost/1/2", 2326 wantCode: http.StatusOK, 2327 }, 2328 { 2329 title: "match PATCH handler", 2330 router: router, 2331 method: "PATCH", 2332 path: "http://localhost/1/2", 2333 wantCode: http.StatusOK, 2334 }, 2335 { 2336 title: "match PUT handler", 2337 router: router, 2338 method: "PUT", 2339 path: "http://localhost/1/2", 2340 wantCode: http.StatusOK, 2341 }, 2342 { 2343 title: "disallow DELETE method", 2344 router: router, 2345 method: "DELETE", 2346 path: "http://localhost/1/2", 2347 wantCode: http.StatusMethodNotAllowed, 2348 }, 2349 } 2350 2351 for _, test := range tests { 2352 t.Run(test.title, func(t *testing.T) { 2353 testMethodsSubrouter(t, test) 2354 }) 2355 } 2356 } 2357 2358 // TestMethodsSubrouterPathVariable matches handlers on matching paths 2359 // with path variables in them. 2360 func TestMethodsSubrouterPathVariable(t *testing.T) { 2361 t.Parallel() 2362 2363 router := NewRouter() 2364 router.Methods("GET").Subrouter().HandleFunc("/foo", methodHandler("GET")) 2365 router.Methods("POST").Subrouter().HandleFunc("/{any}", methodHandler("POST")) 2366 router.Methods("DELETE").Subrouter().HandleFunc("/1/{any}", methodHandler("DELETE")) 2367 router.Methods("PUT").Subrouter().HandleFunc("/1/{any}", methodHandler("PUT")) 2368 2369 tests := []methodsSubrouterTest{ 2370 { 2371 title: "match GET handler", 2372 router: router, 2373 method: "GET", 2374 path: "http://localhost/foo", 2375 wantCode: http.StatusOK, 2376 }, 2377 { 2378 title: "match POST handler", 2379 router: router, 2380 method: "POST", 2381 path: "http://localhost/foo", 2382 wantCode: http.StatusOK, 2383 }, 2384 { 2385 title: "match DELETE handler", 2386 router: router, 2387 method: "DELETE", 2388 path: "http://localhost/1/foo", 2389 wantCode: http.StatusOK, 2390 }, 2391 { 2392 title: "match PUT handler", 2393 router: router, 2394 method: "PUT", 2395 path: "http://localhost/1/foo", 2396 wantCode: http.StatusOK, 2397 }, 2398 { 2399 title: "disallow PATCH method", 2400 router: router, 2401 method: "PATCH", 2402 path: "http://localhost/1/foo", 2403 wantCode: http.StatusMethodNotAllowed, 2404 }, 2405 } 2406 2407 for _, test := range tests { 2408 t.Run(test.title, func(t *testing.T) { 2409 testMethodsSubrouter(t, test) 2410 }) 2411 } 2412 } 2413 2414 func ExampleSetURLVars() { 2415 req, _ := http.NewRequest("GET", "/foo", nil) 2416 req = SetURLVars(req, map[string]string{"foo": "bar"}) 2417 2418 fmt.Println(Vars(req)["foo"]) 2419 2420 // Output: bar 2421 } 2422 2423 // testMethodsSubrouter runs an individual methodsSubrouterTest. 2424 func testMethodsSubrouter(t *testing.T, test methodsSubrouterTest) { 2425 // Execute request 2426 req, _ := http.NewRequest(test.method, test.path, nil) 2427 resp := NewRecorder() 2428 test.router.ServeHTTP(resp, req) 2429 2430 switch test.wantCode { 2431 case http.StatusMethodNotAllowed: 2432 if resp.Code != http.StatusMethodNotAllowed { 2433 t.Errorf(`(%s) Expected "405 Method Not Allowed", but got %d code`, test.title, resp.Code) 2434 } else if matchedMethod := resp.Body.String(); matchedMethod != "" { 2435 t.Errorf(`(%s) Expected "405 Method Not Allowed", but %q handler was called`, test.title, matchedMethod) 2436 } 2437 2438 case http.StatusMovedPermanently: 2439 if gotLocation := resp.HeaderMap.Get("Location"); gotLocation != test.redirectTo { 2440 t.Errorf("(%s) Expected %q route-match to redirect to %q, but got %q", test.title, test.method, test.redirectTo, gotLocation) 2441 } 2442 2443 case http.StatusOK: 2444 if matchedMethod := resp.Body.String(); matchedMethod != test.method { 2445 t.Errorf("(%s) Expected %q handler to be called, but %q handler was called", test.title, test.method, matchedMethod) 2446 } 2447 2448 default: 2449 expectedCodes := []int{http.StatusMethodNotAllowed, http.StatusMovedPermanently, http.StatusOK} 2450 t.Errorf("(%s) Expected wantCode to be one of: %v, but got %d", test.title, expectedCodes, test.wantCode) 2451 } 2452 } 2453 2454 func TestSubrouterMatching(t *testing.T) { 2455 const ( 2456 none, stdOnly, subOnly uint8 = 0, 1 << 0, 1 << 1 2457 both = subOnly | stdOnly 2458 ) 2459 2460 type request struct { 2461 Name string 2462 Request *http.Request 2463 Flags uint8 2464 } 2465 2466 cases := []struct { 2467 Name string 2468 Standard, Subrouter func(*Router) 2469 Requests []request 2470 }{ 2471 { 2472 "pathPrefix", 2473 func(r *Router) { 2474 r.PathPrefix("/before").PathPrefix("/after") 2475 }, 2476 func(r *Router) { 2477 r.PathPrefix("/before").Subrouter().PathPrefix("/after") 2478 }, 2479 []request{ 2480 {"no match final path prefix", newRequest("GET", "/after"), none}, 2481 {"no match parent path prefix", newRequest("GET", "/before"), none}, 2482 {"matches append", newRequest("GET", "/before/after"), both}, 2483 {"matches as prefix", newRequest("GET", "/before/after/1234"), both}, 2484 }, 2485 }, 2486 { 2487 "path", 2488 func(r *Router) { 2489 r.Path("/before").Path("/after") 2490 }, 2491 func(r *Router) { 2492 r.Path("/before").Subrouter().Path("/after") 2493 }, 2494 []request{ 2495 {"no match subroute path", newRequest("GET", "/after"), none}, 2496 {"no match parent path", newRequest("GET", "/before"), none}, 2497 {"no match as prefix", newRequest("GET", "/before/after/1234"), none}, 2498 {"no match append", newRequest("GET", "/before/after"), none}, 2499 }, 2500 }, 2501 { 2502 "host", 2503 func(r *Router) { 2504 r.Host("before.com").Host("after.com") 2505 }, 2506 func(r *Router) { 2507 r.Host("before.com").Subrouter().Host("after.com") 2508 }, 2509 []request{ 2510 {"no match before", newRequestHost("GET", "/", "before.com"), none}, 2511 {"no match other", newRequestHost("GET", "/", "other.com"), none}, 2512 {"matches after", newRequestHost("GET", "/", "after.com"), none}, 2513 }, 2514 }, 2515 { 2516 "queries variant keys", 2517 func(r *Router) { 2518 r.Queries("foo", "bar").Queries("cricket", "baseball") 2519 }, 2520 func(r *Router) { 2521 r.Queries("foo", "bar").Subrouter().Queries("cricket", "baseball") 2522 }, 2523 []request{ 2524 {"matches with all", newRequest("GET", "/?foo=bar&cricket=baseball"), both}, 2525 {"matches with more", newRequest("GET", "/?foo=bar&cricket=baseball&something=else"), both}, 2526 {"no match with none", newRequest("GET", "/"), none}, 2527 {"no match with some", newRequest("GET", "/?cricket=baseball"), none}, 2528 }, 2529 }, 2530 { 2531 "queries overlapping keys", 2532 func(r *Router) { 2533 r.Queries("foo", "bar").Queries("foo", "baz") 2534 }, 2535 func(r *Router) { 2536 r.Queries("foo", "bar").Subrouter().Queries("foo", "baz") 2537 }, 2538 []request{ 2539 {"no match old value", newRequest("GET", "/?foo=bar"), none}, 2540 {"no match diff value", newRequest("GET", "/?foo=bak"), none}, 2541 {"no match with none", newRequest("GET", "/"), none}, 2542 {"matches override", newRequest("GET", "/?foo=baz"), none}, 2543 }, 2544 }, 2545 { 2546 "header variant keys", 2547 func(r *Router) { 2548 r.Headers("foo", "bar").Headers("cricket", "baseball") 2549 }, 2550 func(r *Router) { 2551 r.Headers("foo", "bar").Subrouter().Headers("cricket", "baseball") 2552 }, 2553 []request{ 2554 { 2555 "matches with all", 2556 newRequestWithHeaders("GET", "/", "foo", "bar", "cricket", "baseball"), 2557 both, 2558 }, 2559 { 2560 "matches with more", 2561 newRequestWithHeaders("GET", "/", "foo", "bar", "cricket", "baseball", "something", "else"), 2562 both, 2563 }, 2564 {"no match with none", newRequest("GET", "/"), none}, 2565 {"no match with some", newRequestWithHeaders("GET", "/", "cricket", "baseball"), none}, 2566 }, 2567 }, 2568 { 2569 "header overlapping keys", 2570 func(r *Router) { 2571 r.Headers("foo", "bar").Headers("foo", "baz") 2572 }, 2573 func(r *Router) { 2574 r.Headers("foo", "bar").Subrouter().Headers("foo", "baz") 2575 }, 2576 []request{ 2577 {"no match old value", newRequestWithHeaders("GET", "/", "foo", "bar"), none}, 2578 {"no match diff value", newRequestWithHeaders("GET", "/", "foo", "bak"), none}, 2579 {"no match with none", newRequest("GET", "/"), none}, 2580 {"matches override", newRequestWithHeaders("GET", "/", "foo", "baz"), none}, 2581 }, 2582 }, 2583 { 2584 "method", 2585 func(r *Router) { 2586 r.Methods("POST").Methods("GET") 2587 }, 2588 func(r *Router) { 2589 r.Methods("POST").Subrouter().Methods("GET") 2590 }, 2591 []request{ 2592 {"matches before", newRequest("POST", "/"), none}, 2593 {"no match other", newRequest("HEAD", "/"), none}, 2594 {"matches override", newRequest("GET", "/"), none}, 2595 }, 2596 }, 2597 { 2598 "schemes", 2599 func(r *Router) { 2600 r.Schemes("http").Schemes("https") 2601 }, 2602 func(r *Router) { 2603 r.Schemes("http").Subrouter().Schemes("https") 2604 }, 2605 []request{ 2606 {"matches overrides", newRequest("GET", "https://www.example.com/"), none}, 2607 {"matches original", newRequest("GET", "http://www.example.com/"), none}, 2608 {"no match other", newRequest("GET", "ftp://www.example.com/"), none}, 2609 }, 2610 }, 2611 } 2612 2613 // case -> request -> router 2614 for _, c := range cases { 2615 t.Run(c.Name, func(t *testing.T) { 2616 for _, req := range c.Requests { 2617 t.Run(req.Name, func(t *testing.T) { 2618 for _, v := range []struct { 2619 Name string 2620 Config func(*Router) 2621 Expected bool 2622 }{ 2623 {"subrouter", c.Subrouter, (req.Flags & subOnly) != 0}, 2624 {"standard", c.Standard, (req.Flags & stdOnly) != 0}, 2625 } { 2626 r := NewRouter() 2627 v.Config(r) 2628 if r.Match(req.Request, &RouteMatch{}) != v.Expected { 2629 if v.Expected { 2630 t.Errorf("expected %v match", v.Name) 2631 } else { 2632 t.Errorf("expected %v no match", v.Name) 2633 } 2634 } 2635 } 2636 }) 2637 } 2638 }) 2639 } 2640 } 2641 2642 // verify that copyRouteConf copies fields as expected. 2643 func Test_copyRouteConf(t *testing.T) { 2644 var ( 2645 m MatcherFunc = func(*http.Request, *RouteMatch) bool { 2646 return true 2647 } 2648 b BuildVarsFunc = func(i map[string]string) map[string]string { 2649 return i 2650 } 2651 r, _ = newRouteRegexp("hi", regexpTypeHost, routeRegexpOptions{}) 2652 ) 2653 2654 tests := []struct { 2655 name string 2656 args routeConf 2657 want routeConf 2658 }{ 2659 { 2660 "empty", 2661 routeConf{}, 2662 routeConf{}, 2663 }, 2664 { 2665 "full", 2666 routeConf{ 2667 useEncodedPath: true, 2668 strictSlash: true, 2669 skipClean: true, 2670 regexp: routeRegexpGroup{host: r, path: r, queries: []*routeRegexp{r}}, 2671 matchers: []matcher{m}, 2672 buildScheme: "https", 2673 buildVarsFunc: b, 2674 }, 2675 routeConf{ 2676 useEncodedPath: true, 2677 strictSlash: true, 2678 skipClean: true, 2679 regexp: routeRegexpGroup{host: r, path: r, queries: []*routeRegexp{r}}, 2680 matchers: []matcher{m}, 2681 buildScheme: "https", 2682 buildVarsFunc: b, 2683 }, 2684 }, 2685 } 2686 2687 for _, tt := range tests { 2688 t.Run(tt.name, func(t *testing.T) { 2689 // special case some incomparable fields of routeConf before delegating to reflect.DeepEqual 2690 got := copyRouteConf(tt.args) 2691 2692 // funcs not comparable, just compare length of slices 2693 if len(got.matchers) != len(tt.want.matchers) { 2694 t.Errorf("matchers different lengths: %v %v", len(got.matchers), len(tt.want.matchers)) 2695 } 2696 got.matchers, tt.want.matchers = nil, nil 2697 2698 // deep equal treats nil slice differently to empty slice so check for zero len first 2699 { 2700 bothZero := len(got.regexp.queries) == 0 && len(tt.want.regexp.queries) == 0 2701 if !bothZero && !reflect.DeepEqual(got.regexp.queries, tt.want.regexp.queries) { 2702 t.Errorf("queries unequal: %v %v", got.regexp.queries, tt.want.regexp.queries) 2703 } 2704 got.regexp.queries, tt.want.regexp.queries = nil, nil 2705 } 2706 2707 // funcs not comparable, just compare nullity 2708 if (got.buildVarsFunc == nil) != (tt.want.buildVarsFunc == nil) { 2709 t.Errorf("build vars funcs unequal: %v %v", got.buildVarsFunc == nil, tt.want.buildVarsFunc == nil) 2710 } 2711 got.buildVarsFunc, tt.want.buildVarsFunc = nil, nil 2712 2713 // finish the deal 2714 if !reflect.DeepEqual(got, tt.want) { 2715 t.Errorf("route confs unequal: %v %v", got, tt.want) 2716 } 2717 }) 2718 } 2719 } 2720 2721 func TestMethodNotAllowed(t *testing.T) { 2722 handler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } 2723 router := NewRouter() 2724 router.HandleFunc("/thing", handler).Methods(http.MethodGet) 2725 router.HandleFunc("/something", handler).Methods(http.MethodGet) 2726 2727 w := NewRecorder() 2728 req := newRequest(http.MethodPut, "/thing") 2729 2730 router.ServeHTTP(w, req) 2731 2732 if w.Code != http.StatusMethodNotAllowed { 2733 t.Fatalf("Expected status code 405 (got %d)", w.Code) 2734 } 2735 } 2736 2737 type customMethodNotAllowedHandler struct { 2738 msg string 2739 } 2740 2741 func (h customMethodNotAllowedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 2742 w.WriteHeader(http.StatusMethodNotAllowed) 2743 fmt.Fprint(w, h.msg) 2744 } 2745 2746 func TestSubrouterCustomMethodNotAllowed(t *testing.T) { 2747 handler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } 2748 2749 router := NewRouter() 2750 router.HandleFunc("/test", handler).Methods(http.MethodGet) 2751 router.MethodNotAllowedHandler = customMethodNotAllowedHandler{msg: "custom router handler"} 2752 2753 subrouter := router.PathPrefix("/sub").Subrouter() 2754 subrouter.HandleFunc("/test", handler).Methods(http.MethodGet) 2755 subrouter.MethodNotAllowedHandler = customMethodNotAllowedHandler{msg: "custom sub router handler"} 2756 2757 testCases := map[string]struct { 2758 path string 2759 expMsg string 2760 }{ 2761 "router method not allowed": { 2762 path: "/test", 2763 expMsg: "custom router handler", 2764 }, 2765 "subrouter method not allowed": { 2766 path: "/sub/test", 2767 expMsg: "custom sub router handler", 2768 }, 2769 } 2770 2771 for name, tc := range testCases { 2772 t.Run(name, func(tt *testing.T) { 2773 w := NewRecorder() 2774 req := newRequest(http.MethodPut, tc.path) 2775 2776 router.ServeHTTP(w, req) 2777 2778 if w.Code != http.StatusMethodNotAllowed { 2779 tt.Errorf("Expected status code 405 (got %d)", w.Code) 2780 } 2781 2782 b, err := ioutil.ReadAll(w.Body) 2783 if err != nil { 2784 tt.Errorf("failed to read body: %v", err) 2785 } 2786 2787 if string(b) != tc.expMsg { 2788 tt.Errorf("expected msg %q, got %q", tc.expMsg, string(b)) 2789 } 2790 }) 2791 } 2792 } 2793 2794 func TestSubrouterNotFound(t *testing.T) { 2795 handler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } 2796 router := NewRouter() 2797 router.Path("/a").Subrouter().HandleFunc("/thing", handler).Methods(http.MethodGet) 2798 router.Path("/b").Subrouter().HandleFunc("/something", handler).Methods(http.MethodGet) 2799 2800 w := NewRecorder() 2801 req := newRequest(http.MethodPut, "/not-present") 2802 2803 router.ServeHTTP(w, req) 2804 2805 if w.Code != http.StatusNotFound { 2806 t.Fatalf("Expected status code 404 (got %d)", w.Code) 2807 } 2808 } 2809 2810 func TestContextMiddleware(t *testing.T) { 2811 withTimeout := func(h http.Handler) http.Handler { 2812 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2813 ctx, cancel := context.WithTimeout(r.Context(), time.Minute) 2814 defer cancel() 2815 h.ServeHTTP(w, r.WithContext(ctx)) 2816 }) 2817 } 2818 2819 r := NewRouter() 2820 r.Handle("/path/{foo}", withTimeout(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2821 vars := Vars(r) 2822 if vars["foo"] != "bar" { 2823 t.Fatal("Expected foo var to be set") 2824 } 2825 }))) 2826 2827 rec := NewRecorder() 2828 req := newRequest("GET", "/path/bar") 2829 r.ServeHTTP(rec, req) 2830 } 2831 2832 // mapToPairs converts a string map to a slice of string pairs 2833 func mapToPairs(m map[string]string) []string { 2834 var i int 2835 p := make([]string, len(m)*2) 2836 for k, v := range m { 2837 p[i] = k 2838 p[i+1] = v 2839 i += 2 2840 } 2841 return p 2842 } 2843 2844 // stringMapEqual checks the equality of two string maps 2845 func stringMapEqual(m1, m2 map[string]string) bool { 2846 nil1 := m1 == nil 2847 nil2 := m2 == nil 2848 if nil1 != nil2 || len(m1) != len(m2) { 2849 return false 2850 } 2851 for k, v := range m1 { 2852 if v != m2[k] { 2853 return false 2854 } 2855 } 2856 return true 2857 } 2858 2859 // stringHandler returns a handler func that writes a message 's' to the 2860 // http.ResponseWriter. 2861 func stringHandler(s string) http.HandlerFunc { 2862 return func(w http.ResponseWriter, r *http.Request) { 2863 w.Write([]byte(s)) 2864 } 2865 } 2866 2867 // newRequest is a helper function to create a new request with a method and url. 2868 // The request returned is a 'server' request as opposed to a 'client' one through 2869 // simulated write onto the wire and read off of the wire. 2870 // The differences between requests are detailed in the net/http package. 2871 func newRequest(method, url string) *http.Request { 2872 req, err := http.NewRequest(method, url, nil) 2873 if err != nil { 2874 panic(err) 2875 } 2876 // extract the escaped original host+path from url 2877 // http://localhost/path/here?v=1#frag -> //localhost/path/here 2878 opaque := "" 2879 if i := len(req.URL.Scheme); i > 0 { 2880 opaque = url[i+1:] 2881 } 2882 2883 if i := strings.LastIndex(opaque, "?"); i > -1 { 2884 opaque = opaque[:i] 2885 } 2886 if i := strings.LastIndex(opaque, "#"); i > -1 { 2887 opaque = opaque[:i] 2888 } 2889 2890 // Escaped host+path workaround as detailed in https://golang.org/pkg/net/url/#URL 2891 // for < 1.5 client side workaround 2892 req.URL.Opaque = opaque 2893 2894 // Simulate writing to wire 2895 var buff bytes.Buffer 2896 req.Write(&buff) 2897 ioreader := bufio.NewReader(&buff) 2898 2899 // Parse request off of 'wire' 2900 req, err = http.ReadRequest(ioreader) 2901 if err != nil { 2902 panic(err) 2903 } 2904 return req 2905 } 2906 2907 // create a new request with the provided headers 2908 func newRequestWithHeaders(method, url string, headers ...string) *http.Request { 2909 req := newRequest(method, url) 2910 2911 if len(headers)%2 != 0 { 2912 panic(fmt.Sprintf("Expected headers length divisible by 2 but got %v", len(headers))) 2913 } 2914 2915 for i := 0; i < len(headers); i += 2 { 2916 req.Header.Set(headers[i], headers[i+1]) 2917 } 2918 2919 return req 2920 } 2921 2922 // newRequestHost a new request with a method, url, and host header 2923 func newRequestHost(method, url, host string) *http.Request { 2924 req := httptest.NewRequest(method, url, nil) 2925 req.Host = host 2926 return req 2927 }