ExceptionRendererTest.php 28 KB

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