FoundationExceptionsHandlerTest.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. <?php
  2. namespace Illuminate\Tests\Foundation;
  3. use Exception;
  4. use Illuminate\Config\Repository as Config;
  5. use Illuminate\Container\Container;
  6. use Illuminate\Contracts\Routing\ResponseFactory as ResponseFactoryContract;
  7. use Illuminate\Contracts\Support\Responsable;
  8. use Illuminate\Contracts\View\Factory;
  9. use Illuminate\Database\RecordsNotFoundException;
  10. use Illuminate\Foundation\Exceptions\Handler;
  11. use Illuminate\Http\RedirectResponse;
  12. use Illuminate\Http\Request;
  13. use Illuminate\Routing\Redirector;
  14. use Illuminate\Routing\ResponseFactory;
  15. use Illuminate\Support\MessageBag;
  16. use Illuminate\Validation\ValidationException;
  17. use Illuminate\Validation\Validator;
  18. use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
  19. use Mockery as m;
  20. use PHPUnit\Framework\TestCase;
  21. use Psr\Log\LoggerInterface;
  22. use RuntimeException;
  23. use stdClass;
  24. use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException;
  25. use Symfony\Component\HttpFoundation\File\UploadedFile;
  26. use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
  27. use Symfony\Component\HttpKernel\Exception\HttpException;
  28. class FoundationExceptionsHandlerTest extends TestCase
  29. {
  30. use MockeryPHPUnitIntegration;
  31. protected $config;
  32. protected $container;
  33. protected $handler;
  34. protected $request;
  35. protected function setUp(): void
  36. {
  37. $this->config = m::mock(Config::class);
  38. $this->request = m::mock(stdClass::class);
  39. $this->container = Container::setInstance(new Container);
  40. $this->container->singleton('config', function () {
  41. return $this->config;
  42. });
  43. $this->container->singleton(ResponseFactoryContract::class, function () {
  44. return new ResponseFactory(
  45. m::mock(Factory::class),
  46. m::mock(Redirector::class)
  47. );
  48. });
  49. $this->handler = new Handler($this->container);
  50. }
  51. protected function tearDown(): void
  52. {
  53. Container::setInstance(null);
  54. }
  55. public function testHandlerReportsExceptionAsContext()
  56. {
  57. $logger = m::mock(LoggerInterface::class);
  58. $this->container->instance(LoggerInterface::class, $logger);
  59. $logger->shouldReceive('error')->withArgs(['Exception message', m::hasKey('exception')])->once();
  60. $this->handler->report(new RuntimeException('Exception message'));
  61. }
  62. public function testHandlerCallsContextMethodIfPresent()
  63. {
  64. $logger = m::mock(LoggerInterface::class);
  65. $this->container->instance(LoggerInterface::class, $logger);
  66. $logger->shouldReceive('error')->withArgs(['Exception message', m::subset(['foo' => 'bar'])])->once();
  67. $this->handler->report(new ContextProvidingException('Exception message'));
  68. }
  69. public function testHandlerReportsExceptionWhenUnReportable()
  70. {
  71. $logger = m::mock(LoggerInterface::class);
  72. $this->container->instance(LoggerInterface::class, $logger);
  73. $logger->shouldReceive('error')->withArgs(['Exception message', m::hasKey('exception')])->once();
  74. $this->handler->report(new UnReportableException('Exception message'));
  75. }
  76. public function testHandlerCallsReportMethodWithDependencies()
  77. {
  78. $reporter = m::mock(ReportingService::class);
  79. $this->container->instance(ReportingService::class, $reporter);
  80. $reporter->shouldReceive('send')->withArgs(['Exception message'])->once();
  81. $logger = m::mock(LoggerInterface::class);
  82. $this->container->instance(LoggerInterface::class, $logger);
  83. $logger->shouldNotReceive('error');
  84. $this->handler->report(new ReportableException('Exception message'));
  85. }
  86. public function testHandlerReportsExceptionUsingCallableClass()
  87. {
  88. $reporter = m::mock(ReportingService::class);
  89. $reporter->shouldReceive('send')->withArgs(['Exception message'])->once();
  90. $logger = m::mock(LoggerInterface::class);
  91. $this->container->instance(LoggerInterface::class, $logger);
  92. $logger->shouldNotReceive('error');
  93. $this->handler->reportable(new CustomReporter($reporter));
  94. $this->handler->report(new CustomException('Exception message'));
  95. }
  96. public function testReturnsJsonWithStackTraceWhenAjaxRequestAndDebugTrue()
  97. {
  98. $this->config->shouldReceive('get')->with('app.debug', null)->once()->andReturn(true);
  99. $this->request->shouldReceive('expectsJson')->once()->andReturn(true);
  100. $response = $this->handler->render($this->request, new Exception('My custom error message'))->getContent();
  101. $this->assertStringNotContainsString('<!DOCTYPE html>', $response);
  102. $this->assertStringContainsString('"message": "My custom error message"', $response);
  103. $this->assertStringContainsString('"file":', $response);
  104. $this->assertStringContainsString('"line":', $response);
  105. $this->assertStringContainsString('"trace":', $response);
  106. }
  107. public function testReturnsCustomResponseFromRenderableCallback()
  108. {
  109. $this->handler->renderable(function (CustomException $e, $request) {
  110. $this->assertSame($this->request, $request);
  111. return response()->json(['response' => 'My custom exception response']);
  112. });
  113. $response = $this->handler->render($this->request, new CustomException)->getContent();
  114. $this->assertSame('{"response":"My custom exception response"}', $response);
  115. }
  116. public function testReturnsCustomResponseFromCallableClass()
  117. {
  118. $this->handler->renderable(new CustomRenderer);
  119. $response = $this->handler->render($this->request, new CustomException)->getContent();
  120. $this->assertSame('{"response":"The CustomRenderer response"}', $response);
  121. }
  122. public function testReturnsCustomResponseWhenExceptionImplementsResponsable()
  123. {
  124. $response = $this->handler->render($this->request, new ResponsableException)->getContent();
  125. $this->assertSame('{"response":"My responsable exception response"}', $response);
  126. }
  127. public function testReturnsJsonWithoutStackTraceWhenAjaxRequestAndDebugFalseAndExceptionMessageIsMasked()
  128. {
  129. $this->config->shouldReceive('get')->with('app.debug', null)->once()->andReturn(false);
  130. $this->request->shouldReceive('expectsJson')->once()->andReturn(true);
  131. $response = $this->handler->render($this->request, new Exception('This error message should not be visible'))->getContent();
  132. $this->assertStringContainsString('"message": "Server Error"', $response);
  133. $this->assertStringNotContainsString('<!DOCTYPE html>', $response);
  134. $this->assertStringNotContainsString('This error message should not be visible', $response);
  135. $this->assertStringNotContainsString('"file":', $response);
  136. $this->assertStringNotContainsString('"line":', $response);
  137. $this->assertStringNotContainsString('"trace":', $response);
  138. }
  139. public function testReturnsJsonWithoutStackTraceWhenAjaxRequestAndDebugFalseAndHttpExceptionErrorIsShown()
  140. {
  141. $this->config->shouldReceive('get')->with('app.debug', null)->once()->andReturn(false);
  142. $this->request->shouldReceive('expectsJson')->once()->andReturn(true);
  143. $response = $this->handler->render($this->request, new HttpException(403, 'My custom error message'))->getContent();
  144. $this->assertStringContainsString('"message": "My custom error message"', $response);
  145. $this->assertStringNotContainsString('<!DOCTYPE html>', $response);
  146. $this->assertStringNotContainsString('"message": "Server Error"', $response);
  147. $this->assertStringNotContainsString('"file":', $response);
  148. $this->assertStringNotContainsString('"line":', $response);
  149. $this->assertStringNotContainsString('"trace":', $response);
  150. }
  151. public function testReturnsJsonWithoutStackTraceWhenAjaxRequestAndDebugFalseAndAccessDeniedHttpExceptionErrorIsShown()
  152. {
  153. $this->config->shouldReceive('get')->with('app.debug', null)->once()->andReturn(false);
  154. $this->request->shouldReceive('expectsJson')->once()->andReturn(true);
  155. $response = $this->handler->render($this->request, new AccessDeniedHttpException('My custom error message'))->getContent();
  156. $this->assertStringContainsString('"message": "My custom error message"', $response);
  157. $this->assertStringNotContainsString('<!DOCTYPE html>', $response);
  158. $this->assertStringNotContainsString('"message": "Server Error"', $response);
  159. $this->assertStringNotContainsString('"file":', $response);
  160. $this->assertStringNotContainsString('"line":', $response);
  161. $this->assertStringNotContainsString('"trace":', $response);
  162. }
  163. public function testValidateFileMethod()
  164. {
  165. $argumentExpected = ['input' => 'My input value'];
  166. $argumentActual = null;
  167. $this->container->singleton('redirect', function () use (&$argumentActual) {
  168. $redirector = m::mock(Redirector::class);
  169. $redirector->shouldReceive('to')->once()
  170. ->andReturn($responser = m::mock(RedirectResponse::class));
  171. $responser->shouldReceive('withInput')->once()->with(m::on(
  172. function ($argument) use (&$argumentActual) {
  173. $argumentActual = $argument;
  174. return true;
  175. }))->andReturn($responser);
  176. $responser->shouldReceive('withErrors')->once()
  177. ->andReturn($responser);
  178. return $redirector;
  179. });
  180. $file = m::mock(UploadedFile::class);
  181. $file->shouldReceive('getPathname')->andReturn('photo.jpg');
  182. $file->shouldReceive('getClientOriginalName')->andReturn('photo.jpg');
  183. $file->shouldReceive('getClientMimeType')->andReturn(null);
  184. $file->shouldReceive('getError')->andReturn(null);
  185. $request = Request::create('/', 'POST', $argumentExpected, [], ['photo' => $file]);
  186. $validator = m::mock(Validator::class);
  187. $validator->shouldReceive('errors')->andReturn(new MessageBag(['error' => 'My custom validation exception']));
  188. $validationException = new ValidationException($validator);
  189. $validationException->redirectTo = '/';
  190. $this->handler->render($request, $validationException);
  191. $this->assertEquals($argumentExpected, $argumentActual);
  192. }
  193. public function testSuspiciousOperationReturns404WithoutReporting()
  194. {
  195. $this->config->shouldReceive('get')->with('app.debug', null)->once()->andReturn(true);
  196. $this->request->shouldReceive('expectsJson')->once()->andReturn(true);
  197. $response = $this->handler->render($this->request, new SuspiciousOperationException('Invalid method override "__CONSTRUCT"'));
  198. $this->assertEquals(404, $response->getStatusCode());
  199. $this->assertStringContainsString('"message": "Bad hostname provided."', $response->getContent());
  200. $logger = m::mock(LoggerInterface::class);
  201. $this->container->instance(LoggerInterface::class, $logger);
  202. $logger->shouldNotReceive('error');
  203. $this->handler->report(new SuspiciousOperationException('Invalid method override "__CONSTRUCT"'));
  204. }
  205. public function testRecordsNotFoundReturns404WithoutReporting()
  206. {
  207. $this->config->shouldReceive('get')->with('app.debug', null)->once()->andReturn(true);
  208. $this->request->shouldReceive('expectsJson')->once()->andReturn(true);
  209. $response = $this->handler->render($this->request, new RecordsNotFoundException);
  210. $this->assertEquals(404, $response->getStatusCode());
  211. $this->assertStringContainsString('"message": "Not found."', $response->getContent());
  212. $logger = m::mock(LoggerInterface::class);
  213. $this->container->instance(LoggerInterface::class, $logger);
  214. $logger->shouldNotReceive('error');
  215. $this->handler->report(new RecordsNotFoundException);
  216. }
  217. }
  218. class CustomException extends Exception
  219. {
  220. }
  221. class ResponsableException extends Exception implements Responsable
  222. {
  223. public function toResponse($request)
  224. {
  225. return response()->json(['response' => 'My responsable exception response']);
  226. }
  227. }
  228. class ReportableException extends Exception
  229. {
  230. public function report(ReportingService $reportingService)
  231. {
  232. $reportingService->send($this->getMessage());
  233. }
  234. }
  235. class UnReportableException extends Exception
  236. {
  237. public function report()
  238. {
  239. return false;
  240. }
  241. }
  242. class ContextProvidingException extends Exception
  243. {
  244. public function context()
  245. {
  246. return [
  247. 'foo' => 'bar',
  248. ];
  249. }
  250. }
  251. class CustomReporter
  252. {
  253. private $service;
  254. public function __construct(ReportingService $service)
  255. {
  256. $this->service = $service;
  257. }
  258. public function __invoke(CustomException $e)
  259. {
  260. $this->service->send($e->getMessage());
  261. return false;
  262. }
  263. }
  264. class CustomRenderer
  265. {
  266. public function __invoke(CustomException $e, $request)
  267. {
  268. return response()->json(['response' => 'The CustomRenderer response']);
  269. }
  270. }
  271. interface ReportingService
  272. {
  273. public function send($message);
  274. }