|
@@ -0,0 +1,114 @@
|
|
|
|
|
+<?php
|
|
|
|
|
+
|
|
|
|
|
+declare(strict_types=1);
|
|
|
|
|
+
|
|
|
|
|
+namespace App\Tests\Unit\Http;
|
|
|
|
|
+
|
|
|
|
|
+use App\Infrastructure\Http\Middleware\RequestBodySizeLimitMiddleware;
|
|
|
|
|
+use PHPUnit\Framework\TestCase;
|
|
|
|
|
+use Psr\Http\Message\ResponseInterface;
|
|
|
|
|
+use Psr\Http\Message\ServerRequestInterface;
|
|
|
|
|
+use Psr\Http\Server\RequestHandlerInterface;
|
|
|
|
|
+use Slim\Psr7\Factory\ResponseFactory;
|
|
|
|
|
+use Slim\Psr7\Factory\ServerRequestFactory;
|
|
|
|
|
+use Slim\Psr7\Factory\StreamFactory;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * SEC_REVIEW F69 — bound inbound request body size before Slim's
|
|
|
|
|
+ * `BodyParsingMiddleware` reads it into memory. Two layers:
|
|
|
|
|
+ * `Content-Length` header check (catches the well-behaved client) and
|
|
|
|
|
+ * `getSize()` fallback (catches a stream that knows its length even
|
|
|
|
|
+ * if the header lied).
|
|
|
|
|
+ */
|
|
|
|
|
+final class RequestBodySizeLimitMiddlewareTest extends TestCase
|
|
|
|
|
+{
|
|
|
|
|
+ public function testRequestUnderCapPasses(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $mw = new RequestBodySizeLimitMiddleware(new ResponseFactory(), 1024);
|
|
|
|
|
+ $request = (new ServerRequestFactory())
|
|
|
|
|
+ ->createServerRequest('POST', '/x')
|
|
|
|
|
+ ->withBody((new StreamFactory())->createStream(str_repeat('a', 512)))
|
|
|
|
|
+ ->withHeader('Content-Length', '512');
|
|
|
|
|
+
|
|
|
|
|
+ $response = $mw->process($request, $this->okHandler());
|
|
|
|
|
+
|
|
|
|
|
+ self::assertSame(200, $response->getStatusCode());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function testRequestOverCapByContentLengthIs413(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $mw = new RequestBodySizeLimitMiddleware(new ResponseFactory(), 1024);
|
|
|
|
|
+ $request = (new ServerRequestFactory())
|
|
|
|
|
+ ->createServerRequest('POST', '/x')
|
|
|
|
|
+ ->withBody((new StreamFactory())->createStream(str_repeat('a', 4096)))
|
|
|
|
|
+ ->withHeader('Content-Length', '4096');
|
|
|
|
|
+
|
|
|
|
|
+ $response = $mw->process($request, $this->shouldNotRunHandler());
|
|
|
|
|
+
|
|
|
|
|
+ self::assertSame(413, $response->getStatusCode());
|
|
|
|
|
+ $body = json_decode((string) $response->getBody(), true);
|
|
|
|
|
+ self::assertSame('payload_too_large', $body['error'] ?? null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function testRequestOverCapByStreamSizeIs413(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ // No Content-Length header (chunked-style request); fall back
|
|
|
|
|
+ // to the stream's reported size.
|
|
|
|
|
+ $mw = new RequestBodySizeLimitMiddleware(new ResponseFactory(), 1024);
|
|
|
|
|
+ $request = (new ServerRequestFactory())
|
|
|
|
|
+ ->createServerRequest('POST', '/x')
|
|
|
|
|
+ ->withBody((new StreamFactory())->createStream(str_repeat('a', 4096)));
|
|
|
|
|
+ // Strip Content-Length the factory might have set.
|
|
|
|
|
+ $request = $request->withoutHeader('Content-Length');
|
|
|
|
|
+
|
|
|
|
|
+ $response = $mw->process($request, $this->shouldNotRunHandler());
|
|
|
|
|
+
|
|
|
|
|
+ self::assertSame(413, $response->getStatusCode());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function testEmptyBodyPasses(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $mw = new RequestBodySizeLimitMiddleware(new ResponseFactory(), 1024);
|
|
|
|
|
+ $request = (new ServerRequestFactory())->createServerRequest('GET', '/healthz');
|
|
|
|
|
+
|
|
|
|
|
+ $response = $mw->process($request, $this->okHandler());
|
|
|
|
|
+
|
|
|
|
|
+ self::assertSame(200, $response->getStatusCode());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function testNonNumericContentLengthDoesNotShortCircuitButStreamSizeStillCaught(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ // A garbage Content-Length is ignored (not a digit string), but
|
|
|
|
|
+ // the stream-size fallback catches the actual oversize body.
|
|
|
|
|
+ $mw = new RequestBodySizeLimitMiddleware(new ResponseFactory(), 1024);
|
|
|
|
|
+ $body = (new StreamFactory())->createStream(str_repeat('a', 4096));
|
|
|
|
|
+ $request = (new ServerRequestFactory())
|
|
|
|
|
+ ->createServerRequest('POST', '/x')
|
|
|
|
|
+ ->withBody($body)
|
|
|
|
|
+ ->withHeader('Content-Length', 'garbage');
|
|
|
|
|
+
|
|
|
|
|
+ $response = $mw->process($request, $this->shouldNotRunHandler());
|
|
|
|
|
+
|
|
|
|
|
+ self::assertSame(413, $response->getStatusCode());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function okHandler(): RequestHandlerInterface
|
|
|
|
|
+ {
|
|
|
|
|
+ return new class () implements RequestHandlerInterface {
|
|
|
|
|
+ public function handle(ServerRequestInterface $request): ResponseInterface
|
|
|
|
|
+ {
|
|
|
|
|
+ return (new ResponseFactory())->createResponse(200);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function shouldNotRunHandler(): RequestHandlerInterface
|
|
|
|
|
+ {
|
|
|
|
|
+ return new class () implements RequestHandlerInterface {
|
|
|
|
|
+ public function handle(ServerRequestInterface $request): ResponseInterface
|
|
|
|
|
+ {
|
|
|
|
|
+ throw new \LogicException('handler must not be invoked when middleware rejects oversize');
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+}
|