DispatcherTest.php 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  1. <?php
  2. namespace FastRoute\Dispatcher;
  3. use FastRoute\RouteCollector;
  4. use PHPUnit\Framework\TestCase;
  5. abstract class DispatcherTest extends TestCase
  6. {
  7. /**
  8. * Delegate dispatcher selection to child test classes
  9. */
  10. abstract protected function getDispatcherClass();
  11. /**
  12. * Delegate dataGenerator selection to child test classes
  13. */
  14. abstract protected function getDataGeneratorClass();
  15. /**
  16. * Set appropriate options for the specific Dispatcher class we're testing
  17. */
  18. private function generateDispatcherOptions()
  19. {
  20. return [
  21. 'dataGenerator' => $this->getDataGeneratorClass(),
  22. 'dispatcher' => $this->getDispatcherClass()
  23. ];
  24. }
  25. /**
  26. * @dataProvider provideFoundDispatchCases
  27. */
  28. public function testFoundDispatches($method, $uri, $callback, $handler, $argDict)
  29. {
  30. $dispatcher = \FastRoute\simpleDispatcher($callback, $this->generateDispatcherOptions());
  31. $info = $dispatcher->dispatch($method, $uri);
  32. $this->assertSame($dispatcher::FOUND, $info[0]);
  33. $this->assertSame($handler, $info[1]);
  34. $this->assertSame($argDict, $info[2]);
  35. }
  36. /**
  37. * @dataProvider provideNotFoundDispatchCases
  38. */
  39. public function testNotFoundDispatches($method, $uri, $callback)
  40. {
  41. $dispatcher = \FastRoute\simpleDispatcher($callback, $this->generateDispatcherOptions());
  42. $routeInfo = $dispatcher->dispatch($method, $uri);
  43. $this->assertArrayNotHasKey(1, $routeInfo,
  44. 'NOT_FOUND result must only contain a single element in the returned info array'
  45. );
  46. $this->assertSame($dispatcher::NOT_FOUND, $routeInfo[0]);
  47. }
  48. /**
  49. * @dataProvider provideMethodNotAllowedDispatchCases
  50. */
  51. public function testMethodNotAllowedDispatches($method, $uri, $callback, $availableMethods)
  52. {
  53. $dispatcher = \FastRoute\simpleDispatcher($callback, $this->generateDispatcherOptions());
  54. $routeInfo = $dispatcher->dispatch($method, $uri);
  55. $this->assertArrayHasKey(1, $routeInfo,
  56. 'METHOD_NOT_ALLOWED result must return an array of allowed methods at index 1'
  57. );
  58. list($routedStatus, $methodArray) = $dispatcher->dispatch($method, $uri);
  59. $this->assertSame($dispatcher::METHOD_NOT_ALLOWED, $routedStatus);
  60. $this->assertSame($availableMethods, $methodArray);
  61. }
  62. /**
  63. * @expectedException \FastRoute\BadRouteException
  64. * @expectedExceptionMessage Cannot use the same placeholder "test" twice
  65. */
  66. public function testDuplicateVariableNameError()
  67. {
  68. \FastRoute\simpleDispatcher(function (RouteCollector $r) {
  69. $r->addRoute('GET', '/foo/{test}/{test:\d+}', 'handler0');
  70. }, $this->generateDispatcherOptions());
  71. }
  72. /**
  73. * @expectedException \FastRoute\BadRouteException
  74. * @expectedExceptionMessage Cannot register two routes matching "/user/([^/]+)" for method "GET"
  75. */
  76. public function testDuplicateVariableRoute()
  77. {
  78. \FastRoute\simpleDispatcher(function (RouteCollector $r) {
  79. $r->addRoute('GET', '/user/{id}', 'handler0'); // oops, forgot \d+ restriction ;)
  80. $r->addRoute('GET', '/user/{name}', 'handler1');
  81. }, $this->generateDispatcherOptions());
  82. }
  83. /**
  84. * @expectedException \FastRoute\BadRouteException
  85. * @expectedExceptionMessage Cannot register two routes matching "/user" for method "GET"
  86. */
  87. public function testDuplicateStaticRoute()
  88. {
  89. \FastRoute\simpleDispatcher(function (RouteCollector $r) {
  90. $r->addRoute('GET', '/user', 'handler0');
  91. $r->addRoute('GET', '/user', 'handler1');
  92. }, $this->generateDispatcherOptions());
  93. }
  94. /**
  95. * @expectedException \FastRoute\BadRouteException
  96. * @expectedExceptionMessage Static route "/user/nikic" is shadowed by previously defined variable route "/user/([^/]+)" for method "GET"
  97. */
  98. public function testShadowedStaticRoute()
  99. {
  100. \FastRoute\simpleDispatcher(function (RouteCollector $r) {
  101. $r->addRoute('GET', '/user/{name}', 'handler0');
  102. $r->addRoute('GET', '/user/nikic', 'handler1');
  103. }, $this->generateDispatcherOptions());
  104. }
  105. /**
  106. * @expectedException \FastRoute\BadRouteException
  107. * @expectedExceptionMessage Regex "(en|de)" for parameter "lang" contains a capturing group
  108. */
  109. public function testCapturing()
  110. {
  111. \FastRoute\simpleDispatcher(function (RouteCollector $r) {
  112. $r->addRoute('GET', '/{lang:(en|de)}', 'handler0');
  113. }, $this->generateDispatcherOptions());
  114. }
  115. public function provideFoundDispatchCases()
  116. {
  117. $cases = [];
  118. // 0 -------------------------------------------------------------------------------------->
  119. $callback = function (RouteCollector $r) {
  120. $r->addRoute('GET', '/resource/123/456', 'handler0');
  121. };
  122. $method = 'GET';
  123. $uri = '/resource/123/456';
  124. $handler = 'handler0';
  125. $argDict = [];
  126. $cases[] = [$method, $uri, $callback, $handler, $argDict];
  127. // 1 -------------------------------------------------------------------------------------->
  128. $callback = function (RouteCollector $r) {
  129. $r->addRoute('GET', '/handler0', 'handler0');
  130. $r->addRoute('GET', '/handler1', 'handler1');
  131. $r->addRoute('GET', '/handler2', 'handler2');
  132. };
  133. $method = 'GET';
  134. $uri = '/handler2';
  135. $handler = 'handler2';
  136. $argDict = [];
  137. $cases[] = [$method, $uri, $callback, $handler, $argDict];
  138. // 2 -------------------------------------------------------------------------------------->
  139. $callback = function (RouteCollector $r) {
  140. $r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0');
  141. $r->addRoute('GET', '/user/{id:[0-9]+}', 'handler1');
  142. $r->addRoute('GET', '/user/{name}', 'handler2');
  143. };
  144. $method = 'GET';
  145. $uri = '/user/rdlowrey';
  146. $handler = 'handler2';
  147. $argDict = ['name' => 'rdlowrey'];
  148. $cases[] = [$method, $uri, $callback, $handler, $argDict];
  149. // 3 -------------------------------------------------------------------------------------->
  150. // reuse $callback from #2
  151. $method = 'GET';
  152. $uri = '/user/12345';
  153. $handler = 'handler1';
  154. $argDict = ['id' => '12345'];
  155. $cases[] = [$method, $uri, $callback, $handler, $argDict];
  156. // 4 -------------------------------------------------------------------------------------->
  157. // reuse $callback from #3
  158. $method = 'GET';
  159. $uri = '/user/NaN';
  160. $handler = 'handler2';
  161. $argDict = ['name' => 'NaN'];
  162. $cases[] = [$method, $uri, $callback, $handler, $argDict];
  163. // 5 -------------------------------------------------------------------------------------->
  164. // reuse $callback from #4
  165. $method = 'GET';
  166. $uri = '/user/rdlowrey/12345';
  167. $handler = 'handler0';
  168. $argDict = ['name' => 'rdlowrey', 'id' => '12345'];
  169. $cases[] = [$method, $uri, $callback, $handler, $argDict];
  170. // 6 -------------------------------------------------------------------------------------->
  171. $callback = function (RouteCollector $r) {
  172. $r->addRoute('GET', '/user/{id:[0-9]+}', 'handler0');
  173. $r->addRoute('GET', '/user/12345/extension', 'handler1');
  174. $r->addRoute('GET', '/user/{id:[0-9]+}.{extension}', 'handler2');
  175. };
  176. $method = 'GET';
  177. $uri = '/user/12345.svg';
  178. $handler = 'handler2';
  179. $argDict = ['id' => '12345', 'extension' => 'svg'];
  180. $cases[] = [$method, $uri, $callback, $handler, $argDict];
  181. // 7 ----- Test GET method fallback on HEAD route miss ------------------------------------>
  182. $callback = function (RouteCollector $r) {
  183. $r->addRoute('GET', '/user/{name}', 'handler0');
  184. $r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler1');
  185. $r->addRoute('GET', '/static0', 'handler2');
  186. $r->addRoute('GET', '/static1', 'handler3');
  187. $r->addRoute('HEAD', '/static1', 'handler4');
  188. };
  189. $method = 'HEAD';
  190. $uri = '/user/rdlowrey';
  191. $handler = 'handler0';
  192. $argDict = ['name' => 'rdlowrey'];
  193. $cases[] = [$method, $uri, $callback, $handler, $argDict];
  194. // 8 ----- Test GET method fallback on HEAD route miss ------------------------------------>
  195. // reuse $callback from #7
  196. $method = 'HEAD';
  197. $uri = '/user/rdlowrey/1234';
  198. $handler = 'handler1';
  199. $argDict = ['name' => 'rdlowrey', 'id' => '1234'];
  200. $cases[] = [$method, $uri, $callback, $handler, $argDict];
  201. // 9 ----- Test GET method fallback on HEAD route miss ------------------------------------>
  202. // reuse $callback from #8
  203. $method = 'HEAD';
  204. $uri = '/static0';
  205. $handler = 'handler2';
  206. $argDict = [];
  207. $cases[] = [$method, $uri, $callback, $handler, $argDict];
  208. // 10 ---- Test existing HEAD route used if available (no fallback) ----------------------->
  209. // reuse $callback from #9
  210. $method = 'HEAD';
  211. $uri = '/static1';
  212. $handler = 'handler4';
  213. $argDict = [];
  214. $cases[] = [$method, $uri, $callback, $handler, $argDict];
  215. // 11 ---- More specified routes are not shadowed by less specific of another method ------>
  216. $callback = function (RouteCollector $r) {
  217. $r->addRoute('GET', '/user/{name}', 'handler0');
  218. $r->addRoute('POST', '/user/{name:[a-z]+}', 'handler1');
  219. };
  220. $method = 'POST';
  221. $uri = '/user/rdlowrey';
  222. $handler = 'handler1';
  223. $argDict = ['name' => 'rdlowrey'];
  224. $cases[] = [$method, $uri, $callback, $handler, $argDict];
  225. // 12 ---- Handler of more specific routes is used, if it occurs first -------------------->
  226. $callback = function (RouteCollector $r) {
  227. $r->addRoute('GET', '/user/{name}', 'handler0');
  228. $r->addRoute('POST', '/user/{name:[a-z]+}', 'handler1');
  229. $r->addRoute('POST', '/user/{name}', 'handler2');
  230. };
  231. $method = 'POST';
  232. $uri = '/user/rdlowrey';
  233. $handler = 'handler1';
  234. $argDict = ['name' => 'rdlowrey'];
  235. $cases[] = [$method, $uri, $callback, $handler, $argDict];
  236. // 13 ---- Route with constant suffix ----------------------------------------------------->
  237. $callback = function (RouteCollector $r) {
  238. $r->addRoute('GET', '/user/{name}', 'handler0');
  239. $r->addRoute('GET', '/user/{name}/edit', 'handler1');
  240. };
  241. $method = 'GET';
  242. $uri = '/user/rdlowrey/edit';
  243. $handler = 'handler1';
  244. $argDict = ['name' => 'rdlowrey'];
  245. $cases[] = [$method, $uri, $callback, $handler, $argDict];
  246. // 14 ---- Handle multiple methods with the same handler ---------------------------------->
  247. $callback = function (RouteCollector $r) {
  248. $r->addRoute(['GET', 'POST'], '/user', 'handlerGetPost');
  249. $r->addRoute(['DELETE'], '/user', 'handlerDelete');
  250. $r->addRoute([], '/user', 'handlerNone');
  251. };
  252. $argDict = [];
  253. $cases[] = ['GET', '/user', $callback, 'handlerGetPost', $argDict];
  254. $cases[] = ['POST', '/user', $callback, 'handlerGetPost', $argDict];
  255. $cases[] = ['DELETE', '/user', $callback, 'handlerDelete', $argDict];
  256. // 17 ----
  257. $callback = function (RouteCollector $r) {
  258. $r->addRoute('POST', '/user.json', 'handler0');
  259. $r->addRoute('GET', '/{entity}.json', 'handler1');
  260. };
  261. $cases[] = ['GET', '/user.json', $callback, 'handler1', ['entity' => 'user']];
  262. // 18 ----
  263. $callback = function (RouteCollector $r) {
  264. $r->addRoute('GET', '', 'handler0');
  265. };
  266. $cases[] = ['GET', '', $callback, 'handler0', []];
  267. // 19 ----
  268. $callback = function (RouteCollector $r) {
  269. $r->addRoute('HEAD', '/a/{foo}', 'handler0');
  270. $r->addRoute('GET', '/b/{foo}', 'handler1');
  271. };
  272. $cases[] = ['HEAD', '/b/bar', $callback, 'handler1', ['foo' => 'bar']];
  273. // 20 ----
  274. $callback = function (RouteCollector $r) {
  275. $r->addRoute('HEAD', '/a', 'handler0');
  276. $r->addRoute('GET', '/b', 'handler1');
  277. };
  278. $cases[] = ['HEAD', '/b', $callback, 'handler1', []];
  279. // 21 ----
  280. $callback = function (RouteCollector $r) {
  281. $r->addRoute('GET', '/foo', 'handler0');
  282. $r->addRoute('HEAD', '/{bar}', 'handler1');
  283. };
  284. $cases[] = ['HEAD', '/foo', $callback, 'handler1', ['bar' => 'foo']];
  285. // 22 ----
  286. $callback = function (RouteCollector $r) {
  287. $r->addRoute('*', '/user', 'handler0');
  288. $r->addRoute('*', '/{user}', 'handler1');
  289. $r->addRoute('GET', '/user', 'handler2');
  290. };
  291. $cases[] = ['GET', '/user', $callback, 'handler2', []];
  292. // 23 ----
  293. $callback = function (RouteCollector $r) {
  294. $r->addRoute('*', '/user', 'handler0');
  295. $r->addRoute('GET', '/user', 'handler1');
  296. };
  297. $cases[] = ['POST', '/user', $callback, 'handler0', []];
  298. // 24 ----
  299. $cases[] = ['HEAD', '/user', $callback, 'handler1', []];
  300. // 25 ----
  301. $callback = function (RouteCollector $r) {
  302. $r->addRoute('GET', '/{bar}', 'handler0');
  303. $r->addRoute('*', '/foo', 'handler1');
  304. };
  305. $cases[] = ['GET', '/foo', $callback, 'handler0', ['bar' => 'foo']];
  306. // 26 ----
  307. $callback = function(RouteCollector $r) {
  308. $r->addRoute('GET', '/user', 'handler0');
  309. $r->addRoute('*', '/{foo:.*}', 'handler1');
  310. };
  311. $cases[] = ['POST', '/bar', $callback, 'handler1', ['foo' => 'bar']];
  312. // x -------------------------------------------------------------------------------------->
  313. return $cases;
  314. }
  315. public function provideNotFoundDispatchCases()
  316. {
  317. $cases = [];
  318. // 0 -------------------------------------------------------------------------------------->
  319. $callback = function (RouteCollector $r) {
  320. $r->addRoute('GET', '/resource/123/456', 'handler0');
  321. };
  322. $method = 'GET';
  323. $uri = '/not-found';
  324. $cases[] = [$method, $uri, $callback];
  325. // 1 -------------------------------------------------------------------------------------->
  326. // reuse callback from #0
  327. $method = 'POST';
  328. $uri = '/not-found';
  329. $cases[] = [$method, $uri, $callback];
  330. // 2 -------------------------------------------------------------------------------------->
  331. // reuse callback from #1
  332. $method = 'PUT';
  333. $uri = '/not-found';
  334. $cases[] = [$method, $uri, $callback];
  335. // 3 -------------------------------------------------------------------------------------->
  336. $callback = function (RouteCollector $r) {
  337. $r->addRoute('GET', '/handler0', 'handler0');
  338. $r->addRoute('GET', '/handler1', 'handler1');
  339. $r->addRoute('GET', '/handler2', 'handler2');
  340. };
  341. $method = 'GET';
  342. $uri = '/not-found';
  343. $cases[] = [$method, $uri, $callback];
  344. // 4 -------------------------------------------------------------------------------------->
  345. $callback = function (RouteCollector $r) {
  346. $r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0');
  347. $r->addRoute('GET', '/user/{id:[0-9]+}', 'handler1');
  348. $r->addRoute('GET', '/user/{name}', 'handler2');
  349. };
  350. $method = 'GET';
  351. $uri = '/not-found';
  352. $cases[] = [$method, $uri, $callback];
  353. // 5 -------------------------------------------------------------------------------------->
  354. // reuse callback from #4
  355. $method = 'GET';
  356. $uri = '/user/rdlowrey/12345/not-found';
  357. $cases[] = [$method, $uri, $callback];
  358. // 6 -------------------------------------------------------------------------------------->
  359. // reuse callback from #5
  360. $method = 'HEAD';
  361. $cases[] = [$method, $uri, $callback];
  362. // x -------------------------------------------------------------------------------------->
  363. return $cases;
  364. }
  365. public function provideMethodNotAllowedDispatchCases()
  366. {
  367. $cases = [];
  368. // 0 -------------------------------------------------------------------------------------->
  369. $callback = function (RouteCollector $r) {
  370. $r->addRoute('GET', '/resource/123/456', 'handler0');
  371. };
  372. $method = 'POST';
  373. $uri = '/resource/123/456';
  374. $allowedMethods = ['GET'];
  375. $cases[] = [$method, $uri, $callback, $allowedMethods];
  376. // 1 -------------------------------------------------------------------------------------->
  377. $callback = function (RouteCollector $r) {
  378. $r->addRoute('GET', '/resource/123/456', 'handler0');
  379. $r->addRoute('POST', '/resource/123/456', 'handler1');
  380. $r->addRoute('PUT', '/resource/123/456', 'handler2');
  381. $r->addRoute('*', '/', 'handler3');
  382. };
  383. $method = 'DELETE';
  384. $uri = '/resource/123/456';
  385. $allowedMethods = ['GET', 'POST', 'PUT'];
  386. $cases[] = [$method, $uri, $callback, $allowedMethods];
  387. // 2 -------------------------------------------------------------------------------------->
  388. $callback = function (RouteCollector $r) {
  389. $r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0');
  390. $r->addRoute('POST', '/user/{name}/{id:[0-9]+}', 'handler1');
  391. $r->addRoute('PUT', '/user/{name}/{id:[0-9]+}', 'handler2');
  392. $r->addRoute('PATCH', '/user/{name}/{id:[0-9]+}', 'handler3');
  393. };
  394. $method = 'DELETE';
  395. $uri = '/user/rdlowrey/42';
  396. $allowedMethods = ['GET', 'POST', 'PUT', 'PATCH'];
  397. $cases[] = [$method, $uri, $callback, $allowedMethods];
  398. // 3 -------------------------------------------------------------------------------------->
  399. $callback = function (RouteCollector $r) {
  400. $r->addRoute('POST', '/user/{name}', 'handler1');
  401. $r->addRoute('PUT', '/user/{name:[a-z]+}', 'handler2');
  402. $r->addRoute('PATCH', '/user/{name:[a-z]+}', 'handler3');
  403. };
  404. $method = 'GET';
  405. $uri = '/user/rdlowrey';
  406. $allowedMethods = ['POST', 'PUT', 'PATCH'];
  407. $cases[] = [$method, $uri, $callback, $allowedMethods];
  408. // 4 -------------------------------------------------------------------------------------->
  409. $callback = function (RouteCollector $r) {
  410. $r->addRoute(['GET', 'POST'], '/user', 'handlerGetPost');
  411. $r->addRoute(['DELETE'], '/user', 'handlerDelete');
  412. $r->addRoute([], '/user', 'handlerNone');
  413. };
  414. $cases[] = ['PUT', '/user', $callback, ['GET', 'POST', 'DELETE']];
  415. // 5
  416. $callback = function (RouteCollector $r) {
  417. $r->addRoute('POST', '/user.json', 'handler0');
  418. $r->addRoute('GET', '/{entity}.json', 'handler1');
  419. };
  420. $cases[] = ['PUT', '/user.json', $callback, ['POST', 'GET']];
  421. // x -------------------------------------------------------------------------------------->
  422. return $cases;
  423. }
  424. }