PageRenderTime 44ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/tests/TestCase/Error/ErrorHandlerTest.php

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