Няма описание

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