| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112 |
- <?php
- declare(strict_types=1);
- namespace App\Tests\Unit\Http;
- use App\Http\JsonExceptionHandler;
- use Monolog\Handler\NullHandler;
- use Monolog\Logger;
- use PHPUnit\Framework\TestCase;
- use Slim\Psr7\Factory\ResponseFactory;
- use Slim\Psr7\Factory\ServerRequestFactory;
- use Slim\Views\Twig;
- /**
- * SEC_REVIEW F73 — `Throwable::getCode()` is typed `int` in the
- * interface contract, but PDO-derived exceptions return SQLSTATE
- * strings. The previous loose `>= 400 && < 600` comparison
- * coerced strings to int and could land on a real HTTP status
- * for the wrong reason. The handler now requires `is_int($code)`
- * before treating it as an HTTP status.
- */
- final class JsonExceptionHandlerTest extends TestCase
- {
- public function testStringCodeFallsBackTo500(): void
- {
- // PDOException uses a string SQLSTATE for getCode(). A
- // hand-rolled subclass below mimics the real-world shape:
- // string `'400'` would loose-compare as 400 and previously
- // set the response status to 400, skipping the prod
- // message-suppression. Now it falls back to 500.
- $exception = new class () extends \RuntimeException {
- public function __construct()
- {
- parent::__construct('SQLSTATE-style error');
- $this->code = '400'; // PDOException stores a string here.
- }
- };
- $handler = $this->makeHandler(isDev: false);
- $request = (new ServerRequestFactory())->createServerRequest('GET', '/x');
- $response = $handler($request, $exception, false, false, false);
- self::assertSame(500, $response->getStatusCode());
- }
- public function testIntCodeInRangeIsHonored(): void
- {
- $exception = new \RuntimeException('forbidden', 403);
- $handler = $this->makeHandler(isDev: false);
- $request = (new ServerRequestFactory())->createServerRequest('GET', '/x');
- $response = $handler($request, $exception, false, false, false);
- self::assertSame(403, $response->getStatusCode());
- }
- public function testIntCodeOutOfRangeFallsBackTo500(): void
- {
- $exception = new \RuntimeException('not http', 200);
- $handler = $this->makeHandler(isDev: false);
- $request = (new ServerRequestFactory())->createServerRequest('GET', '/x');
- $response = $handler($request, $exception, false, false, false);
- self::assertSame(500, $response->getStatusCode());
- }
- public function testCodeOfZeroFallsBackTo500(): void
- {
- // Code 0 is the default for `new \Exception('msg')` — we
- // must not let it accidentally land on a 400-class status.
- $exception = new \RuntimeException('plain');
- $handler = $this->makeHandler(isDev: false);
- $request = (new ServerRequestFactory())->createServerRequest('GET', '/x');
- $response = $handler($request, $exception, false, false, false);
- self::assertSame(500, $response->getStatusCode());
- }
- public function testSqlstateLikeStringIsNotCoercedIntoStatus(): void
- {
- // PDOException for a missing table is `'42S02'`. Loose
- // coerce to int gives 42 (out of HTTP range, falls back to
- // 500 even before the F73 fix). But the SEC_REVIEW called
- // out a `'400'` that would coerce neatly. Both must be
- // rejected by the int-only gate.
- $exception = new class () extends \RuntimeException {
- public function __construct()
- {
- parent::__construct('SQL error');
- $this->code = '42S02';
- }
- };
- $handler = $this->makeHandler(isDev: false);
- $request = (new ServerRequestFactory())->createServerRequest('GET', '/x');
- $response = $handler($request, $exception, false, false, false);
- self::assertSame(500, $response->getStatusCode());
- }
- private function makeHandler(bool $isDev): JsonExceptionHandler
- {
- $twig = Twig::create(__DIR__ . '/../../../resources/views', ['cache' => false]);
- $logger = new Logger('test');
- $logger->pushHandler(new NullHandler());
- return new JsonExceptionHandler($twig, new ResponseFactory(), $logger, $isDev);
- }
- }
|