ExceptionTrapTest.php 12 KB

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