ErrorHandlerTest.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  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. $this->deprecated(function () {
  102. $errorHandler = new ErrorHandler();
  103. $errorHandler->register();
  104. ob_start();
  105. $wrong = $wrong + 1;
  106. });
  107. $result = ob_get_clean();
  108. $this->assertMatchesRegularExpression('/<div class="cake-error">/', $result);
  109. if (version_compare(PHP_VERSION, '8.0.0-dev', '<')) {
  110. $this->assertMatchesRegularExpression('/<b>Notice<\/b>/', $result);
  111. $this->assertMatchesRegularExpression('/variable:\s+wrong/', $result);
  112. } else {
  113. $this->assertMatchesRegularExpression('/<b>Warning<\/b>/', $result);
  114. $this->assertMatchesRegularExpression('/variable \$wrong/', $result);
  115. }
  116. $this->assertMatchesRegularExpression(
  117. '/ErrorHandlerTest\.php[^,]+, line[^\d]+' . (__LINE__ - 13) . '/',
  118. $result,
  119. 'Should contain file and line reference'
  120. );
  121. }
  122. /**
  123. * test error handling with the _trace_offset context variable
  124. */
  125. public function testHandleErrorTraceOffset(): void
  126. {
  127. set_error_handler(function ($code, $message, $file, $line, $context = null): void {
  128. $errorHandler = new ErrorHandler();
  129. $context['_trace_frame_offset'] = 3;
  130. $errorHandler->handleError($code, $message, $file, $line, $context);
  131. });
  132. ob_start();
  133. $wrong = $wrong + 1;
  134. $result = ob_get_clean();
  135. restore_error_handler();
  136. $this->assertStringNotContainsString(
  137. 'ErrorHandlerTest.php, line ' . (__LINE__ - 4),
  138. $result,
  139. 'Should not contain file and line reference'
  140. );
  141. $this->assertStringNotContainsString('_trace_frame_offset', $result);
  142. }
  143. /**
  144. * provides errors for mapping tests.
  145. *
  146. * @return array
  147. */
  148. public static function errorProvider(): array
  149. {
  150. return [
  151. [E_USER_NOTICE, 'Notice'],
  152. [E_USER_WARNING, 'Warning'],
  153. ];
  154. }
  155. /**
  156. * test error mappings
  157. *
  158. * @runInSeparateProcess
  159. * @preserveGlobalState disabled
  160. * @dataProvider errorProvider
  161. */
  162. public function testErrorMapping(int $error, string $expected): void
  163. {
  164. $this->deprecated(function () use ($error, $expected) {
  165. $errorHandler = new ErrorHandler();
  166. $errorHandler->register();
  167. ob_start();
  168. trigger_error('Test error', $error);
  169. $this->assertStringContainsString('<b>' . $expected . '</b>', ob_get_clean());
  170. });
  171. }
  172. /**
  173. * test error prepended by @
  174. *
  175. * @runInSeparateProcess
  176. * @preserveGlobalState disabled
  177. */
  178. public function testErrorSuppressed(): void
  179. {
  180. $errorHandler = new ErrorHandler();
  181. $this->deprecated(function () use ($errorHandler) {
  182. $errorHandler->register();
  183. ob_start();
  184. // phpcs:disable
  185. @include 'invalid.file';
  186. // phpcs:enable
  187. $this->assertEmpty(ob_get_clean());
  188. });
  189. }
  190. /**
  191. * Test that errors go into Cake Log when debug = 0.
  192. *
  193. * @runInSeparateProcess
  194. * @preserveGlobalState disabled
  195. */
  196. public function testHandleErrorDebugOff(): void
  197. {
  198. Configure::write('debug', false);
  199. $errorHandler = new ErrorHandler();
  200. $this->deprecated(function () use ($errorHandler) {
  201. $errorHandler->register();
  202. $out = $out + 1;
  203. });
  204. $messages = $this->logger->read();
  205. $this->assertMatchesRegularExpression('/^(notice|debug|warning)/', $messages[0]);
  206. if (version_compare(PHP_VERSION, '8.0.0-dev', '<')) {
  207. $this->assertStringContainsString(
  208. 'Notice (8): Undefined variable: out in [' . __FILE__ . ', line ' . (__LINE__ - 8) . ']',
  209. $messages[0]
  210. );
  211. } else {
  212. $this->assertStringContainsString(
  213. 'Warning (2): Undefined variable $out in [' . __FILE__ . ', line ' . (__LINE__ - 13) . ']',
  214. $messages[0]
  215. );
  216. }
  217. }
  218. /**
  219. * Test that errors going into Cake Log include traces.
  220. *
  221. * @runInSeparateProcess
  222. * @preserveGlobalState disabled
  223. */
  224. public function testHandleErrorLoggingTrace(): void
  225. {
  226. Configure::write('debug', false);
  227. $errorHandler = new ErrorHandler(['trace' => true]);
  228. $this->deprecated(function () use ($errorHandler) {
  229. $errorHandler->register();
  230. $out = $out + 1;
  231. });
  232. $messages = $this->logger->read();
  233. $this->assertMatchesRegularExpression('/^(notice|debug|warning)/', $messages[0]);
  234. $this->assertMatchesRegularExpression('/Undefined variable\:? \$?out in/', $messages[0]);
  235. $this->assertStringContainsString('[' . __FILE__ . ', line ' . (__LINE__ - 6) . ']', $messages[0]);
  236. $this->assertStringContainsString('Trace:', $messages[0]);
  237. $this->assertStringContainsString(__NAMESPACE__ . '\ErrorHandlerTest->testHandleErrorLoggingTrace()', $messages[0]);
  238. $this->assertStringContainsString('Request URL:', $messages[0]);
  239. $this->assertStringContainsString('Referer URL:', $messages[0]);
  240. }
  241. /**
  242. * test handleException generating a page.
  243. */
  244. public function testHandleException(): void
  245. {
  246. $error = new NotFoundException('Kaboom!');
  247. $errorHandler = new TestErrorHandler();
  248. $errorHandler->handleException($error);
  249. $this->assertStringContainsString('Kaboom!', (string)$errorHandler->response->getBody(), 'message missing.');
  250. }
  251. /**
  252. * test handleException generating log.
  253. */
  254. public function testHandleExceptionLog(): void
  255. {
  256. $errorHandler = new TestErrorHandler([
  257. 'log' => true,
  258. 'trace' => true,
  259. ]);
  260. $error = new NotFoundException('Kaboom!');
  261. $errorHandler->handleException($error);
  262. $this->assertStringContainsString('Kaboom!', (string)$errorHandler->response->getBody(), 'message missing.');
  263. $messages = $this->logger->read();
  264. $this->assertMatchesRegularExpression('/^error/', $messages[0]);
  265. $this->assertStringContainsString('[Cake\Http\Exception\NotFoundException] Kaboom!', $messages[0]);
  266. $this->assertStringContainsString(
  267. str_replace('/', DS, 'vendor/phpunit/phpunit/src/Framework/TestCase.php'),
  268. $messages[0]
  269. );
  270. $errorHandler = new TestErrorHandler([
  271. 'log' => true,
  272. 'trace' => false,
  273. ]);
  274. $errorHandler->handleException($error);
  275. $messages = $this->logger->read();
  276. $this->assertMatchesRegularExpression('/^error/', $messages[1]);
  277. $this->assertStringContainsString('[Cake\Http\Exception\NotFoundException] Kaboom!', $messages[1]);
  278. $this->assertStringNotContainsString(
  279. str_replace('/', DS, 'vendor/phpunit/phpunit/src/Framework/TestCase.php'),
  280. $messages[1]
  281. );
  282. }
  283. /**
  284. * test logging attributes with/without debug
  285. */
  286. public function testHandleExceptionLogAttributes(): void
  287. {
  288. $errorHandler = new TestErrorHandler([
  289. 'log' => true,
  290. 'trace' => true,
  291. ]);
  292. $error = new MissingControllerException(['class' => 'Derp']);
  293. $errorHandler->handleException($error);
  294. Configure::write('debug', false);
  295. $errorHandler->handleException($error);
  296. $messages = $this->logger->read();
  297. $this->assertMatchesRegularExpression('/^error/', $messages[0]);
  298. $this->assertStringContainsString(
  299. '[Cake\Http\Exception\MissingControllerException] Controller class Derp could not be found.',
  300. $messages[0]
  301. );
  302. $this->assertStringContainsString('Exception Attributes:', $messages[0]);
  303. $this->assertStringContainsString('Request URL:', $messages[0]);
  304. $this->assertStringContainsString('Referer URL:', $messages[0]);
  305. $this->assertStringContainsString(
  306. '[Cake\Http\Exception\MissingControllerException] Controller class Derp could not be found.',
  307. $messages[1]
  308. );
  309. $this->assertStringNotContainsString('Exception Attributes:', $messages[1]);
  310. }
  311. /**
  312. * test logging attributes with previous exception
  313. */
  314. public function testHandleExceptionLogPrevious(): void
  315. {
  316. $errorHandler = new TestErrorHandler([
  317. 'log' => true,
  318. 'trace' => true,
  319. ]);
  320. $previous = new RecordNotFoundException('Previous logged');
  321. $error = new NotFoundException('Kaboom!', null, $previous);
  322. $errorHandler->handleException($error);
  323. $messages = $this->logger->read();
  324. $this->assertStringContainsString('[Cake\Http\Exception\NotFoundException] Kaboom!', $messages[0]);
  325. $this->assertStringContainsString(
  326. 'Caused by: [Cake\Datasource\Exception\RecordNotFoundException] Previous logged',
  327. $messages[0]
  328. );
  329. $this->assertStringContainsString(
  330. str_replace('/', DS, 'vendor/phpunit/phpunit/src/Framework/TestCase.php'),
  331. $messages[0]
  332. );
  333. }
  334. /**
  335. * test handleException generating log.
  336. */
  337. public function testHandleExceptionLogSkipping(): void
  338. {
  339. $notFound = new NotFoundException('Kaboom!');
  340. $forbidden = new ForbiddenException('Fooled you!');
  341. $errorHandler = new TestErrorHandler([
  342. 'log' => true,
  343. 'skipLog' => ['Cake\Http\Exception\NotFoundException'],
  344. ]);
  345. $errorHandler->handleException($notFound);
  346. $this->assertStringContainsString('Kaboom!', (string)$errorHandler->response->getBody(), 'message missing.');
  347. $errorHandler->handleException($forbidden);
  348. $this->assertStringContainsString('Fooled you!', (string)$errorHandler->response->getBody(), 'message missing.');
  349. $messages = $this->logger->read();
  350. $this->assertCount(1, $messages);
  351. $this->assertMatchesRegularExpression('/^error/', $messages[0]);
  352. $this->assertStringContainsString(
  353. '[Cake\Http\Exception\ForbiddenException] Fooled you!',
  354. $messages[0]
  355. );
  356. }
  357. /**
  358. * tests it is possible to load a plugin exception renderer
  359. */
  360. public function testLoadPluginHandler(): void
  361. {
  362. $this->loadPlugins(['TestPlugin']);
  363. $errorHandler = new TestErrorHandler([
  364. 'exceptionRenderer' => 'TestPlugin.TestPluginExceptionRenderer',
  365. ]);
  366. $error = new NotFoundException('Kaboom!');
  367. $errorHandler->handleException($error);
  368. $result = $errorHandler->response;
  369. $this->assertSame('Rendered by test plugin', (string)$result);
  370. }
  371. /**
  372. * test handleFatalError generating a page.
  373. *
  374. * These tests start two buffers as handleFatalError blows the outer one up.
  375. */
  376. public function testHandleFatalErrorPage(): void
  377. {
  378. $line = __LINE__;
  379. $errorHandler = new TestErrorHandler();
  380. Configure::write('debug', true);
  381. $errorHandler->handleFatalError(E_ERROR, 'Something wrong', __FILE__, $line);
  382. $result = (string)$errorHandler->response->getBody();
  383. $this->assertStringContainsString('Something wrong', $result, 'message missing.');
  384. $this->assertStringContainsString(__FILE__, $result, 'filename missing.');
  385. $this->assertStringContainsString((string)$line, $result, 'line missing.');
  386. Configure::write('debug', false);
  387. $errorHandler->handleFatalError(E_ERROR, 'Something wrong', __FILE__, $line);
  388. $result = (string)$errorHandler->response->getBody();
  389. $this->assertStringNotContainsString('Something wrong', $result, 'message must not appear.');
  390. $this->assertStringNotContainsString(__FILE__, $result, 'filename must not appear.');
  391. $this->assertStringContainsString('An Internal Error Has Occurred.', $result);
  392. }
  393. /**
  394. * test handleFatalError generating log.
  395. */
  396. public function testHandleFatalErrorLog(): void
  397. {
  398. $errorHandler = new TestErrorHandler(['log' => true]);
  399. $errorHandler->handleFatalError(E_ERROR, 'Something wrong', __FILE__, __LINE__);
  400. $messages = $this->logger->read();
  401. $this->assertCount(2, $messages);
  402. $this->assertStringContainsString(__FILE__ . ', line ' . (__LINE__ - 4), $messages[0]);
  403. $this->assertStringContainsString('Fatal Error (1)', $messages[0]);
  404. $this->assertStringContainsString('Something wrong', $messages[0]);
  405. $this->assertStringContainsString('[Cake\Error\FatalErrorException] Something wrong', $messages[1]);
  406. }
  407. /**
  408. * Data provider for memory limit changing.
  409. *
  410. * @return array
  411. */
  412. public function memoryLimitProvider(): array
  413. {
  414. return [
  415. // start, adjust, expected
  416. ['256M', 4, '262148K'],
  417. ['262144K', 4, '262148K'],
  418. ['1G', 128, '1048704K'],
  419. ];
  420. }
  421. /**
  422. * Test increasing the memory limit.
  423. *
  424. * @dataProvider memoryLimitProvider
  425. */
  426. public function testIncreaseMemoryLimit(string $start, int $adjust, string $expected): void
  427. {
  428. $initial = ini_get('memory_limit');
  429. $this->skipIf(strlen($initial) === 0, 'Cannot read memory limit, and cannot test increasing it.');
  430. // phpunit.xml often has -1 as memory limit
  431. ini_set('memory_limit', $start);
  432. $errorHandler = new TestErrorHandler();
  433. $errorHandler->increaseMemoryLimit($adjust);
  434. $new = ini_get('memory_limit');
  435. $this->assertEquals($expected, $new, 'memory limit did not get increased.');
  436. ini_set('memory_limit', $initial);
  437. }
  438. /**
  439. * Test getting a logger
  440. */
  441. public function testGetLogger(): void
  442. {
  443. $errorHandler = new TestErrorHandler(['key' => 'value', 'log' => true]);
  444. $logger = $errorHandler->getLogger();
  445. $this->assertInstanceOf(ErrorLoggerInterface::class, $logger);
  446. $this->assertSame('value', $logger->getConfig('key'), 'config should be forwarded.');
  447. $this->assertSame($logger, $errorHandler->getLogger());
  448. }
  449. /**
  450. * Test getting a logger
  451. */
  452. public function testGetLoggerInvalid(): void
  453. {
  454. $errorHandler = new TestErrorHandler(['errorLogger' => stdClass::class]);
  455. $this->expectException(RuntimeException::class);
  456. $this->expectExceptionMessage('Cannot create logger');
  457. $errorHandler->getLogger();
  458. }
  459. }