RouteBuilderTest.php 40 KB

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