ErrorHandlerTest.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  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->assertMatchesRegularExpression('/Undefined variable\:? \$?out in/', $messages[0]);
  190. $this->assertStringContainsString('[' . __FILE__ . ', line ' . (__LINE__ - 7) . ']', $messages[0]);
  191. }
  192. /**
  193. * Test that errors going into Cake Log include traces.
  194. *
  195. * @runInSeparateProcess
  196. * @preserveGlobalState disabled
  197. */
  198. public function testHandleErrorLoggingTrace(): void
  199. {
  200. Configure::write('debug', false);
  201. $errorHandler = new ErrorHandler(['trace' => true]);
  202. $this->deprecated(function () use ($errorHandler) {
  203. $errorHandler->register();
  204. $out = $out + 1;
  205. });
  206. $messages = $this->logger->read();
  207. $this->assertMatchesRegularExpression('/^(notice|debug|warning)/', $messages[0]);
  208. $this->assertMatchesRegularExpression('/Undefined variable\:? \$?out in/', $messages[0]);
  209. $this->assertStringContainsString('[' . __FILE__ . ', line ' . (__LINE__ - 6) . ']', $messages[0]);
  210. $this->assertStringContainsString('Trace:', $messages[0]);
  211. $this->assertStringContainsString(__NAMESPACE__ . '\ErrorHandlerTest::testHandleErrorLoggingTrace()', $messages[0]);
  212. $this->assertStringContainsString('Request URL:', $messages[0]);
  213. $this->assertStringContainsString('Referer URL:', $messages[0]);
  214. }
  215. /**
  216. * test handleException generating a page.
  217. */
  218. public function testHandleException(): void
  219. {
  220. $error = new NotFoundException('Kaboom!');
  221. $errorHandler = new TestErrorHandler();
  222. $errorHandler->handleException($error);
  223. $this->assertStringContainsString('Kaboom!', (string)$errorHandler->response->getBody(), 'message missing.');
  224. }
  225. /**
  226. * test handleException generating log.
  227. */
  228. public function testHandleExceptionLog(): void
  229. {
  230. $errorHandler = new TestErrorHandler([
  231. 'log' => true,
  232. 'trace' => true,
  233. ]);
  234. $error = new NotFoundException('Kaboom!');
  235. $errorHandler->handleException($error);
  236. $this->assertStringContainsString('Kaboom!', (string)$errorHandler->response->getBody(), 'message missing.');
  237. $messages = $this->logger->read();
  238. $this->assertMatchesRegularExpression('/^error/', $messages[0]);
  239. $this->assertStringContainsString('[Cake\Http\Exception\NotFoundException] Kaboom!', $messages[0]);
  240. $this->assertStringContainsString(
  241. str_replace('/', DS, 'vendor/phpunit/phpunit/src/Framework/TestCase.php'),
  242. $messages[0]
  243. );
  244. $errorHandler = new TestErrorHandler([
  245. 'log' => true,
  246. 'trace' => false,
  247. ]);
  248. $errorHandler->handleException($error);
  249. $messages = $this->logger->read();
  250. $this->assertMatchesRegularExpression('/^error/', $messages[1]);
  251. $this->assertStringContainsString('[Cake\Http\Exception\NotFoundException] Kaboom!', $messages[1]);
  252. $this->assertStringNotContainsString(
  253. str_replace('/', DS, 'vendor/phpunit/phpunit/src/Framework/TestCase.php'),
  254. $messages[1]
  255. );
  256. }
  257. /**
  258. * test logging attributes with/without debug
  259. */
  260. public function testHandleExceptionLogAttributes(): void
  261. {
  262. $errorHandler = new TestErrorHandler([
  263. 'log' => true,
  264. 'trace' => true,
  265. ]);
  266. $error = new MissingControllerException(['class' => 'Derp']);
  267. $errorHandler->handleException($error);
  268. Configure::write('debug', false);
  269. $errorHandler->handleException($error);
  270. $messages = $this->logger->read();
  271. $this->assertMatchesRegularExpression('/^error/', $messages[0]);
  272. $this->assertStringContainsString(
  273. '[Cake\Http\Exception\MissingControllerException] Controller class Derp could not be found.',
  274. $messages[0]
  275. );
  276. $this->assertStringContainsString('Exception Attributes:', $messages[0]);
  277. $this->assertStringContainsString('Request URL:', $messages[0]);
  278. $this->assertStringContainsString('Referer URL:', $messages[0]);
  279. $this->assertStringContainsString(
  280. '[Cake\Http\Exception\MissingControllerException] Controller class Derp could not be found.',
  281. $messages[1]
  282. );
  283. $this->assertStringNotContainsString('Exception Attributes:', $messages[1]);
  284. }
  285. /**
  286. * test logging attributes with previous exception
  287. */
  288. public function testHandleExceptionLogPrevious(): void
  289. {
  290. $errorHandler = new TestErrorHandler([
  291. 'log' => true,
  292. 'trace' => true,
  293. ]);
  294. $previous = new RecordNotFoundException('Previous logged');
  295. $error = new NotFoundException('Kaboom!', null, $previous);
  296. $errorHandler->handleException($error);
  297. $messages = $this->logger->read();
  298. $this->assertStringContainsString('[Cake\Http\Exception\NotFoundException] Kaboom!', $messages[0]);
  299. $this->assertStringContainsString(
  300. 'Caused by: [Cake\Datasource\Exception\RecordNotFoundException] Previous logged',
  301. $messages[0]
  302. );
  303. $this->assertStringContainsString(
  304. str_replace('/', DS, 'vendor/phpunit/phpunit/src/Framework/TestCase.php'),
  305. $messages[0]
  306. );
  307. }
  308. /**
  309. * test handleException generating log.
  310. */
  311. public function testHandleExceptionLogSkipping(): void
  312. {
  313. $notFound = new NotFoundException('Kaboom!');
  314. $forbidden = new ForbiddenException('Fooled you!');
  315. $errorHandler = new TestErrorHandler([
  316. 'log' => true,
  317. 'skipLog' => ['Cake\Http\Exception\NotFoundException'],
  318. ]);
  319. $errorHandler->handleException($notFound);
  320. $this->assertStringContainsString('Kaboom!', (string)$errorHandler->response->getBody(), 'message missing.');
  321. $errorHandler->handleException($forbidden);
  322. $this->assertStringContainsString('Fooled you!', (string)$errorHandler->response->getBody(), 'message missing.');
  323. $messages = $this->logger->read();
  324. $this->assertCount(1, $messages);
  325. $this->assertMatchesRegularExpression('/^error/', $messages[0]);
  326. $this->assertStringContainsString(
  327. '[Cake\Http\Exception\ForbiddenException] Fooled you!',
  328. $messages[0]
  329. );
  330. }
  331. /**
  332. * tests it is possible to load a plugin exception renderer
  333. */
  334. public function testLoadPluginHandler(): void
  335. {
  336. $this->loadPlugins(['TestPlugin']);
  337. $errorHandler = new TestErrorHandler([
  338. 'exceptionRenderer' => 'TestPlugin.TestPluginExceptionRenderer',
  339. ]);
  340. $error = new NotFoundException('Kaboom!');
  341. $errorHandler->handleException($error);
  342. $result = $errorHandler->response;
  343. $this->assertSame('Rendered by test plugin', (string)$result);
  344. }
  345. /**
  346. * test handleFatalError generating a page.
  347. *
  348. * These tests start two buffers as handleFatalError blows the outer one up.
  349. */
  350. public function testHandleFatalErrorPage(): void
  351. {
  352. $line = __LINE__;
  353. $errorHandler = new TestErrorHandler();
  354. Configure::write('debug', true);
  355. $errorHandler->handleFatalError(E_ERROR, 'Something wrong', __FILE__, $line);
  356. $result = (string)$errorHandler->response->getBody();
  357. $this->assertStringContainsString('Something wrong', $result, 'message missing.');
  358. $this->assertStringContainsString(__FILE__, $result, 'filename missing.');
  359. $this->assertStringContainsString((string)$line, $result, 'line missing.');
  360. Configure::write('debug', false);
  361. $errorHandler->handleFatalError(E_ERROR, 'Something wrong', __FILE__, $line);
  362. $result = (string)$errorHandler->response->getBody();
  363. $this->assertStringNotContainsString('Something wrong', $result, 'message must not appear.');
  364. $this->assertStringNotContainsString(__FILE__, $result, 'filename must not appear.');
  365. $this->assertStringContainsString('An Internal Error Has Occurred.', $result);
  366. }
  367. /**
  368. * test handleFatalError generating log.
  369. */
  370. public function testHandleFatalErrorLog(): void
  371. {
  372. $errorHandler = new TestErrorHandler(['log' => true]);
  373. $errorHandler->handleFatalError(E_ERROR, 'Something wrong', __FILE__, __LINE__);
  374. $messages = $this->logger->read();
  375. $this->assertCount(2, $messages);
  376. $this->assertStringContainsString(__FILE__ . ', line ' . (__LINE__ - 4), $messages[0]);
  377. $this->assertStringContainsString('Fatal Error (1)', $messages[0]);
  378. $this->assertStringContainsString('Something wrong', $messages[0]);
  379. $this->assertStringContainsString('[Cake\Error\FatalErrorException] Something wrong', $messages[1]);
  380. }
  381. /**
  382. * Data provider for memory limit changing.
  383. *
  384. * @return array
  385. */
  386. public function memoryLimitProvider(): array
  387. {
  388. return [
  389. // start, adjust, expected
  390. ['256M', 4, '262148K'],
  391. ['262144K', 4, '262148K'],
  392. ['1G', 128, '1048704K'],
  393. ];
  394. }
  395. /**
  396. * Test increasing the memory limit.
  397. *
  398. * @dataProvider memoryLimitProvider
  399. */
  400. public function testIncreaseMemoryLimit(string $start, int $adjust, string $expected): void
  401. {
  402. $initial = ini_get('memory_limit');
  403. $this->skipIf(strlen($initial) === 0, 'Cannot read memory limit, and cannot test increasing it.');
  404. // phpunit.xml often has -1 as memory limit
  405. ini_set('memory_limit', $start);
  406. $errorHandler = new TestErrorHandler();
  407. $errorHandler->increaseMemoryLimit($adjust);
  408. $new = ini_get('memory_limit');
  409. $this->assertEquals($expected, $new, 'memory limit did not get increased.');
  410. ini_set('memory_limit', $initial);
  411. }
  412. /**
  413. * Test getting a logger
  414. */
  415. public function testGetLogger(): void
  416. {
  417. $errorHandler = new TestErrorHandler(['key' => 'value', 'log' => true]);
  418. $logger = $errorHandler->getLogger();
  419. $this->assertInstanceOf(ErrorLoggerInterface::class, $logger);
  420. $this->assertSame('value', $logger->getConfig('key'), 'config should be forwarded.');
  421. $this->assertSame($logger, $errorHandler->getLogger());
  422. }
  423. /**
  424. * Test getting a logger
  425. */
  426. public function testGetLoggerInvalid(): void
  427. {
  428. $errorHandler = new TestErrorHandler(['errorLogger' => stdClass::class]);
  429. $this->expectException(RuntimeException::class);
  430. $this->expectExceptionMessage('Cannot create logger');
  431. $errorHandler->getLogger();
  432. }
  433. }