RequestTest.php 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Tests\Http;
  4. use App\Http\Request;
  5. use App\Tests\TestCase;
  6. /**
  7. * Smoke tests that `Request::ip()` and `Request::isHttps()` actually consult
  8. * `TRUSTED_PROXIES` (R01-N05 / R01-N07). The detail-level CIDR / XFF logic
  9. * is exercised in `TrustedProxiesTest`; here we only check the wiring.
  10. */
  11. final class RequestTest extends TestCase
  12. {
  13. /**
  14. * @param array<string,mixed> $server
  15. */
  16. private function makeRequest(array $server): Request
  17. {
  18. return new Request(
  19. method: 'GET',
  20. path: '/',
  21. query: [],
  22. post: [],
  23. rawBody: '',
  24. headers: [],
  25. server: $server,
  26. );
  27. }
  28. public function testIpReturnsRemoteAddrWhenNoTrustedProxiesConfigured(): void
  29. {
  30. $prev = getenv('TRUSTED_PROXIES');
  31. try {
  32. putenv('TRUSTED_PROXIES');
  33. $req = $this->makeRequest([
  34. 'REMOTE_ADDR' => '203.0.113.42',
  35. 'HTTP_X_FORWARDED_FOR' => '198.51.100.7',
  36. ]);
  37. self::assertSame('203.0.113.42', $req->ip());
  38. } finally {
  39. $prev === false ? putenv('TRUSTED_PROXIES') : putenv('TRUSTED_PROXIES=' . $prev);
  40. }
  41. }
  42. public function testIpHonoursXffWhenRemoteIsTrusted(): void
  43. {
  44. $prev = getenv('TRUSTED_PROXIES');
  45. try {
  46. putenv('TRUSTED_PROXIES=10.0.0.0/8');
  47. $req = $this->makeRequest([
  48. 'REMOTE_ADDR' => '10.0.0.1',
  49. 'HTTP_X_FORWARDED_FOR' => '198.51.100.7',
  50. ]);
  51. self::assertSame('198.51.100.7', $req->ip());
  52. } finally {
  53. $prev === false ? putenv('TRUSTED_PROXIES') : putenv('TRUSTED_PROXIES=' . $prev);
  54. }
  55. }
  56. public function testIsHttpsHonoursXfpOnlyFromTrustedProxy(): void
  57. {
  58. $prev = getenv('TRUSTED_PROXIES');
  59. try {
  60. putenv('TRUSTED_PROXIES=10.0.0.0/8');
  61. $trustedReq = $this->makeRequest([
  62. 'REMOTE_ADDR' => '10.0.0.1',
  63. 'HTTP_X_FORWARDED_PROTO' => 'https',
  64. ]);
  65. self::assertTrue($trustedReq->isHttps());
  66. $untrustedReq = $this->makeRequest([
  67. 'REMOTE_ADDR' => '203.0.113.5',
  68. 'HTTP_X_FORWARDED_PROTO' => 'https',
  69. ]);
  70. self::assertFalse($untrustedReq->isHttps());
  71. } finally {
  72. $prev === false ? putenv('TRUSTED_PROXIES') : putenv('TRUSTED_PROXIES=' . $prev);
  73. }
  74. }
  75. public function testIsHttpsRecognisesDirectTls(): void
  76. {
  77. $req = $this->makeRequest(['HTTPS' => 'on']);
  78. self::assertTrue($req->isHttps());
  79. $req = $this->makeRequest(['HTTPS' => 'off']);
  80. self::assertFalse($req->isHttps());
  81. }
  82. // ------------------------------------------------------------------
  83. // R01-N24: body size cap
  84. // ------------------------------------------------------------------
  85. public function testMaxBodyBytesCapIsExactlyOneMebibyte(): void
  86. {
  87. // Drift fence — bumping this changes a published HTTP contract
  88. // (clients that rely on the 413 boundary). Update REVIEW_01.md
  89. // §R01-N24, public/index.php's error message, and any per-cap
  90. // operator docs alongside the value here.
  91. self::assertSame(1024 * 1024, Request::MAX_BODY_BYTES);
  92. }
  93. public function testBodyTooLargeFlagDefaultsToFalse(): void
  94. {
  95. $req = $this->makeRequest([]);
  96. self::assertFalse($req->bodyTooLarge);
  97. }
  98. public function testBodyTooLargeFlagWiresThroughConstructor(): void
  99. {
  100. // The front controller (`public/index.php`) reads this property to
  101. // emit a 413 before dispatch. A future refactor must not lose the
  102. // wiring or the cap silently disappears.
  103. $req = new Request(
  104. method: 'POST',
  105. path: '/sprints/1/week-cells',
  106. query: [],
  107. post: [],
  108. rawBody: '',
  109. headers: [],
  110. server: [],
  111. bodyTooLarge: true,
  112. );
  113. self::assertTrue($req->bodyTooLarge);
  114. }
  115. public function testJsonReturnsNullWhenBodyWasOversized(): void
  116. {
  117. // `fromGlobals()` blanks `rawBody` once it decides the request was
  118. // oversized, so the existing `json()` parser naturally returns null.
  119. // Pin that downstream-safety contract — controllers that fall back
  120. // to `?? []` continue to work; the front-controller 413 has
  121. // already replied so this branch should never run in production,
  122. // but defending against a misuse path is cheap.
  123. $req = new Request(
  124. method: 'POST',
  125. path: '/x',
  126. query: [],
  127. post: [],
  128. rawBody: '',
  129. headers: ['content-type' => 'application/json'],
  130. server: [],
  131. bodyTooLarge: true,
  132. );
  133. self::assertNull($req->json());
  134. }
  135. }