ControllerFactoryTest.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706
  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.3.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Test\TestCase\Controller;
  17. use ArgumentCountError;
  18. use Cake\Controller\ControllerFactory;
  19. use Cake\Core\Container;
  20. use Cake\Http\Exception\MissingControllerException;
  21. use Cake\Http\Response;
  22. use Cake\Http\ServerRequest;
  23. use Cake\TestSuite\TestCase;
  24. use InvalidArgumentException;
  25. use stdClass;
  26. use TestApp\Controller\DependenciesController;
  27. /**
  28. * Test case for ControllerFactory.
  29. */
  30. class ControllerFactoryTest extends TestCase
  31. {
  32. /**
  33. * @var \Cake\Controller\ControllerFactory
  34. */
  35. protected $factory;
  36. /**
  37. * Setup
  38. */
  39. public function setUp(): void
  40. {
  41. parent::setUp();
  42. static::setAppNamespace();
  43. $this->container = new Container();
  44. $this->factory = new ControllerFactory($this->container);
  45. }
  46. /**
  47. * Test building an application controller
  48. */
  49. public function testApplicationController(): void
  50. {
  51. $request = new ServerRequest([
  52. 'url' => 'cakes/index',
  53. 'params' => [
  54. 'controller' => 'Cakes',
  55. 'action' => 'index',
  56. ],
  57. ]);
  58. $result = $this->factory->create($request);
  59. $this->assertInstanceOf('TestApp\Controller\CakesController', $result);
  60. $this->assertSame($request, $result->getRequest());
  61. }
  62. /**
  63. * Test building a prefixed app controller.
  64. */
  65. public function testPrefixedAppController(): void
  66. {
  67. $request = new ServerRequest([
  68. 'url' => 'admin/posts/index',
  69. 'params' => [
  70. 'prefix' => 'Admin',
  71. 'controller' => 'Posts',
  72. 'action' => 'index',
  73. ],
  74. ]);
  75. $result = $this->factory->create($request);
  76. $this->assertInstanceOf(
  77. 'TestApp\Controller\Admin\PostsController',
  78. $result
  79. );
  80. $this->assertSame($request, $result->getRequest());
  81. }
  82. /**
  83. * Test building a nested prefix app controller
  84. */
  85. public function testNestedPrefixedAppController(): void
  86. {
  87. $request = new ServerRequest([
  88. 'url' => 'admin/sub/posts/index',
  89. 'params' => [
  90. 'prefix' => 'Admin/Sub',
  91. 'controller' => 'Posts',
  92. 'action' => 'index',
  93. ],
  94. ]);
  95. $result = $this->factory->create($request);
  96. $this->assertInstanceOf(
  97. 'TestApp\Controller\Admin\Sub\PostsController',
  98. $result
  99. );
  100. $this->assertSame($request, $result->getRequest());
  101. }
  102. /**
  103. * Test building a plugin controller
  104. */
  105. public function testPluginController(): void
  106. {
  107. $request = new ServerRequest([
  108. 'url' => 'test_plugin/test_plugin/index',
  109. 'params' => [
  110. 'plugin' => 'TestPlugin',
  111. 'controller' => 'TestPlugin',
  112. 'action' => 'index',
  113. ],
  114. ]);
  115. $result = $this->factory->create($request);
  116. $this->assertInstanceOf(
  117. 'TestPlugin\Controller\TestPluginController',
  118. $result
  119. );
  120. $this->assertSame($request, $result->getRequest());
  121. }
  122. /**
  123. * Test building a vendored plugin controller.
  124. */
  125. public function testVendorPluginController(): void
  126. {
  127. $request = new ServerRequest([
  128. 'url' => 'test_plugin_three/ovens/index',
  129. 'params' => [
  130. 'plugin' => 'Company/TestPluginThree',
  131. 'controller' => 'Ovens',
  132. 'action' => 'index',
  133. ],
  134. ]);
  135. $result = $this->factory->create($request);
  136. $this->assertInstanceOf(
  137. 'Company\TestPluginThree\Controller\OvensController',
  138. $result
  139. );
  140. $this->assertSame($request, $result->getRequest());
  141. }
  142. /**
  143. * Test building a prefixed plugin controller
  144. */
  145. public function testPrefixedPluginController(): void
  146. {
  147. $request = new ServerRequest([
  148. 'url' => 'test_plugin/admin/comments',
  149. 'params' => [
  150. 'prefix' => 'Admin',
  151. 'plugin' => 'TestPlugin',
  152. 'controller' => 'Comments',
  153. 'action' => 'index',
  154. ],
  155. ]);
  156. $result = $this->factory->create($request);
  157. $this->assertInstanceOf(
  158. 'TestPlugin\Controller\Admin\CommentsController',
  159. $result
  160. );
  161. $this->assertSame($request, $result->getRequest());
  162. }
  163. public function testAbstractClassFailure(): void
  164. {
  165. $this->expectException(MissingControllerException::class);
  166. $this->expectExceptionMessage('Controller class Abstract could not be found.');
  167. $request = new ServerRequest([
  168. 'url' => 'abstract/index',
  169. 'params' => [
  170. 'controller' => 'Abstract',
  171. 'action' => 'index',
  172. ],
  173. ]);
  174. $this->factory->create($request);
  175. }
  176. public function testInterfaceFailure(): void
  177. {
  178. $this->expectException(MissingControllerException::class);
  179. $this->expectExceptionMessage('Controller class Interface could not be found.');
  180. $request = new ServerRequest([
  181. 'url' => 'interface/index',
  182. 'params' => [
  183. 'controller' => 'Interface',
  184. 'action' => 'index',
  185. ],
  186. ]);
  187. $this->factory->create($request);
  188. }
  189. public function testMissingClassFailure(): void
  190. {
  191. $this->expectException(MissingControllerException::class);
  192. $this->expectExceptionMessage('Controller class Invisible could not be found.');
  193. $request = new ServerRequest([
  194. 'url' => 'interface/index',
  195. 'params' => [
  196. 'controller' => 'Invisible',
  197. 'action' => 'index',
  198. ],
  199. ]);
  200. $this->factory->create($request);
  201. }
  202. public function testSlashedControllerFailure(): void
  203. {
  204. $this->expectException(MissingControllerException::class);
  205. $this->expectExceptionMessage('Controller class Admin/Posts could not be found.');
  206. $request = new ServerRequest([
  207. 'url' => 'admin/posts/index',
  208. 'params' => [
  209. 'controller' => 'Admin/Posts',
  210. 'action' => 'index',
  211. ],
  212. ]);
  213. $this->factory->create($request);
  214. }
  215. public function testAbsoluteReferenceFailure(): void
  216. {
  217. $this->expectException(MissingControllerException::class);
  218. $this->expectExceptionMessage('Controller class TestApp\Controller\CakesController could not be found.');
  219. $request = new ServerRequest([
  220. 'url' => 'interface/index',
  221. 'params' => [
  222. 'controller' => 'TestApp\Controller\CakesController',
  223. 'action' => 'index',
  224. ],
  225. ]);
  226. $this->factory->create($request);
  227. }
  228. /**
  229. * Test create() injecting dependcies on defined controllers.
  230. */
  231. public function testCreateWithContainerDependenciesNoController(): void
  232. {
  233. $this->container->add(stdClass::class, json_decode('{"key":"value"}'));
  234. $request = new ServerRequest([
  235. 'url' => 'test_plugin_three/dependencies/index',
  236. 'params' => [
  237. 'plugin' => null,
  238. 'controller' => 'Dependencies',
  239. 'action' => 'index',
  240. ],
  241. ]);
  242. $controller = $this->factory->create($request);
  243. $this->assertNull($controller->inject);
  244. }
  245. /**
  246. * Test create() injecting dependcies on defined controllers.
  247. */
  248. public function testCreateWithContainerDependenciesWithController(): void
  249. {
  250. $this->container->add(stdClass::class, json_decode('{"key":"value"}'));
  251. $this->container->add(DependenciesController::class)
  252. ->addArgument(ServerRequest::class)
  253. ->addArgument(null)
  254. ->addArgument(null)
  255. ->addArgument(null)
  256. ->addArgument(null)
  257. ->addArgument(stdClass::class);
  258. $request = new ServerRequest([
  259. 'url' => 'test_plugin_three/dependencies/index',
  260. 'params' => [
  261. 'plugin' => null,
  262. 'controller' => 'Dependencies',
  263. 'action' => 'index',
  264. ],
  265. ]);
  266. $controller = $this->factory->create($request);
  267. $this->assertInstanceOf(DependenciesController::class, $controller);
  268. $this->assertSame($controller->inject, $this->container->get(stdClass::class));
  269. }
  270. /**
  271. * Test building controller name when passing no controller name
  272. */
  273. public function testGetControllerClassNoControllerName(): void
  274. {
  275. $request = new ServerRequest([
  276. 'url' => 'test_plugin_three/ovens/index',
  277. 'params' => [
  278. 'plugin' => 'Company/TestPluginThree',
  279. 'controller' => 'Ovens',
  280. 'action' => 'index',
  281. ],
  282. ]);
  283. $result = $this->factory->getControllerClass($request);
  284. $this->assertSame('Company\TestPluginThree\Controller\OvensController', $result);
  285. }
  286. /**
  287. * Test invoke with autorender
  288. */
  289. public function testInvokeAutoRender(): void
  290. {
  291. $request = new ServerRequest([
  292. 'url' => 'posts',
  293. 'params' => [
  294. 'controller' => 'Posts',
  295. 'action' => 'index',
  296. 'pass' => [],
  297. ],
  298. ]);
  299. $controller = $this->factory->create($request);
  300. $result = $this->factory->invoke($controller);
  301. $this->assertInstanceOf(Response::class, $result);
  302. $this->assertStringContainsString('posts index', (string)$result->getBody());
  303. }
  304. /**
  305. * Test dispatch with autorender=false
  306. */
  307. public function testInvokeAutoRenderFalse(): void
  308. {
  309. $request = new ServerRequest([
  310. 'url' => 'posts',
  311. 'params' => [
  312. 'controller' => 'Cakes',
  313. 'action' => 'noRender',
  314. 'pass' => [],
  315. ],
  316. ]);
  317. $controller = $this->factory->create($request);
  318. $result = $this->factory->invoke($controller);
  319. $this->assertInstanceOf(Response::class, $result);
  320. $this->assertStringContainsString('autoRender false body', (string)$result->getBody());
  321. }
  322. /**
  323. * Ensure that a controller's startup event can stop the request.
  324. */
  325. public function testStartupProcessAbort(): void
  326. {
  327. $request = new ServerRequest([
  328. 'url' => 'cakes/index',
  329. 'params' => [
  330. 'plugin' => null,
  331. 'controller' => 'Cakes',
  332. 'action' => 'index',
  333. 'stop' => 'startup',
  334. 'pass' => [],
  335. ],
  336. ]);
  337. $controller = $this->factory->create($request);
  338. $result = $this->factory->invoke($controller);
  339. $this->assertSame('startup stop', (string)$result->getBody());
  340. }
  341. /**
  342. * Ensure that a controllers startup process can emit a response
  343. */
  344. public function testShutdownProcessResponse(): void
  345. {
  346. $request = new ServerRequest([
  347. 'url' => 'cakes/index',
  348. 'params' => [
  349. 'plugin' => null,
  350. 'controller' => 'Cakes',
  351. 'action' => 'index',
  352. 'stop' => 'shutdown',
  353. 'pass' => [],
  354. ],
  355. ]);
  356. $controller = $this->factory->create($request);
  357. $result = $this->factory->invoke($controller);
  358. $this->assertSame('shutdown stop', (string)$result->getBody());
  359. }
  360. /**
  361. * Ensure that a controllers startup process can emit a response
  362. */
  363. public function testInvokeInjectOptionalParameterDefined(): void
  364. {
  365. $this->container->add(stdClass::class, json_decode('{"key":"value"}'));
  366. $request = new ServerRequest([
  367. 'url' => 'test_plugin_three/dependencies/optionalDep',
  368. 'params' => [
  369. 'plugin' => null,
  370. 'controller' => 'Dependencies',
  371. 'action' => 'optionalDep',
  372. ],
  373. ]);
  374. $controller = $this->factory->create($request);
  375. $result = $this->factory->invoke($controller);
  376. $data = json_decode((string)$result->getBody());
  377. $this->assertNotNull($data);
  378. $this->assertNull($data->any);
  379. $this->assertNull($data->str);
  380. $this->assertSame('value', $data->dep->key);
  381. }
  382. /**
  383. * Ensure that a controllers startup process can emit a response
  384. */
  385. public function testInvokeInjectParametersOptionalNotDefined(): void
  386. {
  387. $request = new ServerRequest([
  388. 'url' => 'test_plugin_three/dependencies/index',
  389. 'params' => [
  390. 'plugin' => null,
  391. 'controller' => 'Dependencies',
  392. 'action' => 'optionalDep',
  393. ],
  394. ]);
  395. $controller = $this->factory->create($request);
  396. $result = $this->factory->invoke($controller);
  397. $data = json_decode((string)$result->getBody());
  398. $this->assertNotNull($data);
  399. $this->assertNull($data->any);
  400. $this->assertNull($data->str);
  401. $this->assertNull($data->dep);
  402. }
  403. /**
  404. * Ensure that a controllers startup process can emit a response
  405. */
  406. public function testInvokeInjectParametersOptionalWithPassedParameters(): void
  407. {
  408. $this->container->add(stdClass::class, json_decode('{"key":"value"}'));
  409. $request = new ServerRequest([
  410. 'url' => 'test_plugin_three/dependencies/optionalDep',
  411. 'params' => [
  412. 'plugin' => null,
  413. 'controller' => 'Dependencies',
  414. 'action' => 'optionalDep',
  415. 'pass' => ['one', 'two'],
  416. ],
  417. ]);
  418. $controller = $this->factory->create($request);
  419. $result = $this->factory->invoke($controller);
  420. $data = json_decode((string)$result->getBody());
  421. $this->assertNotNull($data);
  422. $this->assertSame($data->any, 'one');
  423. $this->assertSame($data->str, 'two');
  424. $this->assertSame('value', $data->dep->key);
  425. }
  426. /**
  427. * Ensure that a controllers startup process can emit a response
  428. */
  429. public function testInvokeInjectParametersRequiredDefined(): void
  430. {
  431. $this->container->add(stdClass::class, json_decode('{"key":"value"}'));
  432. $request = new ServerRequest([
  433. 'url' => 'test_plugin_three/dependencies/requiredDep',
  434. 'params' => [
  435. 'plugin' => null,
  436. 'controller' => 'Dependencies',
  437. 'action' => 'requiredDep',
  438. ],
  439. ]);
  440. $controller = $this->factory->create($request);
  441. $result = $this->factory->invoke($controller);
  442. $data = json_decode((string)$result->getBody());
  443. $this->assertNotNull($data);
  444. $this->assertNull($data->any);
  445. $this->assertNull($data->str);
  446. $this->assertSame('value', $data->dep->key);
  447. }
  448. /**
  449. * Ensure that a controllers startup process can emit a response
  450. */
  451. public function testInvokeInjectParametersRequiredNotDefined(): void
  452. {
  453. $request = new ServerRequest([
  454. 'url' => 'test_plugin_three/dependencies/index',
  455. 'params' => [
  456. 'plugin' => null,
  457. 'controller' => 'Dependencies',
  458. 'action' => 'requiredDep',
  459. ],
  460. ]);
  461. $controller = $this->factory->create($request);
  462. $this->expectException(InvalidArgumentException::class);
  463. $this->expectExceptionMessage('Could not resolve action argument `dep`');
  464. $this->factory->invoke($controller);
  465. }
  466. public function testInvokeInjectParametersRequiredMissingUntyped(): void
  467. {
  468. $request = new ServerRequest([
  469. 'url' => 'test_plugin_three/dependencies/requiredParam',
  470. 'params' => [
  471. 'plugin' => null,
  472. 'controller' => 'Dependencies',
  473. 'action' => 'requiredParam',
  474. ],
  475. ]);
  476. $controller = $this->factory->create($request);
  477. $this->expectException(ArgumentCountError::class);
  478. $this->expectExceptionMessage('Too few arguments');
  479. $this->factory->invoke($controller);
  480. }
  481. public function testInvokeInjectParametersRequiredUntyped(): void
  482. {
  483. $request = new ServerRequest([
  484. 'url' => 'test_plugin_three/dependencies/requiredParam',
  485. 'params' => [
  486. 'plugin' => null,
  487. 'controller' => 'Dependencies',
  488. 'action' => 'requiredParam',
  489. 'pass' => ['one'],
  490. ],
  491. ]);
  492. $controller = $this->factory->create($request);
  493. $result = $this->factory->invoke($controller);
  494. $data = json_decode((string)$result->getBody());
  495. $this->assertNotNull($data);
  496. $this->assertSame($data->one, 'one');
  497. }
  498. public function testInvokeInjectParametersRequiredWithPassedParameters(): void
  499. {
  500. $this->container->add(stdClass::class, json_decode('{"key":"value"}'));
  501. $request = new ServerRequest([
  502. 'url' => 'test_plugin_three/dependencies/requiredDep',
  503. 'params' => [
  504. 'plugin' => null,
  505. 'controller' => 'Dependencies',
  506. 'action' => 'requiredDep',
  507. 'pass' => ['one', 'two'],
  508. ],
  509. ]);
  510. $controller = $this->factory->create($request);
  511. $result = $this->factory->invoke($controller);
  512. $data = json_decode((string)$result->getBody());
  513. $this->assertNotNull($data);
  514. $this->assertSame($data->any, 'one');
  515. $this->assertSame($data->str, 'two');
  516. $this->assertSame('value', $data->dep->key);
  517. }
  518. /**
  519. * Test that routing parameters are passed into variadic controller functions
  520. */
  521. public function testInvokeInjectPassedParametersVariadic(): void
  522. {
  523. $request = new ServerRequest([
  524. 'url' => 'test_plugin_three/dependencies/variadic',
  525. 'params' => [
  526. 'plugin' => null,
  527. 'controller' => 'Dependencies',
  528. 'action' => 'variadic',
  529. 'pass' => ['one', 'two'],
  530. ],
  531. ]);
  532. $controller = $this->factory->create($request);
  533. $result = $this->factory->invoke($controller);
  534. $data = json_decode((string)$result->getBody());
  535. $this->assertNotNull($data);
  536. $this->assertSame(['one', 'two'], $data->args);
  537. }
  538. /**
  539. * Test that routing parameters are passed into controller action using spread operator
  540. */
  541. public function testInvokeInjectPassedParametersSpread(): void
  542. {
  543. $request = new ServerRequest([
  544. 'url' => 'test_plugin_three/dependencies/spread',
  545. 'params' => [
  546. 'plugin' => null,
  547. 'controller' => 'Dependencies',
  548. 'action' => 'spread',
  549. 'pass' => ['one', 'two'],
  550. ],
  551. ]);
  552. $controller = $this->factory->create($request);
  553. $result = $this->factory->invoke($controller);
  554. $data = json_decode((string)$result->getBody());
  555. $this->assertNotNull($data);
  556. $this->assertSame(['one', 'two'], $data->args);
  557. }
  558. /**
  559. * Test that routing parameters are passed into controller action using spread operator
  560. */
  561. public function testInvokeInjectPassedParametersSpreadNoParams(): void
  562. {
  563. $request = new ServerRequest([
  564. 'url' => 'test_plugin_three/dependencies/spread',
  565. 'params' => [
  566. 'plugin' => null,
  567. 'controller' => 'Dependencies',
  568. 'action' => 'spread',
  569. 'pass' => [],
  570. ],
  571. ]);
  572. $controller = $this->factory->create($request);
  573. $result = $this->factory->invoke($controller);
  574. $data = json_decode((string)$result->getBody());
  575. $this->assertNotNull($data);
  576. $this->assertSame([], $data->args);
  577. }
  578. /**
  579. * Test that default parameters work for controller methods
  580. */
  581. public function testInvokeOptionalStringParam(): void
  582. {
  583. $request = new ServerRequest([
  584. 'url' => 'test_plugin_three/dependencies/optionalString',
  585. 'params' => [
  586. 'plugin' => null,
  587. 'controller' => 'Dependencies',
  588. 'action' => 'optionalString',
  589. ],
  590. ]);
  591. $controller = $this->factory->create($request);
  592. $result = $this->factory->invoke($controller);
  593. $data = json_decode((string)$result->getBody());
  594. $this->assertNotNull($data);
  595. $this->assertSame('default val', $data->str);
  596. }
  597. /**
  598. * Test that required strings a default value.
  599. */
  600. public function testInvokeRequiredStringParam(): void
  601. {
  602. $request = new ServerRequest([
  603. 'url' => 'test_plugin_three/dependencies/requiredString',
  604. 'params' => [
  605. 'plugin' => null,
  606. 'controller' => 'Dependencies',
  607. 'action' => 'requiredString',
  608. ],
  609. ]);
  610. $controller = $this->factory->create($request);
  611. $this->expectException(ArgumentCountError::class);
  612. $this->expectExceptionMessage('Too few arguments');
  613. $this->factory->invoke($controller);
  614. }
  615. public function testMiddleware(): void
  616. {
  617. $request = new ServerRequest([
  618. 'url' => 'posts',
  619. 'params' => [
  620. 'controller' => 'Posts',
  621. 'action' => 'index',
  622. 'pass' => [],
  623. ],
  624. ]);
  625. $controller = $this->factory->create($request);
  626. $this->factory->invoke($controller);
  627. $request = $controller->getRequest();
  628. $this->assertTrue($request->getAttribute('for-all'));
  629. $this->assertTrue($request->getAttribute('index-only'));
  630. $this->assertNull($request->getAttribute('all-except-index'));
  631. $request = new ServerRequest([
  632. 'url' => 'posts/get',
  633. 'params' => [
  634. 'controller' => 'Posts',
  635. 'action' => 'get',
  636. 'pass' => [],
  637. ],
  638. ]);
  639. $controller = $this->factory->create($request);
  640. $this->factory->invoke($controller);
  641. $request = $controller->getRequest();
  642. $this->assertTrue($request->getAttribute('for-all'));
  643. $this->assertNull($request->getAttribute('index-only'));
  644. $this->assertTrue($request->getAttribute('all-except-index'));
  645. }
  646. }