RequestBodySizeLimitMiddlewareTest.php 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Tests\Unit\Http;
  4. use App\Infrastructure\Http\Middleware\RequestBodySizeLimitMiddleware;
  5. use PHPUnit\Framework\TestCase;
  6. use Psr\Http\Message\ResponseInterface;
  7. use Psr\Http\Message\ServerRequestInterface;
  8. use Psr\Http\Server\RequestHandlerInterface;
  9. use Slim\Psr7\Factory\ResponseFactory;
  10. use Slim\Psr7\Factory\ServerRequestFactory;
  11. use Slim\Psr7\Factory\StreamFactory;
  12. /**
  13. * SEC_REVIEW F69 — bound inbound request body size before Slim's
  14. * `BodyParsingMiddleware` reads it into memory. Two layers:
  15. * `Content-Length` header check (catches the well-behaved client) and
  16. * `getSize()` fallback (catches a stream that knows its length even
  17. * if the header lied).
  18. */
  19. final class RequestBodySizeLimitMiddlewareTest extends TestCase
  20. {
  21. public function testRequestUnderCapPasses(): void
  22. {
  23. $mw = new RequestBodySizeLimitMiddleware(new ResponseFactory(), 1024);
  24. $request = (new ServerRequestFactory())
  25. ->createServerRequest('POST', '/x')
  26. ->withBody((new StreamFactory())->createStream(str_repeat('a', 512)))
  27. ->withHeader('Content-Length', '512');
  28. $response = $mw->process($request, $this->okHandler());
  29. self::assertSame(200, $response->getStatusCode());
  30. }
  31. public function testRequestOverCapByContentLengthIs413(): void
  32. {
  33. $mw = new RequestBodySizeLimitMiddleware(new ResponseFactory(), 1024);
  34. $request = (new ServerRequestFactory())
  35. ->createServerRequest('POST', '/x')
  36. ->withBody((new StreamFactory())->createStream(str_repeat('a', 4096)))
  37. ->withHeader('Content-Length', '4096');
  38. $response = $mw->process($request, $this->shouldNotRunHandler());
  39. self::assertSame(413, $response->getStatusCode());
  40. $body = json_decode((string) $response->getBody(), true);
  41. self::assertSame('payload_too_large', $body['error'] ?? null);
  42. }
  43. public function testRequestOverCapByStreamSizeIs413(): void
  44. {
  45. // No Content-Length header (chunked-style request); fall back
  46. // to the stream's reported size.
  47. $mw = new RequestBodySizeLimitMiddleware(new ResponseFactory(), 1024);
  48. $request = (new ServerRequestFactory())
  49. ->createServerRequest('POST', '/x')
  50. ->withBody((new StreamFactory())->createStream(str_repeat('a', 4096)));
  51. // Strip Content-Length the factory might have set.
  52. $request = $request->withoutHeader('Content-Length');
  53. $response = $mw->process($request, $this->shouldNotRunHandler());
  54. self::assertSame(413, $response->getStatusCode());
  55. }
  56. public function testEmptyBodyPasses(): void
  57. {
  58. $mw = new RequestBodySizeLimitMiddleware(new ResponseFactory(), 1024);
  59. $request = (new ServerRequestFactory())->createServerRequest('GET', '/healthz');
  60. $response = $mw->process($request, $this->okHandler());
  61. self::assertSame(200, $response->getStatusCode());
  62. }
  63. public function testNonNumericContentLengthDoesNotShortCircuitButStreamSizeStillCaught(): void
  64. {
  65. // A garbage Content-Length is ignored (not a digit string), but
  66. // the stream-size fallback catches the actual oversize body.
  67. $mw = new RequestBodySizeLimitMiddleware(new ResponseFactory(), 1024);
  68. $body = (new StreamFactory())->createStream(str_repeat('a', 4096));
  69. $request = (new ServerRequestFactory())
  70. ->createServerRequest('POST', '/x')
  71. ->withBody($body)
  72. ->withHeader('Content-Length', 'garbage');
  73. $response = $mw->process($request, $this->shouldNotRunHandler());
  74. self::assertSame(413, $response->getStatusCode());
  75. }
  76. private function okHandler(): RequestHandlerInterface
  77. {
  78. return new class () implements RequestHandlerInterface {
  79. public function handle(ServerRequestInterface $request): ResponseInterface
  80. {
  81. return (new ResponseFactory())->createResponse(200);
  82. }
  83. };
  84. }
  85. private function shouldNotRunHandler(): RequestHandlerInterface
  86. {
  87. return new class () implements RequestHandlerInterface {
  88. public function handle(ServerRequestInterface $request): ResponseInterface
  89. {
  90. throw new \LogicException('handler must not be invoked when middleware rejects oversize');
  91. }
  92. };
  93. }
  94. }