RouteBuilderTest.php 39 KB

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