RouteCollectionTest.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://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. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since 3.0.0
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Test\TestCase\Routing;
  16. use Cake\Http\ServerRequest;
  17. use Cake\Routing\Exception\MissingRouteException;
  18. use Cake\Routing\RouteBuilder;
  19. use Cake\Routing\RouteCollection;
  20. use Cake\Routing\Route\Route;
  21. use Cake\TestSuite\TestCase;
  22. class RouteCollectionTest extends TestCase
  23. {
  24. /**
  25. * Setup method
  26. *
  27. * @return void
  28. */
  29. public function setUp()
  30. {
  31. parent::setUp();
  32. $this->collection = new RouteCollection();
  33. }
  34. /**
  35. * Test parse() throws an error on unknown routes.
  36. *
  37. * @expectedException \Cake\Routing\Exception\MissingRouteException
  38. * @expectedExceptionMessage A route matching "/" could not be found
  39. */
  40. public function testParseMissingRoute()
  41. {
  42. $routes = new RouteBuilder($this->collection, '/b', ['key' => 'value']);
  43. $routes->connect('/', ['controller' => 'Articles']);
  44. $routes->connect('/:id', ['controller' => 'Articles', 'action' => 'view']);
  45. $result = $this->collection->parse('/');
  46. $this->assertEquals([], $result, 'Should not match, missing /b');
  47. }
  48. /**
  49. * Test parse() throws an error on known routes called with unknown methods.
  50. *
  51. * @expectedException \Cake\Routing\Exception\MissingRouteException
  52. * @expectedExceptionMessage A "POST" route matching "/b" could not be found
  53. */
  54. public function testParseMissingRouteMethod()
  55. {
  56. $routes = new RouteBuilder($this->collection, '/b', ['key' => 'value']);
  57. $routes->connect('/', ['controller' => 'Articles', '_method' => ['GET']]);
  58. $result = $this->collection->parse('/b', 'GET');
  59. $this->assertNotEmpty($result, 'Route should be found');
  60. $result = $this->collection->parse('/b', 'POST');
  61. $this->assertEquals([], $result, 'Should not match with missing method');
  62. }
  63. /**
  64. * Test parsing routes.
  65. *
  66. * @return void
  67. */
  68. public function testParse()
  69. {
  70. $routes = new RouteBuilder($this->collection, '/b', ['key' => 'value']);
  71. $routes->connect('/', ['controller' => 'Articles']);
  72. $routes->connect('/:id', ['controller' => 'Articles', 'action' => 'view']);
  73. $routes->connect('/media/search/*', ['controller' => 'Media', 'action' => 'search']);
  74. $result = $this->collection->parse('/b/');
  75. $expected = [
  76. 'controller' => 'Articles',
  77. 'action' => 'index',
  78. 'pass' => [],
  79. 'plugin' => null,
  80. 'key' => 'value',
  81. '_matchedRoute' => '/b',
  82. ];
  83. $this->assertEquals($expected, $result);
  84. $result = $this->collection->parse('/b/the-thing?one=two');
  85. $expected = [
  86. 'controller' => 'Articles',
  87. 'action' => 'view',
  88. 'id' => 'the-thing',
  89. 'pass' => [],
  90. 'plugin' => null,
  91. 'key' => 'value',
  92. '?' => ['one' => 'two'],
  93. '_matchedRoute' => '/b/:id',
  94. ];
  95. $this->assertEquals($expected, $result);
  96. $result = $this->collection->parse('/b/media/search');
  97. $expected = [
  98. 'key' => 'value',
  99. 'pass' => [],
  100. 'plugin' => null,
  101. 'controller' => 'Media',
  102. 'action' => 'search',
  103. '_matchedRoute' => '/b/media/search/*',
  104. ];
  105. $this->assertEquals($expected, $result);
  106. $result = $this->collection->parse('/b/media/search/thing');
  107. $expected = [
  108. 'key' => 'value',
  109. 'pass' => ['thing'],
  110. 'plugin' => null,
  111. 'controller' => 'Media',
  112. 'action' => 'search',
  113. '_matchedRoute' => '/b/media/search/*',
  114. ];
  115. $this->assertEquals($expected, $result);
  116. }
  117. /**
  118. * Test that parse decodes URL data before matching.
  119. * This is important for multibyte URLs that pass through URL rewriting.
  120. *
  121. * @return void
  122. */
  123. public function testParseEncodedBytesInFixedSegment()
  124. {
  125. $routes = new RouteBuilder($this->collection, '/');
  126. $routes->connect('/ден/:day-:month', ['controller' => 'Events', 'action' => 'index']);
  127. $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';
  128. $result = $this->collection->parse($url);
  129. $expected = [
  130. 'pass' => [],
  131. 'plugin' => null,
  132. 'controller' => 'Events',
  133. 'action' => 'index',
  134. 'day' => '15',
  135. 'month' => 'октомври',
  136. '?' => ['test' => 'foo'],
  137. '_matchedRoute' => '/ден/:day-:month',
  138. ];
  139. $this->assertEquals($expected, $result);
  140. }
  141. /**
  142. * Test that parsing checks all the related path scopes.
  143. *
  144. * @return void
  145. */
  146. public function testParseFallback()
  147. {
  148. $routes = new RouteBuilder($this->collection, '/', []);
  149. $routes->resources('Articles');
  150. $routes->connect('/:controller', ['action' => 'index'], ['routeClass' => 'InflectedRoute']);
  151. $routes->connect('/:controller/:action', [], ['routeClass' => 'InflectedRoute']);
  152. $result = $this->collection->parse('/articles/add');
  153. $expected = [
  154. 'controller' => 'Articles',
  155. 'action' => 'add',
  156. 'plugin' => null,
  157. 'pass' => [],
  158. '_matchedRoute' => '/:controller/:action',
  159. ];
  160. $this->assertEquals($expected, $result);
  161. }
  162. /**
  163. * Test parseRequest() throws an error on unknown routes.
  164. *
  165. * @expectedException \Cake\Routing\Exception\MissingRouteException
  166. * @expectedExceptionMessage A route matching "/" could not be found
  167. */
  168. public function testParseRequestMissingRoute()
  169. {
  170. $routes = new RouteBuilder($this->collection, '/b', ['key' => 'value']);
  171. $routes->connect('/', ['controller' => 'Articles']);
  172. $routes->connect('/:id', ['controller' => 'Articles', 'action' => 'view']);
  173. $request = new ServerRequest(['url' => '/']);
  174. $result = $this->collection->parseRequest($request);
  175. $this->assertEquals([], $result, 'Should not match, missing /b');
  176. }
  177. /**
  178. * Test parseRequest() checks host conditions
  179. *
  180. * @return void
  181. */
  182. public function testParseRequestCheckHostCondition()
  183. {
  184. $routes = new RouteBuilder($this->collection, '/');
  185. $routes->connect(
  186. '/fallback',
  187. ['controller' => 'Articles', 'action' => 'index'],
  188. ['_host' => '*.example.com']
  189. );
  190. $request = new ServerRequest([
  191. 'environment' => [
  192. 'HTTP_HOST' => 'a.example.com',
  193. 'PATH_INFO' => '/fallback'
  194. ]
  195. ]);
  196. $result = $this->collection->parseRequest($request);
  197. $expected = [
  198. 'controller' => 'Articles',
  199. 'action' => 'index',
  200. 'pass' => [],
  201. 'plugin' => null,
  202. '_matchedRoute' => '/fallback'
  203. ];
  204. $this->assertEquals($expected, $result, 'Should match, domain is correct');
  205. $request = new ServerRequest([
  206. 'environment' => [
  207. 'HTTP_HOST' => 'foo.bar.example.com',
  208. 'PATH_INFO' => '/fallback'
  209. ]
  210. ]);
  211. $result = $this->collection->parseRequest($request);
  212. $this->assertEquals($expected, $result, 'Should match, domain is a matching subdomain');
  213. $request = new ServerRequest([
  214. 'environment' => [
  215. 'HTTP_HOST' => 'example.test.com',
  216. 'PATH_INFO' => '/fallback'
  217. ]
  218. ]);
  219. try {
  220. $this->collection->parseRequest($request);
  221. $this->fail('No exception raised');
  222. } catch (MissingRouteException $e) {
  223. $this->assertContains('/fallback', $e->getMessage());
  224. }
  225. }
  226. /**
  227. * Get a list of hostnames
  228. *
  229. * @return array
  230. */
  231. public static function hostProvider()
  232. {
  233. return [
  234. ['wrong.example'],
  235. ['example.com'],
  236. ['aexample.com'],
  237. ];
  238. }
  239. /**
  240. * Test parseRequest() checks host conditions
  241. *
  242. * @dataProvider hostProvider
  243. * @expectedException \Cake\Routing\Exception\MissingRouteException
  244. * @expectedExceptionMessage A route matching "/fallback" could not be found
  245. */
  246. public function testParseRequestCheckHostConditionFail($host)
  247. {
  248. $routes = new RouteBuilder($this->collection, '/');
  249. $routes->connect(
  250. '/fallback',
  251. ['controller' => 'Articles', 'action' => 'index'],
  252. ['_host' => '*.example.com']
  253. );
  254. $request = new ServerRequest([
  255. 'environment' => [
  256. 'HTTP_HOST' => $host,
  257. 'PATH_INFO' => '/fallback'
  258. ]
  259. ]);
  260. $this->collection->parseRequest($request);
  261. }
  262. /**
  263. * Test parsing routes.
  264. *
  265. * @return void
  266. */
  267. public function testParseRequest()
  268. {
  269. $routes = new RouteBuilder($this->collection, '/b', ['key' => 'value']);
  270. $routes->connect('/', ['controller' => 'Articles']);
  271. $routes->connect('/:id', ['controller' => 'Articles', 'action' => 'view']);
  272. $routes->connect('/media/search/*', ['controller' => 'Media', 'action' => 'search']);
  273. $request = new ServerRequest(['url' => '/b/']);
  274. $result = $this->collection->parseRequest($request);
  275. $expected = [
  276. 'controller' => 'Articles',
  277. 'action' => 'index',
  278. 'pass' => [],
  279. 'plugin' => null,
  280. 'key' => 'value',
  281. '_matchedRoute' => '/b',
  282. ];
  283. $this->assertEquals($expected, $result);
  284. $request = new ServerRequest(['url' => '/b/media/search']);
  285. $result = $this->collection->parseRequest($request);
  286. $expected = [
  287. 'key' => 'value',
  288. 'pass' => [],
  289. 'plugin' => null,
  290. 'controller' => 'Media',
  291. 'action' => 'search',
  292. '_matchedRoute' => '/b/media/search/*',
  293. ];
  294. $this->assertEquals($expected, $result);
  295. $request = new ServerRequest(['url' => '/b/media/search/thing']);
  296. $result = $this->collection->parseRequest($request);
  297. $expected = [
  298. 'key' => 'value',
  299. 'pass' => ['thing'],
  300. 'plugin' => null,
  301. 'controller' => 'Media',
  302. 'action' => 'search',
  303. '_matchedRoute' => '/b/media/search/*',
  304. ];
  305. $this->assertEquals($expected, $result);
  306. $request = new ServerRequest(['url' => '/b/the-thing?one=two']);
  307. $result = $this->collection->parseRequest($request);
  308. $expected = [
  309. 'controller' => 'Articles',
  310. 'action' => 'view',
  311. 'id' => 'the-thing',
  312. 'pass' => [],
  313. 'plugin' => null,
  314. 'key' => 'value',
  315. '?' => ['one' => 'two'],
  316. '_matchedRoute' => '/b/:id',
  317. ];
  318. $this->assertEquals($expected, $result);
  319. }
  320. /**
  321. * Test match() throws an error on unknown routes.
  322. *
  323. * @expectedException \Cake\Routing\Exception\MissingRouteException
  324. * @expectedExceptionMessage A route matching "array (
  325. */
  326. public function testMatchError()
  327. {
  328. $context = [
  329. '_base' => '/',
  330. '_scheme' => 'http',
  331. '_host' => 'example.org',
  332. ];
  333. $routes = new RouteBuilder($this->collection, '/b');
  334. $routes->connect('/', ['controller' => 'Articles']);
  335. $this->collection->match(['plugin' => null, 'controller' => 'Articles', 'action' => 'add'], $context);
  336. }
  337. /**
  338. * Test matching routes.
  339. *
  340. * @return void
  341. */
  342. public function testMatch()
  343. {
  344. $context = [
  345. '_base' => '/',
  346. '_scheme' => 'http',
  347. '_host' => 'example.org',
  348. ];
  349. $routes = new RouteBuilder($this->collection, '/b');
  350. $routes->connect('/', ['controller' => 'Articles']);
  351. $routes->connect('/:id', ['controller' => 'Articles', 'action' => 'view']);
  352. $result = $this->collection->match(['plugin' => null, 'controller' => 'Articles', 'action' => 'index'], $context);
  353. $this->assertEquals('b', $result);
  354. $result = $this->collection->match(
  355. ['id' => 'thing', 'plugin' => null, 'controller' => 'Articles', 'action' => 'view'],
  356. $context
  357. );
  358. $this->assertEquals('b/thing', $result);
  359. }
  360. /**
  361. * Test matching routes with names
  362. *
  363. * @return void
  364. */
  365. public function testMatchNamed()
  366. {
  367. $context = [
  368. '_base' => '/',
  369. '_scheme' => 'http',
  370. '_host' => 'example.org',
  371. ];
  372. $routes = new RouteBuilder($this->collection, '/b');
  373. $routes->connect('/', ['controller' => 'Articles']);
  374. $routes->connect('/:id', ['controller' => 'Articles', 'action' => 'view'], ['_name' => 'article:view']);
  375. $result = $this->collection->match(['_name' => 'article:view', 'id' => '2'], $context);
  376. $this->assertEquals('/b/2', $result);
  377. $result = $this->collection->match(['plugin' => null, 'controller' => 'Articles', 'action' => 'view', 'id' => '2'], $context);
  378. $this->assertEquals('b/2', $result);
  379. }
  380. /**
  381. * Test match() throws an error on named routes that fail to match
  382. *
  383. * @expectedException \Cake\Routing\Exception\MissingRouteException
  384. * @expectedExceptionMessage A named route was found for "fail", but matching failed
  385. */
  386. public function testMatchNamedError()
  387. {
  388. $context = [
  389. '_base' => '/',
  390. '_scheme' => 'http',
  391. '_host' => 'example.org',
  392. ];
  393. $routes = new RouteBuilder($this->collection, '/b');
  394. $routes->connect('/:lang/articles', ['controller' => 'Articles'], ['_name' => 'fail']);
  395. $this->collection->match(['_name' => 'fail'], $context);
  396. }
  397. /**
  398. * Test matching routes with names and failing
  399. *
  400. * @expectedException \Cake\Routing\Exception\MissingRouteException
  401. * @return void
  402. */
  403. public function testMatchNamedMissingError()
  404. {
  405. $context = [
  406. '_base' => '/',
  407. '_scheme' => 'http',
  408. '_host' => 'example.org',
  409. ];
  410. $routes = new RouteBuilder($this->collection, '/b');
  411. $routes->connect('/:id', ['controller' => 'Articles', 'action' => 'view'], ['_name' => 'article:view']);
  412. $this->collection->match(['_name' => 'derp'], $context);
  413. }
  414. /**
  415. * Test matching plugin routes.
  416. *
  417. * @return void
  418. */
  419. public function testMatchPlugin()
  420. {
  421. $context = [
  422. '_base' => '/',
  423. '_scheme' => 'http',
  424. '_host' => 'example.org',
  425. ];
  426. $routes = new RouteBuilder($this->collection, '/contacts', ['plugin' => 'Contacts']);
  427. $routes->connect('/', ['controller' => 'Contacts']);
  428. $result = $this->collection->match(['plugin' => 'Contacts', 'controller' => 'Contacts', 'action' => 'index'], $context);
  429. $this->assertEquals('contacts', $result);
  430. }
  431. /**
  432. * Test that prefixes increase the specificity of a route match.
  433. *
  434. * Connect the admin route after the non prefixed version, this means
  435. * the non-prefix route would have a more specific name (users:index)
  436. *
  437. * @return void
  438. */
  439. public function testMatchPrefixSpecificity()
  440. {
  441. $context = [
  442. '_base' => '/',
  443. '_scheme' => 'http',
  444. '_host' => 'example.org',
  445. ];
  446. $routes = new RouteBuilder($this->collection, '/');
  447. $routes->connect('/:action/*', ['controller' => 'Users']);
  448. $routes->connect('/admin/:controller', ['prefix' => 'admin', 'action' => 'index']);
  449. $url = [
  450. 'plugin' => null,
  451. 'prefix' => 'admin',
  452. 'controller' => 'Users',
  453. 'action' => 'index'
  454. ];
  455. $result = $this->collection->match($url, $context);
  456. $this->assertEquals('admin/Users', $result);
  457. $url = [
  458. 'plugin' => null,
  459. 'controller' => 'Users',
  460. 'action' => 'index'
  461. ];
  462. $result = $this->collection->match($url, $context);
  463. $this->assertEquals('index', $result);
  464. }
  465. /**
  466. * Test getting named routes.
  467. *
  468. * @return void
  469. */
  470. public function testNamed()
  471. {
  472. $routes = new RouteBuilder($this->collection, '/l');
  473. $routes->connect('/:controller', ['action' => 'index'], ['_name' => 'cntrl']);
  474. $routes->connect('/:controller/:action/*');
  475. $all = $this->collection->named();
  476. $this->assertCount(1, $all);
  477. $this->assertInstanceOf('Cake\Routing\Route\Route', $all['cntrl']);
  478. $this->assertEquals('/l/:controller', $all['cntrl']->template);
  479. }
  480. /**
  481. * Test the add() and routes() method.
  482. *
  483. * @return void
  484. */
  485. public function testAddingRoutes()
  486. {
  487. $one = new Route('/pages/*', ['controller' => 'Pages', 'action' => 'display']);
  488. $two = new Route('/', ['controller' => 'Dashboards', 'action' => 'display']);
  489. $this->collection->add($one);
  490. $this->collection->add($two);
  491. $routes = $this->collection->routes();
  492. $this->assertCount(2, $routes);
  493. $this->assertSame($one, $routes[0]);
  494. $this->assertSame($two, $routes[1]);
  495. }
  496. /**
  497. * Test the add() with some _name.
  498. *
  499. * @expectedException \Cake\Routing\Exception\DuplicateNamedRouteException
  500. *
  501. * @return void
  502. */
  503. public function testAddingDuplicateNamedRoutes()
  504. {
  505. $one = new Route('/pages/*', ['controller' => 'Pages', 'action' => 'display']);
  506. $two = new Route('/', ['controller' => 'Dashboards', 'action' => 'display']);
  507. $this->collection->add($one, ['_name' => 'test']);
  508. $this->collection->add($two, ['_name' => 'test']);
  509. }
  510. /**
  511. * Test basic get/set of extensions.
  512. *
  513. * @return void
  514. */
  515. public function testExtensions()
  516. {
  517. $this->assertEquals([], $this->collection->extensions());
  518. $this->collection->extensions('json');
  519. $this->assertEquals(['json'], $this->collection->extensions());
  520. $this->collection->extensions(['rss', 'xml']);
  521. $this->assertEquals(['json', 'rss', 'xml'], $this->collection->extensions());
  522. $this->collection->extensions(['csv'], false);
  523. $this->assertEquals(['csv'], $this->collection->extensions());
  524. }
  525. }