538 lines
20 KiB
PHP
538 lines
20 KiB
PHP
<?php
|
|
// HttpUriTest.php
|
|
// Created: 2025-02-28
|
|
// Updated: 2025-03-08
|
|
|
|
declare(strict_types=1);
|
|
|
|
use Index\Http\{HttpRequest,HttpUri};
|
|
use PHPUnit\Framework\TestCase;
|
|
use PHPUnit\Framework\Attributes\{CoversClass,DataProvider};
|
|
use Psr\Http\Message\UriInterface;
|
|
|
|
// based on https://github.com/bakame-php/psr7-uri-interface-tests/blob/5a556fdfe668a6c6a14772efeba6134c0b7dae34/tests/AbstractUriTestCase.php
|
|
#[CoversClass(HttpRequest::class)]
|
|
#[CoversClass(HttpUri::class)]
|
|
final class HttpUriTest extends TestCase {
|
|
private const string URI = 'http://username:pwd@secure.example.com:443/meow/soap.php?soup=beans#mewow';
|
|
|
|
/** @return string[][] */
|
|
public static function schemeProvider(): array {
|
|
return [
|
|
['HtTpS', 'https'], // normalized scheme
|
|
['http', 'http'], // simple scheme
|
|
['', ''], // no scheme
|
|
];
|
|
}
|
|
|
|
#[DataProvider('schemeProvider')]
|
|
public function testGetScheme(string $scheme, string $expected): void {
|
|
$uri = HttpUri::createUri(self::URI)->withScheme($scheme);
|
|
$this->assertInstanceOf(UriInterface::class, $uri);
|
|
$this->assertSame($expected, $uri->getScheme());
|
|
$this->assertInstanceOf(HttpUri::class, $uri);
|
|
$this->assertSame($expected, $uri->scheme);
|
|
}
|
|
|
|
/** @return array{string, ?string, string}[] */
|
|
public static function userInfoProvider(): array {
|
|
return [
|
|
['FreakyFurball', 'Express1', 'FreakyFurball:Express1'], // with userinfo
|
|
['', '', ''], // no userinfo
|
|
['Unko', '', 'Unko:'], // no pass
|
|
['flash', null, 'flash'], // pass is null
|
|
['Reemo', 'BuddyMan5', 'Reemo:BuddyMan5'], // case sensitive
|
|
];
|
|
}
|
|
|
|
#[DataProvider('userInfoProvider')]
|
|
public function testGetUserInfo(string $user, ?string $pass, string $expected): void {
|
|
$uri = HttpUri::createUri(self::URI)->withUserInfo($user, $pass);
|
|
$this->assertInstanceOf(UriInterface::class, $uri);
|
|
$this->assertSame($expected, $uri->getUserInfo());
|
|
$this->assertInstanceOf(HttpUri::class, $uri);
|
|
$this->assertSame($expected, $uri->userInfo);
|
|
}
|
|
|
|
/** @return string[][] */
|
|
public static function hostProvider(): array {
|
|
return [
|
|
['MaStEr.eXaMpLe.CoM', 'master.example.com'], // normalized host
|
|
['www.example.com', 'www.example.com'], // simple host
|
|
['[::1]', '[::1]'], // IPv6 Host
|
|
];
|
|
}
|
|
|
|
#[DataProvider('hostProvider')]
|
|
public function testGetHost(string $host, string $expected): void {
|
|
$uri = HttpUri::createUri(self::URI)->withHost($host);
|
|
$this->assertInstanceOf(UriInterface::class, $uri);
|
|
$this->assertSame($expected, $uri->getHost());
|
|
$this->assertInstanceOf(HttpUri::class, $uri);
|
|
$this->assertSame($expected, $uri->host);
|
|
}
|
|
|
|
/** @return array{string, ?int, ?int}[] */
|
|
public static function portProvider(): array {
|
|
return [
|
|
['http://www.example.com', 443, 443], // non standard port for http
|
|
['http://www.example.com', null, null], // remove port
|
|
['//www.example.com', 80, 80], // standard port on schemeless http url
|
|
];
|
|
}
|
|
|
|
#[DataProvider('portProvider')]
|
|
public function testGetPort(string $uri, ?int $port, ?int $expected): void {
|
|
$uri = HttpUri::createUri($uri)->withPort($port);
|
|
$this->assertInstanceOf(UriInterface::class, $uri);
|
|
$this->assertSame($expected, $uri->getPort());
|
|
$this->assertInstanceOf(HttpUri::class, $uri);
|
|
$this->assertSame($expected, $uri->port);
|
|
}
|
|
|
|
/** @return array{scheme: string, user: string, pass: ?string, host: string, port: ?int, expected: string}[] */
|
|
public static function authorityProvider(): array {
|
|
return [
|
|
[
|
|
'scheme' => 'http',
|
|
'user' => 'FreakyFurball',
|
|
'pass' => 'Express1',
|
|
'host' => 'example.com',
|
|
'port' => 443,
|
|
'expected' => 'FreakyFurball:Express1@example.com:443',
|
|
],
|
|
[
|
|
'scheme' => 'http',
|
|
'user' => 'Reemo',
|
|
'pass' => 'BuddyMan5',
|
|
'host' => 'example.com',
|
|
'port' => null,
|
|
'expected' => 'Reemo:BuddyMan5@example.com',
|
|
],
|
|
[
|
|
'scheme' => 'http',
|
|
'user' => 'Unko',
|
|
'pass' => 'SoapSoapSoapSoapSoap',
|
|
'host' => 'example.com',
|
|
'port' => 80,
|
|
'expected' => 'Unko:SoapSoapSoapSoapSoap@example.com:80',
|
|
],
|
|
[
|
|
'scheme' => 'http',
|
|
'user' => 'flash',
|
|
'pass' => '',
|
|
'host' => 'example.com',
|
|
'port' => null,
|
|
'expected' => 'flash:@example.com',
|
|
],
|
|
[
|
|
'scheme' => 'http',
|
|
'user' => 'Satori',
|
|
'pass' => null,
|
|
'host' => 'example.com',
|
|
'port' => null,
|
|
'expected' => 'Satori@example.com',
|
|
],
|
|
[
|
|
'scheme' => 'http',
|
|
'user' => '',
|
|
'pass' => '',
|
|
'host' => 'example.com',
|
|
'port' => null,
|
|
'expected' => 'example.com',
|
|
],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('authorityProvider')]
|
|
public function testGetAuthority(string $scheme, string $user, ?string $pass, string $host, ?int $port, string $expected): void {
|
|
$uri = HttpUri::createUri()->withHost($host)->withScheme($scheme)->withUserInfo($user, $pass)->withPort($port);
|
|
$this->assertInstanceOf(UriInterface::class, $uri);
|
|
$this->assertSame($expected, $uri->getAuthority());
|
|
$this->assertInstanceOf(HttpUri::class, $uri);
|
|
$this->assertSame($expected, $uri->authority);
|
|
}
|
|
|
|
/** @return string[][] */
|
|
public static function queryProvider(): array {
|
|
return [
|
|
['foo.bar=%7evalue', 'foo.bar=%7evalue'], // normalized query
|
|
['', ''], // empty query
|
|
['foo.bar=1&foo.bar=1', 'foo.bar=1&foo.bar=1'], // same param query
|
|
['?foo=', '?foo='], // same param query, may include ?
|
|
];
|
|
}
|
|
|
|
#[DataProvider('queryProvider')]
|
|
public function testGetQuery(string $query, string $expected): void {
|
|
$uri = HttpUri::createUri(self::URI)->withQuery($query);
|
|
$this->assertInstanceOf(UriInterface::class, $uri);
|
|
$this->assertSame($expected, $uri->getQuery());
|
|
$this->assertInstanceOf(HttpUri::class, $uri);
|
|
$this->assertSame($expected, $uri->query);
|
|
}
|
|
|
|
/** @return string[][] */
|
|
public static function fragmentProvider(): array {
|
|
return [
|
|
['fragment', 'fragment'], // all components
|
|
['azAZ0-9/?-._~!$&\'()*+,;=:@', 'azAZ0-9/?-._~!$&\'()*+,;=:@'], // non-encodable
|
|
];
|
|
}
|
|
|
|
#[DataProvider('fragmentProvider')]
|
|
public function testGetFragment(string $fragment, string $expected): void {
|
|
$uri = HttpUri::createUri(self::URI)->withFragment($fragment);
|
|
$this->assertInstanceOf(UriInterface::class, $uri);
|
|
$this->assertSame($expected, $uri->getFragment());
|
|
$this->assertInstanceOf(HttpUri::class, $uri);
|
|
$this->assertSame($expected, $uri->fragment);
|
|
}
|
|
|
|
/** @return array{scheme: string, user: string, pass: ?string, host: string, port: ?int, path: string, query: string, fragment: string, expected: string}[] */
|
|
public static function stringProvider(): array {
|
|
return [
|
|
[
|
|
'scheme' => 'HtTps',
|
|
'user' => 'FreakyFurball',
|
|
'pass' => 'Express1',
|
|
'host' => 'MeWow.eXaMpLe.CoM',
|
|
'port' => 443,
|
|
'path' => '/%7ejanedoe/%a1/index.php',
|
|
'query' => 'foo.bar=%7evalue',
|
|
'fragment' => 'fragment',
|
|
'expected' => 'https://FreakyFurball:Express1@mewow.example.com:443/%7ejanedoe/%a1/index.php?foo.bar=%7evalue#fragment'
|
|
],
|
|
[
|
|
'scheme' => '',
|
|
'user' => '',
|
|
'pass' => '',
|
|
'host' => 'www.example.com',
|
|
'port' => 443,
|
|
'path' => '/foo/bar',
|
|
'query' => 'param=value',
|
|
'fragment' => 'fragment',
|
|
'expected' => '//www.example.com:443/foo/bar?param=value#fragment',
|
|
],
|
|
[
|
|
'scheme' => '',
|
|
'user' => '',
|
|
'pass' => '',
|
|
'host' => '',
|
|
'port' => null,
|
|
'path' => 'foo/bar',
|
|
'query' => '',
|
|
'fragment' => '',
|
|
'expected' => 'foo/bar',
|
|
],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('stringProvider')]
|
|
public function testToString(
|
|
string $scheme,
|
|
string $user,
|
|
string $pass,
|
|
string $host,
|
|
?int $port,
|
|
string $path,
|
|
string $query,
|
|
string $fragment,
|
|
string $expected
|
|
): void {
|
|
$uri = HttpUri::createUri()->withHost($host)->withScheme($scheme)
|
|
->withUserInfo($user, $pass)->withPort($port)
|
|
->withPath($path)->withQuery($query)->withFragment($fragment);
|
|
$this->assertInstanceOf(UriInterface::class, $uri);
|
|
$this->assertInstanceOf(Stringable::class, $uri); // HttpUri also implements this
|
|
$this->assertSame($expected, (string)$uri);
|
|
}
|
|
|
|
public function testRemoveScheme(): void {
|
|
$this->assertSame(
|
|
'//example.com/this/is/a/path',
|
|
(string)HttpUri::createUri('http://example.com/this/is/a/path')->withScheme('')
|
|
);
|
|
}
|
|
|
|
public function testRemoveAuthority(): void {
|
|
$uri = HttpUri::createUri('http://user:login@example.com:82/path?q=v#doc')
|
|
->withScheme('')->withUserInfo('')->withPort(null)->withHost('');
|
|
|
|
$this->assertSame('/path?q=v#doc', (string)$uri);
|
|
}
|
|
|
|
public function testRemoveUserInfo(): void {
|
|
$this->assertSame(
|
|
'http://example.com/this/is/a/path',
|
|
(string)HttpUri::createUri('http://user:pass@example.com/this/is/a/path')->withUserInfo('')
|
|
);
|
|
}
|
|
|
|
public function testRemovePort(): void {
|
|
$this->assertSame(
|
|
'http://example.com/this/is/a/path',
|
|
(string)HttpUri::createUri('http://example.com:9001/this/is/a/path')->withPort(null)
|
|
);
|
|
}
|
|
|
|
public function testRemovePath(): void {
|
|
$uri = 'http://example.com';
|
|
$this->assertSame($uri, (string)HttpUri::createUri($uri . '/this/is/a/path')->withPath(''));
|
|
}
|
|
|
|
public function testRemoveQuery(): void {
|
|
$uri = 'http://example.com/this/is/a/path';
|
|
$this->assertSame($uri, (string)HttpUri::createUri($uri . '?name=value')->withQuery(''));
|
|
}
|
|
|
|
public function testRemoveFragment(): void {
|
|
$uri = 'http://example.com/this/is/a/path';
|
|
$this->assertSame($uri, (string)HttpUri::createUri($uri . '#soap')->withFragment(''));
|
|
}
|
|
|
|
/** @return string[][] */
|
|
public static function withSchemeFailProvider(): array {
|
|
return [
|
|
['un,acceptable'], // unacceptable char
|
|
['123'], // number string
|
|
];
|
|
}
|
|
|
|
#[DataProvider('withSchemeFailProvider')]
|
|
public function testWithSchemeFail(string $scheme): void {
|
|
$this->expectException(InvalidArgumentException::class);
|
|
HttpUri::createUri(self::URI)->withScheme($scheme);
|
|
}
|
|
|
|
/** @return string[][] */
|
|
public static function withUserInfoFailProvider(): array {
|
|
return [
|
|
['mew:ow', 'Soap'],
|
|
['be@ns', 'Soup'],
|
|
['Meow', 'ok@y'],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('withUserInfoFailProvider')]
|
|
public function testWithUserInfoFail(string $user, ?string $pass): void {
|
|
$this->expectException(InvalidArgumentException::class);
|
|
HttpUri::createUri(self::URI)->withUserInfo($user, $pass);
|
|
}
|
|
|
|
/** @return string[][] */
|
|
public static function withHostFailProvider(): array {
|
|
return [
|
|
['.example.com'], // dot in front
|
|
['host.com-'], // hyphen suffix
|
|
['.......'], // multiple dot
|
|
['.'], // one dot
|
|
['tot. .coucou.com'], // empty label
|
|
['re view'], // space in the label
|
|
['_bad.host.com'], // underscore in label
|
|
[implode('', array_fill(0, 12, 'banana')).'.secure.example.com'], // label too long
|
|
[implode('.', array_fill(0, 128, 'a'))], // too many labels
|
|
['[127.0.0.1]'], // Invalid IPv4 format
|
|
['[[::1]]'], // Invalid IPv6 format
|
|
['[::1'], // Invalid IPv6 format 2
|
|
['example. com'], // space character in starting label
|
|
["examp\0le.com"], // invalid character in host label
|
|
['[127.2.0.1%253]'], // invalid IP with scope
|
|
['ab23::1234%251'], // invalid scope IPv6
|
|
['fe80::1234%25?@'], // invalid scope ID
|
|
['fe80::1234%25€'], // invalid scope ID with utf8 character
|
|
];
|
|
}
|
|
|
|
#[DataProvider('withHostFailProvider')]
|
|
public function testWithHostFail(string $host): void {
|
|
$this->expectException(InvalidArgumentException::class);
|
|
HttpUri::createUri(self::URI)->withHost($host);
|
|
}
|
|
|
|
public function testWithPathFailWithInvalidPathRelativeToAuthority(): void {
|
|
$this->expectException(InvalidArgumentException::class);
|
|
HttpUri::createUri('https://example.com')->withPath('me/ow');
|
|
}
|
|
|
|
public function testWithPathFailWithInvalidChars(): void {
|
|
$this->expectException(InvalidArgumentException::class);
|
|
HttpUri::createUri('https://example.com')->withPath('/?');
|
|
}
|
|
|
|
public function testWithQueryFailWithInvalidChars(): void {
|
|
$this->expectException(InvalidArgumentException::class);
|
|
HttpUri::createUri('https://example.com')->withQuery('#');
|
|
}
|
|
|
|
public function testWithPortFailWithTooLowPort(): void {
|
|
$this->expectException(InvalidArgumentException::class);
|
|
HttpUri::createUri('https://example.com')->withPort(0);
|
|
}
|
|
|
|
public function testWithPortFailWithTooHighPort(): void {
|
|
$this->expectException(InvalidArgumentException::class);
|
|
HttpUri::createUri('https://example.com')->withPort(0x10000);
|
|
}
|
|
|
|
public function testWithHostFailWithInvalidHost(): void {
|
|
$this->expectException(InvalidArgumentException::class);
|
|
HttpUri::createUri('https://example.com')->withHost('?');
|
|
}
|
|
|
|
/** @return string[][] */
|
|
public static function invalidUriProvider(): array {
|
|
return [
|
|
['https://user@:443'],
|
|
[':'],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('invalidUriProvider')]
|
|
public function testCreateUriWithInvalidUri(string $uri): void {
|
|
$this->expectException(InvalidArgumentException::class);
|
|
HttpUri::createUri($uri);
|
|
}
|
|
|
|
public function testZeroValues(): void {
|
|
$expected = '//0:0@host/0?0#0';
|
|
$this->assertSame($expected, (string)HttpUri::createUri($expected));
|
|
}
|
|
|
|
public function testPathDetection(): void {
|
|
$expected = 'foo/bar:';
|
|
$this->assertSame($expected, HttpUri::createUri($expected)->path);
|
|
}
|
|
|
|
/** @return array<array{0: string, 1: array<string, list<?string>>}> */
|
|
public static function queryStringProvider(): array {
|
|
return [
|
|
[
|
|
'username=meow&password=beans',
|
|
['username' => ['meow'], 'password' => ['beans']]
|
|
],
|
|
[
|
|
'username=meow&password=beans&password=soap',
|
|
['username' => ['meow'], 'password' => ['beans', 'soap']]
|
|
],
|
|
[
|
|
'arg&arg&arg=maybe&arg&the=ok',
|
|
['arg' => [null, null, 'maybe', null], 'the' => ['ok']]
|
|
],
|
|
[
|
|
'array[]=old&array%5B%5D=syntax&array[meow]=soup',
|
|
['array[]' => ['old', 'syntax'], 'array[meow]' => ['soup']]
|
|
],
|
|
[
|
|
'plus=this+one+uses+plus+as+space&twenty=this%20uses%20percent%20encoding',
|
|
['plus' => ['this one uses plus as space'], 'twenty' => ['this uses percent encoding']]
|
|
],
|
|
[
|
|
'&&=&&=&', // there's no reason why this shouldn't be valid but it is quirky!
|
|
['' => [null, null, '', null, '', null]]
|
|
],
|
|
[
|
|
'',
|
|
[]
|
|
],
|
|
[
|
|
' ',
|
|
[' ' => [null]]
|
|
],
|
|
];
|
|
}
|
|
|
|
/** @param array<string, string[]> $expected */
|
|
#[DataProvider('queryStringProvider')]
|
|
public function testParseQueryString(string $queryString, array $expected): void {
|
|
$this->assertEquals(HttpUri::parseQueryString($queryString), $expected);
|
|
}
|
|
|
|
/** @return array<array{0: array<string, Stringable|scalar|null|list<Stringable|scalar|null>>, 1: string}> */
|
|
public static function queryParamsProvider(): array {
|
|
return [
|
|
[
|
|
['username' => ['meow'], 'password' => ['beans']],
|
|
'username=meow&password=beans'
|
|
],
|
|
[
|
|
['username' => ['meow'], 'password' => ['beans', 'soap']],
|
|
'username=meow&password=beans&password=soap'
|
|
],
|
|
[
|
|
['arg' => [null, null, 'maybe', null], 'the' => ['ok']],
|
|
'arg&arg&arg=maybe&arg&the=ok'
|
|
],
|
|
[
|
|
['array[]' => ['old', 'syntax'], 'array[meow]' => ['soup']],
|
|
'array%5B%5D=old&array%5B%5D=syntax&array%5Bmeow%5D=soup'
|
|
],
|
|
[
|
|
['twenty' => ['this one always uses percent'], 'twenty only' => ['this uses percent encoding']],
|
|
'twenty=this%20one%20always%20uses%20percent&twenty%20only=this%20uses%20percent%20encoding'
|
|
],
|
|
[
|
|
['' => [null, null, '', null, '', null]],
|
|
'&&=&&=&' // there's no reason why this shouldn't be valid but it is quirky!
|
|
],
|
|
[
|
|
[],
|
|
''
|
|
],
|
|
[
|
|
[' ' => [null]],
|
|
'%20'
|
|
],
|
|
[ // scalar types
|
|
['null' => [null], 'int' => [1234], 'float' => [56.78], 'bool' => [true, false], 'string' => ['why not ig']],
|
|
'null&int=1234&float=56.78&bool=1&bool=&string=why%20not%20ig'
|
|
],
|
|
[ // stringable
|
|
['stringable' => [new class implements Stringable { public function __toString(): string { return 'a'; } }]],
|
|
'stringable=a'
|
|
],
|
|
[ // accept non-array
|
|
['null' => null, 'int' => 1234, 'float' => 56.78, 'bool' => true, 'string' => 'why not ig'],
|
|
'null&int=1234&float=56.78&bool=1&string=why%20not%20ig'
|
|
],
|
|
];
|
|
}
|
|
|
|
/** @param array<string, list<Stringable|scalar|null>> $queryParams */
|
|
#[DataProvider('queryParamsProvider')]
|
|
public function testBuildQueryString(array $queryParams, string $expected): void {
|
|
$this->assertEquals(HttpUri::buildQueryString($queryParams), $expected);
|
|
}
|
|
|
|
/** @return array<array{0: string, 1: array<string, string>}> */
|
|
public static function cookieStringProvider(): array {
|
|
return [
|
|
[
|
|
'',
|
|
[]
|
|
],
|
|
[
|
|
'soup=meow',
|
|
['soup' => 'meow']
|
|
],
|
|
[
|
|
'soup=meow;the=the1; the2=the3; the4=the5;',
|
|
['soup' => 'meow', 'the' => 'the1', 'the2' => 'the3', 'the4' => 'the5']
|
|
],
|
|
[
|
|
'test%5B%5D=%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82%E3%81%82',
|
|
['test[]' => 'あああああああ']
|
|
],
|
|
[
|
|
' =empty',
|
|
['' => 'empty']
|
|
],
|
|
];
|
|
}
|
|
|
|
/** @param array<string, string> $expected */
|
|
#[DataProvider('cookieStringProvider')]
|
|
public function testParseCookieString(string $cookieString, array $expected): void {
|
|
$this->assertEquals(HttpRequest::parseCookieString($cookieString), $expected);
|
|
}
|
|
}
|