RouteBuilderTest.php 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP(tm) Project
  13. * @since 3.0.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Test\TestCase\Routing;
  17. use BadMethodCallException;
  18. use Cake\Core\Exception\MissingPluginException;
  19. use Cake\Core\Plugin;
  20. use Cake\Routing\Route\InflectedRoute;
  21. use Cake\Routing\Route\RedirectRoute;
  22. use Cake\Routing\Route\Route;
  23. use Cake\Routing\RouteBuilder;
  24. use Cake\Routing\RouteCollection;
  25. use Cake\Routing\Router;
  26. use Cake\TestSuite\TestCase;
  27. use InvalidArgumentException;
  28. use RuntimeException;
  29. /**
  30. * RouteBuilder test case
  31. */
  32. class RouteBuilderTest extends TestCase
  33. {
  34. /**
  35. * @var \Cake\Routing\RouteCollection
  36. */
  37. protected $collection;
  38. /**
  39. * Setup method
  40. */
  41. public function setUp(): void
  42. {
  43. parent::setUp();
  44. $this->collection = new RouteCollection();
  45. }
  46. /**
  47. * Teardown method
  48. */
  49. public function tearDown(): void
  50. {
  51. parent::tearDown();
  52. $this->clearPlugins();
  53. }
  54. /**
  55. * Test path()
  56. */
  57. public function testPath(): void
  58. {
  59. $routes = new RouteBuilder($this->collection, '/some/path');
  60. $this->assertSame('/some/path', $routes->path());
  61. $routes = new RouteBuilder($this->collection, '/{book_id}');
  62. $this->assertSame('/', $routes->path());
  63. $routes = new RouteBuilder($this->collection, '/path/{book_id}');
  64. $this->assertSame('/path/', $routes->path());
  65. $routes = new RouteBuilder($this->collection, '/path/book{book_id}');
  66. $this->assertSame('/path/book', $routes->path());
  67. }
  68. /**
  69. * Test params()
  70. */
  71. public function testParams(): void
  72. {
  73. $routes = new RouteBuilder($this->collection, '/api', ['prefix' => 'Api']);
  74. $this->assertEquals(['prefix' => 'Api'], $routes->params());
  75. }
  76. /**
  77. * Test getting connected routes.
  78. */
  79. public function testRoutes(): void
  80. {
  81. $routes = new RouteBuilder($this->collection, '/l');
  82. $routes->connect('/{controller}', ['action' => 'index']);
  83. $routes->connect('/{controller}/{action}/*');
  84. $all = $this->collection->routes();
  85. $this->assertCount(2, $all);
  86. $this->assertInstanceOf(Route::class, $all[0]);
  87. $this->assertInstanceOf(Route::class, $all[1]);
  88. }
  89. /**
  90. * Test setting default route class
  91. */
  92. public function testRouteClass(): void
  93. {
  94. $routes = new RouteBuilder(
  95. $this->collection,
  96. '/l',
  97. [],
  98. ['routeClass' => 'InflectedRoute']
  99. );
  100. $routes->connect('/{controller}', ['action' => 'index']);
  101. $routes->connect('/{controller}/{action}/*');
  102. $all = $this->collection->routes();
  103. $this->assertInstanceOf(InflectedRoute::class, $all[0]);
  104. $this->assertInstanceOf(InflectedRoute::class, $all[1]);
  105. $this->collection = new RouteCollection();
  106. $routes = new RouteBuilder($this->collection, '/l');
  107. $this->assertSame($routes, $routes->setRouteClass('TestApp\Routing\Route\DashedRoute'));
  108. $this->assertSame('TestApp\Routing\Route\DashedRoute', $routes->getRouteClass());
  109. $routes->connect('/{controller}', ['action' => 'index']);
  110. $all = $this->collection->routes();
  111. $this->assertInstanceOf('TestApp\Routing\Route\DashedRoute', $all[0]);
  112. }
  113. /**
  114. * Test connecting an instance routes.
  115. */
  116. public function testConnectInstance(): void
  117. {
  118. $routes = new RouteBuilder($this->collection, '/l', ['prefix' => 'Api']);
  119. $route = new Route('/{controller}');
  120. $this->assertSame($route, $routes->connect($route));
  121. $result = $this->collection->routes()[0];
  122. $this->assertSame($route, $result);
  123. }
  124. /**
  125. * Test connecting basic routes.
  126. */
  127. public function testConnectBasic(): void
  128. {
  129. $routes = new RouteBuilder($this->collection, '/l', ['prefix' => 'Api']);
  130. $route = $routes->connect('/{controller}');
  131. $this->assertInstanceOf(Route::class, $route);
  132. $this->assertSame($route, $this->collection->routes()[0]);
  133. $this->assertSame('/l/{controller}', $route->template);
  134. $expected = ['prefix' => 'Api', 'action' => 'index', 'plugin' => null];
  135. $this->assertEquals($expected, $route->defaults);
  136. }
  137. /**
  138. * Test that compiling a route results in an trailing / optional pattern.
  139. */
  140. public function testConnectTrimTrailingSlash(): void
  141. {
  142. $routes = new RouteBuilder($this->collection, '/articles', ['controller' => 'Articles']);
  143. $routes->connect('/', ['action' => 'index']);
  144. $expected = [
  145. 'plugin' => null,
  146. 'controller' => 'Articles',
  147. 'action' => 'index',
  148. 'pass' => [],
  149. '_matchedRoute' => '/articles',
  150. ];
  151. $result = $this->collection->parse('/articles');
  152. unset($result['_route']);
  153. $this->assertEquals($expected, $result);
  154. $result = $this->collection->parse('/articles/');
  155. unset($result['_route']);
  156. $this->assertEquals($expected, $result);
  157. }
  158. /**
  159. * Test connect() with short string syntax
  160. */
  161. public function testConnectShortStringInvalid(): void
  162. {
  163. $this->expectException(InvalidArgumentException::class);
  164. $routes = new RouteBuilder($this->collection, '/');
  165. $routes->connect('/my-articles/view', 'Articles:no');
  166. }
  167. /**
  168. * Test connect() with short string syntax
  169. */
  170. public function testConnectShortString(): void
  171. {
  172. $routes = new RouteBuilder($this->collection, '/');
  173. $routes->connect('/my-articles/view', 'Articles::view');
  174. $expected = [
  175. 'pass' => [],
  176. 'controller' => 'Articles',
  177. 'action' => 'view',
  178. 'plugin' => null,
  179. '_matchedRoute' => '/my-articles/view',
  180. ];
  181. $result = $this->collection->parse('/my-articles/view');
  182. unset($result['_route']);
  183. $this->assertEquals($expected, $result);
  184. $url = $expected['_matchedRoute'];
  185. unset($expected['_matchedRoute']);
  186. $this->assertSame($url, '/' . $this->collection->match($expected, []));
  187. }
  188. /**
  189. * Test connect() with short string syntax
  190. */
  191. public function testConnectShortStringPrefix(): void
  192. {
  193. $routes = new RouteBuilder($this->collection, '/');
  194. $routes->connect('/admin/bookmarks', 'Admin/Bookmarks::index');
  195. $expected = [
  196. 'pass' => [],
  197. 'plugin' => null,
  198. 'prefix' => 'Admin',
  199. 'controller' => 'Bookmarks',
  200. 'action' => 'index',
  201. '_matchedRoute' => '/admin/bookmarks',
  202. ];
  203. $result = $this->collection->parse('/admin/bookmarks');
  204. unset($result['_route']);
  205. $this->assertEquals($expected, $result);
  206. $url = $expected['_matchedRoute'];
  207. unset($expected['_matchedRoute']);
  208. $this->assertSame($url, '/' . $this->collection->match($expected, []));
  209. }
  210. /**
  211. * Test connect() with short string syntax
  212. */
  213. public function testConnectShortStringPlugin(): void
  214. {
  215. $routes = new RouteBuilder($this->collection, '/');
  216. $routes->connect('/blog/articles/view', 'Blog.Articles::view');
  217. $expected = [
  218. 'pass' => [],
  219. 'plugin' => 'Blog',
  220. 'controller' => 'Articles',
  221. 'action' => 'view',
  222. '_matchedRoute' => '/blog/articles/view',
  223. ];
  224. $result = $this->collection->parse('/blog/articles/view');
  225. unset($result['_route']);
  226. $this->assertEquals($expected, $result);
  227. $url = $expected['_matchedRoute'];
  228. unset($expected['_matchedRoute']);
  229. $this->assertSame($url, '/' . $this->collection->match($expected, []));
  230. }
  231. /**
  232. * Test connect() with short string syntax
  233. */
  234. public function testConnectShortStringPluginPrefix(): void
  235. {
  236. $routes = new RouteBuilder($this->collection, '/');
  237. $routes->connect('/admin/blog/articles/view', 'Vendor/Blog.Management/Admin/Articles::view');
  238. $expected = [
  239. 'pass' => [],
  240. 'plugin' => 'Vendor/Blog',
  241. 'prefix' => 'Management/Admin',
  242. 'controller' => 'Articles',
  243. 'action' => 'view',
  244. '_matchedRoute' => '/admin/blog/articles/view',
  245. ];
  246. $result = $this->collection->parse('/admin/blog/articles/view');
  247. unset($result['_route']);
  248. $this->assertEquals($expected, $result);
  249. $url = $expected['_matchedRoute'];
  250. unset($expected['_matchedRoute']);
  251. $this->assertSame($url, '/' . $this->collection->match($expected, []));
  252. }
  253. /**
  254. * Test if a route name already exist
  255. */
  256. public function testNameExists(): void
  257. {
  258. $routes = new RouteBuilder($this->collection, '/l', ['prefix' => 'Api']);
  259. $this->assertFalse($routes->nameExists('myRouteName'));
  260. $routes->connect('myRouteUrl', ['action' => 'index'], ['_name' => 'myRouteName']);
  261. $this->assertTrue($routes->nameExists('myRouteName'));
  262. }
  263. /**
  264. * Test setExtensions() and getExtensions().
  265. */
  266. public function testExtensions(): void
  267. {
  268. $routes = new RouteBuilder($this->collection, '/l');
  269. $this->assertSame($routes, $routes->setExtensions(['html']));
  270. $this->assertSame(['html'], $routes->getExtensions());
  271. }
  272. /**
  273. * Test extensions being connected to routes.
  274. */
  275. public function testConnectExtensions(): void
  276. {
  277. $routes = new RouteBuilder(
  278. $this->collection,
  279. '/l',
  280. [],
  281. ['extensions' => ['json']]
  282. );
  283. $this->assertEquals(['json'], $routes->getExtensions());
  284. $routes->connect('/{controller}');
  285. $route = $this->collection->routes()[0];
  286. $this->assertEquals(['json'], $route->options['_ext']);
  287. $routes->setExtensions(['xml', 'json']);
  288. $routes->connect('/{controller}/{action}');
  289. $new = $this->collection->routes()[1];
  290. $this->assertEquals(['json'], $route->options['_ext']);
  291. $this->assertEquals(['xml', 'json'], $new->options['_ext']);
  292. }
  293. /**
  294. * Test adding additional extensions will be merged with current.
  295. */
  296. public function testConnectExtensionsAdd(): void
  297. {
  298. $routes = new RouteBuilder(
  299. $this->collection,
  300. '/l',
  301. [],
  302. ['extensions' => ['json']]
  303. );
  304. $this->assertEquals(['json'], $routes->getExtensions());
  305. $routes->addExtensions(['xml']);
  306. $this->assertEquals(['json', 'xml'], $routes->getExtensions());
  307. $routes->addExtensions('csv');
  308. $this->assertEquals(['json', 'xml', 'csv'], $routes->getExtensions());
  309. }
  310. /**
  311. * test that setExtensions() accepts a string.
  312. */
  313. public function testExtensionsString(): void
  314. {
  315. $routes = new RouteBuilder($this->collection, '/l');
  316. $routes->setExtensions('json');
  317. $this->assertEquals(['json'], $routes->getExtensions());
  318. }
  319. /**
  320. * Test conflicting parameters raises an exception.
  321. */
  322. public function testConnectConflictingParameters(): void
  323. {
  324. $this->expectException(BadMethodCallException::class);
  325. $this->expectExceptionMessage('You cannot define routes that conflict with the scope.');
  326. $routes = new RouteBuilder($this->collection, '/admin', ['plugin' => 'TestPlugin']);
  327. $routes->connect('/', ['plugin' => 'TestPlugin2', 'controller' => 'Dashboard', 'action' => 'view']);
  328. }
  329. /**
  330. * Test connecting redirect routes.
  331. */
  332. public function testRedirect(): void
  333. {
  334. $routes = new RouteBuilder($this->collection, '/');
  335. $routes->redirect('/p/{id}', ['controller' => 'Posts', 'action' => 'view'], ['status' => 301]);
  336. $route = $this->collection->routes()[0];
  337. $this->assertInstanceOf(RedirectRoute::class, $route);
  338. $routes->redirect('/old', '/forums', ['status' => 301]);
  339. $route = $this->collection->routes()[1];
  340. $this->assertInstanceOf(RedirectRoute::class, $route);
  341. $this->assertSame('/forums', $route->redirect[0]);
  342. $route = $routes->redirect('/old', '/forums');
  343. $this->assertInstanceOf(RedirectRoute::class, $route);
  344. $this->assertSame($route, $this->collection->routes()[2]);
  345. }
  346. /**
  347. * Test using a custom route class for redirect routes.
  348. */
  349. public function testRedirectWithCustomRouteClass(): void
  350. {
  351. $routes = new RouteBuilder($this->collection, '/');
  352. $routes->redirect('/old', '/forums', ['status' => 301, 'routeClass' => 'InflectedRoute']);
  353. $route = $this->collection->routes()[0];
  354. $this->assertInstanceOf(InflectedRoute::class, $route);
  355. }
  356. /**
  357. * Test creating sub-scopes with prefix()
  358. */
  359. public function testPrefix(): void
  360. {
  361. $routes = new RouteBuilder($this->collection, '/path', ['key' => 'value']);
  362. $res = $routes->prefix('admin', ['param' => 'value'], function (RouteBuilder $r): void {
  363. $this->assertInstanceOf(RouteBuilder::class, $r);
  364. $this->assertCount(0, $this->collection->routes());
  365. $this->assertSame('/path/admin', $r->path());
  366. $this->assertEquals(['prefix' => 'Admin', 'key' => 'value', 'param' => 'value'], $r->params());
  367. });
  368. $this->assertSame($routes, $res);
  369. }
  370. /**
  371. * Test creating sub-scopes with prefix()
  372. */
  373. public function testPrefixWithNoParams(): void
  374. {
  375. $routes = new RouteBuilder($this->collection, '/path', ['key' => 'value']);
  376. $res = $routes->prefix('admin', function (RouteBuilder $r): void {
  377. $this->assertInstanceOf(RouteBuilder::class, $r);
  378. $this->assertCount(0, $this->collection->routes());
  379. $this->assertSame('/path/admin', $r->path());
  380. $this->assertEquals(['prefix' => 'Admin', 'key' => 'value'], $r->params());
  381. });
  382. $this->assertSame($routes, $res);
  383. }
  384. /**
  385. * Test creating sub-scopes with prefix()
  386. */
  387. public function testNestedPrefix(): void
  388. {
  389. $routes = new RouteBuilder($this->collection, '/admin', ['prefix' => 'Admin']);
  390. $res = $routes->prefix('api', ['_namePrefix' => 'api:'], function (RouteBuilder $r): void {
  391. $this->assertSame('/admin/api', $r->path());
  392. $this->assertEquals(['prefix' => 'Admin/Api'], $r->params());
  393. $this->assertSame('api:', $r->namePrefix());
  394. });
  395. $this->assertSame($routes, $res);
  396. }
  397. /**
  398. * Test creating sub-scopes with prefix()
  399. */
  400. public function testPathWithDotInPrefix(): void
  401. {
  402. $routes = new RouteBuilder($this->collection, '/admin', ['prefix' => 'Admin']);
  403. $res = $routes->prefix('Api', function (RouteBuilder $r): void {
  404. $r->prefix('v10', ['path' => '/v1.0'], function (RouteBuilder $r2): void {
  405. $this->assertSame('/admin/api/v1.0', $r2->path());
  406. $this->assertEquals(['prefix' => 'Admin/Api/V10'], $r2->params());
  407. $r2->prefix('b1', ['path' => '/beta.1'], function (RouteBuilder $r3): void {
  408. $this->assertSame('/admin/api/v1.0/beta.1', $r3->path());
  409. $this->assertEquals(['prefix' => 'Admin/Api/V10/B1'], $r3->params());
  410. });
  411. });
  412. });
  413. $this->assertSame($routes, $res);
  414. }
  415. /**
  416. * Test creating sub-scopes with plugin()
  417. */
  418. public function testPlugin(): void
  419. {
  420. $routes = new RouteBuilder($this->collection, '/b', ['key' => 'value']);
  421. $res = $routes->plugin('Contacts', function (RouteBuilder $r): void {
  422. $this->assertSame('/b/contacts', $r->path());
  423. $this->assertEquals(['plugin' => 'Contacts', 'key' => 'value'], $r->params());
  424. $r->connect('/{controller}');
  425. $route = $this->collection->routes()[0];
  426. $this->assertEquals(
  427. ['key' => 'value', 'plugin' => 'Contacts', 'action' => 'index'],
  428. $route->defaults
  429. );
  430. });
  431. $this->assertSame($routes, $res);
  432. }
  433. /**
  434. * Test creating sub-scopes with plugin() + path option
  435. */
  436. public function testPluginPathOption(): void
  437. {
  438. $routes = new RouteBuilder($this->collection, '/b', ['key' => 'value']);
  439. $routes->plugin('Contacts', ['path' => '/people'], function (RouteBuilder $r): void {
  440. $this->assertSame('/b/people', $r->path());
  441. $this->assertEquals(['plugin' => 'Contacts', 'key' => 'value'], $r->params());
  442. });
  443. }
  444. /**
  445. * Test creating sub-scopes with plugin() + namePrefix option
  446. */
  447. public function testPluginNamePrefix(): void
  448. {
  449. $routes = new RouteBuilder($this->collection, '/b', ['key' => 'value']);
  450. $routes->plugin('Contacts', ['_namePrefix' => 'contacts.'], function (RouteBuilder $r): void {
  451. $this->assertEquals('contacts.', $r->namePrefix());
  452. });
  453. $routes = new RouteBuilder($this->collection, '/b', ['key' => 'value']);
  454. $routes->namePrefix('default.');
  455. $routes->plugin('Blog', ['_namePrefix' => 'blog.'], function (RouteBuilder $r): void {
  456. $this->assertEquals('default.blog.', $r->namePrefix(), 'Should combine nameprefix');
  457. });
  458. }
  459. /**
  460. * Test connecting resources.
  461. */
  462. public function testResources(): void
  463. {
  464. $routes = new RouteBuilder($this->collection, '/api', ['prefix' => 'Api']);
  465. $routes->resources('Articles', ['_ext' => 'json']);
  466. $all = $this->collection->routes();
  467. $this->assertCount(5, $all);
  468. $this->assertSame('/api/articles', $all[4]->template);
  469. $this->assertEquals(
  470. ['controller', 'action', '_method', 'prefix', 'plugin'],
  471. array_keys($all[0]->defaults)
  472. );
  473. $this->assertSame('json', $all[0]->options['_ext']);
  474. $this->assertSame('Articles', $all[0]->defaults['controller']);
  475. }
  476. /**
  477. * Test connecting resources with a path
  478. */
  479. public function testResourcesPathOption(): void
  480. {
  481. $routes = new RouteBuilder($this->collection, '/api');
  482. $routes->resources('Articles', ['path' => 'posts'], function (RouteBuilder $routes): void {
  483. $routes->resources('Comments');
  484. });
  485. $all = $this->collection->routes();
  486. $this->assertSame('Articles', $all[8]->defaults['controller']);
  487. $this->assertSame('/api/posts', $all[8]->template);
  488. $this->assertSame('/api/posts/{id}', $all[1]->template);
  489. $this->assertSame(
  490. '/api/posts/{article_id}/comments',
  491. $all[4]->template,
  492. 'parameter name should reflect resource name'
  493. );
  494. }
  495. /**
  496. * Test connecting resources with a prefix
  497. */
  498. public function testResourcesPrefix(): void
  499. {
  500. $routes = new RouteBuilder($this->collection, '/api');
  501. $routes->resources('Articles', ['prefix' => 'Rest']);
  502. $all = $this->collection->routes();
  503. $this->assertSame('Rest', $all[0]->defaults['prefix']);
  504. }
  505. /**
  506. * Test that resource prefixes work within a prefixed scope.
  507. */
  508. public function testResourcesNestedPrefix(): void
  509. {
  510. $routes = new RouteBuilder($this->collection, '/api', ['prefix' => 'Api']);
  511. $routes->resources('Articles', ['prefix' => 'Rest']);
  512. $all = $this->collection->routes();
  513. $this->assertCount(5, $all);
  514. $this->assertSame('/api/articles', $all[4]->template);
  515. foreach ($all as $route) {
  516. $this->assertSame('Api/Rest', $route->defaults['prefix']);
  517. $this->assertSame('Articles', $route->defaults['controller']);
  518. }
  519. }
  520. /**
  521. * Test connecting resources with the inflection option
  522. */
  523. public function testResourcesInflection(): void
  524. {
  525. $routes = new RouteBuilder($this->collection, '/api', ['prefix' => 'Api']);
  526. $routes->resources('BlogPosts', ['_ext' => 'json', 'inflect' => 'dasherize']);
  527. $all = $this->collection->routes();
  528. $this->assertCount(5, $all);
  529. $this->assertSame('/api/blog-posts', $all[4]->template);
  530. $this->assertEquals(
  531. ['controller', 'action', '_method', 'prefix', 'plugin'],
  532. array_keys($all[0]->defaults)
  533. );
  534. $this->assertSame('BlogPosts', $all[0]->defaults['controller']);
  535. }
  536. /**
  537. * Test connecting nested resources with the inflection option
  538. */
  539. public function testResourcesNestedInflection(): void
  540. {
  541. $routes = new RouteBuilder($this->collection, '/api');
  542. $routes->resources(
  543. 'NetworkObjects',
  544. ['inflect' => 'dasherize'],
  545. function (RouteBuilder $routes): void {
  546. $routes->resources('Attributes');
  547. }
  548. );
  549. $all = $this->collection->routes();
  550. $this->assertCount(10, $all);
  551. $this->assertSame('/api/network-objects', $all[8]->template);
  552. $this->assertSame('/api/network-objects/{id}', $all[2]->template);
  553. $this->assertSame('/api/network-objects/{network_object_id}/attributes', $all[4]->template);
  554. }
  555. /**
  556. * Test connecting resources with additional mappings
  557. */
  558. public function testResourcesMappings(): void
  559. {
  560. $routes = new RouteBuilder($this->collection, '/api', ['prefix' => 'Api']);
  561. $routes->resources('Articles', [
  562. '_ext' => 'json',
  563. 'map' => [
  564. 'delete_all' => ['action' => 'deleteAll', 'method' => 'DELETE'],
  565. 'update_many' => ['action' => 'updateAll', 'method' => 'DELETE', 'path' => '/updateAll'],
  566. ],
  567. ]);
  568. $all = $this->collection->routes();
  569. $this->assertCount(7, $all);
  570. $this->assertSame('/api/articles/delete_all', $all[1]->template, 'Path defaults to key name.');
  571. $this->assertEquals(
  572. ['controller', 'action', '_method', 'prefix', 'plugin'],
  573. array_keys($all[5]->defaults)
  574. );
  575. $this->assertSame('Articles', $all[5]->defaults['controller']);
  576. $this->assertSame('deleteAll', $all[1]->defaults['action']);
  577. $this->assertSame('/api/articles/updateAll', $all[0]->template, 'Explicit path option');
  578. $this->assertEquals(
  579. ['controller', 'action', '_method', 'prefix', 'plugin'],
  580. array_keys($all[6]->defaults)
  581. );
  582. $this->assertSame('Articles', $all[6]->defaults['controller']);
  583. $this->assertSame('updateAll', $all[0]->defaults['action']);
  584. }
  585. /**
  586. * Test connecting resources with restricted mappings.
  587. */
  588. public function testResourcesWithMapOnly(): void
  589. {
  590. $routes = new RouteBuilder($this->collection, '/api', ['prefix' => 'Api']);
  591. $routes->resources('Articles', [
  592. 'map' => [
  593. 'conditions' => ['action' => 'conditions', 'method' => 'DeLeTe'],
  594. ],
  595. 'only' => ['conditions'],
  596. ]);
  597. $all = $this->collection->routes();
  598. $this->assertCount(1, $all);
  599. $this->assertSame('DELETE', $all[0]->defaults['_method'], 'method should be normalized.');
  600. $this->assertSame('Articles', $all[0]->defaults['controller']);
  601. $this->assertSame('conditions', $all[0]->defaults['action']);
  602. $result = $this->collection->parse('/api/articles/conditions', 'DELETE');
  603. $this->assertNotNull($result);
  604. }
  605. /**
  606. * Test connecting resources.
  607. */
  608. public function testResourcesInScope(): void
  609. {
  610. $builder = Router::createRouteBuilder('/');
  611. $builder->scope('/api', ['prefix' => 'Api'], function (RouteBuilder $routes): void {
  612. $routes->setExtensions(['json']);
  613. $routes->resources('Articles');
  614. });
  615. $url = Router::url([
  616. 'prefix' => 'Api',
  617. 'controller' => 'Articles',
  618. 'action' => 'edit',
  619. '_method' => 'PUT',
  620. 'id' => '99',
  621. ]);
  622. $this->assertSame('/api/articles/99', $url);
  623. $url = Router::url([
  624. 'prefix' => 'Api',
  625. 'controller' => 'Articles',
  626. 'action' => 'edit',
  627. '_method' => 'PUT',
  628. '_ext' => 'json',
  629. 'id' => '99',
  630. ]);
  631. $this->assertSame('/api/articles/99.json', $url);
  632. }
  633. /**
  634. * Test resource parsing.
  635. */
  636. public function testResourcesParsing(): void
  637. {
  638. $routes = new RouteBuilder($this->collection, '/');
  639. $routes->resources('Articles');
  640. $result = $this->collection->parse('/articles', 'GET');
  641. $this->assertSame('Articles', $result['controller']);
  642. $this->assertSame('index', $result['action']);
  643. $this->assertEquals([], $result['pass']);
  644. $result = $this->collection->parse('/articles/1', 'GET');
  645. $this->assertSame('Articles', $result['controller']);
  646. $this->assertSame('view', $result['action']);
  647. $this->assertEquals([1], $result['pass']);
  648. $result = $this->collection->parse('/articles', 'POST');
  649. $this->assertSame('Articles', $result['controller']);
  650. $this->assertSame('add', $result['action']);
  651. $this->assertEquals([], $result['pass']);
  652. $result = $this->collection->parse('/articles/1', 'PUT');
  653. $this->assertSame('Articles', $result['controller']);
  654. $this->assertSame('edit', $result['action']);
  655. $this->assertEquals([1], $result['pass']);
  656. $result = $this->collection->parse('/articles/1', 'DELETE');
  657. $this->assertSame('Articles', $result['controller']);
  658. $this->assertSame('delete', $result['action']);
  659. $this->assertEquals([1], $result['pass']);
  660. }
  661. /**
  662. * Test the only option of RouteBuilder.
  663. */
  664. public function testResourcesOnlyString(): void
  665. {
  666. $routes = new RouteBuilder($this->collection, '/');
  667. $routes->resources('Articles', ['only' => 'index']);
  668. $result = $this->collection->routes();
  669. $this->assertCount(1, $result);
  670. $this->assertSame('/articles', $result[0]->template);
  671. }
  672. /**
  673. * Test the only option of RouteBuilder.
  674. */
  675. public function testResourcesOnlyArray(): void
  676. {
  677. $routes = new RouteBuilder($this->collection, '/');
  678. $routes->resources('Articles', ['only' => ['index', 'delete']]);
  679. $result = $this->collection->routes();
  680. $this->assertCount(2, $result);
  681. $this->assertSame('/articles', $result[1]->template);
  682. $this->assertSame('index', $result[1]->defaults['action']);
  683. $this->assertSame('GET', $result[1]->defaults['_method']);
  684. $this->assertSame('/articles/{id}', $result[0]->template);
  685. $this->assertSame('delete', $result[0]->defaults['action']);
  686. $this->assertSame('DELETE', $result[0]->defaults['_method']);
  687. }
  688. /**
  689. * Test the actions option of RouteBuilder.
  690. */
  691. public function testResourcesActions(): void
  692. {
  693. $routes = new RouteBuilder($this->collection, '/');
  694. $routes->resources('Articles', [
  695. 'only' => ['index', 'delete'],
  696. 'actions' => ['index' => 'showList'],
  697. ]);
  698. $result = $this->collection->routes();
  699. $this->assertCount(2, $result);
  700. $this->assertSame('/articles', $result[1]->template);
  701. $this->assertSame('showList', $result[1]->defaults['action']);
  702. $this->assertSame('/articles/{id}', $result[0]->template);
  703. $this->assertSame('delete', $result[0]->defaults['action']);
  704. }
  705. /**
  706. * Test nesting resources
  707. */
  708. public function testResourcesNested(): void
  709. {
  710. $routes = new RouteBuilder($this->collection, '/api', ['prefix' => 'Api']);
  711. $routes->resources('Articles', function (RouteBuilder $routes): void {
  712. $this->assertSame('/api/articles/', $routes->path());
  713. $this->assertEquals(['prefix' => 'Api'], $routes->params());
  714. $routes->resources('Comments');
  715. $route = $this->collection->routes()[3];
  716. $this->assertSame('/api/articles/{article_id}/comments', $route->template);
  717. });
  718. }
  719. /**
  720. * Test connecting fallback routes.
  721. */
  722. public function testFallbacks(): void
  723. {
  724. $routes = new RouteBuilder($this->collection, '/api', ['prefix' => 'Api']);
  725. $routes->fallbacks();
  726. $all = $this->collection->routes();
  727. $this->assertSame('/api/{controller}', $all[0]->template);
  728. $this->assertSame('/api/{controller}/{action}/*', $all[1]->template);
  729. $this->assertInstanceOf(Route::class, $all[0]);
  730. }
  731. /**
  732. * Test connecting fallback routes with specific route class
  733. */
  734. public function testFallbacksWithClass(): void
  735. {
  736. $routes = new RouteBuilder($this->collection, '/api', ['prefix' => 'Api']);
  737. $routes->fallbacks('InflectedRoute');
  738. $all = $this->collection->routes();
  739. $this->assertSame('/api/{controller}', $all[0]->template);
  740. $this->assertSame('/api/{controller}/{action}/*', $all[1]->template);
  741. $this->assertInstanceOf(InflectedRoute::class, $all[0]);
  742. }
  743. /**
  744. * Test connecting fallback routes after setting default route class.
  745. */
  746. public function testDefaultRouteClassFallbacks(): void
  747. {
  748. $routes = new RouteBuilder($this->collection, '/api', ['prefix' => 'Api']);
  749. $routes->setRouteClass('TestApp\Routing\Route\DashedRoute');
  750. $routes->fallbacks();
  751. $all = $this->collection->routes();
  752. $this->assertInstanceOf('TestApp\Routing\Route\DashedRoute', $all[0]);
  753. }
  754. /**
  755. * Test adding a scope.
  756. */
  757. public function testScope(): void
  758. {
  759. $routes = new RouteBuilder($this->collection, '/api', ['prefix' => 'Api']);
  760. $routes->scope('/v1', ['version' => 1], function (RouteBuilder $routes): void {
  761. $this->assertSame('/api/v1', $routes->path());
  762. $this->assertEquals(['prefix' => 'Api', 'version' => 1], $routes->params());
  763. });
  764. }
  765. /**
  766. * Test adding a scope with action in the scope
  767. */
  768. public function testScopeWithAction(): void
  769. {
  770. $routes = new RouteBuilder($this->collection, '/api', ['prefix' => 'Api']);
  771. $routes->scope('/prices', ['controller' => 'Prices', 'action' => 'view'], function (RouteBuilder $routes): void {
  772. $routes->connect('/shared', ['shared' => true]);
  773. $routes->get('/exclusive', ['exclusive' => true]);
  774. });
  775. $all = $this->collection->routes();
  776. $this->assertCount(2, $all);
  777. $this->assertSame('view', $all[0]->defaults['action']);
  778. $this->assertArrayHasKey('shared', $all[0]->defaults);
  779. $this->assertSame('view', $all[1]->defaults['action']);
  780. $this->assertArrayHasKey('exclusive', $all[1]->defaults);
  781. }
  782. /**
  783. * Test that exception is thrown if callback is not a valid callable.
  784. */
  785. public function testScopeException(): void
  786. {
  787. $this->expectException(InvalidArgumentException::class);
  788. $this->expectExceptionMessage('Need a valid Closure to connect routes.');
  789. $routes = new RouteBuilder($this->collection, '/api', ['prefix' => 'Api']);
  790. $routes->scope('/v1', ['fail'], null);
  791. }
  792. /**
  793. * Test that nested scopes inherit middleware.
  794. */
  795. public function testScopeInheritMiddleware(): void
  796. {
  797. $routes = new RouteBuilder(
  798. $this->collection,
  799. '/api',
  800. ['prefix' => 'Api'],
  801. ['middleware' => ['auth']]
  802. );
  803. $routes->scope('/v1', function (RouteBuilder $routes): void {
  804. $this->assertSame(['auth'], $routes->getMiddleware(), 'Should inherit middleware');
  805. $this->assertSame('/api/v1', $routes->path());
  806. $this->assertEquals(['prefix' => 'Api'], $routes->params());
  807. });
  808. }
  809. /**
  810. * Test using name prefixes.
  811. */
  812. public function testNamePrefixes(): void
  813. {
  814. $routes = new RouteBuilder($this->collection, '/api', [], ['namePrefix' => 'api:']);
  815. $routes->scope('/v1', ['version' => 1, '_namePrefix' => 'v1:'], function (RouteBuilder $routes): void {
  816. $this->assertSame('api:v1:', $routes->namePrefix());
  817. $routes->connect('/ping', ['controller' => 'Pings'], ['_name' => 'ping']);
  818. $routes->namePrefix('web:');
  819. $routes->connect('/pong', ['controller' => 'Pongs'], ['_name' => 'pong']);
  820. });
  821. $all = $this->collection->named();
  822. $this->assertArrayHasKey('api:v1:ping', $all);
  823. $this->assertArrayHasKey('web:pong', $all);
  824. }
  825. /**
  826. * Test adding middleware to the collection.
  827. */
  828. public function testRegisterMiddleware(): void
  829. {
  830. $func = function (): void {
  831. };
  832. $routes = new RouteBuilder($this->collection, '/api');
  833. $result = $routes->registerMiddleware('test', $func);
  834. $this->assertSame($result, $routes);
  835. $this->assertTrue($this->collection->hasMiddleware('test'));
  836. $this->assertTrue($this->collection->middlewareExists('test'));
  837. }
  838. /**
  839. * Test middleware group
  840. */
  841. public function testMiddlewareGroup(): void
  842. {
  843. $func = function (): void {
  844. };
  845. $routes = new RouteBuilder($this->collection, '/api');
  846. $routes->registerMiddleware('test', $func);
  847. $routes->registerMiddleware('test_two', $func);
  848. $result = $routes->middlewareGroup('group', ['test', 'test_two']);
  849. $this->assertSame($result, $routes);
  850. $this->assertTrue($this->collection->hasMiddlewareGroup('group'));
  851. $this->assertTrue($this->collection->middlewareExists('group'));
  852. }
  853. /**
  854. * Test overlap between middleware name and group name
  855. */
  856. public function testMiddlewareGroupOverlap(): void
  857. {
  858. $this->expectException(RuntimeException::class);
  859. $this->expectExceptionMessage('Cannot add middleware group \'test\'. A middleware by this name has already been registered.');
  860. $func = function (): void {
  861. };
  862. $routes = new RouteBuilder($this->collection, '/api');
  863. $routes->registerMiddleware('test', $func);
  864. $result = $routes->middlewareGroup('test', ['test']);
  865. }
  866. /**
  867. * Test applying middleware to a scope when it doesn't exist
  868. */
  869. public function testApplyMiddlewareInvalidName(): void
  870. {
  871. $this->expectException(RuntimeException::class);
  872. $this->expectExceptionMessage('Cannot apply \'bad\' middleware or middleware group. Use registerMiddleware() to register middleware');
  873. $routes = new RouteBuilder($this->collection, '/api');
  874. $routes->applyMiddleware('bad');
  875. }
  876. /**
  877. * Test applying middleware to a scope
  878. */
  879. public function testApplyMiddleware(): void
  880. {
  881. $func = function (): void {
  882. };
  883. $routes = new RouteBuilder($this->collection, '/api');
  884. $routes->registerMiddleware('test', $func)
  885. ->registerMiddleware('test2', $func);
  886. $result = $routes->applyMiddleware('test', 'test2');
  887. $this->assertSame($result, $routes);
  888. }
  889. /**
  890. * Test that applyMiddleware() merges with previous data.
  891. */
  892. public function testApplyMiddlewareMerges(): void
  893. {
  894. $func = function (): void {
  895. };
  896. $routes = new RouteBuilder($this->collection, '/api');
  897. $routes->registerMiddleware('test', $func)
  898. ->registerMiddleware('test2', $func);
  899. $routes->applyMiddleware('test');
  900. $routes->applyMiddleware('test2');
  901. $this->assertSame(['test', 'test2'], $routes->getMiddleware());
  902. }
  903. /**
  904. * Test that applyMiddleware() uses unique middleware set
  905. */
  906. public function testApplyMiddlewareUnique(): void
  907. {
  908. $func = function (): void {
  909. };
  910. $routes = new RouteBuilder($this->collection, '/api');
  911. $routes->registerMiddleware('test', $func)
  912. ->registerMiddleware('test2', $func);
  913. $routes->applyMiddleware('test', 'test2');
  914. $routes->applyMiddleware('test2', 'test');
  915. $this->assertEquals(['test', 'test2'], $routes->getMiddleware());
  916. }
  917. /**
  918. * Test applying middleware results in middleware attached to the route.
  919. */
  920. public function testApplyMiddlewareAttachToRoutes(): void
  921. {
  922. $func = function (): void {
  923. };
  924. $routes = new RouteBuilder($this->collection, '/api');
  925. $routes->registerMiddleware('test', $func)
  926. ->registerMiddleware('test2', $func);
  927. $routes->applyMiddleware('test', 'test2');
  928. $route = $routes->get('/docs', ['controller' => 'Docs']);
  929. $this->assertSame(['test', 'test2'], $route->getMiddleware());
  930. }
  931. /**
  932. * @return array
  933. */
  934. public static function httpMethodProvider(): array
  935. {
  936. return [
  937. ['GET'],
  938. ['POST'],
  939. ['PUT'],
  940. ['PATCH'],
  941. ['DELETE'],
  942. ['OPTIONS'],
  943. ['HEAD'],
  944. ];
  945. }
  946. /**
  947. * Test that the HTTP method helpers create the right kind of routes.
  948. *
  949. * @dataProvider httpMethodProvider
  950. */
  951. public function testHttpMethods(string $method): void
  952. {
  953. $routes = new RouteBuilder($this->collection, '/', [], ['namePrefix' => 'app:']);
  954. $route = $routes->{strtolower($method)}(
  955. '/bookmarks/{id}',
  956. ['controller' => 'Bookmarks', 'action' => 'view'],
  957. 'route-name'
  958. );
  959. $this->assertInstanceOf(Route::class, $route, 'Should return a route');
  960. $this->assertSame($method, $route->defaults['_method']);
  961. $this->assertSame('app:route-name', $route->options['_name']);
  962. $this->assertSame('/bookmarks/{id}', $route->template);
  963. $this->assertEquals(
  964. ['plugin' => null, 'controller' => 'Bookmarks', 'action' => 'view', '_method' => $method],
  965. $route->defaults
  966. );
  967. }
  968. /**
  969. * Test that the HTTP method helpers create the right kind of routes.
  970. *
  971. * @dataProvider httpMethodProvider
  972. */
  973. public function testHttpMethodsStringTarget(string $method): void
  974. {
  975. $routes = new RouteBuilder($this->collection, '/', [], ['namePrefix' => 'app:']);
  976. $route = $routes->{strtolower($method)}(
  977. '/bookmarks/{id}',
  978. 'Bookmarks::view',
  979. 'route-name'
  980. );
  981. $this->assertInstanceOf(Route::class, $route, 'Should return a route');
  982. $this->assertSame($method, $route->defaults['_method']);
  983. $this->assertSame('app:route-name', $route->options['_name']);
  984. $this->assertSame('/bookmarks/{id}', $route->template);
  985. $this->assertEquals(
  986. ['plugin' => null, 'controller' => 'Bookmarks', 'action' => 'view', '_method' => $method],
  987. $route->defaults
  988. );
  989. }
  990. /**
  991. * Integration test for http method helpers and route fluent method
  992. */
  993. public function testHttpMethodIntegration(): void
  994. {
  995. $routes = new RouteBuilder($this->collection, '/');
  996. $routes->scope('/', function (RouteBuilder $routes): void {
  997. $routes->get('/faq/{page}', ['controller' => 'Pages', 'action' => 'faq'], 'faq')
  998. ->setPatterns(['page' => '[a-z0-9_]+'])
  999. ->setHost('docs.example.com');
  1000. $routes->post('/articles/{id}', ['controller' => 'Articles', 'action' => 'update'], 'article:update')
  1001. ->setPatterns(['id' => '[0-9]+'])
  1002. ->setPass(['id']);
  1003. });
  1004. $this->assertCount(2, $this->collection->routes());
  1005. $this->assertEquals(['faq', 'article:update'], array_keys($this->collection->named()));
  1006. $this->assertNotEmpty($this->collection->parse('/faq/things_you_know', 'GET'));
  1007. $result = $this->collection->parse('/articles/123', 'POST');
  1008. $this->assertEquals(['123'], $result['pass']);
  1009. }
  1010. /**
  1011. * Test loading routes from a missing plugin
  1012. */
  1013. public function testLoadPluginBadPlugin(): void
  1014. {
  1015. $this->expectException(MissingPluginException::class);
  1016. $routes = new RouteBuilder($this->collection, '/');
  1017. $routes->loadPlugin('Nope');
  1018. }
  1019. /**
  1020. * Test loading routes with success
  1021. */
  1022. public function testLoadPlugin(): void
  1023. {
  1024. $this->loadPlugins(['TestPlugin']);
  1025. $routes = new RouteBuilder($this->collection, '/');
  1026. $routes->loadPlugin('TestPlugin');
  1027. $this->assertCount(1, $this->collection->routes());
  1028. $this->assertNotEmpty($this->collection->parse('/test_plugin', 'GET'));
  1029. $plugin = Plugin::getCollection()->get('TestPlugin');
  1030. $this->assertFalse($plugin->isEnabled('routes'), 'Hook should be disabled preventing duplicate routes');
  1031. }
  1032. }