ActionDispatcherTest.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  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.3.0
  13. * @license https://opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Test\TestCase\Http;
  16. use Cake\Event\Event;
  17. use Cake\Http\ActionDispatcher;
  18. use Cake\Http\Response;
  19. use Cake\Http\ServerRequest;
  20. use Cake\Http\Session;
  21. use Cake\Routing\DispatcherFactory;
  22. use Cake\Routing\Filter\ControllerFactoryFilter;
  23. use Cake\Routing\Router;
  24. use Cake\TestSuite\TestCase;
  25. /**
  26. * Test case for the ActionDispatcher.
  27. */
  28. class ActionDispatcherTest extends TestCase
  29. {
  30. /**
  31. * Setup
  32. *
  33. * @return void
  34. */
  35. public function setUp()
  36. {
  37. parent::setUp();
  38. Router::reload();
  39. static::setAppNamespace();
  40. $this->dispatcher = new ActionDispatcher();
  41. }
  42. /**
  43. * Teardown
  44. *
  45. * @return void
  46. */
  47. public function tearDown()
  48. {
  49. parent::tearDown();
  50. DispatcherFactory::clear();
  51. }
  52. /**
  53. * Ensure the constructor args end up on the right protected properties.
  54. *
  55. * @return void
  56. */
  57. public function testConstructorArgs()
  58. {
  59. $factory = $this->getMockBuilder('Cake\Http\ControllerFactory')->getMock();
  60. $events = $this->getMockBuilder('Cake\Event\EventManager')->getMock();
  61. $dispatcher = new ActionDispatcher($factory, $events);
  62. $this->assertAttributeSame($events, '_eventManager', $dispatcher);
  63. $this->assertAttributeSame($factory, 'factory', $dispatcher);
  64. }
  65. /**
  66. * Ensure that filters connected to the DispatcherFactory are
  67. * also applied
  68. *
  69. * @group deprecated
  70. * @return void
  71. */
  72. public function testDispatcherFactoryCompat()
  73. {
  74. $this->deprecated(function () {
  75. $filter = $this->getMockBuilder('Cake\Routing\DispatcherFilter')
  76. ->setMethods(['beforeDispatch', 'afterDispatch'])
  77. ->getMock();
  78. DispatcherFactory::add($filter);
  79. $dispatcher = new ActionDispatcher(null, null, DispatcherFactory::filters());
  80. $this->assertCount(1, $dispatcher->getFilters());
  81. $this->assertSame($filter, $dispatcher->getFilters()[0]);
  82. });
  83. }
  84. /**
  85. * Test adding routing filters
  86. *
  87. * @group deprecated
  88. * @return void
  89. */
  90. public function testAddFilter()
  91. {
  92. $this->deprecated(function () {
  93. $this->assertCount(0, $this->dispatcher->getFilters());
  94. $events = $this->dispatcher->getEventManager();
  95. $this->assertCount(0, $events->listeners('Dispatcher.beforeDispatch'));
  96. $this->assertCount(0, $events->listeners('Dispatcher.afterDispatch'));
  97. $filter = $this->getMockBuilder('Cake\Routing\DispatcherFilter')
  98. ->setMethods(['beforeDispatch', 'afterDispatch'])
  99. ->getMock();
  100. $this->dispatcher->addFilter($filter);
  101. $this->assertCount(1, $this->dispatcher->getFilters());
  102. $this->assertCount(1, $events->listeners('Dispatcher.beforeDispatch'));
  103. $this->assertCount(1, $events->listeners('Dispatcher.afterDispatch'));
  104. });
  105. }
  106. /**
  107. * Ensure that aborting in the beforeDispatch doesn't invoke the controller
  108. *
  109. * @return void
  110. */
  111. public function testBeforeDispatchEventAbort()
  112. {
  113. $response = new Response();
  114. $dispatcher = new ActionDispatcher();
  115. $req = new ServerRequest();
  116. $res = new Response();
  117. $dispatcher->getEventManager()->on('Dispatcher.beforeDispatch', function () use ($response) {
  118. return $response;
  119. });
  120. $result = $dispatcher->dispatch($req, $res);
  121. $this->assertSame($response, $result, 'Should be response from filter.');
  122. }
  123. /**
  124. * Ensure afterDispatch can replace the response
  125. *
  126. * @return void
  127. */
  128. public function testDispatchAfterDispatchEventModifyResponse()
  129. {
  130. $req = new ServerRequest([
  131. 'url' => '/cakes',
  132. 'params' => [
  133. 'plugin' => null,
  134. 'controller' => 'Cakes',
  135. 'action' => 'index',
  136. 'pass' => [],
  137. ],
  138. 'session' => new Session
  139. ]);
  140. $res = new Response();
  141. $this->dispatcher->getEventManager()->on('Dispatcher.afterDispatch', function (Event $event) {
  142. $response = $event->getData('response');
  143. $event->setData('response', $response->withStringBody('Filter body'));
  144. });
  145. $result = $this->dispatcher->dispatch($req, $res);
  146. $this->assertSame('Filter body', (string)$result->getBody(), 'Should be response from filter.');
  147. }
  148. /**
  149. * Test that a controller action returning a response
  150. * results in no afterDispatch event.
  151. *
  152. * @return void
  153. */
  154. public function testDispatchActionReturnResponseNoAfterDispatch()
  155. {
  156. $req = new ServerRequest([
  157. 'url' => '/cakes',
  158. 'params' => [
  159. 'plugin' => null,
  160. 'controller' => 'Cakes',
  161. 'action' => 'index',
  162. 'pass' => [],
  163. 'return' => true,
  164. ],
  165. ]);
  166. $res = new Response();
  167. $this->dispatcher->getEventManager()->on('Dispatcher.afterDispatch', function () {
  168. $this->fail('no afterDispatch event should be fired');
  169. });
  170. $result = $this->dispatcher->dispatch($req, $res);
  171. $this->assertSame('Hello Jane', (string)$result->getBody(), 'Response from controller.');
  172. }
  173. /**
  174. * Test that dispatching sets the Router request state.
  175. *
  176. * @return void
  177. */
  178. public function testDispatchSetsRequestContext()
  179. {
  180. $this->assertNull(Router::getRequest());
  181. $req = new ServerRequest([
  182. 'url' => '/cakes',
  183. 'params' => [
  184. 'plugin' => null,
  185. 'controller' => 'Cakes',
  186. 'action' => 'index',
  187. 'pass' => [],
  188. 'return' => true,
  189. ],
  190. ]);
  191. $res = new Response();
  192. $this->dispatcher->dispatch($req, $res);
  193. $this->assertSame($req, Router::getRequest(true));
  194. }
  195. /**
  196. * test invalid response from dispatch process.
  197. *
  198. * @return void
  199. */
  200. public function testDispatchInvalidResponse()
  201. {
  202. $this->expectException(\LogicException::class);
  203. $this->expectExceptionMessage('Controller actions can only return Cake\Http\Response or null');
  204. $req = new ServerRequest([
  205. 'url' => '/cakes',
  206. 'params' => [
  207. 'plugin' => null,
  208. 'controller' => 'Cakes',
  209. 'action' => 'invalid',
  210. 'pass' => [],
  211. ],
  212. ]);
  213. $res = new Response();
  214. $result = $this->dispatcher->dispatch($req, $res);
  215. }
  216. /**
  217. * Test dispatch with autorender
  218. *
  219. * @return void
  220. */
  221. public function testDispatchAutoRender()
  222. {
  223. $request = new ServerRequest([
  224. 'url' => 'posts',
  225. 'params' => [
  226. 'controller' => 'Posts',
  227. 'action' => 'index',
  228. 'pass' => [],
  229. ]
  230. ]);
  231. $response = new Response();
  232. $result = $this->dispatcher->dispatch($request, $response);
  233. $this->assertInstanceOf('Cake\Http\Response', $result);
  234. $this->assertContains('posts index', (string)$result->getBody());
  235. }
  236. /**
  237. * Test dispatch with autorender=false
  238. *
  239. * @return void
  240. */
  241. public function testDispatchAutoRenderFalse()
  242. {
  243. $request = new ServerRequest([
  244. 'url' => 'posts',
  245. 'params' => [
  246. 'controller' => 'Cakes',
  247. 'action' => 'noRender',
  248. 'pass' => [],
  249. ]
  250. ]);
  251. $response = new Response();
  252. $result = $this->dispatcher->dispatch($request, $response);
  253. $this->assertInstanceOf('Cake\Http\Response', $result);
  254. $this->assertContains('autoRender false body', (string)$result->getBody());
  255. }
  256. /**
  257. * testMissingController method
  258. *
  259. * @return void
  260. */
  261. public function testMissingController()
  262. {
  263. $this->expectException(\Cake\Routing\Exception\MissingControllerException::class);
  264. $this->expectExceptionMessage('Controller class SomeController could not be found.');
  265. $request = new ServerRequest([
  266. 'url' => 'some_controller/home',
  267. 'params' => [
  268. 'controller' => 'SomeController',
  269. 'action' => 'home',
  270. ]
  271. ]);
  272. $response = $this->getMockBuilder('Cake\Http\Response')->getMock();
  273. $this->dispatcher->dispatch($request, $response);
  274. }
  275. /**
  276. * testMissingControllerInterface method
  277. *
  278. * @return void
  279. */
  280. public function testMissingControllerInterface()
  281. {
  282. $this->expectException(\Cake\Routing\Exception\MissingControllerException::class);
  283. $this->expectExceptionMessage('Controller class Interface could not be found.');
  284. $request = new ServerRequest([
  285. 'url' => 'interface/index',
  286. 'params' => [
  287. 'controller' => 'Interface',
  288. 'action' => 'index',
  289. ]
  290. ]);
  291. $response = $this->getMockBuilder('Cake\Http\Response')->getMock();
  292. $this->dispatcher->dispatch($request, $response);
  293. }
  294. /**
  295. * testMissingControllerInterface method
  296. *
  297. * @return void
  298. */
  299. public function testMissingControllerAbstract()
  300. {
  301. $this->expectException(\Cake\Routing\Exception\MissingControllerException::class);
  302. $this->expectExceptionMessage('Controller class Abstract could not be found.');
  303. $request = new ServerRequest([
  304. 'url' => 'abstract/index',
  305. 'params' => [
  306. 'controller' => 'Abstract',
  307. 'action' => 'index',
  308. ]
  309. ]);
  310. $response = $this->getMockBuilder('Cake\Http\Response')->getMock();
  311. $this->dispatcher->dispatch($request, $response);
  312. }
  313. /**
  314. * Test that lowercase controller names result in missing controller errors.
  315. *
  316. * In case-insensitive file systems, lowercase controller names will kind of work.
  317. * This causes annoying deployment issues for lots of folks.
  318. *
  319. * @return void
  320. */
  321. public function testMissingControllerLowercase()
  322. {
  323. $this->expectException(\Cake\Routing\Exception\MissingControllerException::class);
  324. $this->expectExceptionMessage('Controller class somepages could not be found.');
  325. $request = new ServerRequest([
  326. 'url' => 'pages/home',
  327. 'params' => [
  328. 'plugin' => null,
  329. 'controller' => 'somepages',
  330. 'action' => 'display',
  331. 'pass' => ['home'],
  332. ]
  333. ]);
  334. $response = $this->getMockBuilder('Cake\Http\Response')->getMock();
  335. $this->dispatcher->dispatch($request, $response);
  336. }
  337. /**
  338. * Ensure that a controller's startup event can stop the request.
  339. *
  340. * @return void
  341. */
  342. public function testStartupProcessAbort()
  343. {
  344. $request = new ServerRequest([
  345. 'url' => 'cakes/index',
  346. 'params' => [
  347. 'plugin' => null,
  348. 'controller' => 'Cakes',
  349. 'action' => 'index',
  350. 'stop' => 'startup',
  351. 'pass' => [],
  352. ]
  353. ]);
  354. $response = new Response();
  355. $result = $this->dispatcher->dispatch($request, $response);
  356. $this->assertSame('startup stop', (string)$result->getBody());
  357. }
  358. /**
  359. * Ensure that a controllers startup process can emit a response
  360. *
  361. * @return void
  362. */
  363. public function testShutdownProcessResponse()
  364. {
  365. $request = new ServerRequest([
  366. 'url' => 'cakes/index',
  367. 'params' => [
  368. 'plugin' => null,
  369. 'controller' => 'Cakes',
  370. 'action' => 'index',
  371. 'stop' => 'shutdown',
  372. 'pass' => [],
  373. ]
  374. ]);
  375. $response = new Response();
  376. $result = $this->dispatcher->dispatch($request, $response);
  377. $this->assertSame('shutdown stop', (string)$result->getBody());
  378. }
  379. }