ExceptionRendererTest.php 32 KB

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