ErrorHandlerMiddlewareTest.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP(tm) Project
  13. * @since 3.3.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Test\TestCase\Error\Middleware;
  17. use Cake\Error\ExceptionRendererInterface;
  18. use Cake\Error\Middleware\ErrorHandlerMiddleware;
  19. use Cake\Http\Response;
  20. use Cake\Http\ServerRequestFactory;
  21. use Cake\Log\Log;
  22. use Cake\TestSuite\TestCase;
  23. use Error;
  24. use LogicException;
  25. use Psr\Log\LoggerInterface;
  26. use TestApp\Http\TestRequestHandler;
  27. /**
  28. * Test for ErrorHandlerMiddleware
  29. */
  30. class ErrorHandlerMiddlewareTest extends TestCase
  31. {
  32. protected $logger;
  33. /**
  34. * setup
  35. *
  36. * @return void
  37. */
  38. public function setUp()
  39. {
  40. parent::setUp();
  41. static::setAppNamespace();
  42. $this->logger = $this->getMockBuilder(LoggerInterface::class)->getMock();
  43. Log::reset();
  44. Log::setConfig('error_test', [
  45. 'engine' => $this->logger,
  46. ]);
  47. }
  48. /**
  49. * Teardown
  50. *
  51. * @return void
  52. */
  53. public function tearDown()
  54. {
  55. parent::tearDown();
  56. Log::drop('error_test');
  57. }
  58. /**
  59. * Test returning a response works ok.
  60. *
  61. * @return void
  62. */
  63. public function testNoErrorResponse()
  64. {
  65. $this->logger->expects($this->never())->method('log');
  66. $request = ServerRequestFactory::fromGlobals();
  67. $middleware = new ErrorHandlerMiddleware();
  68. $result = $middleware->process($request, new TestRequestHandler());
  69. $this->assertInstanceOf(Response::class, $result);
  70. }
  71. /**
  72. * Test an invalid rendering class.
  73. *
  74. */
  75. public function testInvalidRenderer()
  76. {
  77. $this->expectException(\Exception::class);
  78. $this->expectExceptionMessage('The \'TotallyInvalid\' renderer class could not be found');
  79. $request = ServerRequestFactory::fromGlobals();
  80. $middleware = new ErrorHandlerMiddleware('TotallyInvalid');
  81. $handler = new TestRequestHandler(function ($req) {
  82. throw new \Exception('Something bad');
  83. });
  84. $middleware->process($request, $handler);
  85. }
  86. /**
  87. * Test using a factory method to make a renderer.
  88. *
  89. * @return void
  90. */
  91. public function testRendererFactory()
  92. {
  93. $request = ServerRequestFactory::fromGlobals();
  94. $factory = function ($exception) {
  95. $this->assertInstanceOf('LogicException', $exception);
  96. $response = new Response();
  97. $mock = $this->getMockBuilder(ExceptionRendererInterface::class)
  98. ->setMethods(['render'])
  99. ->getMock();
  100. $mock->expects($this->once())
  101. ->method('render')
  102. ->will($this->returnValue($response));
  103. return $mock;
  104. };
  105. $middleware = new ErrorHandlerMiddleware($factory);
  106. $handler = new TestRequestHandler(function ($req) {
  107. throw new LogicException('Something bad');
  108. });
  109. $middleware->process($request, $handler);
  110. }
  111. /**
  112. * Test rendering an error page
  113. *
  114. * @return void
  115. */
  116. public function testHandleException()
  117. {
  118. $request = ServerRequestFactory::fromGlobals();
  119. $middleware = new ErrorHandlerMiddleware();
  120. $handler = new TestRequestHandler(function ($req) {
  121. throw new \Cake\Http\Exception\NotFoundException('whoops');
  122. });
  123. $result = $middleware->process($request, $handler);
  124. $this->assertInstanceOf('Cake\Http\Response', $result);
  125. $this->assertEquals(404, $result->getStatusCode());
  126. $this->assertContains('was not found', '' . $result->getBody());
  127. }
  128. /**
  129. * Test rendering an error page holds onto the original request.
  130. *
  131. * @return void
  132. */
  133. public function testHandleExceptionPreserveRequest()
  134. {
  135. $request = ServerRequestFactory::fromGlobals();
  136. $request = $request->withHeader('Accept', 'application/json');
  137. $middleware = new ErrorHandlerMiddleware();
  138. $handler = new TestRequestHandler(function ($req) {
  139. throw new \Cake\Http\Exception\NotFoundException('whoops');
  140. });
  141. $result = $middleware->process($request, $handler);
  142. $this->assertInstanceOf('Cake\Http\Response', $result);
  143. $this->assertEquals(404, $result->getStatusCode());
  144. $this->assertContains('"message": "whoops"', '' . $result->getBody());
  145. $this->assertEquals('application/json', $result->getHeaderLine('Content-type'));
  146. }
  147. /**
  148. * Test handling PHP 7's Error instance.
  149. *
  150. * @return void
  151. */
  152. public function testHandlePHP7Error()
  153. {
  154. $middleware = new ErrorHandlerMiddleware();
  155. $request = ServerRequestFactory::fromGlobals();
  156. $error = new Error();
  157. $result = $middleware->handleException($error, $request);
  158. $this->assertInstanceOf(Response::class, $result);
  159. }
  160. /**
  161. * Test rendering an error page logs errors
  162. *
  163. * @return void
  164. */
  165. public function testHandleExceptionLogAndTrace()
  166. {
  167. $this->logger->expects($this->at(0))
  168. ->method('log')
  169. ->with('error', $this->logicalAnd(
  170. $this->stringContains('[Cake\Http\Exception\NotFoundException] Kaboom!'),
  171. $this->stringContains('ErrorHandlerMiddlewareTest->testHandleException'),
  172. $this->stringContains('Request URL: /target/url'),
  173. $this->stringContains('Referer URL: /other/path'),
  174. $this->logicalNot(
  175. $this->stringContains('Previous: ')
  176. )
  177. ));
  178. $request = ServerRequestFactory::fromGlobals([
  179. 'REQUEST_URI' => '/target/url',
  180. 'HTTP_REFERER' => '/other/path',
  181. ]);
  182. $middleware = new ErrorHandlerMiddleware(null, ['log' => true, 'trace' => true]);
  183. $handler = new TestRequestHandler(function ($req) {
  184. throw new \Cake\Http\Exception\NotFoundException('Kaboom!');
  185. });
  186. $result = $middleware->process($request, $handler);
  187. $this->assertEquals(404, $result->getStatusCode());
  188. $this->assertContains('was not found', '' . $result->getBody());
  189. }
  190. /**
  191. * Test rendering an error page logs errors with previous
  192. *
  193. * @return void
  194. */
  195. public function testHandleExceptionLogAndTraceWithPrevious()
  196. {
  197. $this->logger->expects($this->at(0))
  198. ->method('log')
  199. ->with('error', $this->logicalAnd(
  200. $this->stringContains('[Cake\Http\Exception\NotFoundException] Kaboom!'),
  201. $this->stringContains('Caused by: [Cake\Datasource\Exception\RecordNotFoundException] Previous logged'),
  202. $this->stringContains('ErrorHandlerMiddlewareTest->testHandleExceptionLogAndTraceWithPrevious'),
  203. $this->stringContains('Request URL: /target/url'),
  204. $this->stringContains('Referer URL: /other/path')
  205. ));
  206. $request = ServerRequestFactory::fromGlobals([
  207. 'REQUEST_URI' => '/target/url',
  208. 'HTTP_REFERER' => '/other/path',
  209. ]);
  210. $middleware = new ErrorHandlerMiddleware(null, ['log' => true, 'trace' => true]);
  211. $handler = new TestRequestHandler(function ($req) {
  212. $previous = new \Cake\Datasource\Exception\RecordNotFoundException('Previous logged');
  213. throw new \Cake\Http\Exception\NotFoundException('Kaboom!', null, $previous);
  214. });
  215. $result = $middleware->process($request, $handler);
  216. $this->assertEquals(404, $result->getStatusCode());
  217. $this->assertContains('was not found', '' . $result->getBody());
  218. }
  219. /**
  220. * Test rendering an error page skips logging for specific classes
  221. *
  222. * @return void
  223. */
  224. public function testHandleExceptionSkipLog()
  225. {
  226. $this->logger->expects($this->never())->method('log');
  227. $request = ServerRequestFactory::fromGlobals();
  228. $middleware = new ErrorHandlerMiddleware(null, [
  229. 'log' => true,
  230. 'skipLog' => ['Cake\Http\Exception\NotFoundException'],
  231. ]);
  232. $handler = new TestRequestHandler(function ($req) {
  233. throw new \Cake\Http\Exception\NotFoundException('Kaboom!');
  234. });
  235. $result = $middleware->process($request, $handler);
  236. $this->assertEquals(404, $result->getStatusCode());
  237. $this->assertContains('was not found', '' . $result->getBody());
  238. }
  239. /**
  240. * Test rendering an error page logs exception attributes
  241. *
  242. * @return void
  243. */
  244. public function testHandleExceptionLogAttributes()
  245. {
  246. $this->logger->expects($this->at(0))
  247. ->method('log')
  248. ->with('error', $this->logicalAnd(
  249. $this->stringContains(
  250. '[Cake\Routing\Exception\MissingControllerException] ' .
  251. 'Controller class Articles could not be found.'
  252. ),
  253. $this->stringContains('Exception Attributes:'),
  254. $this->stringContains("'class' => 'Articles'"),
  255. $this->stringContains('Request URL:')
  256. ));
  257. $request = ServerRequestFactory::fromGlobals();
  258. $middleware = new ErrorHandlerMiddleware(null, ['log' => true]);
  259. $handler = new TestRequestHandler(function ($req) {
  260. throw new \Cake\Routing\Exception\MissingControllerException(['class' => 'Articles']);
  261. });
  262. $result = $middleware->process($request, $handler);
  263. $this->assertEquals(404, $result->getStatusCode());
  264. }
  265. /**
  266. * Test handling an error and having rendering fail.
  267. *
  268. * @return void
  269. */
  270. public function testHandleExceptionRenderingFails()
  271. {
  272. $request = ServerRequestFactory::fromGlobals();
  273. $factory = function ($exception) {
  274. $mock = $this->getMockBuilder(ExceptionRendererInterface::class)
  275. ->setMethods(['render'])
  276. ->getMock();
  277. $mock->expects($this->once())
  278. ->method('render')
  279. ->will($this->throwException(new LogicException('Rendering failed')));
  280. return $mock;
  281. };
  282. $middleware = new ErrorHandlerMiddleware($factory);
  283. $handler = new TestRequestHandler(function ($req) {
  284. throw new \Cake\Http\Exception\ServiceUnavailableException('whoops');
  285. });
  286. $response = $middleware->process($request, $handler);
  287. $this->assertEquals(500, $response->getStatusCode());
  288. $this->assertEquals('An Internal Server Error Occurred', '' . $response->getBody());
  289. }
  290. }