ExceptionRendererTest.php 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975
  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 2.0.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Test\TestCase\Error;
  17. use Cake\Controller\Controller;
  18. use Cake\Controller\Exception\MissingActionException;
  19. use Cake\Controller\Exception\MissingComponentException;
  20. use Cake\Core\Configure;
  21. use Cake\Core\Exception\Exception as CakeException;
  22. use Cake\Core\Exception\MissingPluginException;
  23. use Cake\Datasource\Exception\MissingDatasourceConfigException;
  24. use Cake\Datasource\Exception\MissingDatasourceException;
  25. use Cake\Error\ExceptionRenderer;
  26. use Cake\Event\EventInterface;
  27. use Cake\Event\EventManager;
  28. use Cake\Http\Exception\HttpException;
  29. use Cake\Http\Exception\InternalErrorException;
  30. use Cake\Http\Exception\MethodNotAllowedException;
  31. use Cake\Http\Exception\MissingControllerException;
  32. use Cake\Http\Exception\NotFoundException;
  33. use Cake\Http\ServerRequest;
  34. use Cake\Mailer\Exception\MissingActionException as MissingMailerActionException;
  35. use Cake\ORM\Exception\MissingBehaviorException;
  36. use Cake\Routing\Router;
  37. use Cake\TestSuite\TestCase;
  38. use Cake\View\Exception\MissingHelperException;
  39. use Cake\View\Exception\MissingLayoutException;
  40. use Cake\View\Exception\MissingTemplateException;
  41. use Exception;
  42. use RuntimeException;
  43. use TestApp\Controller\Admin\ErrorController;
  44. use TestApp\Error\Exception\MissingWidgetThing;
  45. use TestApp\Error\Exception\MissingWidgetThingException;
  46. use TestApp\Error\MyCustomExceptionRenderer;
  47. class ExceptionRendererTest extends TestCase
  48. {
  49. /**
  50. * @var bool
  51. */
  52. protected $_restoreError = false;
  53. /**
  54. * setup create a request object to get out of router later.
  55. *
  56. * @return void
  57. */
  58. public function setUp(): void
  59. {
  60. parent::setUp();
  61. Configure::write('Config.language', 'eng');
  62. Router::reload();
  63. $request = new ServerRequest(['base' => '']);
  64. Router::setRequest($request);
  65. Configure::write('debug', true);
  66. }
  67. /**
  68. * tearDown
  69. *
  70. * @return void
  71. */
  72. public function tearDown(): void
  73. {
  74. parent::tearDown();
  75. $this->clearPlugins();
  76. if ($this->_restoreError) {
  77. restore_error_handler();
  78. }
  79. }
  80. public function testControllerInstanceForPrefixedRequest()
  81. {
  82. $namespace = Configure::read('App.namespace');
  83. Configure::write('App.namespace', 'TestApp');
  84. $exception = new NotFoundException('Page not found');
  85. $request = new ServerRequest();
  86. $request = $request
  87. ->withParam('controller', 'Articles')
  88. ->withParam('prefix', 'Admin');
  89. $ExceptionRenderer = new MyCustomExceptionRenderer($exception, $request);
  90. $this->assertInstanceOf(
  91. ErrorController::class,
  92. $ExceptionRenderer->__debugInfo()['controller']
  93. );
  94. Configure::write('App.namespace', $namespace);
  95. }
  96. /**
  97. * testTemplatePath
  98. *
  99. * @return void
  100. */
  101. public function testTemplatePath()
  102. {
  103. $request = (new ServerRequest())
  104. ->withParam('controller', 'Foo')
  105. ->withParam('action', 'bar');
  106. $exception = new NotFoundException();
  107. $ExceptionRenderer = new ExceptionRenderer($exception, $request);
  108. $ExceptionRenderer->render();
  109. $controller = $ExceptionRenderer->__debugInfo()['controller'];
  110. $this->assertSame('error400', $controller->viewBuilder()->getTemplate());
  111. $this->assertSame('Error', $controller->viewBuilder()->getTemplatePath());
  112. $request = $request->withParam('prefix', 'Admin');
  113. $exception = new MissingActionException(['controller' => 'Foo', 'action' => 'bar']);
  114. $ExceptionRenderer = new ExceptionRenderer($exception, $request);
  115. $ExceptionRenderer->render();
  116. $controller = $ExceptionRenderer->__debugInfo()['controller'];
  117. $this->assertSame('missingAction', $controller->viewBuilder()->getTemplate());
  118. $this->assertSame('Error', $controller->viewBuilder()->getTemplatePath());
  119. Configure::write('debug', false);
  120. $ExceptionRenderer = new ExceptionRenderer($exception, $request);
  121. $ExceptionRenderer->render();
  122. $controller = $ExceptionRenderer->__debugInfo()['controller'];
  123. $this->assertSame('error400', $controller->viewBuilder()->getTemplate());
  124. $this->assertEquals(
  125. 'Admin' . DIRECTORY_SEPARATOR . 'Error',
  126. $controller->viewBuilder()->getTemplatePath()
  127. );
  128. }
  129. /**
  130. * test that methods declared in an ExceptionRenderer subclass are not converted
  131. * into error400 when debug > 0
  132. *
  133. * @return void
  134. */
  135. public function testSubclassMethodsNotBeingConvertedToError()
  136. {
  137. $exception = new MissingWidgetThingException('Widget not found');
  138. $ExceptionRenderer = new MyCustomExceptionRenderer($exception);
  139. $result = $ExceptionRenderer->render();
  140. $this->assertSame('widget thing is missing', (string)$result->getBody());
  141. }
  142. /**
  143. * test that subclass methods are not converted when debug = 0
  144. *
  145. * @return void
  146. */
  147. public function testSubclassMethodsNotBeingConvertedDebug0()
  148. {
  149. Configure::write('debug', false);
  150. $exception = new MissingWidgetThingException('Widget not found');
  151. $ExceptionRenderer = new MyCustomExceptionRenderer($exception);
  152. $result = $ExceptionRenderer->render();
  153. $this->assertEquals(
  154. 'missingWidgetThing',
  155. $ExceptionRenderer->__debugInfo()['method']
  156. );
  157. $this->assertEquals(
  158. 'widget thing is missing',
  159. (string)$result->getBody(),
  160. 'Method declared in subclass converted to error400'
  161. );
  162. }
  163. /**
  164. * test that ExceptionRenderer subclasses properly convert framework errors.
  165. *
  166. * @return void
  167. */
  168. public function testSubclassConvertingFrameworkErrors()
  169. {
  170. Configure::write('debug', false);
  171. $exception = new MissingControllerException('PostsController');
  172. $ExceptionRenderer = new MyCustomExceptionRenderer($exception);
  173. $result = $ExceptionRenderer->render();
  174. $this->assertRegExp(
  175. '/Not Found/',
  176. (string)$result->getBody(),
  177. 'Method declared in error handler not converted to error400. %s'
  178. );
  179. }
  180. /**
  181. * test things in the constructor.
  182. *
  183. * @return void
  184. */
  185. public function testConstruction()
  186. {
  187. $exception = new NotFoundException('Page not found');
  188. $ExceptionRenderer = new ExceptionRenderer($exception);
  189. $this->assertInstanceOf(
  190. 'Cake\Controller\ErrorController',
  191. $ExceptionRenderer->__debugInfo()['controller']
  192. );
  193. $this->assertEquals($exception, $ExceptionRenderer->__debugInfo()['error']);
  194. }
  195. /**
  196. * test that exception message gets coerced when debug = 0
  197. *
  198. * @return void
  199. */
  200. public function testExceptionMessageCoercion()
  201. {
  202. Configure::write('debug', false);
  203. $exception = new MissingActionException('Secret info not to be leaked');
  204. $ExceptionRenderer = new ExceptionRenderer($exception);
  205. $this->assertInstanceOf(
  206. 'Cake\Controller\ErrorController',
  207. $ExceptionRenderer->__debugInfo()['controller']
  208. );
  209. $this->assertEquals($exception, $ExceptionRenderer->__debugInfo()['error']);
  210. $result = (string)$ExceptionRenderer->render()->getBody();
  211. $this->assertSame('error400', $ExceptionRenderer->__debugInfo()['template']);
  212. $this->assertStringContainsString('Not Found', $result);
  213. $this->assertStringNotContainsString('Secret info not to be leaked', $result);
  214. }
  215. /**
  216. * test that helpers in custom CakeErrorController are not lost
  217. *
  218. * @return void
  219. */
  220. public function testCakeErrorHelpersNotLost()
  221. {
  222. static::setAppNamespace();
  223. $exception = new NotFoundException();
  224. $renderer = new \TestApp\Error\TestAppsExceptionRenderer($exception);
  225. $result = $renderer->render();
  226. $this->assertStringContainsString('<b>peeled</b>', (string)$result->getBody());
  227. }
  228. /**
  229. * test that unknown exception types with valid status codes are treated correctly.
  230. *
  231. * @return void
  232. */
  233. public function testUnknownExceptionTypeWithExceptionThatHasA400Code()
  234. {
  235. $exception = new MissingWidgetThingException('coding fail.');
  236. $ExceptionRenderer = new ExceptionRenderer($exception);
  237. $response = $ExceptionRenderer->render();
  238. $this->assertEquals(404, $response->getStatusCode());
  239. $this->assertFalse(method_exists($ExceptionRenderer, 'missingWidgetThing'), 'no method should exist.');
  240. $this->assertStringContainsString('coding fail', (string)$response->getBody(), 'Text should show up.');
  241. }
  242. /**
  243. * test that unknown exception types with valid status codes are treated correctly.
  244. *
  245. * @return void
  246. */
  247. public function testUnknownExceptionTypeWithNoCodeIsA500()
  248. {
  249. $exception = new \OutOfBoundsException('foul ball.');
  250. $ExceptionRenderer = new ExceptionRenderer($exception);
  251. $result = $ExceptionRenderer->render();
  252. $this->assertEquals(500, $result->getStatusCode());
  253. $this->assertStringContainsString('foul ball.', (string)$result->getBody(), 'Text should show up as its debug mode.');
  254. }
  255. /**
  256. * test that unknown exceptions have messages ignored.
  257. *
  258. * @return void
  259. */
  260. public function testUnknownExceptionInProduction()
  261. {
  262. Configure::write('debug', false);
  263. $exception = new \OutOfBoundsException('foul ball.');
  264. $ExceptionRenderer = new ExceptionRenderer($exception);
  265. $response = $ExceptionRenderer->render();
  266. $result = (string)$response->getBody();
  267. $this->assertEquals(500, $response->getStatusCode());
  268. $this->assertStringNotContainsString('foul ball.', $result, 'Text should no show up.');
  269. $this->assertStringContainsString('Internal Error', $result, 'Generic message only.');
  270. }
  271. /**
  272. * test that unknown exception types with valid status codes are treated correctly.
  273. *
  274. * @return void
  275. */
  276. public function testUnknownExceptionTypeWithCodeHigherThan500()
  277. {
  278. $exception = new \OutOfBoundsException('foul ball.', 501);
  279. $ExceptionRenderer = new ExceptionRenderer($exception);
  280. $response = $ExceptionRenderer->render();
  281. $result = (string)$response->getBody();
  282. $this->assertEquals(501, $response->getStatusCode());
  283. $this->assertStringContainsString('foul ball.', $result, 'Text should show up as its debug mode.');
  284. }
  285. /**
  286. * testerror400 method
  287. *
  288. * @return void
  289. */
  290. public function testError400()
  291. {
  292. Router::reload();
  293. $request = new ServerRequest(['url' => 'posts/view/1000']);
  294. Router::setRequest($request);
  295. $exception = new NotFoundException('Custom message');
  296. $ExceptionRenderer = new ExceptionRenderer($exception);
  297. $response = $ExceptionRenderer->render();
  298. $result = (string)$response->getBody();
  299. $this->assertEquals(404, $response->getStatusCode());
  300. $this->assertStringContainsString('<h2>Custom message</h2>', $result);
  301. $this->assertRegExp("/<strong>'.*?\/posts\/view\/1000'<\/strong>/", $result);
  302. }
  303. /**
  304. * testerror400 method when returning as json
  305. *
  306. * @return void
  307. */
  308. public function testError400AsJson()
  309. {
  310. Router::reload();
  311. $request = new ServerRequest(['url' => 'posts/view/1000?sort=title&direction=desc']);
  312. $request = $request->withHeader('Accept', 'application/json');
  313. $request = $request->withHeader('Content-Type', 'application/json');
  314. Router::setRequest($request);
  315. $exception = new NotFoundException('Custom message');
  316. $exceptionLine = __LINE__ - 1;
  317. $ExceptionRenderer = new ExceptionRenderer($exception);
  318. $response = $ExceptionRenderer->render();
  319. $result = (string)$response->getBody();
  320. $expected = [
  321. 'message' => 'Custom message',
  322. 'url' => '/posts/view/1000?sort=title&amp;direction=desc',
  323. 'code' => 404,
  324. 'file' => __FILE__,
  325. 'line' => $exceptionLine,
  326. ];
  327. $this->assertEquals($expected, json_decode($result, true));
  328. $this->assertEquals(404, $response->getStatusCode());
  329. }
  330. /**
  331. * test that error400 only modifies the messages on Cake Exceptions.
  332. *
  333. * @return void
  334. */
  335. public function testerror400OnlyChangingCakeException()
  336. {
  337. Configure::write('debug', false);
  338. $exception = new NotFoundException('Custom message');
  339. $ExceptionRenderer = new ExceptionRenderer($exception);
  340. $result = $ExceptionRenderer->render();
  341. $this->assertStringContainsString('Custom message', (string)$result->getBody());
  342. $exception = new MissingActionException(['controller' => 'PostsController', 'action' => 'index']);
  343. $ExceptionRenderer = new ExceptionRenderer($exception);
  344. $result = $ExceptionRenderer->render();
  345. $this->assertStringContainsString('Not Found', (string)$result->getBody());
  346. }
  347. /**
  348. * test that error400 doesn't expose XSS
  349. *
  350. * @return void
  351. */
  352. public function testError400NoInjection()
  353. {
  354. Router::reload();
  355. $request = new ServerRequest(['url' => 'pages/<span id=333>pink</span></id><script>document.body.style.background = t=document.getElementById(333).innerHTML;window.alert(t);</script>']);
  356. Router::setRequest($request);
  357. $exception = new NotFoundException('Custom message');
  358. $ExceptionRenderer = new ExceptionRenderer($exception);
  359. $result = (string)$ExceptionRenderer->render()->getBody();
  360. $this->assertStringNotContainsString('<script>document', $result);
  361. $this->assertStringNotContainsString('alert(t);</script>', $result);
  362. }
  363. /**
  364. * testError500 method
  365. *
  366. * @return void
  367. */
  368. public function testError500Message()
  369. {
  370. $exception = new InternalErrorException('An Internal Error Has Occurred.');
  371. $ExceptionRenderer = new ExceptionRenderer($exception);
  372. $response = $ExceptionRenderer->render();
  373. $result = (string)$response->getBody();
  374. $this->assertEquals(500, $response->getStatusCode());
  375. $this->assertStringContainsString('<h2>An Internal Error Has Occurred.</h2>', $result);
  376. $this->assertStringContainsString('An Internal Error Has Occurred.</p>', $result);
  377. }
  378. /**
  379. * testExceptionResponseHeader method
  380. *
  381. * @return void
  382. */
  383. public function testExceptionResponseHeader()
  384. {
  385. $exception = new MethodNotAllowedException('Only allowing POST and DELETE');
  386. $exception->responseHeader(['Allow' => 'POST, DELETE']);
  387. $ExceptionRenderer = new ExceptionRenderer($exception);
  388. $result = $ExceptionRenderer->render();
  389. $this->assertTrue($result->hasHeader('Allow'));
  390. $this->assertSame('POST, DELETE', $result->getHeaderLine('Allow'));
  391. }
  392. /**
  393. * testMissingController method
  394. *
  395. * @return void
  396. */
  397. public function testMissingController()
  398. {
  399. $exception = new MissingControllerException([
  400. 'class' => 'Posts',
  401. 'prefix' => '',
  402. 'plugin' => '',
  403. ]);
  404. $ExceptionRenderer = new MyCustomExceptionRenderer($exception);
  405. $result = (string)$ExceptionRenderer->render()->getBody();
  406. $this->assertEquals(
  407. 'missingController',
  408. $ExceptionRenderer->__debugInfo()['template']
  409. );
  410. $this->assertStringContainsString('Missing Controller', $result);
  411. $this->assertStringContainsString('<em>PostsController</em>', $result);
  412. }
  413. /**
  414. * test missingController method
  415. *
  416. * @return void
  417. */
  418. public function testMissingControllerLowerCase()
  419. {
  420. $exception = new MissingControllerException([
  421. 'class' => 'posts',
  422. 'prefix' => '',
  423. 'plugin' => '',
  424. ]);
  425. $ExceptionRenderer = new MyCustomExceptionRenderer($exception);
  426. $result = (string)$ExceptionRenderer->render()->getBody();
  427. $this->assertEquals(
  428. 'missingController',
  429. $ExceptionRenderer->__debugInfo()['template']
  430. );
  431. $this->assertStringContainsString('Missing Controller', $result);
  432. $this->assertStringContainsString('<em>PostsController</em>', $result);
  433. }
  434. /**
  435. * Returns an array of tests to run for the various Cake Exception classes.
  436. *
  437. * @return array
  438. */
  439. public static function exceptionProvider()
  440. {
  441. return [
  442. [
  443. new MissingActionException([
  444. 'controller' => 'postsController',
  445. 'action' => 'index',
  446. 'prefix' => '',
  447. 'plugin' => '',
  448. ]),
  449. [
  450. '/Missing Method in PostsController/',
  451. '/<em>PostsController::index\(\)<\/em>/',
  452. ],
  453. 404,
  454. ],
  455. [
  456. new MissingActionException([
  457. 'controller' => 'PostsController',
  458. 'action' => 'index',
  459. 'prefix' => '',
  460. 'plugin' => '',
  461. ]),
  462. [
  463. '/Missing Method in PostsController/',
  464. '/<em>PostsController::index\(\)<\/em>/',
  465. ],
  466. 404,
  467. ],
  468. [
  469. new MissingTemplateException(['file' => '/posts/about.ctp']),
  470. [
  471. "/posts\/about.ctp/",
  472. ],
  473. 500,
  474. ],
  475. [
  476. new MissingLayoutException(['file' => 'layouts/my_layout.ctp']),
  477. [
  478. '/Missing Layout/',
  479. "/layouts\/my_layout.ctp/",
  480. ],
  481. 500,
  482. ],
  483. [
  484. new MissingHelperException(['class' => 'MyCustomHelper']),
  485. [
  486. '/Missing Helper/',
  487. '/<em>MyCustomHelper<\/em> could not be found./',
  488. '/Create the class <em>MyCustomHelper<\/em> below in file:/',
  489. '/(\/|\\\)MyCustomHelper.php/',
  490. ],
  491. 500,
  492. ],
  493. [
  494. new MissingBehaviorException(['class' => 'MyCustomBehavior']),
  495. [
  496. '/Missing Behavior/',
  497. '/Create the class <em>MyCustomBehavior<\/em> below in file:/',
  498. '/(\/|\\\)MyCustomBehavior.php/',
  499. ],
  500. 500,
  501. ],
  502. [
  503. new MissingComponentException(['class' => 'SideboxComponent']),
  504. [
  505. '/Missing Component/',
  506. '/Create the class <em>SideboxComponent<\/em> below in file:/',
  507. '/(\/|\\\)SideboxComponent.php/',
  508. ],
  509. 500,
  510. ],
  511. [
  512. new MissingDatasourceConfigException(['name' => 'MyDatasourceConfig']),
  513. [
  514. '/Missing Datasource Configuration/',
  515. '/<em>MyDatasourceConfig<\/em> was not found/',
  516. ],
  517. 500,
  518. ],
  519. [
  520. new MissingDatasourceException(['class' => 'MyDatasource', 'plugin' => 'MyPlugin']),
  521. [
  522. '/Missing Datasource/',
  523. '/<em>MyPlugin.MyDatasource<\/em> could not be found./',
  524. ],
  525. 500,
  526. ],
  527. [
  528. new MissingMailerActionException([
  529. 'mailer' => 'UserMailer',
  530. 'action' => 'welcome',
  531. 'prefix' => '',
  532. 'plugin' => '',
  533. ]),
  534. [
  535. '/Missing Method in UserMailer/',
  536. '/<em>UserMailer::welcome\(\)<\/em>/',
  537. ],
  538. 404,
  539. ],
  540. [
  541. new Exception('boom'),
  542. [
  543. '/Internal Error/',
  544. ],
  545. 500,
  546. ],
  547. [
  548. new RuntimeException('another boom'),
  549. [
  550. '/Internal Error/',
  551. ],
  552. 500,
  553. ],
  554. [
  555. new CakeException('base class'),
  556. ['/Internal Error/'],
  557. 500,
  558. ],
  559. [
  560. new HttpException('Network Authentication Required', 511),
  561. ['/Network Authentication Required/'],
  562. 511,
  563. ],
  564. ];
  565. }
  566. /**
  567. * Test the various Cake Exception sub classes
  568. *
  569. * @dataProvider exceptionProvider
  570. * @return void
  571. */
  572. public function testCakeExceptionHandling($exception, $patterns, $code)
  573. {
  574. $exceptionRenderer = new ExceptionRenderer($exception);
  575. $response = $exceptionRenderer->render();
  576. $this->assertEquals($code, $response->getStatusCode());
  577. $body = (string)$response->getBody();
  578. foreach ($patterns as $pattern) {
  579. $this->assertRegExp($pattern, $body);
  580. }
  581. }
  582. /**
  583. * Test that class names not ending in Exception are not mangled.
  584. *
  585. * @return void
  586. */
  587. public function testExceptionNameMangling()
  588. {
  589. $exceptionRenderer = new MyCustomExceptionRenderer(new MissingWidgetThing());
  590. $result = (string)$exceptionRenderer->render()->getBody();
  591. $this->assertStringContainsString('widget thing is missing', $result);
  592. // Custom method should be called even when debug is off.
  593. Configure::write('debug', false);
  594. $exceptionRenderer = new MyCustomExceptionRenderer(new MissingWidgetThing());
  595. $result = (string)$exceptionRenderer->render()->getBody();
  596. $this->assertStringContainsString('widget thing is missing', $result);
  597. }
  598. /**
  599. * Test exceptions being raised when helpers are missing.
  600. *
  601. * @return void
  602. */
  603. public function testMissingRenderSafe()
  604. {
  605. $exception = new MissingHelperException(['class' => 'Fail']);
  606. $ExceptionRenderer = new MyCustomExceptionRenderer($exception);
  607. /** @var \Cake\Controller\Controller|\PHPUnit\Framework\MockObject\MockObject $controller */
  608. $controller = $this->getMockBuilder('Cake\Controller\Controller')
  609. ->setMethods(['render'])
  610. ->getMock();
  611. $controller->viewBuilder()->setHelpers(['Fail', 'Boom']);
  612. $controller->setRequest(new ServerRequest());
  613. $controller->expects($this->at(0))
  614. ->method('render')
  615. ->with('missingHelper')
  616. ->will($this->throwException($exception));
  617. $ExceptionRenderer->setController($controller);
  618. $response = $ExceptionRenderer->render();
  619. $helpers = $controller->viewBuilder()->getHelpers();
  620. sort($helpers);
  621. $this->assertEquals([], $helpers);
  622. $this->assertStringContainsString('Helper class Fail', (string)$response->getBody());
  623. }
  624. /**
  625. * Test that exceptions in beforeRender() are handled by outputMessageSafe
  626. *
  627. * @return void
  628. */
  629. public function testRenderExceptionInBeforeRender()
  630. {
  631. $exception = new NotFoundException('Not there, sorry');
  632. $ExceptionRenderer = new MyCustomExceptionRenderer($exception);
  633. /** @var \Cake\Controller\Controller|\PHPUnit\Framework\MockObject\MockObject $controller */
  634. $controller = $this->getMockBuilder('Cake\Controller\Controller')
  635. ->setMethods(['beforeRender'])
  636. ->getMock();
  637. $controller->request = new ServerRequest();
  638. $controller->expects($this->any())
  639. ->method('beforeRender')
  640. ->will($this->throwException($exception));
  641. $ExceptionRenderer->setController($controller);
  642. $response = $ExceptionRenderer->render();
  643. $this->assertStringContainsString('Not there, sorry', (string)$response->getBody());
  644. }
  645. /**
  646. * Test that missing layoutPath don't cause other fatal errors.
  647. *
  648. * @return void
  649. */
  650. public function testMissingLayoutPathRenderSafe()
  651. {
  652. $this->called = false;
  653. $exception = new NotFoundException();
  654. $ExceptionRenderer = new MyCustomExceptionRenderer($exception);
  655. $controller = new Controller();
  656. $controller->viewBuilder()->setHelpers(['Fail', 'Boom']);
  657. $controller->getEventManager()->on(
  658. 'Controller.beforeRender',
  659. function (EventInterface $event) {
  660. $this->called = true;
  661. $event->getSubject()->viewBuilder()->setLayoutPath('boom');
  662. }
  663. );
  664. $controller->setRequest(new ServerRequest());
  665. $ExceptionRenderer->setController($controller);
  666. $response = $ExceptionRenderer->render();
  667. $this->assertSame('text/html', $response->getType());
  668. $this->assertStringContainsString('Not Found', (string)$response->getBody());
  669. $this->assertTrue($this->called, 'Listener added was not triggered.');
  670. $this->assertSame('', $controller->viewBuilder()->getLayoutPath());
  671. $this->assertSame('Error', $controller->viewBuilder()->getTemplatePath());
  672. }
  673. /**
  674. * Test that missing layout don't cause other fatal errors.
  675. *
  676. * @return void
  677. */
  678. public function testMissingLayoutRenderSafe()
  679. {
  680. $this->called = false;
  681. $exception = new NotFoundException();
  682. $ExceptionRenderer = new MyCustomExceptionRenderer($exception);
  683. $controller = new Controller();
  684. $controller->getEventManager()->on(
  685. 'Controller.beforeRender',
  686. function (EventInterface $event) {
  687. $this->called = true;
  688. $event->getSubject()->viewBuilder()->setTemplatePath('Error');
  689. $event->getSubject()->viewBuilder()->setLayout('does-not-exist');
  690. }
  691. );
  692. $controller->setRequest(new ServerRequest());
  693. $ExceptionRenderer->setController($controller);
  694. $response = $ExceptionRenderer->render();
  695. $this->assertSame('text/html', $response->getType());
  696. $this->assertStringContainsString('Not Found', (string)$response->getBody());
  697. $this->assertTrue($this->called, 'Listener added was not triggered.');
  698. $this->assertSame('', $controller->viewBuilder()->getLayoutPath());
  699. $this->assertSame('Error', $controller->viewBuilder()->getTemplatePath());
  700. }
  701. /**
  702. * Test that missing plugin disables Controller::$plugin if the two are the same plugin.
  703. *
  704. * @return void
  705. */
  706. public function testMissingPluginRenderSafe()
  707. {
  708. $exception = new NotFoundException();
  709. $ExceptionRenderer = new MyCustomExceptionRenderer($exception);
  710. /** @var \Cake\Controller\Controller|\PHPUnit\Framework\MockObject\MockObject $controller */
  711. $controller = $this->getMockBuilder('Cake\Controller\Controller')
  712. ->setMethods(['render'])
  713. ->getMock();
  714. $controller->setPlugin('TestPlugin');
  715. $controller->request = new ServerRequest();
  716. $exception = new MissingPluginException(['plugin' => 'TestPlugin']);
  717. $controller->expects($this->once())
  718. ->method('render')
  719. ->with('error400')
  720. ->will($this->throwException($exception));
  721. $ExceptionRenderer->setController($controller);
  722. $response = $ExceptionRenderer->render();
  723. $body = (string)$response->getBody();
  724. $this->assertStringNotContainsString('test plugin error500', $body);
  725. $this->assertStringContainsString('Not Found', $body);
  726. }
  727. /**
  728. * Test that missing plugin doesn't disable Controller::$plugin if the two aren't the same plugin.
  729. *
  730. * @return void
  731. */
  732. public function testMissingPluginRenderSafeWithPlugin()
  733. {
  734. $this->loadPlugins(['TestPlugin']);
  735. $exception = new NotFoundException();
  736. $ExceptionRenderer = new MyCustomExceptionRenderer($exception);
  737. /** @var \Cake\Controller\Controller|\PHPUnit\Framework\MockObject\MockObject $controller */
  738. $controller = $this->getMockBuilder('Cake\Controller\Controller')
  739. ->setMethods(['render'])
  740. ->getMock();
  741. $controller->setPlugin('TestPlugin');
  742. $controller->request = new ServerRequest();
  743. $exception = new MissingPluginException(['plugin' => 'TestPluginTwo']);
  744. $controller->expects($this->once())
  745. ->method('render')
  746. ->with('error400')
  747. ->will($this->throwException($exception));
  748. $ExceptionRenderer->setController($controller);
  749. $response = $ExceptionRenderer->render();
  750. $body = (string)$response->getBody();
  751. $this->assertStringContainsString('test plugin error500', $body);
  752. $this->assertStringContainsString('Not Found', $body);
  753. }
  754. /**
  755. * Test that exceptions can be rendered when a request hasn't been registered
  756. * with Router
  757. *
  758. * @return void
  759. */
  760. public function testRenderWithNoRequest()
  761. {
  762. Router::reload();
  763. $this->assertNull(Router::getRequest());
  764. $exception = new Exception('Terrible');
  765. $ExceptionRenderer = new ExceptionRenderer($exception);
  766. $result = $ExceptionRenderer->render();
  767. $this->assertStringContainsString('Internal Error', (string)$result->getBody());
  768. $this->assertEquals(500, $result->getStatusCode());
  769. }
  770. /**
  771. * Test that router request parameters are applied when the passed
  772. * request has no params.
  773. *
  774. * @return void
  775. */
  776. public function testRenderInheritRoutingParams()
  777. {
  778. $routerRequest = new ServerRequest([
  779. 'params' => [
  780. 'controller' => 'Articles',
  781. 'action' => 'index',
  782. 'plugin' => null,
  783. 'pass' => [],
  784. '_ext' => 'json',
  785. ],
  786. ]);
  787. // Simulate a request having routing applied and stored in router
  788. Router::setRequest($routerRequest);
  789. $exceptionRenderer = new ExceptionRenderer(new Exception('Terrible'), new ServerRequest());
  790. $exceptionRenderer->render();
  791. $properties = $exceptionRenderer->__debugInfo();
  792. /** @var \Cake\Http\ServerRequest $request */
  793. $request = $properties['controller']->getRequest();
  794. foreach (['controller', 'action', '_ext'] as $key) {
  795. $this->assertSame($routerRequest->getParam($key), $request->getParam($key));
  796. }
  797. }
  798. /**
  799. * Test that rendering exceptions triggers shutdown events.
  800. *
  801. * @return void
  802. */
  803. public function testRenderShutdownEvents()
  804. {
  805. $fired = [];
  806. $listener = function (EventInterface $event) use (&$fired) {
  807. $fired[] = $event->getName();
  808. };
  809. $events = EventManager::instance();
  810. $events->on('Controller.shutdown', $listener);
  811. $exception = new Exception('Terrible');
  812. $renderer = new ExceptionRenderer($exception);
  813. $renderer->render();
  814. $expected = ['Controller.shutdown'];
  815. $this->assertEquals($expected, $fired);
  816. }
  817. /**
  818. * test that subclass methods fire shutdown events.
  819. *
  820. * @return void
  821. */
  822. public function testSubclassTriggerShutdownEvents()
  823. {
  824. $fired = [];
  825. $listener = function (EventInterface $event) use (&$fired) {
  826. $fired[] = $event->getName();
  827. };
  828. $events = EventManager::instance();
  829. $events->on('Controller.shutdown', $listener);
  830. $exception = new MissingWidgetThingException('Widget not found');
  831. $renderer = new MyCustomExceptionRenderer($exception);
  832. $renderer->render();
  833. $expected = ['Controller.shutdown'];
  834. $this->assertEquals($expected, $fired);
  835. }
  836. /**
  837. * Tests the output of rendering a PDOException
  838. *
  839. * @return void
  840. */
  841. public function testPDOException()
  842. {
  843. $exception = new \PDOException('There was an error in the SQL query');
  844. $exception->queryString = 'SELECT * from poo_query < 5 and :seven';
  845. $exception->params = ['seven' => 7];
  846. $ExceptionRenderer = new ExceptionRenderer($exception);
  847. $response = $ExceptionRenderer->render();
  848. $this->assertEquals(500, $response->getStatusCode());
  849. $result = (string)$response->getBody();
  850. $this->assertStringContainsString('Database Error', $result);
  851. $this->assertStringContainsString('There was an error in the SQL query', $result);
  852. $this->assertStringContainsString(h('SELECT * from poo_query < 5 and :seven'), $result);
  853. $this->assertStringContainsString("'seven' => (int) 7", $result);
  854. }
  855. }