ExceptionRendererTest.php 30 KB

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