RouteCollectionTest.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  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('/fallback', ['controller' => 'Articles', 'action' => 'index'], ['_host' => '*.example.com']);
  171. $request = new ServerRequest([
  172. 'environment' => [
  173. 'HTTP_HOST' => 'a.example.com',
  174. 'PATH_INFO' => '/fallback'
  175. ]
  176. ]);
  177. $result = $this->collection->parseRequest($request);
  178. $expected = [
  179. 'controller' => 'Articles',
  180. 'action' => 'index',
  181. 'pass' => [],
  182. 'plugin' => null,
  183. '_matchedRoute' => '/fallback'
  184. ];
  185. $this->assertEquals($expected, $result, 'Should match, domain is correct');
  186. $request = new ServerRequest([
  187. 'environment' => [
  188. 'HTTP_HOST' => 'foo.bar.example.com',
  189. 'PATH_INFO' => '/fallback'
  190. ]
  191. ]);
  192. $result = $this->collection->parseRequest($request);
  193. $this->assertEquals($expected, $result, 'Should match, domain is a matching subdomain');
  194. $request = new ServerRequest([
  195. 'environment' => [
  196. 'HTTP_HOST' => 'example.test.com',
  197. 'PATH_INFO' => '/fallback'
  198. ]
  199. ]);
  200. try {
  201. $this->collection->parseRequest($request);
  202. $this->fail('No exception raised');
  203. } catch (MissingRouteException $e) {
  204. $this->assertContains('/fallback', $e->getMessage());
  205. }
  206. }
  207. /**
  208. * Get a list of hostnames
  209. *
  210. * @return array
  211. */
  212. public static function hostProvider()
  213. {
  214. return [
  215. ['wrong.example'],
  216. ['example.com'],
  217. ['aexample.com'],
  218. ];
  219. }
  220. /**
  221. * Test parseRequest() checks host conditions
  222. *
  223. * @dataProvider hostProvider
  224. * @expectedException \Cake\Routing\Exception\MissingRouteException
  225. * @expectedExceptionMessage A route matching "/fallback" could not be found
  226. */
  227. public function testParseRequestCheckHostConditionFail($host)
  228. {
  229. $routes = new RouteBuilder($this->collection, '/');
  230. $routes->connect('/fallback', ['controller' => 'Articles', 'action' => 'index'], ['_host' => '*.example.com']);
  231. $request = new ServerRequest([
  232. 'environment' => [
  233. 'HTTP_HOST' => $host,
  234. 'PATH_INFO' => '/fallback'
  235. ]
  236. ]);
  237. $this->collection->parseRequest($request);
  238. }
  239. /**
  240. * Test parsing routes.
  241. *
  242. * @return void
  243. */
  244. public function testParseRequest()
  245. {
  246. $routes = new RouteBuilder($this->collection, '/b', ['key' => 'value']);
  247. $routes->connect('/', ['controller' => 'Articles']);
  248. $routes->connect('/:id', ['controller' => 'Articles', 'action' => 'view']);
  249. $routes->connect('/media/search/*', ['controller' => 'Media', 'action' => 'search']);
  250. $request = new ServerRequest(['url' => '/b/']);
  251. $result = $this->collection->parseRequest($request);
  252. $expected = [
  253. 'controller' => 'Articles',
  254. 'action' => 'index',
  255. 'pass' => [],
  256. 'plugin' => null,
  257. 'key' => 'value',
  258. '_matchedRoute' => '/b',
  259. ];
  260. $this->assertEquals($expected, $result);
  261. $request = new ServerRequest(['url' => '/b/media/search']);
  262. $result = $this->collection->parseRequest($request);
  263. $expected = [
  264. 'key' => 'value',
  265. 'pass' => [],
  266. 'plugin' => null,
  267. 'controller' => 'Media',
  268. 'action' => 'search',
  269. '_matchedRoute' => '/b/media/search/*',
  270. ];
  271. $this->assertEquals($expected, $result);
  272. $request = new ServerRequest(['url' => '/b/media/search/thing']);
  273. $result = $this->collection->parseRequest($request);
  274. $expected = [
  275. 'key' => 'value',
  276. 'pass' => ['thing'],
  277. 'plugin' => null,
  278. 'controller' => 'Media',
  279. 'action' => 'search',
  280. '_matchedRoute' => '/b/media/search/*',
  281. ];
  282. $this->assertEquals($expected, $result);
  283. $request = new ServerRequest(['url' => '/b/the-thing?one=two']);
  284. $result = $this->collection->parseRequest($request);
  285. $expected = [
  286. 'controller' => 'Articles',
  287. 'action' => 'view',
  288. 'id' => 'the-thing',
  289. 'pass' => [],
  290. 'plugin' => null,
  291. 'key' => 'value',
  292. '?' => ['one' => 'two'],
  293. '_matchedRoute' => '/b/:id',
  294. ];
  295. $this->assertEquals($expected, $result);
  296. }
  297. /**
  298. * Test match() throws an error on unknown routes.
  299. *
  300. * @expectedException \Cake\Routing\Exception\MissingRouteException
  301. * @expectedExceptionMessage A route matching "array (
  302. */
  303. public function testMatchError()
  304. {
  305. $context = [
  306. '_base' => '/',
  307. '_scheme' => 'http',
  308. '_host' => 'example.org',
  309. ];
  310. $routes = new RouteBuilder($this->collection, '/b');
  311. $routes->connect('/', ['controller' => 'Articles']);
  312. $this->collection->match(['plugin' => null, 'controller' => 'Articles', 'action' => 'add'], $context);
  313. }
  314. /**
  315. * Test matching routes.
  316. *
  317. * @return void
  318. */
  319. public function testMatch()
  320. {
  321. $context = [
  322. '_base' => '/',
  323. '_scheme' => 'http',
  324. '_host' => 'example.org',
  325. ];
  326. $routes = new RouteBuilder($this->collection, '/b');
  327. $routes->connect('/', ['controller' => 'Articles']);
  328. $routes->connect('/:id', ['controller' => 'Articles', 'action' => 'view']);
  329. $result = $this->collection->match(['plugin' => null, 'controller' => 'Articles', 'action' => 'index'], $context);
  330. $this->assertEquals('b', $result);
  331. $result = $this->collection->match(
  332. ['id' => 'thing', 'plugin' => null, 'controller' => 'Articles', 'action' => 'view'],
  333. $context
  334. );
  335. $this->assertEquals('b/thing', $result);
  336. }
  337. /**
  338. * Test matching routes with names
  339. *
  340. * @return void
  341. */
  342. public function testMatchNamed()
  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'], ['_name' => 'article:view']);
  352. $result = $this->collection->match(['_name' => 'article:view', 'id' => '2'], $context);
  353. $this->assertEquals('/b/2', $result);
  354. $result = $this->collection->match(['plugin' => null, 'controller' => 'Articles', 'action' => 'view', 'id' => '2'], $context);
  355. $this->assertEquals('b/2', $result);
  356. }
  357. /**
  358. * Test match() throws an error on named routes that fail to match
  359. *
  360. * @expectedException \Cake\Routing\Exception\MissingRouteException
  361. * @expectedExceptionMessage A named route was found for "fail", but matching failed
  362. */
  363. public function testMatchNamedError()
  364. {
  365. $context = [
  366. '_base' => '/',
  367. '_scheme' => 'http',
  368. '_host' => 'example.org',
  369. ];
  370. $routes = new RouteBuilder($this->collection, '/b');
  371. $routes->connect('/:lang/articles', ['controller' => 'Articles'], ['_name' => 'fail']);
  372. $this->collection->match(['_name' => 'fail'], $context);
  373. }
  374. /**
  375. * Test matching routes with names and failing
  376. *
  377. * @expectedException \Cake\Routing\Exception\MissingRouteException
  378. * @return void
  379. */
  380. public function testMatchNamedMissingError()
  381. {
  382. $context = [
  383. '_base' => '/',
  384. '_scheme' => 'http',
  385. '_host' => 'example.org',
  386. ];
  387. $routes = new RouteBuilder($this->collection, '/b');
  388. $routes->connect('/:id', ['controller' => 'Articles', 'action' => 'view'], ['_name' => 'article:view']);
  389. $this->collection->match(['_name' => 'derp'], $context);
  390. }
  391. /**
  392. * Test matching plugin routes.
  393. *
  394. * @return void
  395. */
  396. public function testMatchPlugin()
  397. {
  398. $context = [
  399. '_base' => '/',
  400. '_scheme' => 'http',
  401. '_host' => 'example.org',
  402. ];
  403. $routes = new RouteBuilder($this->collection, '/contacts', ['plugin' => 'Contacts']);
  404. $routes->connect('/', ['controller' => 'Contacts']);
  405. $result = $this->collection->match(['plugin' => 'Contacts', 'controller' => 'Contacts', 'action' => 'index'], $context);
  406. $this->assertEquals('contacts', $result);
  407. }
  408. /**
  409. * Test that prefixes increase the specificity of a route match.
  410. *
  411. * Connect the admin route after the non prefixed version, this means
  412. * the non-prefix route would have a more specific name (users:index)
  413. *
  414. * @return void
  415. */
  416. public function testMatchPrefixSpecificity()
  417. {
  418. $context = [
  419. '_base' => '/',
  420. '_scheme' => 'http',
  421. '_host' => 'example.org',
  422. ];
  423. $routes = new RouteBuilder($this->collection, '/');
  424. $routes->connect('/:action/*', ['controller' => 'Users']);
  425. $routes->connect('/admin/:controller', ['prefix' => 'admin', 'action' => 'index']);
  426. $url = [
  427. 'plugin' => null,
  428. 'prefix' => 'admin',
  429. 'controller' => 'Users',
  430. 'action' => 'index'
  431. ];
  432. $result = $this->collection->match($url, $context);
  433. $this->assertEquals('admin/Users', $result);
  434. $url = [
  435. 'plugin' => null,
  436. 'controller' => 'Users',
  437. 'action' => 'index'
  438. ];
  439. $result = $this->collection->match($url, $context);
  440. $this->assertEquals('index', $result);
  441. }
  442. /**
  443. * Test getting named routes.
  444. *
  445. * @return void
  446. */
  447. public function testNamed()
  448. {
  449. $routes = new RouteBuilder($this->collection, '/l');
  450. $routes->connect('/:controller', ['action' => 'index'], ['_name' => 'cntrl']);
  451. $routes->connect('/:controller/:action/*');
  452. $all = $this->collection->named();
  453. $this->assertCount(1, $all);
  454. $this->assertInstanceOf('Cake\Routing\Route\Route', $all['cntrl']);
  455. $this->assertEquals('/l/:controller', $all['cntrl']->template);
  456. }
  457. /**
  458. * Test the add() and routes() method.
  459. *
  460. * @return void
  461. */
  462. public function testAddingRoutes()
  463. {
  464. $one = new Route('/pages/*', ['controller' => 'Pages', 'action' => 'display']);
  465. $two = new Route('/', ['controller' => 'Dashboards', 'action' => 'display']);
  466. $this->collection->add($one);
  467. $this->collection->add($two);
  468. $routes = $this->collection->routes();
  469. $this->assertCount(2, $routes);
  470. $this->assertSame($one, $routes[0]);
  471. $this->assertSame($two, $routes[1]);
  472. }
  473. /**
  474. * Test the add() with some _name.
  475. *
  476. * @expectedException \Cake\Routing\Exception\DuplicateNamedRouteException
  477. *
  478. * @return void
  479. */
  480. public function testAddingDuplicateNamedRoutes()
  481. {
  482. $one = new Route('/pages/*', ['controller' => 'Pages', 'action' => 'display']);
  483. $two = new Route('/', ['controller' => 'Dashboards', 'action' => 'display']);
  484. $this->collection->add($one, ['_name' => 'test']);
  485. $this->collection->add($two, ['_name' => 'test']);
  486. }
  487. /**
  488. * Test basic get/set of extensions.
  489. *
  490. * @return void
  491. */
  492. public function testExtensions()
  493. {
  494. $this->assertEquals([], $this->collection->extensions());
  495. $this->collection->extensions('json');
  496. $this->assertEquals(['json'], $this->collection->extensions());
  497. $this->collection->extensions(['rss', 'xml']);
  498. $this->assertEquals(['json', 'rss', 'xml'], $this->collection->extensions());
  499. $this->collection->extensions(['csv'], false);
  500. $this->assertEquals(['csv'], $this->collection->extensions());
  501. }
  502. }