tree_test.go 15 KB

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