getScheme())->toBe('https'); expect($url->getHost())->toBe('example.com'); expect($url->getPath())->toBe('/path'); }); it('parses URLs with all components', function () { $url = Rfc3986Url::parse('https://user:pass@example.com:8080/path?query=1#fragment'); expect($url->getScheme())->toBe('https'); expect($url->getUserInfo())->toBe('user:pass'); expect($url->getHost())->toBe('example.com'); expect($url->getPort())->toBe(8080); expect($url->getPath())->toBe('/path'); expect($url->getQuery())->toBe('query=1'); expect($url->getFragment())->toBe('fragment'); }); it('handles default ports correctly', function () { $url = Rfc3986Url::parse('https://example.com/path'); expect($url->getPort())->toBeNull(); }); it('parses relative URLs with base', function () { $base = Rfc3986Url::parse('https://example.com/base/path'); $url = Rfc3986Url::parse('../other', $base); expect($url->getPath())->toContain('other'); }); }); describe('spec compliance', function () { it('reports RFC3986 spec', function () { $url = Rfc3986Url::parse('https://example.com'); expect($url->getSpec())->toBe(UrlSpec::RFC3986); expect($url->getSpec()->isRfc3986())->toBeTrue(); expect($url->getSpec()->isWhatwg())->toBeFalse(); }); }); describe('immutable withers', function () { it('returns new instance with different scheme', function () { $original = Rfc3986Url::parse('https://example.com'); $modified = $original->withScheme('http'); expect($original->getScheme())->toBe('https'); expect($modified->getScheme())->toBe('http'); expect(spl_object_id($original))->notToBe(spl_object_id($modified)); }); it('returns new instance with different host', function () { $original = Rfc3986Url::parse('https://example.com'); $modified = $original->withHost('api.example.com'); expect($original->getHost())->toBe('example.com'); expect($modified->getHost())->toBe('api.example.com'); }); it('returns new instance with different port', function () { $original = Rfc3986Url::parse('https://example.com'); $modified = $original->withPort(8080); expect($original->getPort())->toBeNull(); expect($modified->getPort())->toBe(8080); }); it('returns new instance with different path', function () { $original = Rfc3986Url::parse('https://example.com/old'); $modified = $original->withPath('/new'); expect($original->getPath())->toBe('/old'); expect($modified->getPath())->toBe('/new'); }); it('returns new instance with different query', function () { $original = Rfc3986Url::parse('https://example.com?old=1'); $modified = $original->withQuery('new=2'); expect($original->getQuery())->toBe('old=1'); expect($modified->getQuery())->toBe('new=2'); }); it('returns new instance with different fragment', function () { $original = Rfc3986Url::parse('https://example.com#old'); $modified = $original->withFragment('new'); expect($original->getFragment())->toBe('old'); expect($modified->getFragment())->toBe('new'); }); it('returns new instance with user info', function () { $original = Rfc3986Url::parse('https://example.com'); $modified = $original->withUserInfo('user', 'pass'); expect($original->getUserInfo())->toBe(''); expect($modified->getUserInfo())->toBe('user:pass'); }); it('handles user info without password', function () { $url = Rfc3986Url::parse('https://example.com'); $modified = $url->withUserInfo('user'); expect($modified->getUserInfo())->toBe('user'); }); }); describe('serialization', function () { it('converts to string', function () { $url = Rfc3986Url::parse('https://example.com/path?query=1#fragment'); expect($url->toString())->toBe('https://example.com/path?query=1#fragment'); expect((string) $url)->toBe('https://example.com/path?query=1#fragment'); }); it('converts to ASCII string for IDNA domains', function () { $url = Rfc3986Url::parse('https://例え.jp/path'); $ascii = $url->toAsciiString(); expect($ascii)->toContain('xn--'); }); }); describe('URL resolution', function () { it('resolves relative URLs', function () { $base = Rfc3986Url::parse('https://example.com/base/path'); $resolved = $base->resolve('relative'); expect($resolved->getHost())->toBe('example.com'); expect($resolved->getPath())->toContain('relative'); }); it('resolves absolute URLs', function () { $base = Rfc3986Url::parse('https://example.com/base'); $resolved = $base->resolve('https://other.com/path'); expect($resolved->getHost())->toBe('other.com'); expect($resolved->getPath())->toBe('/path'); }); }); describe('URL comparison', function () { it('compares URLs for equality', function () { $url1 = Rfc3986Url::parse('https://example.com/path'); $url2 = Rfc3986Url::parse('https://example.com/path'); expect($url1->equals($url2))->toBeTrue(); }); it('compares URLs ignoring fragments by default', function () { $url1 = Rfc3986Url::parse('https://example.com/path#frag1'); $url2 = Rfc3986Url::parse('https://example.com/path#frag2'); expect($url1->equals($url2))->toBeTrue(); expect($url1->equals($url2, includeFragment: true))->toBeFalse(); }); it('detects different URLs', function () { $url1 = Rfc3986Url::parse('https://example.com/path1'); $url2 = Rfc3986Url::parse('https://example.com/path2'); expect($url1->equals($url2))->toBeFalse(); }); }); describe('edge cases', function () { it('handles empty path', function () { $url = Rfc3986Url::parse('https://example.com'); expect($url->getPath())->toBe(''); }); it('handles empty query', function () { $url = Rfc3986Url::parse('https://example.com/path'); expect($url->getQuery())->toBe(''); }); it('handles empty fragment', function () { $url = Rfc3986Url::parse('https://example.com/path'); expect($url->getFragment())->toBe(''); }); it('removes port with null', function () { $url = Rfc3986Url::parse('https://example.com:8080/path'); $modified = $url->withPort(null); expect($modified->getPort())->toBeNull(); }); it('removes query with empty string', function () { $url = Rfc3986Url::parse('https://example.com?query=1'); $modified = $url->withQuery(''); expect($modified->getQuery())->toBe(''); }); it('removes fragment with empty string', function () { $url = Rfc3986Url::parse('https://example.com#fragment'); $modified = $url->withFragment(''); expect($modified->getFragment())->toBe(''); }); }); describe('API client use cases', function () { it('preserves exact URL structure for API calls', function () { $url = Rfc3986Url::parse('https://api.example.com/v1/users?filter=active&sort=name'); expect($url->getQuery())->toBe('filter=active&sort=name'); expect($url->toString())->toContain('filter=active&sort=name'); }); it('supports URL signature generation', function () { $url = Rfc3986Url::parse('https://api.example.com/resource'); $withAuth = $url->withQuery('signature=abc123×tamp=1234567890'); $canonical = $withAuth->toString(); expect($canonical)->toContain('signature=abc123'); expect($canonical)->toContain('timestamp=1234567890'); }); }); });