RouteCollectionTest.php 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844
  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\Http\ServerRequest;
  18. use Cake\Routing\Exception\DuplicateNamedRouteException;
  19. use Cake\Routing\Exception\MissingRouteException;
  20. use Cake\Routing\Route\Route;
  21. use Cake\Routing\RouteBuilder;
  22. use Cake\Routing\RouteCollection;
  23. use Cake\TestSuite\TestCase;
  24. use RuntimeException;
  25. class RouteCollectionTest extends TestCase
  26. {
  27. /**
  28. * @var \Cake\Routing\RouteCollection
  29. */
  30. protected $collection;
  31. /**
  32. * Setup method
  33. */
  34. public function setUp(): void
  35. {
  36. parent::setUp();
  37. $this->collection = new RouteCollection();
  38. }
  39. /**
  40. * Test parse() throws an error on unknown routes.
  41. */
  42. public function testParseMissingRoute(): void
  43. {
  44. $this->expectException(MissingRouteException::class);
  45. $this->expectExceptionMessage('A route matching "/" could not be found');
  46. $routes = new RouteBuilder($this->collection, '/b', ['key' => 'value']);
  47. $routes->connect('/', ['controller' => 'Articles']);
  48. $routes->connect('/{id}', ['controller' => 'Articles', 'action' => 'view']);
  49. $this->deprecated(function () {
  50. $result = $this->collection->parse('/');
  51. $this->assertEquals([], $result, 'Should not match, missing /b');
  52. });
  53. }
  54. /**
  55. * Test parse() throws an error on known routes called with unknown methods.
  56. */
  57. public function testParseMissingRouteMethod(): void
  58. {
  59. $this->expectException(MissingRouteException::class);
  60. $this->expectExceptionMessage('A "POST" route matching "/b" could not be found');
  61. $routes = new RouteBuilder($this->collection, '/b', ['key' => 'value']);
  62. $routes->connect('/', ['controller' => 'Articles', '_method' => ['GET']]);
  63. $this->deprecated(function () {
  64. $result = $this->collection->parse('/b', 'GET');
  65. $this->assertNotEmpty($result, 'Route should be found');
  66. $result = $this->collection->parse('/b', 'POST');
  67. $this->assertEquals([], $result, 'Should not match with missing method');
  68. });
  69. }
  70. /**
  71. * Test parsing routes.
  72. */
  73. public function testParse(): void
  74. {
  75. $routes = new RouteBuilder($this->collection, '/b', ['key' => 'value']);
  76. $routes->connect('/', ['controller' => 'Articles']);
  77. $routes->connect('/{id}', ['controller' => 'Articles', 'action' => 'view']);
  78. $routes->connect('/media/search/*', ['controller' => 'Media', 'action' => 'search']);
  79. $this->deprecated(function () {
  80. $result = $this->collection->parse('/b/');
  81. unset($result['_route']);
  82. $expected = [
  83. 'controller' => 'Articles',
  84. 'action' => 'index',
  85. 'pass' => [],
  86. 'plugin' => null,
  87. 'key' => 'value',
  88. '_matchedRoute' => '/b',
  89. ];
  90. $this->assertEquals($expected, $result);
  91. $result = $this->collection->parse('/b/the-thing?one=two');
  92. unset($result['_route']);
  93. $expected = [
  94. 'controller' => 'Articles',
  95. 'action' => 'view',
  96. 'id' => 'the-thing',
  97. 'pass' => [],
  98. 'plugin' => null,
  99. 'key' => 'value',
  100. '?' => ['one' => 'two'],
  101. '_matchedRoute' => '/b/{id}',
  102. ];
  103. $this->assertEquals($expected, $result);
  104. $result = $this->collection->parse('/b/media/search');
  105. unset($result['_route']);
  106. $expected = [
  107. 'key' => 'value',
  108. 'pass' => [],
  109. 'plugin' => null,
  110. 'controller' => 'Media',
  111. 'action' => 'search',
  112. '_matchedRoute' => '/b/media/search/*',
  113. ];
  114. $this->assertEquals($expected, $result);
  115. $result = $this->collection->parse('/b/media/search/thing');
  116. unset($result['_route']);
  117. $expected = [
  118. 'key' => 'value',
  119. 'pass' => ['thing'],
  120. 'plugin' => null,
  121. 'controller' => 'Media',
  122. 'action' => 'search',
  123. '_matchedRoute' => '/b/media/search/*',
  124. ];
  125. $this->assertEquals($expected, $result);
  126. });
  127. }
  128. /**
  129. * Test parse() handling query strings.
  130. */
  131. public function testParseQueryString(): void
  132. {
  133. $routes = new RouteBuilder($this->collection, '/');
  134. $routes->connect('/{id}', ['controller' => 'Articles', 'action' => 'view']);
  135. $routes->connect('/media/search/*', ['controller' => 'Media', 'action' => 'search']);
  136. $this->deprecated(function () {
  137. $result = $this->collection->parse('/media/search/php?one=two');
  138. unset($result['_route']);
  139. $expected = [
  140. 'controller' => 'Media',
  141. 'action' => 'search',
  142. 'pass' => ['php'],
  143. 'plugin' => null,
  144. '_matchedRoute' => '/media/search/*',
  145. '?' => ['one' => 'two'],
  146. ];
  147. $this->assertEquals($expected, $result);
  148. $result = $this->collection->parse('/thing?one=two');
  149. unset($result['_route']);
  150. $expected = [
  151. 'controller' => 'Articles',
  152. 'action' => 'view',
  153. 'pass' => [],
  154. 'id' => 'thing',
  155. 'plugin' => null,
  156. '_matchedRoute' => '/{id}',
  157. '?' => ['one' => 'two'],
  158. ];
  159. $this->assertEquals($expected, $result);
  160. });
  161. }
  162. /**
  163. * Test parsing routes with and without _name.
  164. */
  165. public function testParseWithNameParameter(): void
  166. {
  167. $routes = new RouteBuilder($this->collection, '/b', ['key' => 'value']);
  168. $routes->connect('/', ['controller' => 'Articles']);
  169. $routes->connect('/{id}', ['controller' => 'Articles', 'action' => 'view']);
  170. $routes->connect('/media/search/*', ['controller' => 'Media', 'action' => 'search'], ['_name' => 'media_search']);
  171. $this->deprecated(function () {
  172. $result = $this->collection->parse('/b/');
  173. unset($result['_route']);
  174. $expected = [
  175. 'controller' => 'Articles',
  176. 'action' => 'index',
  177. 'pass' => [],
  178. 'plugin' => null,
  179. 'key' => 'value',
  180. '_matchedRoute' => '/b',
  181. ];
  182. $this->assertEquals($expected, $result);
  183. $result = $this->collection->parse('/b/the-thing?one=two');
  184. unset($result['_route']);
  185. $expected = [
  186. 'controller' => 'Articles',
  187. 'action' => 'view',
  188. 'id' => 'the-thing',
  189. 'pass' => [],
  190. 'plugin' => null,
  191. 'key' => 'value',
  192. '?' => ['one' => 'two'],
  193. '_matchedRoute' => '/b/{id}',
  194. ];
  195. $this->assertEquals($expected, $result);
  196. $result = $this->collection->parse('/b/media/search');
  197. unset($result['_route']);
  198. $expected = [
  199. 'key' => 'value',
  200. 'pass' => [],
  201. 'plugin' => null,
  202. 'controller' => 'Media',
  203. 'action' => 'search',
  204. '_matchedRoute' => '/b/media/search/*',
  205. '_name' => 'media_search',
  206. ];
  207. $this->assertEquals($expected, $result);
  208. $result = $this->collection->parse('/b/media/search/thing');
  209. unset($result['_route']);
  210. $expected = [
  211. 'key' => 'value',
  212. 'pass' => ['thing'],
  213. 'plugin' => null,
  214. 'controller' => 'Media',
  215. 'action' => 'search',
  216. '_matchedRoute' => '/b/media/search/*',
  217. '_name' => 'media_search',
  218. ];
  219. $this->assertEquals($expected, $result);
  220. });
  221. }
  222. /**
  223. * Test that parse decodes URL data before matching.
  224. * This is important for multibyte URLs that pass through URL rewriting.
  225. */
  226. public function testParseEncodedBytesInFixedSegment(): void
  227. {
  228. $routes = new RouteBuilder($this->collection, '/');
  229. $routes->connect('/ден/{day}-{month}', ['controller' => 'Events', 'action' => 'index']);
  230. $this->deprecated(function () {
  231. $url = '/%D0%B4%D0%B5%D0%BD/15-%D0%BE%D0%BA%D1%82%D0%BE%D0%BC%D0%B2%D1%80%D0%B8?test=foo';
  232. $result = $this->collection->parse($url);
  233. unset($result['_route']);
  234. $expected = [
  235. 'pass' => [],
  236. 'plugin' => null,
  237. 'controller' => 'Events',
  238. 'action' => 'index',
  239. 'day' => '15',
  240. 'month' => 'октомври',
  241. '?' => ['test' => 'foo'],
  242. '_matchedRoute' => '/ден/{day}-{month}',
  243. ];
  244. $this->assertEquals($expected, $result);
  245. $request = new ServerRequest(['url' => $url]);
  246. $result = $this->collection->parseRequest($request);
  247. unset($result['_route']);
  248. $this->assertEquals($expected, $result);
  249. });
  250. }
  251. /**
  252. * Test that parsing checks all the related path scopes.
  253. */
  254. public function testParseFallback(): void
  255. {
  256. $routes = new RouteBuilder($this->collection, '/', []);
  257. $routes->resources('Articles');
  258. $routes->connect('/{controller}', ['action' => 'index'], ['routeClass' => 'InflectedRoute']);
  259. $routes->connect('/{controller}/{action}', [], ['routeClass' => 'InflectedRoute']);
  260. $this->deprecated(function () {
  261. $result = $this->collection->parse('/articles/add');
  262. unset($result['_route']);
  263. $expected = [
  264. 'controller' => 'Articles',
  265. 'action' => 'add',
  266. 'plugin' => null,
  267. 'pass' => [],
  268. '_matchedRoute' => '/{controller}/{action}',
  269. ];
  270. $this->assertEquals($expected, $result);
  271. });
  272. }
  273. /**
  274. * Test parseRequest() throws an error on unknown routes.
  275. */
  276. public function testParseRequestMissingRoute(): void
  277. {
  278. $this->expectException(MissingRouteException::class);
  279. $this->expectExceptionMessage('A route matching "/" could not be found');
  280. $routes = new RouteBuilder($this->collection, '/b', ['key' => 'value']);
  281. $routes->connect('/', ['controller' => 'Articles']);
  282. $routes->connect('/{id}', ['controller' => 'Articles', 'action' => 'view']);
  283. $request = new ServerRequest(['url' => '/']);
  284. $result = $this->collection->parseRequest($request);
  285. $this->assertEquals([], $result, 'Should not match, missing /b');
  286. }
  287. /**
  288. * Test parseRequest() handling query strings.
  289. */
  290. public function testParseRequestQueryString(): void
  291. {
  292. $routes = new RouteBuilder($this->collection, '/');
  293. $routes->connect('/{id}', ['controller' => 'Articles', 'action' => 'view']);
  294. $routes->connect('/media/search/*', ['controller' => 'Media', 'action' => 'search']);
  295. $request = new ServerRequest(['url' => '/media/search/php?one=two']);
  296. $result = $this->collection->parseRequest($request);
  297. unset($result['_route']);
  298. $expected = [
  299. 'controller' => 'Media',
  300. 'action' => 'search',
  301. 'pass' => ['php'],
  302. 'plugin' => null,
  303. '_matchedRoute' => '/media/search/*',
  304. '?' => ['one' => 'two'],
  305. ];
  306. $this->assertEquals($expected, $result);
  307. $request = new ServerRequest(['url' => '/thing?one=two']);
  308. $result = $this->collection->parseRequest($request);
  309. unset($result['_route']);
  310. $expected = [
  311. 'controller' => 'Articles',
  312. 'action' => 'view',
  313. 'pass' => [],
  314. 'id' => 'thing',
  315. 'plugin' => null,
  316. '_matchedRoute' => '/{id}',
  317. '?' => ['one' => 'two'],
  318. ];
  319. $this->assertEquals($expected, $result);
  320. }
  321. /**
  322. * Test parseRequest() checks host conditions
  323. */
  324. public function testParseRequestCheckHostCondition(): void
  325. {
  326. $routes = new RouteBuilder($this->collection, '/');
  327. $routes->connect(
  328. '/fallback',
  329. ['controller' => 'Articles', 'action' => 'index'],
  330. ['_host' => '*.example.com']
  331. );
  332. $request = new ServerRequest([
  333. 'environment' => [
  334. 'HTTP_HOST' => 'a.example.com',
  335. 'PATH_INFO' => '/fallback',
  336. ],
  337. ]);
  338. $result = $this->collection->parseRequest($request);
  339. unset($result['_route']);
  340. $expected = [
  341. 'controller' => 'Articles',
  342. 'action' => 'index',
  343. 'pass' => [],
  344. 'plugin' => null,
  345. '_matchedRoute' => '/fallback',
  346. ];
  347. $this->assertEquals($expected, $result, 'Should match, domain is correct');
  348. $request = new ServerRequest([
  349. 'environment' => [
  350. 'HTTP_HOST' => 'foo.bar.example.com',
  351. 'PATH_INFO' => '/fallback',
  352. ],
  353. ]);
  354. $result = $this->collection->parseRequest($request);
  355. unset($result['_route']);
  356. $this->assertEquals($expected, $result, 'Should match, domain is a matching subdomain');
  357. $request = new ServerRequest([
  358. 'environment' => [
  359. 'HTTP_HOST' => 'example.test.com',
  360. 'PATH_INFO' => '/fallback',
  361. ],
  362. ]);
  363. try {
  364. $this->collection->parseRequest($request);
  365. $this->fail('No exception raised');
  366. } catch (MissingRouteException $e) {
  367. $this->assertStringContainsString('/fallback', $e->getMessage());
  368. }
  369. }
  370. /**
  371. * Get a list of hostnames
  372. *
  373. * @return array
  374. */
  375. public static function hostProvider(): array
  376. {
  377. return [
  378. ['wrong.example'],
  379. ['example.com'],
  380. ['aexample.com'],
  381. ];
  382. }
  383. /**
  384. * Test parseRequest() checks host conditions
  385. *
  386. * @dataProvider hostProvider
  387. */
  388. public function testParseRequestCheckHostConditionFail(string $host): void
  389. {
  390. $this->expectException(MissingRouteException::class);
  391. $this->expectExceptionMessage('A route matching "/fallback" could not be found');
  392. $routes = new RouteBuilder($this->collection, '/');
  393. $routes->connect(
  394. '/fallback',
  395. ['controller' => 'Articles', 'action' => 'index'],
  396. ['_host' => '*.example.com']
  397. );
  398. $request = new ServerRequest([
  399. 'environment' => [
  400. 'HTTP_HOST' => $host,
  401. 'PATH_INFO' => '/fallback',
  402. ],
  403. ]);
  404. $this->collection->parseRequest($request);
  405. }
  406. /**
  407. * Test parsing routes.
  408. */
  409. public function testParseRequest(): void
  410. {
  411. $routes = new RouteBuilder($this->collection, '/b', ['key' => 'value']);
  412. $routes->connect('/', ['controller' => 'Articles']);
  413. $routes->connect('/{id}', ['controller' => 'Articles', 'action' => 'view']);
  414. $routes->connect('/media/search/*', ['controller' => 'Media', 'action' => 'search']);
  415. $request = new ServerRequest(['url' => '/b/']);
  416. $result = $this->collection->parseRequest($request);
  417. unset($result['_route']);
  418. $expected = [
  419. 'controller' => 'Articles',
  420. 'action' => 'index',
  421. 'pass' => [],
  422. 'plugin' => null,
  423. 'key' => 'value',
  424. '_matchedRoute' => '/b',
  425. ];
  426. $this->assertEquals($expected, $result);
  427. $request = new ServerRequest(['url' => '/b/media/search']);
  428. $result = $this->collection->parseRequest($request);
  429. unset($result['_route']);
  430. $expected = [
  431. 'key' => 'value',
  432. 'pass' => [],
  433. 'plugin' => null,
  434. 'controller' => 'Media',
  435. 'action' => 'search',
  436. '_matchedRoute' => '/b/media/search/*',
  437. ];
  438. $this->assertEquals($expected, $result);
  439. $request = new ServerRequest(['url' => '/b/media/search/thing']);
  440. $result = $this->collection->parseRequest($request);
  441. unset($result['_route']);
  442. $expected = [
  443. 'key' => 'value',
  444. 'pass' => ['thing'],
  445. 'plugin' => null,
  446. 'controller' => 'Media',
  447. 'action' => 'search',
  448. '_matchedRoute' => '/b/media/search/*',
  449. ];
  450. $this->assertEquals($expected, $result);
  451. $request = new ServerRequest(['url' => '/b/the-thing?one=two']);
  452. $result = $this->collection->parseRequest($request);
  453. unset($result['_route']);
  454. $expected = [
  455. 'controller' => 'Articles',
  456. 'action' => 'view',
  457. 'id' => 'the-thing',
  458. 'pass' => [],
  459. 'plugin' => null,
  460. 'key' => 'value',
  461. '?' => ['one' => 'two'],
  462. '_matchedRoute' => '/b/{id}',
  463. ];
  464. $this->assertEquals($expected, $result);
  465. }
  466. public function testParseRequestExtension(): void
  467. {
  468. $builder = new RouteBuilder($this->collection, '/');
  469. $builder->connect('/foo', ['controller' => 'Articles'])->setExtensions(['json']);
  470. $request = new ServerRequest(['url' => '/foo']);
  471. $result = $this->collection->parseRequest($request);
  472. unset($result['_route']);
  473. $expected = [
  474. 'controller' => 'Articles',
  475. 'action' => 'index',
  476. 'pass' => [],
  477. 'plugin' => null,
  478. '_matchedRoute' => '/foo',
  479. ];
  480. $this->assertEquals($expected, $result);
  481. $request = new ServerRequest(['url' => '/foo.json']);
  482. $result = $this->collection->parseRequest($request);
  483. unset($result['_route']);
  484. $expected = [
  485. 'controller' => 'Articles',
  486. 'action' => 'index',
  487. 'pass' => [],
  488. 'plugin' => null,
  489. '_ext' => 'json',
  490. '_matchedRoute' => '/foo',
  491. ];
  492. $this->assertEquals($expected, $result);
  493. }
  494. /**
  495. * Test parsing routes that match non-ascii urls
  496. */
  497. public function testParseRequestUnicode(): void
  498. {
  499. $routes = new RouteBuilder($this->collection, '/b', []);
  500. $routes->connect('/alta/confirmación', ['controller' => 'Media', 'action' => 'confirm']);
  501. $request = new ServerRequest(['url' => '/b/alta/confirmaci%C3%B3n']);
  502. $result = $this->collection->parseRequest($request);
  503. unset($result['_route']);
  504. $expected = [
  505. 'controller' => 'Media',
  506. 'action' => 'confirm',
  507. 'pass' => [],
  508. 'plugin' => null,
  509. '_matchedRoute' => '/b/alta/confirmación',
  510. ];
  511. $this->assertEquals($expected, $result);
  512. $request = new ServerRequest(['url' => '/b/alta/confirmación']);
  513. $result = $this->collection->parseRequest($request);
  514. unset($result['_route']);
  515. $this->assertEquals($expected, $result);
  516. }
  517. /**
  518. * Test match() throws an error on unknown routes.
  519. */
  520. public function testMatchError(): void
  521. {
  522. $this->expectException(MissingRouteException::class);
  523. $this->expectExceptionMessage('A route matching "array (');
  524. $context = [
  525. '_base' => '/',
  526. '_scheme' => 'http',
  527. '_host' => 'example.org',
  528. ];
  529. $routes = new RouteBuilder($this->collection, '/b');
  530. $routes->connect('/', ['controller' => 'Articles']);
  531. $this->collection->match(['plugin' => null, 'controller' => 'Articles', 'action' => 'add'], $context);
  532. }
  533. /**
  534. * Test matching routes.
  535. */
  536. public function testMatch(): void
  537. {
  538. $context = [
  539. '_base' => '/',
  540. '_scheme' => 'http',
  541. '_host' => 'example.org',
  542. ];
  543. $routes = new RouteBuilder($this->collection, '/b');
  544. $routes->connect('/', ['controller' => 'Articles']);
  545. $routes->connect('/{id}', ['controller' => 'Articles', 'action' => 'view']);
  546. $result = $this->collection->match(['plugin' => null, 'controller' => 'Articles', 'action' => 'index'], $context);
  547. $this->assertSame('b', $result);
  548. $result = $this->collection->match(
  549. ['id' => 'thing', 'plugin' => null, 'controller' => 'Articles', 'action' => 'view'],
  550. $context
  551. );
  552. $this->assertSame('b/thing', $result);
  553. }
  554. /**
  555. * Test matching routes with names
  556. */
  557. public function testMatchNamed(): void
  558. {
  559. $context = [
  560. '_base' => '/',
  561. '_scheme' => 'http',
  562. '_host' => 'example.org',
  563. ];
  564. $routes = new RouteBuilder($this->collection, '/b');
  565. $routes->connect('/', ['controller' => 'Articles']);
  566. $routes->connect('/{id}', ['controller' => 'Articles', 'action' => 'view'], ['_name' => 'article:view']);
  567. $result = $this->collection->match(['_name' => 'article:view', 'id' => '2'], $context);
  568. $this->assertSame('/b/2', $result);
  569. $result = $this->collection->match(['plugin' => null, 'controller' => 'Articles', 'action' => 'view', 'id' => '2'], $context);
  570. $this->assertSame('b/2', $result);
  571. }
  572. /**
  573. * Test match() throws an error on named routes that fail to match
  574. */
  575. public function testMatchNamedError(): void
  576. {
  577. $this->expectException(MissingRouteException::class);
  578. $this->expectExceptionMessage('A named route was found for `fail`, but matching failed');
  579. $context = [
  580. '_base' => '/',
  581. '_scheme' => 'http',
  582. '_host' => 'example.org',
  583. ];
  584. $routes = new RouteBuilder($this->collection, '/b');
  585. $routes->connect('/{lang}/articles', ['controller' => 'Articles'], ['_name' => 'fail']);
  586. $this->collection->match(['_name' => 'fail'], $context);
  587. }
  588. /**
  589. * Test matching routes with names and failing
  590. */
  591. public function testMatchNamedMissingError(): void
  592. {
  593. $this->expectException(MissingRouteException::class);
  594. $context = [
  595. '_base' => '/',
  596. '_scheme' => 'http',
  597. '_host' => 'example.org',
  598. ];
  599. $routes = new RouteBuilder($this->collection, '/b');
  600. $routes->connect('/{id}', ['controller' => 'Articles', 'action' => 'view'], ['_name' => 'article:view']);
  601. $this->collection->match(['_name' => 'derp'], $context);
  602. }
  603. /**
  604. * Test matching plugin routes.
  605. */
  606. public function testMatchPlugin(): void
  607. {
  608. $context = [
  609. '_base' => '/',
  610. '_scheme' => 'http',
  611. '_host' => 'example.org',
  612. ];
  613. $routes = new RouteBuilder($this->collection, '/contacts', ['plugin' => 'Contacts']);
  614. $routes->connect('/', ['controller' => 'Contacts']);
  615. $result = $this->collection->match(['plugin' => 'Contacts', 'controller' => 'Contacts', 'action' => 'index'], $context);
  616. $this->assertSame('contacts', $result);
  617. }
  618. /**
  619. * Test that prefixes increase the specificity of a route match.
  620. *
  621. * Connect the admin route after the non prefixed version, this means
  622. * the non-prefix route would have a more specific name (users:index)
  623. */
  624. public function testMatchPrefixSpecificity(): void
  625. {
  626. $context = [
  627. '_base' => '/',
  628. '_scheme' => 'http',
  629. '_host' => 'example.org',
  630. ];
  631. $routes = new RouteBuilder($this->collection, '/');
  632. $routes->connect('/{action}/*', ['controller' => 'Users']);
  633. $routes->connect('/admin/{controller}', ['prefix' => 'Admin', 'action' => 'index']);
  634. $url = [
  635. 'plugin' => null,
  636. 'prefix' => 'Admin',
  637. 'controller' => 'Users',
  638. 'action' => 'index',
  639. ];
  640. $result = $this->collection->match($url, $context);
  641. $this->assertSame('admin/Users', $result);
  642. $url = [
  643. 'plugin' => null,
  644. 'controller' => 'Users',
  645. 'action' => 'index',
  646. ];
  647. $result = $this->collection->match($url, $context);
  648. $this->assertSame('index', $result);
  649. }
  650. /**
  651. * Test getting named routes.
  652. */
  653. public function testNamed(): void
  654. {
  655. $routes = new RouteBuilder($this->collection, '/l');
  656. $routes->connect('/{controller}', ['action' => 'index'], ['_name' => 'cntrl']);
  657. $routes->connect('/{controller}/{action}/*');
  658. $all = $this->collection->named();
  659. $this->assertCount(1, $all);
  660. $this->assertInstanceOf('Cake\Routing\Route\Route', $all['cntrl']);
  661. $this->assertSame('/l/{controller}', $all['cntrl']->template);
  662. }
  663. /**
  664. * Test the add() and routes() method.
  665. */
  666. public function testAddingRoutes(): void
  667. {
  668. $one = new Route('/pages/*', ['controller' => 'Pages', 'action' => 'display']);
  669. $two = new Route('/', ['controller' => 'Dashboards', 'action' => 'display']);
  670. $this->collection->add($one);
  671. $this->collection->add($two);
  672. $routes = $this->collection->routes();
  673. $this->assertCount(2, $routes);
  674. $this->assertSame($one, $routes[0]);
  675. $this->assertSame($two, $routes[1]);
  676. }
  677. /**
  678. * Test the add() with some _name.
  679. */
  680. public function testAddingDuplicateNamedRoutes(): void
  681. {
  682. $this->expectException(DuplicateNamedRouteException::class);
  683. $one = new Route('/pages/*', ['controller' => 'Pages', 'action' => 'display']);
  684. $two = new Route('/', ['controller' => 'Dashboards', 'action' => 'display']);
  685. $this->collection->add($one, ['_name' => 'test']);
  686. $this->collection->add($two, ['_name' => 'test']);
  687. }
  688. /**
  689. * Test basic setExtension and its getter.
  690. */
  691. public function testSetExtensions(): void
  692. {
  693. $this->assertEquals([], $this->collection->getExtensions());
  694. $this->collection->setExtensions(['json']);
  695. $this->assertEquals(['json'], $this->collection->getExtensions());
  696. $this->collection->setExtensions(['rss', 'xml']);
  697. $this->assertEquals(['json', 'rss', 'xml'], $this->collection->getExtensions());
  698. $this->collection->setExtensions(['csv'], false);
  699. $this->assertEquals(['csv'], $this->collection->getExtensions());
  700. }
  701. /**
  702. * Test adding middleware to the collection.
  703. */
  704. public function testRegisterMiddleware(): void
  705. {
  706. $result = $this->collection->registerMiddleware('closure', function (): void {
  707. });
  708. $this->assertSame($result, $this->collection);
  709. $mock = $this->getMockBuilder('\stdClass')
  710. ->addMethods(['__invoke'])
  711. ->getMock();
  712. $result = $this->collection->registerMiddleware('callable', $mock);
  713. $this->assertSame($result, $this->collection);
  714. $this->assertTrue($this->collection->hasMiddleware('closure'));
  715. $this->assertTrue($this->collection->hasMiddleware('callable'));
  716. $this->collection->registerMiddleware('class', 'Dumb');
  717. }
  718. /**
  719. * Test adding a middleware group to the collection.
  720. */
  721. public function testMiddlewareGroup(): void
  722. {
  723. $this->collection->registerMiddleware('closure', function (): void {
  724. });
  725. $mock = $this->getMockBuilder('\stdClass')
  726. ->addMethods(['__invoke'])
  727. ->getMock();
  728. $result = $this->collection->registerMiddleware('callable', $mock);
  729. $this->collection->registerMiddleware('callable', $mock);
  730. $this->collection->middlewareGroup('group', ['closure', 'callable']);
  731. $this->assertTrue($this->collection->hasMiddlewareGroup('group'));
  732. }
  733. /**
  734. * Test adding a middleware group with the same name overwrites the original list
  735. */
  736. public function testMiddlewareGroupOverwrite(): void
  737. {
  738. $stub = function (): void {
  739. };
  740. $this->collection->registerMiddleware('closure', $stub);
  741. $result = $this->collection->registerMiddleware('callable', $stub);
  742. $this->collection->registerMiddleware('callable', $stub);
  743. $this->collection->middlewareGroup('group', ['callable']);
  744. $this->collection->middlewareGroup('group', ['closure', 'callable']);
  745. $this->assertSame([$stub, $stub], $this->collection->getMiddleware(['group']));
  746. }
  747. /**
  748. * Test adding ab unregistered middleware to a middleware group fails.
  749. */
  750. public function testMiddlewareGroupUnregisteredMiddleware(): void
  751. {
  752. $this->expectException(RuntimeException::class);
  753. $this->expectExceptionMessage('Cannot add \'bad\' middleware to group \'group\'. It has not been registered.');
  754. $this->collection->middlewareGroup('group', ['bad']);
  755. }
  756. }