ErrorHandlerTest.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  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 1.2.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Test\TestCase\Error;
  17. use Cake\Core\Configure;
  18. use Cake\Datasource\Exception\RecordNotFoundException;
  19. use Cake\Error\ErrorHandler;
  20. use Cake\Error\ErrorLoggerInterface;
  21. use Cake\Http\Exception\ForbiddenException;
  22. use Cake\Http\Exception\MissingControllerException;
  23. use Cake\Http\Exception\NotFoundException;
  24. use Cake\Http\ServerRequest;
  25. use Cake\Log\Log;
  26. use Cake\Routing\Router;
  27. use Cake\TestSuite\TestCase;
  28. use Exception;
  29. use RuntimeException;
  30. use stdClass;
  31. use TestApp\Error\TestErrorHandler;
  32. /**
  33. * ErrorHandlerTest class
  34. */
  35. class ErrorHandlerTest extends TestCase
  36. {
  37. /**
  38. * @var \Cake\Log\Engine\ArrayLog
  39. */
  40. protected $logger;
  41. /**
  42. * error level property
  43. */
  44. private static $errorLevel;
  45. /**
  46. * setup create a request object to get out of router later.
  47. */
  48. public function setUp(): void
  49. {
  50. parent::setUp();
  51. Router::reload();
  52. $request = new ServerRequest([
  53. 'base' => '',
  54. 'environment' => [
  55. 'HTTP_REFERER' => '/referer',
  56. ],
  57. ]);
  58. Router::setRequest($request);
  59. Configure::write('debug', true);
  60. Log::reset();
  61. Log::setConfig('error_test', ['className' => 'Array']);
  62. $this->logger = Log::engine('error_test');
  63. }
  64. /**
  65. * tearDown
  66. */
  67. public function tearDown(): void
  68. {
  69. parent::tearDown();
  70. Log::reset();
  71. $this->clearPlugins();
  72. error_reporting(self::$errorLevel);
  73. }
  74. /**
  75. * setUpBeforeClass
  76. */
  77. public static function setUpBeforeClass(): void
  78. {
  79. parent::setUpBeforeClass();
  80. self::$errorLevel = error_reporting();
  81. }
  82. /**
  83. * Test an invalid rendering class.
  84. */
  85. public function testInvalidRenderer(): void
  86. {
  87. $this->expectException(RuntimeException::class);
  88. $this->expectExceptionMessage('The \'TotallyInvalid\' renderer class could not be found');
  89. $errorHandler = new ErrorHandler(['exceptionRenderer' => 'TotallyInvalid']);
  90. $errorHandler->getRenderer(new Exception('Something bad'));
  91. }
  92. /**
  93. * test error handling when debug is on, an error should be printed from Debugger.
  94. *
  95. * @runInSeparateProcess
  96. * @preserveGlobalState disabled
  97. */
  98. public function testHandleErrorDebugOn(): void
  99. {
  100. Configure::write('debug', true);
  101. $errorHandler = new ErrorHandler();
  102. $result = '';
  103. $this->deprecated(function () use ($errorHandler, &$result) {
  104. $errorHandler->register();
  105. ob_start();
  106. $wrong = $wrong + 1;
  107. $result = ob_get_clean();
  108. });
  109. $this->assertMatchesRegularExpression('/<div class="cake-error">/', $result);
  110. if (version_compare(PHP_VERSION, '8.0.0-dev', '<')) {
  111. $this->assertMatchesRegularExpression('/<b>Notice<\/b>/', $result);
  112. $this->assertMatchesRegularExpression('/variable:\s+wrong/', $result);
  113. } else {
  114. $this->assertMatchesRegularExpression('/<b>Warning<\/b>/', $result);
  115. $this->assertMatchesRegularExpression('/variable \$wrong/', $result);
  116. }
  117. $this->assertStringContainsString(
  118. 'ErrorHandlerTest.php, line ' . (__LINE__ - 13),
  119. $result,
  120. 'Should contain file and line reference'
  121. );
  122. }
  123. /**
  124. * test error handling with the _trace_offset context variable
  125. */
  126. public function testHandleErrorTraceOffset(): void
  127. {
  128. set_error_handler(function ($code, $message, $file, $line, $context = null): void {
  129. $errorHandler = new ErrorHandler();
  130. $context['_trace_frame_offset'] = 3;
  131. $errorHandler->handleError($code, $message, $file, $line, $context);
  132. });
  133. ob_start();
  134. $wrong = $wrong + 1;
  135. $result = ob_get_clean();
  136. restore_error_handler();
  137. $this->assertStringNotContainsString(
  138. 'ErrorHandlerTest.php, line ' . (__LINE__ - 4),
  139. $result,
  140. 'Should not contain file and line reference'
  141. );
  142. $this->assertStringNotContainsString('_trace_frame_offset', $result);
  143. }
  144. /**
  145. * provides errors for mapping tests.
  146. *
  147. * @return array
  148. */
  149. public static function errorProvider(): array
  150. {
  151. return [
  152. [E_USER_NOTICE, 'Notice'],
  153. [E_USER_WARNING, 'Warning'],
  154. ];
  155. }
  156. /**
  157. * test error mappings
  158. *
  159. * @runInSeparateProcess
  160. * @preserveGlobalState disabled
  161. * @dataProvider errorProvider
  162. */
  163. public function testErrorMapping(int $error, string $expected): void
  164. {
  165. $errorHandler = new ErrorHandler();
  166. $this->deprecated(function () use ($errorHandler, $error, $expected) {
  167. $errorHandler->register();
  168. ob_start();
  169. trigger_error('Test error', $error);
  170. $this->assertStringContainsString('<b>' . $expected . '</b>', ob_get_clean());
  171. });
  172. }
  173. /**
  174. * Test that errors go into Cake Log when debug = 0.
  175. *
  176. * @runInSeparateProcess
  177. * @preserveGlobalState disabled
  178. */
  179. public function testHandleErrorDebugOff(): void
  180. {
  181. Configure::write('debug', false);
  182. $errorHandler = new ErrorHandler();
  183. $this->deprecated(function () use ($errorHandler) {
  184. $errorHandler->register();
  185. $out = $out + 1;
  186. });
  187. $messages = $this->logger->read();
  188. $this->assertMatchesRegularExpression('/^(notice|debug|warning)/', $messages[0]);
  189. $this->assertStringContainsString(
  190. 'Warning (2): Undefined variable $out in [' . __FILE__ . ', line ' . (__LINE__ - 6) . ']',
  191. $messages[0]
  192. );
  193. }
  194. /**
  195. * Test that errors going into Cake Log include traces.
  196. *
  197. * @runInSeparateProcess
  198. * @preserveGlobalState disabled
  199. */
  200. public function testHandleErrorLoggingTrace(): void
  201. {
  202. Configure::write('debug', false);
  203. $errorHandler = new ErrorHandler(['trace' => true]);
  204. $this->deprecated(function () use ($errorHandler) {
  205. $errorHandler->register();
  206. $out = $out + 1;
  207. });
  208. $messages = $this->logger->read();
  209. $this->assertMatchesRegularExpression('/^(notice|debug|warning)/', $messages[0]);
  210. $this->assertStringContainsString(
  211. 'Warning (2): Undefined variable $out in [' . __FILE__ . ', line ' . (__LINE__ - 5) . ']',
  212. $messages[0]
  213. );
  214. $this->assertStringContainsString('Trace:', $messages[0]);
  215. $this->assertStringContainsString(__NAMESPACE__ . '\ErrorHandlerTest::testHandleErrorLoggingTrace()', $messages[0]);
  216. $this->assertStringContainsString('Request URL:', $messages[0]);
  217. $this->assertStringContainsString('Referer URL:', $messages[0]);
  218. }
  219. /**
  220. * test handleException generating a page.
  221. */
  222. public function testHandleException(): void
  223. {
  224. $error = new NotFoundException('Kaboom!');
  225. $errorHandler = new TestErrorHandler();
  226. $errorHandler->handleException($error);
  227. $this->assertStringContainsString('Kaboom!', (string)$errorHandler->response->getBody(), 'message missing.');
  228. }
  229. /**
  230. * test handleException generating log.
  231. */
  232. public function testHandleExceptionLog(): void
  233. {
  234. $errorHandler = new TestErrorHandler([
  235. 'log' => true,
  236. 'trace' => true,
  237. ]);
  238. $error = new NotFoundException('Kaboom!');
  239. $errorHandler->handleException($error);
  240. $this->assertStringContainsString('Kaboom!', (string)$errorHandler->response->getBody(), 'message missing.');
  241. $messages = $this->logger->read();
  242. $this->assertMatchesRegularExpression('/^error/', $messages[0]);
  243. $this->assertStringContainsString('[Cake\Http\Exception\NotFoundException] Kaboom!', $messages[0]);
  244. $this->assertStringContainsString(
  245. str_replace('/', DS, 'vendor/phpunit/phpunit/src/Framework/TestCase.php'),
  246. $messages[0]
  247. );
  248. $errorHandler = new TestErrorHandler([
  249. 'log' => true,
  250. 'trace' => false,
  251. ]);
  252. $errorHandler->handleException($error);
  253. $messages = $this->logger->read();
  254. $this->assertMatchesRegularExpression('/^error/', $messages[1]);
  255. $this->assertStringContainsString('[Cake\Http\Exception\NotFoundException] Kaboom!', $messages[1]);
  256. $this->assertStringNotContainsString(
  257. str_replace('/', DS, 'vendor/phpunit/phpunit/src/Framework/TestCase.php'),
  258. $messages[1]
  259. );
  260. }
  261. /**
  262. * test logging attributes with/without debug
  263. */
  264. public function testHandleExceptionLogAttributes(): void
  265. {
  266. $errorHandler = new TestErrorHandler([
  267. 'log' => true,
  268. 'trace' => true,
  269. ]);
  270. $error = new MissingControllerException(['class' => 'Derp']);
  271. $errorHandler->handleException($error);
  272. Configure::write('debug', false);
  273. $errorHandler->handleException($error);
  274. $messages = $this->logger->read();
  275. $this->assertMatchesRegularExpression('/^error/', $messages[0]);
  276. $this->assertStringContainsString(
  277. '[Cake\Http\Exception\MissingControllerException] Controller class Derp could not be found.',
  278. $messages[0]
  279. );
  280. $this->assertStringContainsString('Exception Attributes:', $messages[0]);
  281. $this->assertStringContainsString('Request URL:', $messages[0]);
  282. $this->assertStringContainsString('Referer URL:', $messages[0]);
  283. $this->assertStringContainsString(
  284. '[Cake\Http\Exception\MissingControllerException] Controller class Derp could not be found.',
  285. $messages[1]
  286. );
  287. $this->assertStringNotContainsString('Exception Attributes:', $messages[1]);
  288. }
  289. /**
  290. * test logging attributes with previous exception
  291. */
  292. public function testHandleExceptionLogPrevious(): void
  293. {
  294. $errorHandler = new TestErrorHandler([
  295. 'log' => true,
  296. 'trace' => true,
  297. ]);
  298. $previous = new RecordNotFoundException('Previous logged');
  299. $error = new NotFoundException('Kaboom!', null, $previous);
  300. $errorHandler->handleException($error);
  301. $messages = $this->logger->read();
  302. $this->assertStringContainsString('[Cake\Http\Exception\NotFoundException] Kaboom!', $messages[0]);
  303. $this->assertStringContainsString(
  304. 'Caused by: [Cake\Datasource\Exception\RecordNotFoundException] Previous logged',
  305. $messages[0]
  306. );
  307. $this->assertStringContainsString(
  308. str_replace('/', DS, 'vendor/phpunit/phpunit/src/Framework/TestCase.php'),
  309. $messages[0]
  310. );
  311. }
  312. /**
  313. * test handleException generating log.
  314. */
  315. public function testHandleExceptionLogSkipping(): void
  316. {
  317. $notFound = new NotFoundException('Kaboom!');
  318. $forbidden = new ForbiddenException('Fooled you!');
  319. $errorHandler = new TestErrorHandler([
  320. 'log' => true,
  321. 'skipLog' => ['Cake\Http\Exception\NotFoundException'],
  322. ]);
  323. $errorHandler->handleException($notFound);
  324. $this->assertStringContainsString('Kaboom!', (string)$errorHandler->response->getBody(), 'message missing.');
  325. $errorHandler->handleException($forbidden);
  326. $this->assertStringContainsString('Fooled you!', (string)$errorHandler->response->getBody(), 'message missing.');
  327. $messages = $this->logger->read();
  328. $this->assertCount(1, $messages);
  329. $this->assertMatchesRegularExpression('/^error/', $messages[0]);
  330. $this->assertStringContainsString(
  331. '[Cake\Http\Exception\ForbiddenException] Fooled you!',
  332. $messages[0]
  333. );
  334. }
  335. /**
  336. * tests it is possible to load a plugin exception renderer
  337. */
  338. public function testLoadPluginHandler(): void
  339. {
  340. $this->loadPlugins(['TestPlugin']);
  341. $errorHandler = new TestErrorHandler([
  342. 'exceptionRenderer' => 'TestPlugin.TestPluginExceptionRenderer',
  343. ]);
  344. $error = new NotFoundException('Kaboom!');
  345. $errorHandler->handleException($error);
  346. $result = $errorHandler->response;
  347. $this->assertSame('Rendered by test plugin', (string)$result);
  348. }
  349. /**
  350. * test handleFatalError generating a page.
  351. *
  352. * These tests start two buffers as handleFatalError blows the outer one up.
  353. */
  354. public function testHandleFatalErrorPage(): void
  355. {
  356. $line = __LINE__;
  357. $errorHandler = new TestErrorHandler();
  358. Configure::write('debug', true);
  359. $errorHandler->handleFatalError(E_ERROR, 'Something wrong', __FILE__, $line);
  360. $result = (string)$errorHandler->response->getBody();
  361. $this->assertStringContainsString('Something wrong', $result, 'message missing.');
  362. $this->assertStringContainsString(__FILE__, $result, 'filename missing.');
  363. $this->assertStringContainsString((string)$line, $result, 'line missing.');
  364. Configure::write('debug', false);
  365. $errorHandler->handleFatalError(E_ERROR, 'Something wrong', __FILE__, $line);
  366. $result = (string)$errorHandler->response->getBody();
  367. $this->assertStringNotContainsString('Something wrong', $result, 'message must not appear.');
  368. $this->assertStringNotContainsString(__FILE__, $result, 'filename must not appear.');
  369. $this->assertStringContainsString('An Internal Error Has Occurred.', $result);
  370. }
  371. /**
  372. * test handleFatalError generating log.
  373. */
  374. public function testHandleFatalErrorLog(): void
  375. {
  376. $errorHandler = new TestErrorHandler(['log' => true]);
  377. $errorHandler->handleFatalError(E_ERROR, 'Something wrong', __FILE__, __LINE__);
  378. $messages = $this->logger->read();
  379. $this->assertCount(2, $messages);
  380. $this->assertStringContainsString(__FILE__ . ', line ' . (__LINE__ - 4), $messages[0]);
  381. $this->assertStringContainsString('Fatal Error (1)', $messages[0]);
  382. $this->assertStringContainsString('Something wrong', $messages[0]);
  383. $this->assertStringContainsString('[Cake\Error\FatalErrorException] Something wrong', $messages[1]);
  384. }
  385. /**
  386. * Data provider for memory limit changing.
  387. *
  388. * @return array
  389. */
  390. public function memoryLimitProvider(): array
  391. {
  392. return [
  393. // start, adjust, expected
  394. ['256M', 4, '262148K'],
  395. ['262144K', 4, '262148K'],
  396. ['1G', 128, '1048704K'],
  397. ];
  398. }
  399. /**
  400. * Test increasing the memory limit.
  401. *
  402. * @dataProvider memoryLimitProvider
  403. */
  404. public function testIncreaseMemoryLimit(string $start, int $adjust, string $expected): void
  405. {
  406. $initial = ini_get('memory_limit');
  407. $this->skipIf(strlen($initial) === 0, 'Cannot read memory limit, and cannot test increasing it.');
  408. // phpunit.xml often has -1 as memory limit
  409. ini_set('memory_limit', $start);
  410. $errorHandler = new TestErrorHandler();
  411. $errorHandler->increaseMemoryLimit($adjust);
  412. $new = ini_get('memory_limit');
  413. $this->assertEquals($expected, $new, 'memory limit did not get increased.');
  414. ini_set('memory_limit', $initial);
  415. }
  416. /**
  417. * Test getting a logger
  418. */
  419. public function testGetLogger(): void
  420. {
  421. $errorHandler = new TestErrorHandler(['key' => 'value', 'log' => true]);
  422. $logger = $errorHandler->getLogger();
  423. $this->assertInstanceOf(ErrorLoggerInterface::class, $logger);
  424. $this->assertSame('value', $logger->getConfig('key'), 'config should be forwarded.');
  425. $this->assertSame($logger, $errorHandler->getLogger());
  426. }
  427. /**
  428. * Test getting a logger
  429. */
  430. public function testGetLoggerInvalid(): void
  431. {
  432. $errorHandler = new TestErrorHandler(['errorLogger' => stdClass::class]);
  433. $this->expectException(RuntimeException::class);
  434. $this->expectExceptionMessage('Cannot create logger');
  435. $errorHandler->getLogger();
  436. }
  437. }