ExceptionRendererTest.php 31 KB

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