UriTest.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722
  1. <?php
  2. declare(strict_types=1);
  3. namespace GuzzleHttp\Tests\Psr7;
  4. use GuzzleHttp\Psr7\Exception\MalformedUriException;
  5. use GuzzleHttp\Psr7\Uri;
  6. use PHPUnit\Framework\TestCase;
  7. use Psr\Http\Message\UriInterface;
  8. /**
  9. * @covers \GuzzleHttp\Psr7\Uri
  10. */
  11. class UriTest extends TestCase
  12. {
  13. public function testParsesProvidedUri(): void
  14. {
  15. $uri = new Uri('https://user:pass@example.com:8080/path/123?q=abc#test');
  16. self::assertSame('https', $uri->getScheme());
  17. self::assertSame('user:pass@example.com:8080', $uri->getAuthority());
  18. self::assertSame('user:pass', $uri->getUserInfo());
  19. self::assertSame('example.com', $uri->getHost());
  20. self::assertSame(8080, $uri->getPort());
  21. self::assertSame('/path/123', $uri->getPath());
  22. self::assertSame('q=abc', $uri->getQuery());
  23. self::assertSame('test', $uri->getFragment());
  24. self::assertSame('https://user:pass@example.com:8080/path/123?q=abc#test', (string) $uri);
  25. }
  26. public function testCanTransformAndRetrievePartsIndividually(): void
  27. {
  28. $uri = (new Uri())
  29. ->withScheme('https')
  30. ->withUserInfo('user', 'pass')
  31. ->withHost('example.com')
  32. ->withPort(8080)
  33. ->withPath('/path/123')
  34. ->withQuery('q=abc')
  35. ->withFragment('test');
  36. self::assertSame('https', $uri->getScheme());
  37. self::assertSame('user:pass@example.com:8080', $uri->getAuthority());
  38. self::assertSame('user:pass', $uri->getUserInfo());
  39. self::assertSame('example.com', $uri->getHost());
  40. self::assertSame(8080, $uri->getPort());
  41. self::assertSame('/path/123', $uri->getPath());
  42. self::assertSame('q=abc', $uri->getQuery());
  43. self::assertSame('test', $uri->getFragment());
  44. self::assertSame('https://user:pass@example.com:8080/path/123?q=abc#test', (string) $uri);
  45. }
  46. /**
  47. * @dataProvider getValidUris
  48. */
  49. public function testValidUrisStayValid(string $input): void
  50. {
  51. $uri = new Uri($input);
  52. self::assertSame($input, (string) $uri);
  53. }
  54. /**
  55. * @dataProvider getValidUris
  56. */
  57. public function testFromParts(string $input): void
  58. {
  59. $uri = Uri::fromParts(parse_url($input));
  60. self::assertSame($input, (string) $uri);
  61. }
  62. public function getValidUris(): iterable
  63. {
  64. return [
  65. ['urn:path-rootless'],
  66. ['urn:path:with:colon'],
  67. ['urn:/path-absolute'],
  68. ['urn:/'],
  69. // only scheme with empty path
  70. ['urn:'],
  71. // only path
  72. ['/'],
  73. ['relative/'],
  74. ['0'],
  75. // same document reference
  76. [''],
  77. // network path without scheme
  78. ['//example.org'],
  79. ['//example.org/'],
  80. ['//example.org?q#h'],
  81. // only query
  82. ['?q'],
  83. ['?q=abc&foo=bar'],
  84. // only fragment
  85. ['#fragment'],
  86. // dot segments are not removed automatically
  87. ['./foo/../bar'],
  88. ];
  89. }
  90. /**
  91. * @dataProvider getInvalidUris
  92. */
  93. public function testInvalidUrisThrowException(string $invalidUri): void
  94. {
  95. $this->expectException(MalformedUriException::class);
  96. new Uri($invalidUri);
  97. }
  98. public function getInvalidUris(): iterable
  99. {
  100. return [
  101. // parse_url() requires the host component which makes sense for http(s)
  102. // but not when the scheme is not known or different. So '//' or '///' is
  103. // currently invalid as well but should not according to RFC 3986.
  104. ['http://'],
  105. ['urn://host:with:colon'], // host cannot contain ":"
  106. ];
  107. }
  108. public function testPortMustBeValid(): void
  109. {
  110. $this->expectException(\InvalidArgumentException::class);
  111. $this->expectExceptionMessage('Invalid port: 100000. Must be between 0 and 65535');
  112. (new Uri())->withPort(100000);
  113. }
  114. public function testWithPortCannotBeNegative(): void
  115. {
  116. $this->expectException(\InvalidArgumentException::class);
  117. $this->expectExceptionMessage('Invalid port: -1. Must be between 0 and 65535');
  118. (new Uri())->withPort(-1);
  119. }
  120. public function testParseUriPortCannotBeNegative(): void
  121. {
  122. $this->expectException(\InvalidArgumentException::class);
  123. $this->expectExceptionMessage('Unable to parse URI');
  124. new Uri('//example.com:-1');
  125. }
  126. public function testSchemeMustHaveCorrectType(): void
  127. {
  128. $this->expectException(\InvalidArgumentException::class);
  129. (new Uri())->withScheme([]);
  130. }
  131. public function testHostMustHaveCorrectType(): void
  132. {
  133. $this->expectException(\InvalidArgumentException::class);
  134. (new Uri())->withHost([]);
  135. }
  136. public function testPathMustHaveCorrectType(): void
  137. {
  138. $this->expectException(\InvalidArgumentException::class);
  139. (new Uri())->withPath([]);
  140. }
  141. public function testQueryMustHaveCorrectType(): void
  142. {
  143. $this->expectException(\InvalidArgumentException::class);
  144. (new Uri())->withQuery([]);
  145. }
  146. public function testFragmentMustHaveCorrectType(): void
  147. {
  148. $this->expectException(\InvalidArgumentException::class);
  149. (new Uri())->withFragment([]);
  150. }
  151. public function testCanParseFalseyUriParts(): void
  152. {
  153. $uri = new Uri('0://0:0@0/0?0#0');
  154. self::assertSame('0', $uri->getScheme());
  155. self::assertSame('0:0@0', $uri->getAuthority());
  156. self::assertSame('0:0', $uri->getUserInfo());
  157. self::assertSame('0', $uri->getHost());
  158. self::assertSame('/0', $uri->getPath());
  159. self::assertSame('0', $uri->getQuery());
  160. self::assertSame('0', $uri->getFragment());
  161. self::assertSame('0://0:0@0/0?0#0', (string) $uri);
  162. }
  163. public function testCanConstructFalseyUriParts(): void
  164. {
  165. $uri = (new Uri())
  166. ->withScheme('0')
  167. ->withUserInfo('0', '0')
  168. ->withHost('0')
  169. ->withPath('/0')
  170. ->withQuery('0')
  171. ->withFragment('0');
  172. self::assertSame('0', $uri->getScheme());
  173. self::assertSame('0:0@0', $uri->getAuthority());
  174. self::assertSame('0:0', $uri->getUserInfo());
  175. self::assertSame('0', $uri->getHost());
  176. self::assertSame('/0', $uri->getPath());
  177. self::assertSame('0', $uri->getQuery());
  178. self::assertSame('0', $uri->getFragment());
  179. self::assertSame('0://0:0@0/0?0#0', (string) $uri);
  180. }
  181. /**
  182. * @dataProvider getPortTestCases
  183. */
  184. public function testIsDefaultPort(string $scheme, ?int $port, bool $isDefaultPort): void
  185. {
  186. $uri = $this->createMock(UriInterface::class);
  187. $uri->expects(self::any())->method('getScheme')->willReturn($scheme);
  188. $uri->expects(self::any())->method('getPort')->willReturn($port);
  189. self::assertSame($isDefaultPort, Uri::isDefaultPort($uri));
  190. }
  191. public function getPortTestCases(): iterable
  192. {
  193. return [
  194. ['http', null, true],
  195. ['http', 80, true],
  196. ['http', 8080, false],
  197. ['https', null, true],
  198. ['https', 443, true],
  199. ['https', 444, false],
  200. ['ftp', 21, true],
  201. ['gopher', 70, true],
  202. ['nntp', 119, true],
  203. ['news', 119, true],
  204. ['telnet', 23, true],
  205. ['tn3270', 23, true],
  206. ['imap', 143, true],
  207. ['pop', 110, true],
  208. ['ldap', 389, true],
  209. ];
  210. }
  211. public function testIsAbsolute(): void
  212. {
  213. self::assertTrue(Uri::isAbsolute(new Uri('http://example.org')));
  214. self::assertFalse(Uri::isAbsolute(new Uri('//example.org')));
  215. self::assertFalse(Uri::isAbsolute(new Uri('/abs-path')));
  216. self::assertFalse(Uri::isAbsolute(new Uri('rel-path')));
  217. }
  218. public function testIsNetworkPathReference(): void
  219. {
  220. self::assertFalse(Uri::isNetworkPathReference(new Uri('http://example.org')));
  221. self::assertTrue(Uri::isNetworkPathReference(new Uri('//example.org')));
  222. self::assertFalse(Uri::isNetworkPathReference(new Uri('/abs-path')));
  223. self::assertFalse(Uri::isNetworkPathReference(new Uri('rel-path')));
  224. }
  225. public function testIsAbsolutePathReference(): void
  226. {
  227. self::assertFalse(Uri::isAbsolutePathReference(new Uri('http://example.org')));
  228. self::assertFalse(Uri::isAbsolutePathReference(new Uri('//example.org')));
  229. self::assertTrue(Uri::isAbsolutePathReference(new Uri('/abs-path')));
  230. self::assertTrue(Uri::isAbsolutePathReference(new Uri('/')));
  231. self::assertFalse(Uri::isAbsolutePathReference(new Uri('rel-path')));
  232. }
  233. public function testIsRelativePathReference(): void
  234. {
  235. self::assertFalse(Uri::isRelativePathReference(new Uri('http://example.org')));
  236. self::assertFalse(Uri::isRelativePathReference(new Uri('//example.org')));
  237. self::assertFalse(Uri::isRelativePathReference(new Uri('/abs-path')));
  238. self::assertTrue(Uri::isRelativePathReference(new Uri('rel-path')));
  239. self::assertTrue(Uri::isRelativePathReference(new Uri('')));
  240. }
  241. public function testIsSameDocumentReference(): void
  242. {
  243. self::assertFalse(Uri::isSameDocumentReference(new Uri('http://example.org')));
  244. self::assertFalse(Uri::isSameDocumentReference(new Uri('//example.org')));
  245. self::assertFalse(Uri::isSameDocumentReference(new Uri('/abs-path')));
  246. self::assertFalse(Uri::isSameDocumentReference(new Uri('rel-path')));
  247. self::assertFalse(Uri::isSameDocumentReference(new Uri('?query')));
  248. self::assertTrue(Uri::isSameDocumentReference(new Uri('')));
  249. self::assertTrue(Uri::isSameDocumentReference(new Uri('#fragment')));
  250. $baseUri = new Uri('http://example.org/path?foo=bar');
  251. self::assertTrue(Uri::isSameDocumentReference(new Uri('#fragment'), $baseUri));
  252. self::assertTrue(Uri::isSameDocumentReference(new Uri('?foo=bar#fragment'), $baseUri));
  253. self::assertTrue(Uri::isSameDocumentReference(new Uri('/path?foo=bar#fragment'), $baseUri));
  254. self::assertTrue(Uri::isSameDocumentReference(new Uri('path?foo=bar#fragment'), $baseUri));
  255. self::assertTrue(Uri::isSameDocumentReference(new Uri('//example.org/path?foo=bar#fragment'), $baseUri));
  256. self::assertTrue(Uri::isSameDocumentReference(new Uri('http://example.org/path?foo=bar#fragment'), $baseUri));
  257. self::assertFalse(Uri::isSameDocumentReference(new Uri('https://example.org/path?foo=bar'), $baseUri));
  258. self::assertFalse(Uri::isSameDocumentReference(new Uri('http://example.com/path?foo=bar'), $baseUri));
  259. self::assertFalse(Uri::isSameDocumentReference(new Uri('http://example.org/'), $baseUri));
  260. self::assertFalse(Uri::isSameDocumentReference(new Uri('http://example.org'), $baseUri));
  261. self::assertFalse(Uri::isSameDocumentReference(new Uri('urn:/path'), new Uri('urn://example.com/path')));
  262. }
  263. public function testAddAndRemoveQueryValues(): void
  264. {
  265. $uri = new Uri();
  266. $uri = Uri::withQueryValue($uri, 'a', 'b');
  267. $uri = Uri::withQueryValue($uri, 'c', 'd');
  268. $uri = Uri::withQueryValue($uri, 'e', null);
  269. self::assertSame('a=b&c=d&e', $uri->getQuery());
  270. $uri = Uri::withoutQueryValue($uri, 'c');
  271. self::assertSame('a=b&e', $uri->getQuery());
  272. $uri = Uri::withoutQueryValue($uri, 'e');
  273. self::assertSame('a=b', $uri->getQuery());
  274. $uri = Uri::withoutQueryValue($uri, 'a');
  275. self::assertSame('', $uri->getQuery());
  276. }
  277. public function testScalarQueryValues(): void
  278. {
  279. $uri = new Uri();
  280. $uri = Uri::withQueryValues($uri, [
  281. 2 => 2,
  282. 1 => true,
  283. 'false' => false,
  284. 'float' => 3.1,
  285. ]);
  286. self::assertSame('2=2&1=1&false=&float=3.1', $uri->getQuery());
  287. }
  288. public function testWithQueryValues(): void
  289. {
  290. $uri = new Uri();
  291. $uri = Uri::withQueryValues($uri, [
  292. 'key1' => 'value1',
  293. 'key2' => 'value2',
  294. ]);
  295. self::assertSame('key1=value1&key2=value2', $uri->getQuery());
  296. }
  297. public function testWithQueryValuesReplacesSameKeys(): void
  298. {
  299. $uri = new Uri();
  300. $uri = Uri::withQueryValues($uri, [
  301. 'key1' => 'value1',
  302. 'key2' => 'value2',
  303. ]);
  304. $uri = Uri::withQueryValues($uri, [
  305. 'key2' => 'newvalue',
  306. ]);
  307. self::assertSame('key1=value1&key2=newvalue', $uri->getQuery());
  308. }
  309. public function testWithQueryValueReplacesSameKeys(): void
  310. {
  311. $uri = new Uri();
  312. $uri = Uri::withQueryValue($uri, 'a', 'b');
  313. $uri = Uri::withQueryValue($uri, 'c', 'd');
  314. $uri = Uri::withQueryValue($uri, 'a', 'e');
  315. self::assertSame('c=d&a=e', $uri->getQuery());
  316. }
  317. public function testWithoutQueryValueRemovesAllSameKeys(): void
  318. {
  319. $uri = (new Uri())->withQuery('a=b&c=d&a=e');
  320. $uri = Uri::withoutQueryValue($uri, 'a');
  321. self::assertSame('c=d', $uri->getQuery());
  322. }
  323. public function testRemoveNonExistingQueryValue(): void
  324. {
  325. $uri = new Uri();
  326. $uri = Uri::withQueryValue($uri, 'a', 'b');
  327. $uri = Uri::withoutQueryValue($uri, 'c');
  328. self::assertSame('a=b', $uri->getQuery());
  329. }
  330. public function testWithQueryValueHandlesEncoding(): void
  331. {
  332. $uri = new Uri();
  333. $uri = Uri::withQueryValue($uri, 'E=mc^2', 'ein&stein');
  334. self::assertSame('E%3Dmc%5E2=ein%26stein', $uri->getQuery(), 'Decoded key/value get encoded');
  335. $uri = new Uri();
  336. $uri = Uri::withQueryValue($uri, 'E%3Dmc%5e2', 'ein%26stein');
  337. self::assertSame('E%3Dmc%5e2=ein%26stein', $uri->getQuery(), 'Encoded key/value do not get double-encoded');
  338. }
  339. public function testWithoutQueryValueHandlesEncoding(): void
  340. {
  341. // It also tests that the case of the percent-encoding does not matter,
  342. // i.e. both lowercase "%3d" and uppercase "%5E" can be removed.
  343. $uri = (new Uri())->withQuery('E%3dmc%5E2=einstein&foo=bar');
  344. $uri = Uri::withoutQueryValue($uri, 'E=mc^2');
  345. self::assertSame('foo=bar', $uri->getQuery(), 'Handles key in decoded form');
  346. $uri = (new Uri())->withQuery('E%3dmc%5E2=einstein&foo=bar');
  347. $uri = Uri::withoutQueryValue($uri, 'E%3Dmc%5e2');
  348. self::assertSame('foo=bar', $uri->getQuery(), 'Handles key in encoded form');
  349. }
  350. public function testSchemeIsNormalizedToLowercase(): void
  351. {
  352. $uri = new Uri('HTTP://example.com');
  353. self::assertSame('http', $uri->getScheme());
  354. self::assertSame('http://example.com', (string) $uri);
  355. $uri = (new Uri('//example.com'))->withScheme('HTTP');
  356. self::assertSame('http', $uri->getScheme());
  357. self::assertSame('http://example.com', (string) $uri);
  358. }
  359. public function testHostIsNormalizedToLowercase(): void
  360. {
  361. $uri = new Uri('//eXaMpLe.CoM');
  362. self::assertSame('example.com', $uri->getHost());
  363. self::assertSame('//example.com', (string) $uri);
  364. $uri = (new Uri())->withHost('eXaMpLe.CoM');
  365. self::assertSame('example.com', $uri->getHost());
  366. self::assertSame('//example.com', (string) $uri);
  367. }
  368. public function testPortIsNullIfStandardPortForScheme(): void
  369. {
  370. // HTTPS standard port
  371. $uri = new Uri('https://example.com:443');
  372. self::assertNull($uri->getPort());
  373. self::assertSame('example.com', $uri->getAuthority());
  374. $uri = (new Uri('https://example.com'))->withPort(443);
  375. self::assertNull($uri->getPort());
  376. self::assertSame('example.com', $uri->getAuthority());
  377. // HTTP standard port
  378. $uri = new Uri('http://example.com:80');
  379. self::assertNull($uri->getPort());
  380. self::assertSame('example.com', $uri->getAuthority());
  381. $uri = (new Uri('http://example.com'))->withPort(80);
  382. self::assertNull($uri->getPort());
  383. self::assertSame('example.com', $uri->getAuthority());
  384. }
  385. public function testPortIsReturnedIfSchemeUnknown(): void
  386. {
  387. $uri = (new Uri('//example.com'))->withPort(80);
  388. self::assertSame(80, $uri->getPort());
  389. self::assertSame('example.com:80', $uri->getAuthority());
  390. }
  391. public function testStandardPortIsNullIfSchemeChanges(): void
  392. {
  393. $uri = new Uri('http://example.com:443');
  394. self::assertSame('http', $uri->getScheme());
  395. self::assertSame(443, $uri->getPort());
  396. $uri = $uri->withScheme('https');
  397. self::assertNull($uri->getPort());
  398. }
  399. public function testPortPassedAsStringIsCastedToInt(): void
  400. {
  401. $uri = (new Uri('//example.com'))->withPort('8080');
  402. self::assertSame(8080, $uri->getPort(), 'Port is returned as integer');
  403. self::assertSame('example.com:8080', $uri->getAuthority());
  404. }
  405. public function testPortCanBeRemoved(): void
  406. {
  407. $uri = (new Uri('http://example.com:8080'))->withPort(null);
  408. self::assertNull($uri->getPort());
  409. self::assertSame('http://example.com', (string) $uri);
  410. }
  411. /**
  412. * In RFC 8986 the host is optional and the authority can only
  413. * consist of the user info and port.
  414. */
  415. public function testAuthorityWithUserInfoOrPortButWithoutHost(): void
  416. {
  417. $uri = (new Uri())->withUserInfo('user', 'pass');
  418. self::assertSame('user:pass', $uri->getUserInfo());
  419. self::assertSame('user:pass@', $uri->getAuthority());
  420. $uri = $uri->withPort(8080);
  421. self::assertSame(8080, $uri->getPort());
  422. self::assertSame('user:pass@:8080', $uri->getAuthority());
  423. self::assertSame('//user:pass@:8080', (string) $uri);
  424. $uri = $uri->withUserInfo('');
  425. self::assertSame(':8080', $uri->getAuthority());
  426. }
  427. public function testHostInHttpUriDefaultsToLocalhost(): void
  428. {
  429. $uri = (new Uri())->withScheme('http');
  430. self::assertSame('localhost', $uri->getHost());
  431. self::assertSame('localhost', $uri->getAuthority());
  432. self::assertSame('http://localhost', (string) $uri);
  433. }
  434. public function testHostInHttpsUriDefaultsToLocalhost(): void
  435. {
  436. $uri = (new Uri())->withScheme('https');
  437. self::assertSame('localhost', $uri->getHost());
  438. self::assertSame('localhost', $uri->getAuthority());
  439. self::assertSame('https://localhost', (string) $uri);
  440. }
  441. public function testFileSchemeWithEmptyHostReconstruction(): void
  442. {
  443. $uri = new Uri('file:///tmp/filename.ext');
  444. self::assertSame('', $uri->getHost());
  445. self::assertSame('', $uri->getAuthority());
  446. self::assertSame('file:///tmp/filename.ext', (string) $uri);
  447. }
  448. public function uriComponentsEncodingProvider(): iterable
  449. {
  450. $unreserved = 'a-zA-Z0-9.-_~!$&\'()*+,;=:@';
  451. return [
  452. // Percent encode spaces
  453. ['/pa th?q=va lue#frag ment', '/pa%20th', 'q=va%20lue', 'frag%20ment', '/pa%20th?q=va%20lue#frag%20ment'],
  454. // Percent encode multibyte
  455. ['/€?€#€', '/%E2%82%AC', '%E2%82%AC', '%E2%82%AC', '/%E2%82%AC?%E2%82%AC#%E2%82%AC'],
  456. // Don't encode something that's already encoded
  457. ['/pa%20th?q=va%20lue#frag%20ment', '/pa%20th', 'q=va%20lue', 'frag%20ment', '/pa%20th?q=va%20lue#frag%20ment'],
  458. // Percent encode invalid percent encodings
  459. ['/pa%2-th?q=va%2-lue#frag%2-ment', '/pa%252-th', 'q=va%252-lue', 'frag%252-ment', '/pa%252-th?q=va%252-lue#frag%252-ment'],
  460. // Don't encode path segments
  461. ['/pa/th//two?q=va/lue#frag/ment', '/pa/th//two', 'q=va/lue', 'frag/ment', '/pa/th//two?q=va/lue#frag/ment'],
  462. // Don't encode unreserved chars or sub-delimiters
  463. ["/$unreserved?$unreserved#$unreserved", "/$unreserved", $unreserved, $unreserved, "/$unreserved?$unreserved#$unreserved"],
  464. // Encoded unreserved chars are not decoded
  465. ['/p%61th?q=v%61lue#fr%61gment', '/p%61th', 'q=v%61lue', 'fr%61gment', '/p%61th?q=v%61lue#fr%61gment'],
  466. ];
  467. }
  468. /**
  469. * @dataProvider uriComponentsEncodingProvider
  470. */
  471. public function testUriComponentsGetEncodedProperly(string $input, string $path, string $query, string $fragment, string $output): void
  472. {
  473. $uri = new Uri($input);
  474. self::assertSame($path, $uri->getPath());
  475. self::assertSame($query, $uri->getQuery());
  476. self::assertSame($fragment, $uri->getFragment());
  477. self::assertSame($output, (string) $uri);
  478. }
  479. public function testWithPathEncodesProperly(): void
  480. {
  481. $uri = (new Uri())->withPath('/baz?#€/b%61r');
  482. // Query and fragment delimiters and multibyte chars are encoded.
  483. self::assertSame('/baz%3F%23%E2%82%AC/b%61r', $uri->getPath());
  484. self::assertSame('/baz%3F%23%E2%82%AC/b%61r', (string) $uri);
  485. }
  486. public function testWithQueryEncodesProperly(): void
  487. {
  488. $uri = (new Uri())->withQuery('?=#&€=/&b%61r');
  489. // A query starting with a "?" is valid and must not be magically removed. Otherwise it would be impossible to
  490. // construct such an URI. Also the "?" and "/" does not need to be encoded in the query.
  491. self::assertSame('?=%23&%E2%82%AC=/&b%61r', $uri->getQuery());
  492. self::assertSame('??=%23&%E2%82%AC=/&b%61r', (string) $uri);
  493. }
  494. public function testWithFragmentEncodesProperly(): void
  495. {
  496. $uri = (new Uri())->withFragment('#€?/b%61r');
  497. // A fragment starting with a "#" is valid and must not be magically removed. Otherwise it would be impossible to
  498. // construct such an URI. Also the "?" and "/" does not need to be encoded in the fragment.
  499. self::assertSame('%23%E2%82%AC?/b%61r', $uri->getFragment());
  500. self::assertSame('#%23%E2%82%AC?/b%61r', (string) $uri);
  501. }
  502. public function testAllowsForRelativeUri(): void
  503. {
  504. $uri = (new Uri())->withPath('foo');
  505. self::assertSame('foo', $uri->getPath());
  506. self::assertSame('foo', (string) $uri);
  507. }
  508. public function testRelativePathAndAuthority(): void
  509. {
  510. $uri = (new Uri())->withHost('example.com')->withPath('foo');
  511. self::assertSame('foo', $uri->getPath());
  512. self::assertSame('//example.com/foo', $uri->__toString());
  513. }
  514. public function testPathStartingWithTwoSlashesAndNoAuthorityIsInvalid(): void
  515. {
  516. $this->expectException(\InvalidArgumentException::class);
  517. $this->expectExceptionMessage('The path of a URI without an authority must not start with two slashes "//"');
  518. // URI "//foo" would be interpreted as network reference and thus change the original path to the host
  519. (new Uri())->withPath('//foo');
  520. }
  521. public function testPathStartingWithTwoSlashes(): void
  522. {
  523. $uri = new Uri('http://example.org//path-not-host.com');
  524. self::assertSame('//path-not-host.com', $uri->getPath());
  525. $uri = $uri->withScheme('');
  526. self::assertSame('//example.org//path-not-host.com', (string) $uri); // This is still valid
  527. $this->expectException(\InvalidArgumentException::class);
  528. $uri->withHost(''); // Now it becomes invalid
  529. }
  530. public function testRelativeUriWithPathBeginngWithColonSegmentIsInvalid(): void
  531. {
  532. $this->expectException(\InvalidArgumentException::class);
  533. $this->expectExceptionMessage('A relative URI must not have a path beginning with a segment containing a colon');
  534. (new Uri())->withPath('mailto:foo');
  535. }
  536. public function testRelativeUriWithPathHavingColonSegment(): void
  537. {
  538. $uri = (new Uri('urn:/mailto:foo'))->withScheme('');
  539. self::assertSame('/mailto:foo', $uri->getPath());
  540. $this->expectException(\InvalidArgumentException::class);
  541. (new Uri('urn:mailto:foo'))->withScheme('');
  542. }
  543. public function testDefaultReturnValuesOfGetters(): void
  544. {
  545. $uri = new Uri();
  546. self::assertSame('', $uri->getScheme());
  547. self::assertSame('', $uri->getAuthority());
  548. self::assertSame('', $uri->getUserInfo());
  549. self::assertSame('', $uri->getHost());
  550. self::assertNull($uri->getPort());
  551. self::assertSame('', $uri->getPath());
  552. self::assertSame('', $uri->getQuery());
  553. self::assertSame('', $uri->getFragment());
  554. }
  555. public function testImmutability(): void
  556. {
  557. $uri = new Uri();
  558. self::assertNotSame($uri, $uri->withScheme('https'));
  559. self::assertNotSame($uri, $uri->withUserInfo('user', 'pass'));
  560. self::assertNotSame($uri, $uri->withHost('example.com'));
  561. self::assertNotSame($uri, $uri->withPort(8080));
  562. self::assertNotSame($uri, $uri->withPath('/path/123'));
  563. self::assertNotSame($uri, $uri->withQuery('q=abc'));
  564. self::assertNotSame($uri, $uri->withFragment('test'));
  565. }
  566. public function testExtendingClassesInstantiates(): void
  567. {
  568. // The non-standard port triggers a cascade of private methods which
  569. // should not use late static binding to access private static members.
  570. // If they do, this will fatal.
  571. self::assertInstanceOf(
  572. ExtendedUriTest::class,
  573. new ExtendedUriTest('http://h:9/')
  574. );
  575. }
  576. public function testSpecialCharsOfUserInfo(): void
  577. {
  578. // The `userInfo` must always be URL-encoded.
  579. $uri = (new Uri())->withUserInfo('foo@bar.com', 'pass#word');
  580. self::assertSame('foo%40bar.com:pass%23word', $uri->getUserInfo());
  581. // The `userInfo` can already be URL-encoded: it should not be encoded twice.
  582. $uri = (new Uri())->withUserInfo('foo%40bar.com', 'pass%23word');
  583. self::assertSame('foo%40bar.com:pass%23word', $uri->getUserInfo());
  584. }
  585. public function testInternationalizedDomainName(): void
  586. {
  587. $uri = new Uri('https://яндекс.рф');
  588. self::assertSame('яндекс.рф', $uri->getHost());
  589. $uri = new Uri('https://яндекAс.рф');
  590. self::assertSame('яндекaс.рф', $uri->getHost());
  591. }
  592. public function testIPv6Host(): void
  593. {
  594. $uri = new Uri('https://[2a00:f48:1008::212:183:10]');
  595. self::assertSame('[2a00:f48:1008::212:183:10]', $uri->getHost());
  596. $uri = new Uri('http://[2a00:f48:1008::212:183:10]:56?foo=bar');
  597. self::assertSame('[2a00:f48:1008::212:183:10]', $uri->getHost());
  598. self::assertSame(56, $uri->getPort());
  599. self::assertSame('foo=bar', $uri->getQuery());
  600. }
  601. public function testJsonSerializable(): void
  602. {
  603. $uri = new Uri('https://example.com');
  604. self::assertSame('{"uri":"https:\/\/example.com"}', \json_encode(['uri' => $uri]));
  605. }
  606. }
  607. class ExtendedUriTest extends Uri
  608. {
  609. }