1
0

JsonExceptionHandlerTest.php 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Tests\Unit\Http;
  4. use App\Http\JsonExceptionHandler;
  5. use Monolog\Handler\NullHandler;
  6. use Monolog\Logger;
  7. use PHPUnit\Framework\TestCase;
  8. use Slim\Psr7\Factory\ResponseFactory;
  9. use Slim\Psr7\Factory\ServerRequestFactory;
  10. use Slim\Views\Twig;
  11. /**
  12. * SEC_REVIEW F73 — `Throwable::getCode()` is typed `int` in the
  13. * interface contract, but PDO-derived exceptions return SQLSTATE
  14. * strings. The previous loose `>= 400 && < 600` comparison
  15. * coerced strings to int and could land on a real HTTP status
  16. * for the wrong reason. The handler now requires `is_int($code)`
  17. * before treating it as an HTTP status.
  18. */
  19. final class JsonExceptionHandlerTest extends TestCase
  20. {
  21. public function testStringCodeFallsBackTo500(): void
  22. {
  23. // PDOException uses a string SQLSTATE for getCode(). A
  24. // hand-rolled subclass below mimics the real-world shape:
  25. // string `'400'` would loose-compare as 400 and previously
  26. // set the response status to 400, skipping the prod
  27. // message-suppression. Now it falls back to 500.
  28. $exception = new class () extends \RuntimeException {
  29. public function __construct()
  30. {
  31. parent::__construct('SQLSTATE-style error');
  32. $this->code = '400'; // PDOException stores a string here.
  33. }
  34. };
  35. $handler = $this->makeHandler(isDev: false);
  36. $request = (new ServerRequestFactory())->createServerRequest('GET', '/x');
  37. $response = $handler($request, $exception, false, false, false);
  38. self::assertSame(500, $response->getStatusCode());
  39. }
  40. public function testIntCodeInRangeIsHonored(): void
  41. {
  42. $exception = new \RuntimeException('forbidden', 403);
  43. $handler = $this->makeHandler(isDev: false);
  44. $request = (new ServerRequestFactory())->createServerRequest('GET', '/x');
  45. $response = $handler($request, $exception, false, false, false);
  46. self::assertSame(403, $response->getStatusCode());
  47. }
  48. public function testIntCodeOutOfRangeFallsBackTo500(): void
  49. {
  50. $exception = new \RuntimeException('not http', 200);
  51. $handler = $this->makeHandler(isDev: false);
  52. $request = (new ServerRequestFactory())->createServerRequest('GET', '/x');
  53. $response = $handler($request, $exception, false, false, false);
  54. self::assertSame(500, $response->getStatusCode());
  55. }
  56. public function testCodeOfZeroFallsBackTo500(): void
  57. {
  58. // Code 0 is the default for `new \Exception('msg')` — we
  59. // must not let it accidentally land on a 400-class status.
  60. $exception = new \RuntimeException('plain');
  61. $handler = $this->makeHandler(isDev: false);
  62. $request = (new ServerRequestFactory())->createServerRequest('GET', '/x');
  63. $response = $handler($request, $exception, false, false, false);
  64. self::assertSame(500, $response->getStatusCode());
  65. }
  66. public function testSqlstateLikeStringIsNotCoercedIntoStatus(): void
  67. {
  68. // PDOException for a missing table is `'42S02'`. Loose
  69. // coerce to int gives 42 (out of HTTP range, falls back to
  70. // 500 even before the F73 fix). But the SEC_REVIEW called
  71. // out a `'400'` that would coerce neatly. Both must be
  72. // rejected by the int-only gate.
  73. $exception = new class () extends \RuntimeException {
  74. public function __construct()
  75. {
  76. parent::__construct('SQL error');
  77. $this->code = '42S02';
  78. }
  79. };
  80. $handler = $this->makeHandler(isDev: false);
  81. $request = (new ServerRequestFactory())->createServerRequest('GET', '/x');
  82. $response = $handler($request, $exception, false, false, false);
  83. self::assertSame(500, $response->getStatusCode());
  84. }
  85. private function makeHandler(bool $isDev): JsonExceptionHandler
  86. {
  87. $twig = Twig::create(__DIR__ . '/../../../resources/views', ['cache' => false]);
  88. $logger = new Logger('test');
  89. $logger->pushHandler(new NullHandler());
  90. return new JsonExceptionHandler($twig, new ResponseFactory(), $logger, $isDev);
  91. }
  92. }