|
@@ -0,0 +1,112 @@
|
|
|
|
|
+<?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);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|