tree_test.go 16 KB

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