testName = 'BlueberryComponent'; } } /** * TestErrorController class */ class TestErrorController extends Controller { /** * uses property * * @var array */ public $uses = []; /** * components property * * @return void */ public $components = ['Blueberry']; /** * beforeRender method * * @return void */ public function beforeRender(Event $event) { echo $this->Blueberry->testName; } /** * index method * * @return array */ public function index() { $this->autoRender = false; return 'what up'; } } /** * MyCustomExceptionRenderer class */ class MyCustomExceptionRenderer extends ExceptionRenderer { public function setController($controller) { $this->controller = $controller; } /** * custom error message type. * * @return string */ public function missingWidgetThing() { return 'widget thing is missing'; } } /** * Exception class for testing app error handlers and custom errors. */ class MissingWidgetThingException extends NotFoundException { } /** * Exception class for testing app error handlers and custom errors. */ class MissingWidgetThing extends \Exception { } /** * ExceptionRendererTest class */ class ExceptionRendererTest extends TestCase { /** * @var bool */ protected $_restoreError = false; /** * setup create a request object to get out of router later. * * @return void */ public function setUp() { parent::setUp(); Configure::write('Config.language', 'eng'); Router::reload(); $request = new ServerRequest(['base' => '']); Router::setRequestInfo($request); Configure::write('debug', true); } /** * tearDown * * @return void */ public function tearDown() { parent::tearDown(); $this->clearPlugins(); if ($this->_restoreError) { restore_error_handler(); } } public function testControllerInstanceForPrefixedRequest() { $namespace = Configure::read('App.namespace'); Configure::write('App.namespace', 'TestApp'); $exception = new NotFoundException('Page not found'); $request = new ServerRequest(); $request = $request ->withParam('controller', 'Articles') ->withParam('prefix', 'admin'); $ExceptionRenderer = new MyCustomExceptionRenderer($exception, $request); $this->assertInstanceOf( ErrorController::class, $ExceptionRenderer->__debugInfo()['controller'] ); Configure::write('App.namespace', $namespace); } /** * test that methods declared in an ExceptionRenderer subclass are not converted * into error400 when debug > 0 * * @return void */ public function testSubclassMethodsNotBeingConvertedToError() { $exception = new MissingWidgetThingException('Widget not found'); $ExceptionRenderer = new MyCustomExceptionRenderer($exception); $result = $ExceptionRenderer->render(); $this->assertEquals('widget thing is missing', (string)$result->getBody()); } /** * test that subclass methods are not converted when debug = 0 * * @return void */ public function testSubclassMethodsNotBeingConvertedDebug0() { Configure::write('debug', false); $exception = new MissingWidgetThingException('Widget not found'); $ExceptionRenderer = new MyCustomExceptionRenderer($exception); $result = $ExceptionRenderer->render(); $this->assertEquals( 'missingWidgetThing', $ExceptionRenderer->__debugInfo()['method'] ); $this->assertEquals( 'widget thing is missing', (string)$result->getBody(), 'Method declared in subclass converted to error400' ); } /** * test that ExceptionRenderer subclasses properly convert framework errors. * * @return void */ public function testSubclassConvertingFrameworkErrors() { Configure::write('debug', false); $exception = new MissingControllerException('PostsController'); $ExceptionRenderer = new MyCustomExceptionRenderer($exception); $result = $ExceptionRenderer->render(); $this->assertRegExp( '/Not Found/', (string)$result->getBody(), 'Method declared in error handler not converted to error400. %s' ); } /** * test things in the constructor. * * @return void */ public function testConstruction() { $exception = new NotFoundException('Page not found'); $ExceptionRenderer = new ExceptionRenderer($exception); $this->assertInstanceOf( 'Cake\Controller\ErrorController', $ExceptionRenderer->__debugInfo()['controller'] ); $this->assertEquals($exception, $ExceptionRenderer->__debugInfo()['error']); } /** * test that exception message gets coerced when debug = 0 * * @return void */ public function testExceptionMessageCoercion() { Configure::write('debug', false); $exception = new MissingActionException('Secret info not to be leaked'); $ExceptionRenderer = new ExceptionRenderer($exception); $this->assertInstanceOf( 'Cake\Controller\ErrorController', $ExceptionRenderer->__debugInfo()['controller'] ); $this->assertEquals($exception, $ExceptionRenderer->__debugInfo()['error']); $result = (string)$ExceptionRenderer->render()->getBody(); $this->assertEquals('error400', $ExceptionRenderer->__debugInfo()['template']); $this->assertContains('Not Found', $result); $this->assertNotContains('Secret info not to be leaked', $result); } /** * test that helpers in custom CakeErrorController are not lost * * @return void */ public function testCakeErrorHelpersNotLost() { static::setAppNamespace(); $exception = new NotFoundException(); $renderer = new \TestApp\Error\TestAppsExceptionRenderer($exception); $result = $renderer->render(); $this->assertContains('peeled', (string)$result->getBody()); } /** * test that unknown exception types with valid status codes are treated correctly. * * @return void */ public function testUnknownExceptionTypeWithExceptionThatHasA400Code() { $exception = new MissingWidgetThingException('coding fail.'); $ExceptionRenderer = new ExceptionRenderer($exception); $response = $ExceptionRenderer->render(); $this->assertEquals(404, $response->getStatusCode()); $this->assertFalse(method_exists($ExceptionRenderer, 'missingWidgetThing'), 'no method should exist.'); $this->assertContains('coding fail', (string)$response->getBody(), 'Text should show up.'); } /** * test that unknown exception types with valid status codes are treated correctly. * * @return void */ public function testUnknownExceptionTypeWithNoCodeIsA500() { $exception = new \OutOfBoundsException('foul ball.'); $ExceptionRenderer = new ExceptionRenderer($exception); $result = $ExceptionRenderer->render(); $this->assertEquals(500, $result->getStatusCode()); $this->assertContains('foul ball.', (string)$result->getBody(), 'Text should show up as its debug mode.'); } /** * test that unknown exceptions have messages ignored. * * @return void */ public function testUnknownExceptionInProduction() { Configure::write('debug', false); $exception = new \OutOfBoundsException('foul ball.'); $ExceptionRenderer = new ExceptionRenderer($exception); $response = $ExceptionRenderer->render(); $result = (string)$response->getBody(); $this->assertEquals(500, $response->getStatusCode()); $this->assertNotContains('foul ball.', $result, 'Text should no show up.'); $this->assertContains('Internal Error', $result, 'Generic message only.'); } /** * test that unknown exception types with valid status codes are treated correctly. * * @return void */ public function testUnknownExceptionTypeWithCodeHigherThan500() { $exception = new \OutOfBoundsException('foul ball.', 501); $ExceptionRenderer = new ExceptionRenderer($exception); $response = $ExceptionRenderer->render(); $result = (string)$response->getBody(); $this->assertEquals(501, $response->getStatusCode()); $this->assertContains('foul ball.', $result, 'Text should show up as its debug mode.'); } /** * testerror400 method * * @return void */ public function testError400() { Router::reload(); $request = new ServerRequest('posts/view/1000'); Router::setRequestInfo($request); $exception = new NotFoundException('Custom message'); $ExceptionRenderer = new ExceptionRenderer($exception); $response = $ExceptionRenderer->render(); $result = (string)$response->getBody(); $this->assertEquals(404, $response->getStatusCode()); $this->assertContains('

Custom message

', $result); $this->assertRegExp("/'.*?\/posts\/view\/1000'<\/strong>/", $result); } /** * testerror400 method when returning as json * * @return void */ public function testError400AsJson() { Router::reload(); $request = new ServerRequest('posts/view/1000?sort=title&direction=desc'); $request = $request->withHeader('Accept', 'application/json'); $request = $request->withHeader('Content-Type', 'application/json'); Router::setRequestInfo($request); $exception = new NotFoundException('Custom message'); $exceptionLine = __LINE__ - 1; $ExceptionRenderer = new ExceptionRenderer($exception); $response = $ExceptionRenderer->render(); $result = (string)$response->getBody(); $expected = [ 'message' => 'Custom message', 'url' => '/posts/view/1000?sort=title&direction=desc', 'code' => 404, 'file' => __FILE__, 'line' => $exceptionLine, ]; $this->assertEquals($expected, json_decode($result, true)); $this->assertEquals(404, $response->getStatusCode()); } /** * test that error400 only modifies the messages on Cake Exceptions. * * @return void */ public function testerror400OnlyChangingCakeException() { Configure::write('debug', false); $exception = new NotFoundException('Custom message'); $ExceptionRenderer = new ExceptionRenderer($exception); $result = $ExceptionRenderer->render(); $this->assertContains('Custom message', (string)$result->getBody()); $exception = new MissingActionException(['controller' => 'PostsController', 'action' => 'index']); $ExceptionRenderer = new ExceptionRenderer($exception); $result = $ExceptionRenderer->render(); $this->assertContains('Not Found', (string)$result->getBody()); } /** * test that error400 doesn't expose XSS * * @return void */ public function testError400NoInjection() { Router::reload(); $request = new ServerRequest('pages/pink'); Router::setRequestInfo($request); $exception = new NotFoundException('Custom message'); $ExceptionRenderer = new ExceptionRenderer($exception); $result = (string)$ExceptionRenderer->render()->getBody(); $this->assertNotContains('', $result); } /** * testError500 method * * @return void */ public function testError500Message() { $exception = new InternalErrorException('An Internal Error Has Occurred.'); $ExceptionRenderer = new ExceptionRenderer($exception); $response = $ExceptionRenderer->render(); $result = (string)$response->getBody(); $this->assertEquals(500, $response->getStatusCode()); $this->assertContains('

An Internal Error Has Occurred.

', $result); $this->assertContains('An Internal Error Has Occurred.

', $result); } /** * testExceptionResponseHeader method * * @return void */ public function testExceptionResponseHeader() { $exception = new MethodNotAllowedException('Only allowing POST and DELETE'); $exception->responseHeader(['Allow' => 'POST, DELETE']); $ExceptionRenderer = new ExceptionRenderer($exception); $result = $ExceptionRenderer->render(); $this->assertTrue($result->hasHeader('Allow')); $this->assertEquals('POST, DELETE', $result->getHeaderLine('Allow')); } /** * testMissingController method * * @return void */ public function testMissingController() { $exception = new MissingControllerException([ 'class' => 'Posts', 'prefix' => '', 'plugin' => '', ]); $ExceptionRenderer = new MyCustomExceptionRenderer($exception); $result = (string)$ExceptionRenderer->render()->getBody(); $this->assertEquals( 'missingController', $ExceptionRenderer->__debugInfo()['template'] ); $this->assertContains('Missing Controller', $result); $this->assertContains('PostsController', $result); } /** * test missingController method * * @return void */ public function testMissingControllerLowerCase() { $exception = new MissingControllerException([ 'class' => 'posts', 'prefix' => '', 'plugin' => '', ]); $ExceptionRenderer = new MyCustomExceptionRenderer($exception); $result = (string)$ExceptionRenderer->render()->getBody(); $this->assertEquals( 'missingController', $ExceptionRenderer->__debugInfo()['template'] ); $this->assertContains('Missing Controller', $result); $this->assertContains('PostsController', $result); } /** * Returns an array of tests to run for the various Cake Exception classes. * * @return array */ public static function exceptionProvider() { return [ [ new MissingActionException([ 'controller' => 'postsController', 'action' => 'index', 'prefix' => '', 'plugin' => '', ]), [ '/Missing Method in PostsController/', '/PostsController::index\(\)<\/em>/', ], 404, ], [ new MissingActionException([ 'controller' => 'PostsController', 'action' => 'index', 'prefix' => '', 'plugin' => '', ]), [ '/Missing Method in PostsController/', '/PostsController::index\(\)<\/em>/', ], 404, ], [ new MissingTemplateException(['file' => '/posts/about.ctp']), [ "/posts\/about.ctp/", ], 500, ], [ new MissingLayoutException(['file' => 'layouts/my_layout.ctp']), [ '/Missing Layout/', "/layouts\/my_layout.ctp/", ], 500, ], [ new MissingHelperException(['class' => 'MyCustomHelper']), [ '/Missing Helper/', '/MyCustomHelper<\/em> could not be found./', '/Create the class MyCustomHelper<\/em> below in file:/', '/(\/|\\\)MyCustomHelper.php/', ], 500, ], [ new MissingBehaviorException(['class' => 'MyCustomBehavior']), [ '/Missing Behavior/', '/Create the class MyCustomBehavior<\/em> below in file:/', '/(\/|\\\)MyCustomBehavior.php/', ], 500, ], [ new MissingComponentException(['class' => 'SideboxComponent']), [ '/Missing Component/', '/Create the class SideboxComponent<\/em> below in file:/', '/(\/|\\\)SideboxComponent.php/', ], 500, ], [ new MissingDatasourceConfigException(['name' => 'MyDatasourceConfig']), [ '/Missing Datasource Configuration/', '/MyDatasourceConfig<\/em> was not found/', ], 500, ], [ new MissingDatasourceException(['class' => 'MyDatasource', 'plugin' => 'MyPlugin']), [ '/Missing Datasource/', '/MyPlugin.MyDatasource<\/em> could not be found./', ], 500, ], [ new MissingMailerActionException([ 'mailer' => 'UserMailer', 'action' => 'welcome', 'prefix' => '', 'plugin' => '', ]), [ '/Missing Method in UserMailer/', '/UserMailer::welcome\(\)<\/em>/', ], 404, ], [ new Exception('boom'), [ '/Internal Error/', ], 500, ], [ new RuntimeException('another boom'), [ '/Internal Error/', ], 500, ], [ new CakeException('base class'), ['/Internal Error/'], 500, ], [ new HttpException('Network Authentication Required', 511), ['/Network Authentication Required/'], 511, ], ]; } /** * Test the various Cake Exception sub classes * * @dataProvider exceptionProvider * @return void */ public function testCakeExceptionHandling($exception, $patterns, $code) { $exceptionRenderer = new ExceptionRenderer($exception); $response = $exceptionRenderer->render(); $this->assertEquals($code, $response->getStatusCode()); $body = (string)$response->getBody(); foreach ($patterns as $pattern) { $this->assertRegExp($pattern, $body); } } /** * Test that class names not ending in Exception are not mangled. * * @return void */ public function testExceptionNameMangling() { $exceptionRenderer = new MyCustomExceptionRenderer(new MissingWidgetThing()); $result = (string)$exceptionRenderer->render()->getBody(); $this->assertContains('widget thing is missing', $result); // Custom method should be called even when debug is off. Configure::write('debug', false); $exceptionRenderer = new MyCustomExceptionRenderer(new MissingWidgetThing()); $result = (string)$exceptionRenderer->render()->getBody(); $this->assertContains('widget thing is missing', $result); } /** * Test exceptions being raised when helpers are missing. * * @return void */ public function testMissingRenderSafe() { $exception = new MissingHelperException(['class' => 'Fail']); $ExceptionRenderer = new MyCustomExceptionRenderer($exception); $controller = $this->getMockBuilder('Cake\Controller\Controller') ->setMethods(['render']) ->getMock(); $controller->helpers = ['Fail', 'Boom']; $controller->request = new ServerRequest(); $controller->expects($this->at(0)) ->method('render') ->with('missingHelper') ->will($this->throwException($exception)); $ExceptionRenderer->setController($controller); $response = $ExceptionRenderer->render(); sort($controller->helpers); $this->assertEquals(['Form', 'Html'], $controller->helpers); $this->assertContains('Helper class Fail', (string)$response->getBody()); } /** * Test that exceptions in beforeRender() are handled by outputMessageSafe * * @return void */ public function testRenderExceptionInBeforeRender() { $exception = new NotFoundException('Not there, sorry'); $ExceptionRenderer = new MyCustomExceptionRenderer($exception); $controller = $this->getMockBuilder('Cake\Controller\Controller') ->setMethods(['beforeRender']) ->getMock(); $controller->request = new ServerRequest(); $controller->expects($this->any()) ->method('beforeRender') ->will($this->throwException($exception)); $ExceptionRenderer->setController($controller); $response = $ExceptionRenderer->render(); $this->assertContains('Not there, sorry', (string)$response->getBody()); } /** * Test that missing layoutPath don't cause other fatal errors. * * @return void */ public function testMissingLayoutPathRenderSafe() { $this->called = false; $exception = new NotFoundException(); $ExceptionRenderer = new MyCustomExceptionRenderer($exception); $controller = new Controller(); $controller->helpers = ['Fail', 'Boom']; $controller->getEventManager()->on( 'Controller.beforeRender', function (Event $event) { $this->called = true; $event->getSubject()->viewBuilder()->setLayoutPath('boom'); } ); $controller->setRequest(new ServerRequest()); $ExceptionRenderer->setController($controller); $response = $ExceptionRenderer->render(); $this->assertEquals('text/html', $response->getType()); $this->assertContains('Not Found', (string)$response->getBody()); $this->assertTrue($this->called, 'Listener added was not triggered.'); $this->assertEquals('', $controller->viewBuilder()->getLayoutPath()); $this->assertEquals('Error', $controller->viewBuilder()->getTemplatePath()); } /** * Test that missing plugin disables Controller::$plugin if the two are the same plugin. * * @return void */ public function testMissingPluginRenderSafe() { $exception = new NotFoundException(); $ExceptionRenderer = new MyCustomExceptionRenderer($exception); $controller = $this->getMockBuilder('Cake\Controller\Controller') ->setMethods(['render']) ->getMock(); $controller->setPlugin('TestPlugin'); $controller->request = $this->getMockBuilder('Cake\Http\ServerRequest')->getMock(); $exception = new MissingPluginException(['plugin' => 'TestPlugin']); $controller->expects($this->once()) ->method('render') ->with('error400') ->will($this->throwException($exception)); $ExceptionRenderer->setController($controller); $response = $ExceptionRenderer->render(); $body = (string)$response->getBody(); $this->assertNotContains('test plugin error500', $body); $this->assertContains('Not Found', $body); } /** * Test that missing plugin doesn't disable Controller::$plugin if the two aren't the same plugin. * * @return void */ public function testMissingPluginRenderSafeWithPlugin() { $this->loadPlugins(['TestPlugin']); $exception = new NotFoundException(); $ExceptionRenderer = new MyCustomExceptionRenderer($exception); $controller = $this->getMockBuilder('Cake\Controller\Controller') ->setMethods(['render']) ->getMock(); $controller->setPlugin('TestPlugin'); $controller->request = $this->getMockBuilder('Cake\Http\ServerRequest')->getMock(); $exception = new MissingPluginException(['plugin' => 'TestPluginTwo']); $controller->expects($this->once()) ->method('render') ->with('error400') ->will($this->throwException($exception)); $ExceptionRenderer->setController($controller); $response = $ExceptionRenderer->render(); $body = (string)$response->getBody(); $this->assertContains('test plugin error500', $body); $this->assertContains('Not Found', $body); } /** * Test that exceptions can be rendered when a request hasn't been registered * with Router * * @return void */ public function testRenderWithNoRequest() { Router::reload(); $this->assertNull(Router::getRequest(false)); $exception = new Exception('Terrible'); $ExceptionRenderer = new ExceptionRenderer($exception); $result = $ExceptionRenderer->render(); $this->assertContains('Internal Error', (string)$result->getBody()); $this->assertEquals(500, $result->getStatusCode()); } /** * Test that router request parameters are applied when the passed * request has no params. * * @return void */ public function testRenderInheritRoutingParams() { $routerRequest = new ServerRequest([ 'params' => [ 'controller' => 'Articles', 'action' => 'index', 'plugin' => null, 'pass' => [], '_ext' => 'json', ], ]); // Simulate a request having routing applied and stored in router Router::pushRequest($routerRequest); $exceptionRenderer = new ExceptionRenderer(new Exception('Terrible'), new ServerRequest()); $exceptionRenderer->render(); $properties = $exceptionRenderer->__debugInfo(); foreach (['controller', 'action', '_ext'] as $key) { $this->assertSame( $routerRequest->getParam($key), $properties['controller']->request->getParam($key) ); } } /** * Test that rendering exceptions triggers shutdown events. * * @return void */ public function testRenderShutdownEvents() { $fired = []; $listener = function (Event $event) use (&$fired) { $fired[] = $event->getName(); }; $events = EventManager::instance(); $events->on('Controller.shutdown', $listener); $events->on('Dispatcher.afterDispatch', $listener); $exception = new Exception('Terrible'); $renderer = new ExceptionRenderer($exception); $renderer->render(); $expected = ['Controller.shutdown', 'Dispatcher.afterDispatch']; $this->assertEquals($expected, $fired); } /** * Test that rendering exceptions triggers events * on filters attached to dispatcherfactory * * @return void */ public function testRenderShutdownEventsOnDispatcherFactory() { $filter = $this->getMockBuilder('Cake\Routing\DispatcherFilter') ->setMethods(['afterDispatch']) ->getMock(); $filter->expects($this->at(0)) ->method('afterDispatch'); DispatcherFactory::add($filter); $exception = new Exception('Terrible'); $renderer = new ExceptionRenderer($exception); $renderer->render(); } /** * test that subclass methods fire shutdown events. * * @return void */ public function testSubclassTriggerShutdownEvents() { $fired = []; $listener = function (Event $event) use (&$fired) { $fired[] = $event->getName(); }; $events = EventManager::instance(); $events->on('Controller.shutdown', $listener); $events->on('Dispatcher.afterDispatch', $listener); $exception = new MissingWidgetThingException('Widget not found'); $renderer = new MyCustomExceptionRenderer($exception); $renderer->render(); $expected = ['Controller.shutdown', 'Dispatcher.afterDispatch']; $this->assertEquals($expected, $fired); } /** * Tests the output of rendering a PDOException * * @return void */ public function testPDOException() { $exception = new \PDOException('There was an error in the SQL query'); $exception->queryString = 'SELECT * from poo_query < 5 and :seven'; $exception->params = ['seven' => 7]; $ExceptionRenderer = new ExceptionRenderer($exception); $response = $ExceptionRenderer->render(); $this->assertEquals(500, $response->getStatusCode()); $result = (string)$response->getBody(); $this->assertContains('Database Error', $result); $this->assertContains('There was an error in the SQL query', $result); $this->assertContains(h('SELECT * from poo_query < 5 and :seven'), $result); $this->assertContains("'seven' => (int) 7", $result); } }