'', 'environment' => [ 'HTTP_REFERER' => '/referer', ], ]); Router::setRequest($request); Configure::write('debug', true); Log::reset(); Log::setConfig('error_test', ['className' => 'Array']); $this->logger = Log::engine('error_test'); } /** * tearDown */ public function tearDown(): void { parent::tearDown(); Log::reset(); $this->clearPlugins(); error_reporting(self::$errorLevel); } /** * setUpBeforeClass */ public static function setUpBeforeClass(): void { parent::setUpBeforeClass(); self::$errorLevel = error_reporting(); } /** * Test an invalid rendering class. */ public function testInvalidRenderer(): void { $this->expectException(RuntimeException::class); $this->expectExceptionMessage('The \'TotallyInvalid\' renderer class could not be found'); $errorHandler = new ErrorHandler(['exceptionRenderer' => 'TotallyInvalid']); $errorHandler->getRenderer(new Exception('Something bad')); } /** * test error handling when debug is on, an error should be printed from Debugger. * * @runInSeparateProcess * @preserveGlobalState disabled */ public function testHandleErrorDebugOn(): void { Configure::write('debug', true); $this->deprecated(function () { $errorHandler = new ErrorHandler(); $errorHandler->register(); ob_start(); $wrong = $wrong + 1; }); $result = ob_get_clean(); $this->assertMatchesRegularExpression('/
/', $result); if (version_compare(PHP_VERSION, '8.0.0-dev', '<')) { $this->assertMatchesRegularExpression('/Notice<\/b>/', $result); $this->assertMatchesRegularExpression('/variable:\s+wrong/', $result); } else { $this->assertMatchesRegularExpression('/Warning<\/b>/', $result); $this->assertMatchesRegularExpression('/variable \$wrong/', $result); } $this->assertMatchesRegularExpression( '/ErrorHandlerTest\.php[^,]+, line[^\d]+' . (__LINE__ - 13) . '/', $result, 'Should contain file and line reference' ); } /** * test error handling with the _trace_offset context variable */ public function testHandleErrorTraceOffset(): void { set_error_handler(function ($code, $message, $file, $line, $context = null): void { $errorHandler = new ErrorHandler(); $context['_trace_frame_offset'] = 3; $errorHandler->handleError($code, $message, $file, $line, $context); }); ob_start(); $wrong = $wrong + 1; $result = ob_get_clean(); restore_error_handler(); $this->assertStringNotContainsString( 'ErrorHandlerTest.php, line ' . (__LINE__ - 4), $result, 'Should not contain file and line reference' ); $this->assertStringNotContainsString('_trace_frame_offset', $result); } /** * provides errors for mapping tests. * * @return array */ public static function errorProvider(): array { return [ [E_USER_NOTICE, 'Notice'], [E_USER_WARNING, 'Warning'], ]; } /** * test error mappings * * @runInSeparateProcess * @preserveGlobalState disabled * @dataProvider errorProvider */ public function testErrorMapping(int $error, string $expected): void { $this->deprecated(function () use ($error, $expected) { $errorHandler = new ErrorHandler(); $errorHandler->register(); ob_start(); trigger_error('Test error', $error); $this->assertStringContainsString('' . $expected . '', ob_get_clean()); }); } /** * test error prepended by @ * * @runInSeparateProcess * @preserveGlobalState disabled */ public function testErrorSuppressed(): void { $errorHandler = new ErrorHandler(); $this->deprecated(function () use ($errorHandler) { $errorHandler->register(); ob_start(); // phpcs:disable @include 'invalid.file'; // phpcs:enable $this->assertEmpty(ob_get_clean()); }); } /** * Test that errors go into Cake Log when debug = 0. * * @runInSeparateProcess * @preserveGlobalState disabled */ public function testHandleErrorDebugOff(): void { Configure::write('debug', false); $errorHandler = new ErrorHandler(); $this->deprecated(function () use ($errorHandler) { $errorHandler->register(); $out = $out + 1; }); $messages = $this->logger->read(); $this->assertMatchesRegularExpression('/^(notice|debug|warning)/', $messages[0]); if (version_compare(PHP_VERSION, '8.0.0-dev', '<')) { $this->assertStringContainsString( 'Notice (8): Undefined variable: out in [' . __FILE__ . ', line ' . (__LINE__ - 8) . ']', $messages[0] ); } else { $this->assertStringContainsString( 'Warning (2): Undefined variable $out in [' . __FILE__ . ', line ' . (__LINE__ - 13) . ']', $messages[0] ); } } /** * Test that errors going into Cake Log include traces. * * @runInSeparateProcess * @preserveGlobalState disabled */ public function testHandleErrorLoggingTrace(): void { Configure::write('debug', false); $errorHandler = new ErrorHandler(['trace' => true]); $this->deprecated(function () use ($errorHandler) { $errorHandler->register(); $out = $out + 1; }); $messages = $this->logger->read(); $this->assertMatchesRegularExpression('/^(notice|debug|warning)/', $messages[0]); $this->assertMatchesRegularExpression('/Undefined variable\:? \$?out in/', $messages[0]); $this->assertStringContainsString('[' . __FILE__ . ', line ' . (__LINE__ - 6) . ']', $messages[0]); $this->assertStringContainsString('Trace:', $messages[0]); $this->assertStringContainsString(__NAMESPACE__ . '\ErrorHandlerTest->testHandleErrorLoggingTrace()', $messages[0]); $this->assertStringContainsString('Request URL:', $messages[0]); $this->assertStringContainsString('Referer URL:', $messages[0]); } /** * test handleException generating a page. */ public function testHandleException(): void { $error = new NotFoundException('Kaboom!'); $errorHandler = new TestErrorHandler(); $errorHandler->handleException($error); $this->assertStringContainsString('Kaboom!', (string)$errorHandler->response->getBody(), 'message missing.'); } /** * test handleException generating log. */ public function testHandleExceptionLog(): void { $errorHandler = new TestErrorHandler([ 'log' => true, 'trace' => true, ]); $error = new NotFoundException('Kaboom!'); $errorHandler->handleException($error); $this->assertStringContainsString('Kaboom!', (string)$errorHandler->response->getBody(), 'message missing.'); $messages = $this->logger->read(); $this->assertMatchesRegularExpression('/^error/', $messages[0]); $this->assertStringContainsString('[Cake\Http\Exception\NotFoundException] Kaboom!', $messages[0]); $this->assertStringContainsString( str_replace('/', DS, 'vendor/phpunit/phpunit/src/Framework/TestCase.php'), $messages[0] ); $errorHandler = new TestErrorHandler([ 'log' => true, 'trace' => false, ]); $errorHandler->handleException($error); $messages = $this->logger->read(); $this->assertMatchesRegularExpression('/^error/', $messages[1]); $this->assertStringContainsString('[Cake\Http\Exception\NotFoundException] Kaboom!', $messages[1]); $this->assertStringNotContainsString( str_replace('/', DS, 'vendor/phpunit/phpunit/src/Framework/TestCase.php'), $messages[1] ); } /** * test logging attributes with/without debug */ public function testHandleExceptionLogAttributes(): void { $errorHandler = new TestErrorHandler([ 'log' => true, 'trace' => true, ]); $error = new MissingControllerException(['class' => 'Derp']); $errorHandler->handleException($error); Configure::write('debug', false); $errorHandler->handleException($error); $messages = $this->logger->read(); $this->assertMatchesRegularExpression('/^error/', $messages[0]); $this->assertStringContainsString( '[Cake\Http\Exception\MissingControllerException] Controller class Derp could not be found.', $messages[0] ); $this->assertStringContainsString('Exception Attributes:', $messages[0]); $this->assertStringContainsString('Request URL:', $messages[0]); $this->assertStringContainsString('Referer URL:', $messages[0]); $this->assertStringContainsString( '[Cake\Http\Exception\MissingControllerException] Controller class Derp could not be found.', $messages[1] ); $this->assertStringNotContainsString('Exception Attributes:', $messages[1]); } /** * test logging attributes with previous exception */ public function testHandleExceptionLogPrevious(): void { $errorHandler = new TestErrorHandler([ 'log' => true, 'trace' => true, ]); $previous = new RecordNotFoundException('Previous logged'); $error = new NotFoundException('Kaboom!', null, $previous); $errorHandler->handleException($error); $messages = $this->logger->read(); $this->assertStringContainsString('[Cake\Http\Exception\NotFoundException] Kaboom!', $messages[0]); $this->assertStringContainsString( 'Caused by: [Cake\Datasource\Exception\RecordNotFoundException] Previous logged', $messages[0] ); $this->assertStringContainsString( str_replace('/', DS, 'vendor/phpunit/phpunit/src/Framework/TestCase.php'), $messages[0] ); } /** * test handleException generating log. */ public function testHandleExceptionLogSkipping(): void { $notFound = new NotFoundException('Kaboom!'); $forbidden = new ForbiddenException('Fooled you!'); $errorHandler = new TestErrorHandler([ 'log' => true, 'skipLog' => ['Cake\Http\Exception\NotFoundException'], ]); $errorHandler->handleException($notFound); $this->assertStringContainsString('Kaboom!', (string)$errorHandler->response->getBody(), 'message missing.'); $errorHandler->handleException($forbidden); $this->assertStringContainsString('Fooled you!', (string)$errorHandler->response->getBody(), 'message missing.'); $messages = $this->logger->read(); $this->assertCount(1, $messages); $this->assertMatchesRegularExpression('/^error/', $messages[0]); $this->assertStringContainsString( '[Cake\Http\Exception\ForbiddenException] Fooled you!', $messages[0] ); } /** * tests it is possible to load a plugin exception renderer */ public function testLoadPluginHandler(): void { $this->loadPlugins(['TestPlugin']); $errorHandler = new TestErrorHandler([ 'exceptionRenderer' => 'TestPlugin.TestPluginExceptionRenderer', ]); $error = new NotFoundException('Kaboom!'); $errorHandler->handleException($error); $result = $errorHandler->response; $this->assertSame('Rendered by test plugin', (string)$result); } /** * test handleFatalError generating a page. * * These tests start two buffers as handleFatalError blows the outer one up. */ public function testHandleFatalErrorPage(): void { $line = __LINE__; $errorHandler = new TestErrorHandler(); Configure::write('debug', true); $errorHandler->handleFatalError(E_ERROR, 'Something wrong', __FILE__, $line); $result = (string)$errorHandler->response->getBody(); $this->assertStringContainsString('Something wrong', $result, 'message missing.'); $this->assertStringContainsString(__FILE__, $result, 'filename missing.'); $this->assertStringContainsString((string)$line, $result, 'line missing.'); Configure::write('debug', false); $errorHandler->handleFatalError(E_ERROR, 'Something wrong', __FILE__, $line); $result = (string)$errorHandler->response->getBody(); $this->assertStringNotContainsString('Something wrong', $result, 'message must not appear.'); $this->assertStringNotContainsString(__FILE__, $result, 'filename must not appear.'); $this->assertStringContainsString('An Internal Error Has Occurred.', $result); } /** * test handleFatalError generating log. */ public function testHandleFatalErrorLog(): void { $errorHandler = new TestErrorHandler(['log' => true]); $errorHandler->handleFatalError(E_ERROR, 'Something wrong', __FILE__, __LINE__); $messages = $this->logger->read(); $this->assertCount(2, $messages); $this->assertStringContainsString(__FILE__ . ', line ' . (__LINE__ - 4), $messages[0]); $this->assertStringContainsString('Fatal Error (1)', $messages[0]); $this->assertStringContainsString('Something wrong', $messages[0]); $this->assertStringContainsString('[Cake\Error\FatalErrorException] Something wrong', $messages[1]); } /** * Data provider for memory limit changing. * * @return array */ public function memoryLimitProvider(): array { return [ // start, adjust, expected ['256M', 4, '262148K'], ['262144K', 4, '262148K'], ['1G', 128, '1048704K'], ]; } /** * Test increasing the memory limit. * * @dataProvider memoryLimitProvider */ public function testIncreaseMemoryLimit(string $start, int $adjust, string $expected): void { $initial = ini_get('memory_limit'); $this->skipIf(strlen($initial) === 0, 'Cannot read memory limit, and cannot test increasing it.'); // phpunit.xml often has -1 as memory limit ini_set('memory_limit', $start); $errorHandler = new TestErrorHandler(); $errorHandler->increaseMemoryLimit($adjust); $new = ini_get('memory_limit'); $this->assertEquals($expected, $new, 'memory limit did not get increased.'); ini_set('memory_limit', $initial); } /** * Test getting a logger */ public function testGetLogger(): void { $errorHandler = new TestErrorHandler(['key' => 'value', 'log' => true]); $logger = $errorHandler->getLogger(); $this->assertInstanceOf(ErrorLoggerInterface::class, $logger); $this->assertSame('value', $logger->getConfig('key'), 'config should be forwarded.'); $this->assertSame($logger, $errorHandler->getLogger()); } /** * Test getting a logger */ public function testGetLoggerInvalid(): void { $errorHandler = new TestErrorHandler(['errorLogger' => stdClass::class]); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Cannot create logger'); $errorHandler->getLogger(); } }