$server */ private function makeRequest(array $server): Request { return new Request( method: 'GET', path: '/', query: [], post: [], rawBody: '', headers: [], server: $server, ); } public function testIpReturnsRemoteAddrWhenNoTrustedProxiesConfigured(): void { $prev = getenv('TRUSTED_PROXIES'); try { putenv('TRUSTED_PROXIES'); $req = $this->makeRequest([ 'REMOTE_ADDR' => '203.0.113.42', 'HTTP_X_FORWARDED_FOR' => '198.51.100.7', ]); self::assertSame('203.0.113.42', $req->ip()); } finally { $prev === false ? putenv('TRUSTED_PROXIES') : putenv('TRUSTED_PROXIES=' . $prev); } } public function testIpHonoursXffWhenRemoteIsTrusted(): void { $prev = getenv('TRUSTED_PROXIES'); try { putenv('TRUSTED_PROXIES=10.0.0.0/8'); $req = $this->makeRequest([ 'REMOTE_ADDR' => '10.0.0.1', 'HTTP_X_FORWARDED_FOR' => '198.51.100.7', ]); self::assertSame('198.51.100.7', $req->ip()); } finally { $prev === false ? putenv('TRUSTED_PROXIES') : putenv('TRUSTED_PROXIES=' . $prev); } } public function testIsHttpsHonoursXfpOnlyFromTrustedProxy(): void { $prev = getenv('TRUSTED_PROXIES'); try { putenv('TRUSTED_PROXIES=10.0.0.0/8'); $trustedReq = $this->makeRequest([ 'REMOTE_ADDR' => '10.0.0.1', 'HTTP_X_FORWARDED_PROTO' => 'https', ]); self::assertTrue($trustedReq->isHttps()); $untrustedReq = $this->makeRequest([ 'REMOTE_ADDR' => '203.0.113.5', 'HTTP_X_FORWARDED_PROTO' => 'https', ]); self::assertFalse($untrustedReq->isHttps()); } finally { $prev === false ? putenv('TRUSTED_PROXIES') : putenv('TRUSTED_PROXIES=' . $prev); } } public function testIsHttpsRecognisesDirectTls(): void { $req = $this->makeRequest(['HTTPS' => 'on']); self::assertTrue($req->isHttps()); $req = $this->makeRequest(['HTTPS' => 'off']); self::assertFalse($req->isHttps()); } // ------------------------------------------------------------------ // R01-N24: body size cap // ------------------------------------------------------------------ public function testMaxBodyBytesCapIsExactlyOneMebibyte(): void { // Drift fence — bumping this changes a published HTTP contract // (clients that rely on the 413 boundary). Update REVIEW_01.md // §R01-N24, public/index.php's error message, and any per-cap // operator docs alongside the value here. self::assertSame(1024 * 1024, Request::MAX_BODY_BYTES); } public function testBodyTooLargeFlagDefaultsToFalse(): void { $req = $this->makeRequest([]); self::assertFalse($req->bodyTooLarge); } public function testBodyTooLargeFlagWiresThroughConstructor(): void { // The front controller (`public/index.php`) reads this property to // emit a 413 before dispatch. A future refactor must not lose the // wiring or the cap silently disappears. $req = new Request( method: 'POST', path: '/sprints/1/week-cells', query: [], post: [], rawBody: '', headers: [], server: [], bodyTooLarge: true, ); self::assertTrue($req->bodyTooLarge); } public function testJsonReturnsNullWhenBodyWasOversized(): void { // `fromGlobals()` blanks `rawBody` once it decides the request was // oversized, so the existing `json()` parser naturally returns null. // Pin that downstream-safety contract — controllers that fall back // to `?? []` continue to work; the front-controller 413 has // already replied so this branch should never run in production, // but defending against a misuse path is cheap. $req = new Request( method: 'POST', path: '/x', query: [], post: [], rawBody: '', headers: ['content-type' => 'application/json'], server: [], bodyTooLarge: true, ); self::assertNull($req->json()); } }