PageRenderTime 45ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 0ms

/tests/TestCase/Error/ExceptionTrapTest.php

https://github.com/cakephp/cakephp
PHP | 414 lines | 313 code | 57 blank | 44 comment | 0 complexity | e33737dde2747b8b98db9421765efdd0 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 Project
  13. * @since 4.4.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Test\TestCase\Error;
  17. use Cake\Console\TestSuite\StubConsoleOutput;
  18. use Cake\Error\ErrorLogger;
  19. use Cake\Error\ExceptionRenderer;
  20. use Cake\Error\ExceptionTrap;
  21. use Cake\Error\Renderer\ConsoleExceptionRenderer;
  22. use Cake\Error\Renderer\TextExceptionRenderer;
  23. use Cake\Error\Renderer\WebExceptionRenderer;
  24. use Cake\Http\Exception\MissingControllerException;
  25. use Cake\Http\ServerRequest;
  26. use Cake\Log\Log;
  27. use Cake\TestSuite\TestCase;
  28. use Cake\Utility\Text;
  29. use InvalidArgumentException;
  30. use stdClass;
  31. use TestApp\Error\LegacyErrorLogger;
  32. use Throwable;
  33. class ExceptionTrapTest extends TestCase
  34. {
  35. /**
  36. * @var string
  37. */
  38. private $memoryLimit;
  39. private $triggered = false;
  40. public function setUp(): void
  41. {
  42. parent::setUp();
  43. $this->memoryLimit = ini_get('memory_limit');
  44. }
  45. public function tearDown(): void
  46. {
  47. parent::tearDown();
  48. Log::reset();
  49. ini_set('memory_limit', $this->memoryLimit);
  50. }
  51. public function testConfigRendererInvalid()
  52. {
  53. $trap = new ExceptionTrap(['exceptionRenderer' => stdClass::class]);
  54. $this->expectException(InvalidArgumentException::class);
  55. $error = new InvalidArgumentException('nope');
  56. $trap->renderer($error);
  57. }
  58. public function testConfigExceptionRendererFallbackInCli()
  59. {
  60. $this->deprecated(function () {
  61. $output = new StubConsoleOutput();
  62. $trap = new ExceptionTrap(['exceptionRenderer' => ExceptionRenderer::class, 'stderr' => $output]);
  63. $error = new InvalidArgumentException('nope');
  64. // Even though we asked for ExceptionRenderer we should get a
  65. // ConsoleExceptionRenderer as we're in a CLI context.
  66. $this->assertInstanceOf(ConsoleExceptionRenderer::class, $trap->renderer($error));
  67. });
  68. }
  69. public function testConfigExceptionRendererFallback()
  70. {
  71. $output = new StubConsoleOutput();
  72. $trap = new ExceptionTrap(['exceptionRenderer' => null, 'stderr' => $output]);
  73. $error = new InvalidArgumentException('nope');
  74. $this->assertInstanceOf(ConsoleExceptionRenderer::class, $trap->renderer($error));
  75. }
  76. public function testConfigExceptionRenderer()
  77. {
  78. $trap = new ExceptionTrap(['exceptionRenderer' => WebExceptionRenderer::class]);
  79. $error = new InvalidArgumentException('nope');
  80. $this->assertInstanceOf(WebExceptionRenderer::class, $trap->renderer($error));
  81. }
  82. public function testConfigExceptionRendererFactory()
  83. {
  84. $trap = new ExceptionTrap(['exceptionRenderer' => function ($err, $req) {
  85. return new WebExceptionRenderer($err, $req);
  86. }]);
  87. $error = new InvalidArgumentException('nope');
  88. $this->assertInstanceOf(WebExceptionRenderer::class, $trap->renderer($error));
  89. }
  90. public function testConfigRendererHandleUnsafeOverwrite()
  91. {
  92. $output = new StubConsoleOutput();
  93. $trap = new ExceptionTrap(['stderr' => $output]);
  94. $trap->setConfig('exceptionRenderer', null);
  95. $error = new InvalidArgumentException('nope');
  96. $this->assertInstanceOf(ConsoleExceptionRenderer::class, $trap->renderer($error));
  97. }
  98. public function testLoggerConfigInvalid()
  99. {
  100. $trap = new ExceptionTrap(['logger' => stdClass::class]);
  101. $this->expectException(InvalidArgumentException::class);
  102. $trap->logger();
  103. }
  104. public function testLoggerConfig()
  105. {
  106. $trap = new ExceptionTrap(['logger' => ErrorLogger::class]);
  107. $this->assertInstanceOf(ErrorLogger::class, $trap->logger());
  108. }
  109. public function testLoggerHandleUnsafeOverwrite()
  110. {
  111. $trap = new ExceptionTrap();
  112. $trap->setConfig('logger', null);
  113. $this->assertInstanceOf(ErrorLogger::class, $trap->logger());
  114. }
  115. public function testHandleExceptionText()
  116. {
  117. $trap = new ExceptionTrap([
  118. 'exceptionRenderer' => TextExceptionRenderer::class,
  119. ]);
  120. $error = new InvalidArgumentException('nope');
  121. ob_start();
  122. $trap->handleException($error);
  123. $out = ob_get_clean();
  124. $this->assertStringContainsString('nope', $out);
  125. $this->assertStringContainsString('ExceptionTrapTest', $out);
  126. }
  127. public function testHandleExceptionConsoleRenderingNoStack()
  128. {
  129. $output = new StubConsoleOutput();
  130. $trap = new ExceptionTrap([
  131. 'exceptionRenderer' => ConsoleExceptionRenderer::class,
  132. 'stderr' => $output,
  133. ]);
  134. $error = new InvalidArgumentException('nope');
  135. $trap->handleException($error);
  136. $out = $output->messages();
  137. $this->assertStringContainsString('nope', $out[0]);
  138. $this->assertStringNotContainsString('Stack', $out[0]);
  139. }
  140. public function testHandleExceptionConsoleRenderingWithStack()
  141. {
  142. $output = new StubConsoleOutput();
  143. $trap = new ExceptionTrap([
  144. 'exceptionRenderer' => ConsoleExceptionRenderer::class,
  145. 'stderr' => $output,
  146. 'trace' => true,
  147. ]);
  148. $error = new InvalidArgumentException('nope');
  149. $trap->handleException($error);
  150. $out = $output->messages();
  151. $this->assertStringContainsString('nope', $out[0]);
  152. $this->assertStringContainsString('Stack', $out[0]);
  153. $this->assertStringContainsString('->testHandleExceptionConsoleRenderingWithStack', $out[0]);
  154. }
  155. public function testHandleExceptionConsoleWithAttributes()
  156. {
  157. $output = new StubConsoleOutput();
  158. $trap = new ExceptionTrap([
  159. 'exceptionRenderer' => ConsoleExceptionRenderer::class,
  160. 'stderr' => $output,
  161. ]);
  162. $error = new MissingControllerException(['name' => 'Articles']);
  163. $trap->handleException($error);
  164. $out = $output->messages();
  165. $this->assertStringContainsString('Controller class Articles', $out[0]);
  166. $this->assertStringContainsString('Exception Attributes', $out[0]);
  167. $this->assertStringContainsString('Articles', $out[0]);
  168. }
  169. /**
  170. * Test integration with HTML exception rendering
  171. *
  172. * Run in a separate process because HTML output writes headers.
  173. *
  174. * @preserveGlobalState disabled
  175. * @runInSeparateProcess
  176. */
  177. public function testHandleExceptionHtmlRendering()
  178. {
  179. $trap = new ExceptionTrap([
  180. 'exceptionRenderer' => WebExceptionRenderer::class,
  181. ]);
  182. $error = new InvalidArgumentException('nope');
  183. ob_start();
  184. $trap->handleException($error);
  185. $out = ob_get_clean();
  186. $this->assertStringContainsString('<!DOCTYPE', $out);
  187. $this->assertStringContainsString('<html', $out);
  188. $this->assertStringContainsString('nope', $out);
  189. $this->assertStringContainsString('ExceptionTrapTest', $out);
  190. }
  191. public function testLogException()
  192. {
  193. Log::setConfig('test_error', [
  194. 'className' => 'Array',
  195. ]);
  196. $trap = new ExceptionTrap();
  197. $error = new InvalidArgumentException('nope');
  198. $trap->logException($error);
  199. $logs = Log::engine('test_error')->read();
  200. $this->assertStringContainsString('nope', $logs[0]);
  201. }
  202. public function testLogExceptionConfigOff()
  203. {
  204. Log::setConfig('test_error', [
  205. 'className' => 'Array',
  206. ]);
  207. $trap = new ExceptionTrap(['log' => false]);
  208. $error = new InvalidArgumentException('nope');
  209. $trap->logException($error);
  210. $logs = Log::engine('test_error')->read();
  211. $this->assertEmpty($logs);
  212. }
  213. public function testLogExceptionDeprecatedLoggerMethods()
  214. {
  215. Log::setConfig('test_error', [
  216. 'className' => 'Array',
  217. ]);
  218. $trap = new ExceptionTrap([
  219. 'log' => true,
  220. 'logger' => LegacyErrorLogger::class,
  221. 'trace' => true,
  222. ]);
  223. $error = new InvalidArgumentException('nope');
  224. $request = new ServerRequest(['url' => '/articles/view/1']);
  225. $this->deprecated(function () use ($trap, $error, $request) {
  226. $trap->logException($error, $request);
  227. });
  228. $logs = Log::engine('test_error')->read();
  229. $this->assertStringContainsString('nope', $logs[0]);
  230. $this->assertStringContainsString('IncludeTrace', $logs[0]);
  231. $this->assertStringContainsString('URL=http://localhost/articles/view/1', $logs[0]);
  232. }
  233. /**
  234. * @preserveGlobalState disabled
  235. * @runInSeparateProcess
  236. */
  237. public function testSkipLogException(): void
  238. {
  239. Log::setConfig('test_error', [
  240. 'className' => 'Array',
  241. ]);
  242. $trap = new ExceptionTrap([
  243. 'exceptionRenderer' => WebExceptionRenderer::class,
  244. 'skipLog' => [InvalidArgumentException::class],
  245. ]);
  246. $trap->getEventManager()->on('Exception.beforeRender', function () {
  247. $this->triggered = true;
  248. });
  249. ob_start();
  250. $trap->handleException(new InvalidArgumentException('nope'));
  251. ob_get_clean();
  252. $logs = Log::engine('test_error')->read();
  253. $this->assertEmpty($logs);
  254. $this->assertTrue($this->triggered, 'Should have triggered event when skipping logging.');
  255. }
  256. public function testEventTriggered()
  257. {
  258. $trap = new ExceptionTrap(['exceptionRenderer' => TextExceptionRenderer::class]);
  259. $trap->getEventManager()->on('Exception.beforeRender', function ($event, Throwable $error) {
  260. $this->assertEquals(100, $error->getCode());
  261. $this->assertStringContainsString('nope', $error->getMessage());
  262. $event->stopPropagation();
  263. });
  264. $error = new InvalidArgumentException('nope', 100);
  265. ob_start();
  266. $trap->handleException($error);
  267. $out = ob_get_clean();
  268. $this->assertNotEmpty($out);
  269. }
  270. public function testHandleShutdownNoOp()
  271. {
  272. $trap = new ExceptionTrap([
  273. 'exceptionRenderer' => TextExceptionRenderer::class,
  274. ]);
  275. ob_start();
  276. $trap->handleShutdown();
  277. $out = ob_get_clean();
  278. $this->assertEmpty($out);
  279. }
  280. public function testHandleFatalShutdownNoError()
  281. {
  282. $trap = new ExceptionTrap([
  283. 'exceptionRenderer' => TextExceptionRenderer::class,
  284. ]);
  285. error_clear_last();
  286. ob_start();
  287. $trap->handleShutdown();
  288. $out = ob_get_clean();
  289. $this->assertSame('', $out);
  290. }
  291. public function testHandleFatalErrorText()
  292. {
  293. $trap = new ExceptionTrap([
  294. 'exceptionRenderer' => TextExceptionRenderer::class,
  295. ]);
  296. ob_start();
  297. $trap->handleFatalError(E_USER_ERROR, 'Something bad', __FILE__, __LINE__);
  298. $out = ob_get_clean();
  299. $this->assertStringContainsString('500 : Fatal Error', $out);
  300. $this->assertStringContainsString('Something bad', $out);
  301. $this->assertStringContainsString(__FILE__, $out);
  302. }
  303. /**
  304. * Test integration with HTML rendering for fatal errors
  305. *
  306. * Run in a separate process because HTML output writes headers.
  307. *
  308. * @preserveGlobalState disabled
  309. * @runInSeparateProcess
  310. */
  311. public function testHandleFatalErrorHtmlRendering()
  312. {
  313. $trap = new ExceptionTrap([
  314. 'exceptionRenderer' => WebExceptionRenderer::class,
  315. ]);
  316. ob_start();
  317. $trap->handleFatalError(E_USER_ERROR, 'Something bad', __FILE__, __LINE__);
  318. $out = ob_get_clean();
  319. $this->assertStringContainsString('<!DOCTYPE', $out);
  320. $this->assertStringContainsString('<html', $out);
  321. $this->assertStringContainsString('Something bad', $out);
  322. $this->assertStringContainsString(__FILE__, $out);
  323. }
  324. /**
  325. * Data provider for memory limit increase
  326. */
  327. public static function initialMemoryProvider(): array
  328. {
  329. return [
  330. ['256M'],
  331. ['1G'],
  332. ];
  333. }
  334. /**
  335. * @dataProvider initialMemoryProvider
  336. */
  337. public function testIncreaseMemoryLimit($initial)
  338. {
  339. ini_set('memory_limit', $initial);
  340. $this->assertEquals($initial, ini_get('memory_limit'));
  341. $trap = new ExceptionTrap([
  342. 'exceptionRenderer' => TextExceptionRenderer::class,
  343. ]);
  344. $trap->increaseMemoryLimit(4 * 1024);
  345. $initialBytes = Text::parseFileSize($initial, false);
  346. $result = Text::parseFileSize(ini_get('memory_limit'), false);
  347. $this->assertWithinRange($initialBytes + (4 * 1024 * 1024), $result, 1024);
  348. }
  349. public function testSingleton()
  350. {
  351. $trap = new ExceptionTrap();
  352. $trap->register();
  353. $this->assertSame($trap, ExceptionTrap::instance());
  354. $trap->unregister();
  355. $this->assertNull(ExceptionTrap::instance());
  356. }
  357. }