RouteCollectionTest.php 18 KB

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