RouteBuilderTest.php 41 KB

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