'1', 'shared' => 'fromGet'], ['b' => '2', 'shared' => 'fromPost']); $this->assertSame('fromGet', $r->get('shared'), 'get() reads query only'); $this->assertSame('fromPost', $r->post('shared'), 'post() reads post only'); $this->assertSame('fromPost', $r->input('shared'), 'merged input: POST wins'); $this->assertSame('1', $r->input('a')); $this->assertSame('2', $r->input('b')); } public function testInputDefaultsHasAndAll(): void { $r = new Request(['a' => '1'], ['b' => '2']); $this->assertNull($r->input('missing')); $this->assertSame('fallback', $r->input('missing', 'fallback')); $this->assertTrue($r->has('a')); $this->assertFalse($r->has('missing')); $this->assertSame(['a' => '1', 'b' => '2'], $r->all()); } public function testGetIntCoercesValues(): void { $r = new Request([], ['n' => '42abc', 'clean' => '7']); $this->assertSame(42, $r->getInt('n')); $this->assertSame(7, $r->getInt('clean')); $this->assertSame(0, $r->getInt('missing')); $this->assertSame(99, $r->getInt('missing', 99)); } public function testGetBoolCoercesValues(): void { $r = new Request([], ['on' => 'true', 'num' => '1', 'off' => '0', 'word' => 'no']); $this->assertTrue($r->getBool('on')); $this->assertTrue($r->getBool('num')); $this->assertFalse($r->getBool('off')); $this->assertFalse($r->getBool('word')); $this->assertFalse($r->getBool('missing')); $this->assertTrue($r->getBool('missing', true)); } public function testServerAccessorsAndRequestHelpers(): void { $r = new Request([], [], [ 'REQUEST_METHOD' => 'POST', 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest', 'REQUEST_URI' => '/admin/index.php', 'HTTP_USER_AGENT' => 'UnitAgent/1.0', ], ['sid' => 'abc']); $this->assertSame('POST', $r->method()); $this->assertTrue($r->isPost()); $this->assertTrue($r->isAjax(), 'case-insensitive XMLHttpRequest'); $this->assertSame('/admin/index.php', $r->uri()); $this->assertSame('UnitAgent/1.0', $r->userAgent()); $this->assertSame('abc', $r->cookie('sid')); $this->assertNull($r->cookie('missing')); } public function testDefaultsWhenServerIsEmpty(): void { $r = new Request(); $this->assertSame('GET', $r->method()); $this->assertFalse($r->isPost()); $this->assertFalse($r->isAjax()); $this->assertSame('/', $r->uri()); $this->assertSame('', $r->userAgent()); $this->assertSame('', $r->host()); } public function testHostPrefersHttpHostThenServerName(): void { $this->assertSame('example.tv', (new Request([], [], ['HTTP_HOST' => 'example.tv', 'SERVER_NAME' => 'internal']))->host()); $this->assertSame('internal', (new Request([], [], ['SERVER_NAME' => 'internal']))->host()); } public function testClientIpTakesFirstValidForwardedForEntry(): void { $r = new Request([], [], ['HTTP_X_FORWARDED_FOR' => '203.0.113.5, 10.0.0.1', 'REMOTE_ADDR' => '10.0.0.1']); $this->assertSame('203.0.113.5', $r->ip()); } public function testClientIpSkipsInvalidHeadersAndFallsBack(): void { $withRealIp = new Request([], [], ['HTTP_X_FORWARDED_FOR' => 'not-an-ip', 'HTTP_X_REAL_IP' => '198.51.100.7']); $this->assertSame('198.51.100.7', $withRealIp->ip(), 'invalid XFF skipped, X-Real-IP used'); $none = new Request([], [], []); $this->assertSame('0.0.0.0', $none->ip(), 'no usable header → sentinel'); } // ── Sanitization (security-relevant) ───────────────────────────── public function testConstructorStripsNullBytesAndNeutralisesTraversal(): void { $r = new Request(['path' => "../secret\0.txt"]); $value = $r->input('path'); $this->assertStringNotContainsString(chr(0), $value, 'NUL byte removed'); $this->assertStringNotContainsString('../', $value, 'traversal neutralised'); $this->assertStringContainsString('../', $value); } public function testParseCleanValueNeutralisesHtmlPayloads(): void { $this->assertStringNotContainsString('alert(1)')); $this->assertStringContainsString('<script', Request::parseCleanValue(''), 'case-insensitive'); $comment = Request::parseCleanValue('ab'); $this->assertStringNotContainsString('', $comment); $this->assertSame("O'Brien", Request::parseCleanValue("O\\'Brien"), 'stripslashes'); $this->assertSame("a\nb", Request::parseCleanValue("a\r\nb"), 'CRLF normalised to LF'); $this->assertSame('trimmed', Request::parseCleanValue(' trimmed ')); $this->assertSame('', Request::parseCleanValue('')); } public function testParseCleanKeySanitises(): void { $this->assertSame('page_id', Request::parseCleanKey('page_id'), 'valid key preserved'); $this->assertSame('username', Request::parseCleanKey('user..name'), 'double-dot stripped'); $this->assertSame('', Request::parseCleanKey('__proto__'), '__x__ pattern removed'); $this->assertStringStartsWith('a<b', Request::parseCleanKey('aassertSame('', Request::parseCleanKey('')); } public function testCleanGlobalsScrubsRecursivelyInPlace(): void { $data = [ 'nul' => "a\0b", 'rtl' => 'x‮y', 'deep' => ['trav' => '../etc/passwd'], ]; Request::cleanGlobals($data); $this->assertSame('ab', $data['nul']); $this->assertSame('xy', $data['rtl'], 'RTL override removed'); $this->assertSame('../etc/passwd', $data['deep']['trav']); } }