feat(Production): Complete production deployment infrastructure

- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -0,0 +1,237 @@
<?php
declare(strict_types=1);
use App\Framework\Http\Url\Rfc3986Url;
use App\Framework\Http\Url\UrlSpec;
describe('Rfc3986Url', function () {
describe('parsing', function () {
it('parses basic HTTP URLs', function () {
$url = Rfc3986Url::parse('https://example.com/path');
expect($url->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&timestamp=1234567890');
$canonical = $withAuth->toString();
expect($canonical)->toContain('signature=abc123');
expect($canonical)->toContain('timestamp=1234567890');
});
});
});

View File

@@ -0,0 +1,266 @@
<?php
declare(strict_types=1);
use App\Framework\Http\Url\Rfc3986Url;
use App\Framework\Http\Url\UrlFactory;
use App\Framework\Http\Url\UrlSpec;
use App\Framework\Http\Url\UrlUseCase;
use App\Framework\Http\Url\WhatwgUrl;
describe('UrlFactory', function () {
describe('automatic spec selection', function () {
it('selects WHATWG for http/https URLs', function () {
$url = UrlFactory::parse('https://example.com/path');
expect($url)->toBeInstanceOf(WhatwgUrl::class);
expect($url->getSpec())->toBe(UrlSpec::WHATWG);
});
it('selects RFC3986 for non-http schemes', function () {
$url = UrlFactory::parse('ftp://example.com/file');
expect($url)->toBeInstanceOf(Rfc3986Url::class);
expect($url->getSpec())->toBe(UrlSpec::RFC3986);
});
it('selects WHATWG for file:// URLs', function () {
$url = UrlFactory::parse('file:///path/to/file');
expect($url)->toBeInstanceOf(WhatwgUrl::class);
});
it('selects WHATWG for WebSocket URLs', function () {
$url = UrlFactory::parse('ws://example.com/socket');
expect($url)->toBeInstanceOf(WhatwgUrl::class);
});
});
describe('use case factory methods', function () {
it('creates RFC3986 URL for API client', function () {
$url = UrlFactory::forApiClient('https://api.example.com/users');
expect($url)->toBeInstanceOf(Rfc3986Url::class);
expect($url->getHost())->toBe('api.example.com');
expect($url->getPath())->toBe('/users');
});
it('creates RFC3986 URL for cURL request', function () {
$url = UrlFactory::forCurlRequest('https://example.com/endpoint');
expect($url)->toBeInstanceOf(Rfc3986Url::class);
});
it('creates RFC3986 URL for signature generation', function () {
$url = UrlFactory::forSignature('https://api.example.com/resource');
expect($url)->toBeInstanceOf(Rfc3986Url::class);
});
it('creates RFC3986 URL for canonical URLs', function () {
$url = UrlFactory::forCanonical('https://example.com/page');
expect($url)->toBeInstanceOf(Rfc3986Url::class);
});
it('creates WHATWG URL for browser redirect', function () {
$url = UrlFactory::forBrowserRedirect('https://example.com/redirect');
expect($url)->toBeInstanceOf(WhatwgUrl::class);
});
it('creates WHATWG URL for deep link', function () {
$url = UrlFactory::forDeepLink('myapp://open/profile');
expect($url)->toBeInstanceOf(WhatwgUrl::class);
});
it('creates WHATWG URL for form action', function () {
$url = UrlFactory::forFormAction('https://example.com/submit');
expect($url)->toBeInstanceOf(WhatwgUrl::class);
});
it('creates WHATWG URL for client-side JavaScript', function () {
$url = UrlFactory::forClientSide('https://example.com/api/data');
expect($url)->toBeInstanceOf(WhatwgUrl::class);
});
});
describe('use case automatic selection', function () {
it('creates RFC3986 for API_CLIENT use case', function () {
$url = UrlFactory::forUseCase(
UrlUseCase::API_CLIENT,
'https://api.example.com/users'
);
expect($url)->toBeInstanceOf(Rfc3986Url::class);
});
it('creates RFC3986 for CURL_REQUEST use case', function () {
$url = UrlFactory::forUseCase(
UrlUseCase::CURL_REQUEST,
'https://example.com/file'
);
expect($url)->toBeInstanceOf(Rfc3986Url::class);
});
it('creates RFC3986 for SIGNATURE_GENERATION use case', function () {
$url = UrlFactory::forUseCase(
UrlUseCase::SIGNATURE_GENERATION,
'https://api.example.com/sign'
);
expect($url)->toBeInstanceOf(Rfc3986Url::class);
});
it('creates RFC3986 for CANONICAL_URL use case', function () {
$url = UrlFactory::forUseCase(
UrlUseCase::CANONICAL_URL,
'https://example.com/page'
);
expect($url)->toBeInstanceOf(Rfc3986Url::class);
});
it('creates WHATWG for BROWSER_REDIRECT use case', function () {
$url = UrlFactory::forUseCase(
UrlUseCase::BROWSER_REDIRECT,
'https://example.com/redirect'
);
expect($url)->toBeInstanceOf(WhatwgUrl::class);
});
it('creates WHATWG for DEEP_LINK use case', function () {
$url = UrlFactory::forUseCase(
UrlUseCase::DEEP_LINK,
'myapp://open'
);
expect($url)->toBeInstanceOf(WhatwgUrl::class);
});
it('creates WHATWG for HTML_FORM_ACTION use case', function () {
$url = UrlFactory::forUseCase(
UrlUseCase::HTML_FORM_ACTION,
'https://example.com/submit'
);
expect($url)->toBeInstanceOf(WhatwgUrl::class);
});
it('creates WHATWG for CLIENT_SIDE_URL use case', function () {
$url = UrlFactory::forUseCase(
UrlUseCase::CLIENT_SIDE_URL,
'https://example.com/api'
);
expect($url)->toBeInstanceOf(WhatwgUrl::class);
});
});
describe('spec conversion', function () {
it('converts RFC3986 to WHATWG', function () {
$rfc = UrlFactory::forApiClient('https://example.com/path');
$whatwg = UrlFactory::convert($rfc, UrlSpec::WHATWG);
expect($whatwg)->toBeInstanceOf(WhatwgUrl::class);
expect($whatwg->getHost())->toBe('example.com');
expect($whatwg->getPath())->toBe('/path');
});
it('converts WHATWG to RFC3986', function () {
$whatwg = UrlFactory::forBrowserRedirect('https://example.com/path');
$rfc = UrlFactory::convert($whatwg, UrlSpec::RFC3986);
expect($rfc)->toBeInstanceOf(Rfc3986Url::class);
expect($rfc->getHost())->toBe('example.com');
expect($rfc->getPath())->toBe('/path');
});
it('returns same instance if already correct spec', function () {
$rfc = UrlFactory::forApiClient('https://example.com');
$converted = UrlFactory::convert($rfc, UrlSpec::RFC3986);
expect($converted)->toBe($rfc);
});
});
describe('integration scenarios', function () {
it('creates API client URL with authentication', function () {
$url = UrlFactory::forApiClient('https://api.example.com/users');
$withAuth = $url->withUserInfo('api_key', 'secret');
expect($withAuth->getUserInfo())->toBe('api_key:secret');
expect($withAuth->toString())->toContain('api_key:secret');
});
it('creates redirect URL with query parameters', function () {
$url = UrlFactory::forBrowserRedirect('https://example.com/login');
$withParams = $url->withQuery('return_url=/dashboard&status=success');
expect($withParams->getQuery())->toContain('return_url');
});
it('creates canonical URL without query and fragment', function () {
$url = UrlFactory::forCanonical('https://example.com/page?utm=123#section');
$canonical = $url->withQuery('')->withFragment('');
expect($canonical->toString())->toBe('https://example.com/page');
});
it('creates signature-safe URL', function () {
$url = UrlFactory::forSignature('https://api.example.com/resource');
$withSig = $url->withQuery('timestamp=1234567890&signature=abc123');
$urlString = $withSig->toString();
expect($urlString)->toContain('timestamp=1234567890');
expect($urlString)->toContain('signature=abc123');
});
it('creates deep link with custom data', function () {
$url = UrlFactory::forDeepLink('myapp://open/profile');
$withData = $url->withQuery('user_id=123&action=view');
expect($withData->getScheme())->toBe('myapp');
expect($withData->getQuery())->toContain('user_id=123');
});
});
describe('real-world scenarios', function () {
it('handles OAuth URL construction', function () {
$authUrl = UrlFactory::forApiClient('https://oauth.example.com/authorize');
$withParams = $authUrl->withQuery('client_id=123&redirect_uri=https://app.com&scope=read');
expect($withParams->getQuery())->toContain('client_id=123');
expect($withParams->getQuery())->toContain('redirect_uri');
});
it('handles REST API pagination', function () {
$apiUrl = UrlFactory::forApiClient('https://api.example.com/users');
$page2 = $apiUrl->withQuery('page=2&per_page=50');
expect($page2->getQuery())->toBe('page=2&per_page=50');
});
it('handles mobile deep link with fallback', function () {
$deepLink = UrlFactory::forDeepLink('myapp://open/article/123');
$fallback = UrlFactory::forBrowserRedirect('https://example.com/article/123');
expect($deepLink->getScheme())->toBe('myapp');
expect($fallback->getScheme())->toBe('https');
});
it('handles form submission with CSRF token', function () {
$formAction = UrlFactory::forFormAction('https://example.com/submit');
$withToken = $formAction->withQuery('csrf_token=abc123');
expect($withToken->getQuery())->toContain('csrf_token');
});
});
});

View File

@@ -0,0 +1,149 @@
<?php
declare(strict_types=1);
use App\Framework\Http\Url\UrlSpec;
use App\Framework\Http\Url\UrlUseCase;
describe('UrlSpec', function () {
describe('enum values', function () {
it('has RFC3986 case', function () {
expect(UrlSpec::RFC3986->value)->toBe('rfc3986');
});
it('has WHATWG case', function () {
expect(UrlSpec::WHATWG->value)->toBe('whatwg');
});
});
describe('use case mapping', function () {
it('maps API_CLIENT to RFC3986', function () {
$spec = UrlSpec::forUseCase(UrlUseCase::API_CLIENT);
expect($spec)->toBe(UrlSpec::RFC3986);
});
it('maps CURL_REQUEST to RFC3986', function () {
$spec = UrlSpec::forUseCase(UrlUseCase::CURL_REQUEST);
expect($spec)->toBe(UrlSpec::RFC3986);
});
it('maps SIGNATURE_GENERATION to RFC3986', function () {
$spec = UrlSpec::forUseCase(UrlUseCase::SIGNATURE_GENERATION);
expect($spec)->toBe(UrlSpec::RFC3986);
});
it('maps CANONICAL_URL to RFC3986', function () {
$spec = UrlSpec::forUseCase(UrlUseCase::CANONICAL_URL);
expect($spec)->toBe(UrlSpec::RFC3986);
});
it('maps BROWSER_REDIRECT to WHATWG', function () {
$spec = UrlSpec::forUseCase(UrlUseCase::BROWSER_REDIRECT);
expect($spec)->toBe(UrlSpec::WHATWG);
});
it('maps DEEP_LINK to WHATWG', function () {
$spec = UrlSpec::forUseCase(UrlUseCase::DEEP_LINK);
expect($spec)->toBe(UrlSpec::WHATWG);
});
it('maps HTML_FORM_ACTION to WHATWG', function () {
$spec = UrlSpec::forUseCase(UrlUseCase::HTML_FORM_ACTION);
expect($spec)->toBe(UrlSpec::WHATWG);
});
it('maps CLIENT_SIDE_URL to WHATWG', function () {
$spec = UrlSpec::forUseCase(UrlUseCase::CLIENT_SIDE_URL);
expect($spec)->toBe(UrlSpec::WHATWG);
});
});
describe('convenience methods', function () {
it('checks if RFC3986', function () {
expect(UrlSpec::RFC3986->isRfc3986())->toBeTrue();
expect(UrlSpec::WHATWG->isRfc3986())->toBeFalse();
});
it('checks if WHATWG', function () {
expect(UrlSpec::WHATWG->isWhatwg())->toBeTrue();
expect(UrlSpec::RFC3986->isWhatwg())->toBeFalse();
});
});
});
describe('UrlUseCase', function () {
describe('enum cases', function () {
it('has all expected use cases', function () {
$cases = UrlUseCase::cases();
expect($cases)->toHaveCount(8);
expect($cases)->toContain(UrlUseCase::API_CLIENT);
expect($cases)->toContain(UrlUseCase::CURL_REQUEST);
expect($cases)->toContain(UrlUseCase::SIGNATURE_GENERATION);
expect($cases)->toContain(UrlUseCase::CANONICAL_URL);
expect($cases)->toContain(UrlUseCase::BROWSER_REDIRECT);
expect($cases)->toContain(UrlUseCase::DEEP_LINK);
expect($cases)->toContain(UrlUseCase::HTML_FORM_ACTION);
expect($cases)->toContain(UrlUseCase::CLIENT_SIDE_URL);
});
});
describe('descriptions', function () {
it('provides description for API_CLIENT', function () {
$desc = UrlUseCase::API_CLIENT->description();
expect($desc)->toContain('API');
expect($desc)->toContain('REST');
});
it('provides description for BROWSER_REDIRECT', function () {
$desc = UrlUseCase::BROWSER_REDIRECT->description();
expect($desc)->toContain('Browser');
expect($desc)->toContain('redirect');
});
it('provides description for SIGNATURE_GENERATION', function () {
$desc = UrlUseCase::SIGNATURE_GENERATION->description();
expect($desc)->toContain('signature');
expect($desc)->toContain('OAuth');
});
it('provides descriptions for all use cases', function () {
foreach (UrlUseCase::cases() as $useCase) {
expect(strlen($useCase->description()))->toBeGreaterThan(0);
}
});
});
describe('recommended spec', function () {
it('recommends RFC3986 for API_CLIENT', function () {
$spec = UrlUseCase::API_CLIENT->recommendedSpec();
expect($spec)->toBe(UrlSpec::RFC3986);
});
it('recommends WHATWG for BROWSER_REDIRECT', function () {
$spec = UrlUseCase::BROWSER_REDIRECT->recommendedSpec();
expect($spec)->toBe(UrlSpec::WHATWG);
});
it('provides recommendation for all use cases', function () {
foreach (UrlUseCase::cases() as $useCase) {
$spec = $useCase->recommendedSpec();
expect($spec)->toBeInstanceOf(UrlSpec::class);
}
});
});
});

View File

@@ -0,0 +1,263 @@
<?php
declare(strict_types=1);
use App\Framework\Http\Url\UrlSpec;
use App\Framework\Http\Url\WhatwgUrl;
describe('WhatwgUrl', function () {
describe('parsing', function () {
it('parses basic HTTP URLs', function () {
$url = WhatwgUrl::parse('https://example.com/path');
expect($url->getScheme())->toBe('https');
expect($url->getHost())->toBe('example.com');
expect($url->getPath())->toBe('/path');
});
it('parses URLs with all components', function () {
$url = WhatwgUrl::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 = WhatwgUrl::parse('https://example.com/path');
expect($url->getPort())->toBeNull();
});
it('parses relative URLs with base', function () {
$base = WhatwgUrl::parse('https://example.com/base/path');
$url = WhatwgUrl::parse('../other', $base);
expect($url->getPath())->toContain('other');
});
it('normalizes URLs according to WHATWG spec', function () {
$url = WhatwgUrl::parse('https://example.com/./path/../other');
// WHATWG normalizes . and .. in paths
expect($url->getPath())->toBe('/other');
});
});
describe('spec compliance', function () {
it('reports WHATWG spec', function () {
$url = WhatwgUrl::parse('https://example.com');
expect($url->getSpec())->toBe(UrlSpec::WHATWG);
expect($url->getSpec()->isWhatwg())->toBeTrue();
expect($url->getSpec()->isRfc3986())->toBeFalse();
});
});
describe('immutable withers', function () {
it('returns new instance with different scheme', function () {
$original = WhatwgUrl::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 = WhatwgUrl::parse('https://example.com');
$modified = $original->withHost('app.example.com');
expect($original->getHost())->toBe('example.com');
expect($modified->getHost())->toBe('app.example.com');
});
it('returns new instance with different port', function () {
$original = WhatwgUrl::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 = WhatwgUrl::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 = WhatwgUrl::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 = WhatwgUrl::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 = WhatwgUrl::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 = WhatwgUrl::parse('https://example.com');
$modified = $url->withUserInfo('user');
expect($modified->getUserInfo())->toBe('user');
});
});
describe('serialization', function () {
it('converts to string', function () {
$url = WhatwgUrl::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('handles IDNA domains (Punycode)', function () {
$url = WhatwgUrl::parse('https://例え.jp/path');
$str = $url->toString();
// WHATWG automatically Punycode-encodes
expect($str)->toContain('xn--');
});
it('toAsciiString returns same as toString for WHATWG', function () {
$url = WhatwgUrl::parse('https://example.com/path');
expect($url->toAsciiString())->toBe($url->toString());
});
});
describe('URL resolution', function () {
it('resolves relative URLs', function () {
$base = WhatwgUrl::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 = WhatwgUrl::parse('https://example.com/base');
$resolved = $base->resolve('https://other.com/path');
expect($resolved->getHost())->toBe('other.com');
expect($resolved->getPath())->toBe('/path');
});
it('resolves with path normalization', function () {
$base = WhatwgUrl::parse('https://example.com/base');
$resolved = $base->resolve('./path/../other');
expect($resolved->getPath())->toBe('/other');
});
});
describe('URL comparison', function () {
it('compares URLs for equality', function () {
$url1 = WhatwgUrl::parse('https://example.com/path');
$url2 = WhatwgUrl::parse('https://example.com/path');
expect($url1->equals($url2))->toBeTrue();
});
it('compares URLs ignoring fragments by default', function () {
$url1 = WhatwgUrl::parse('https://example.com/path#frag1');
$url2 = WhatwgUrl::parse('https://example.com/path#frag2');
expect($url1->equals($url2))->toBeTrue();
expect($url1->equals($url2, includeFragment: true))->toBeFalse();
});
it('detects different URLs', function () {
$url1 = WhatwgUrl::parse('https://example.com/path1');
$url2 = WhatwgUrl::parse('https://example.com/path2');
expect($url1->equals($url2))->toBeFalse();
});
});
describe('edge cases', function () {
it('handles trailing slashes', function () {
$url = WhatwgUrl::parse('https://example.com/path/');
expect($url->getPath())->toBe('/path/');
});
it('handles empty path', function () {
$url = WhatwgUrl::parse('https://example.com');
expect($url->getPath())->toBe('/');
});
it('handles empty query', function () {
$url = WhatwgUrl::parse('https://example.com/path');
expect($url->getQuery())->toBe('');
});
it('handles empty fragment', function () {
$url = WhatwgUrl::parse('https://example.com/path');
expect($url->getFragment())->toBe('');
});
it('removes port with null', function () {
$url = WhatwgUrl::parse('https://example.com:8080/path');
$modified = $url->withPort(null);
expect($modified->getPort())->toBeNull();
});
});
describe('browser redirect use cases', function () {
it('handles query parameters for redirects', function () {
$url = WhatwgUrl::parse('https://example.com/redirect');
$withParams = $url->withQuery('return_url=https://other.com&status=success');
expect($withParams->getQuery())->toContain('return_url');
expect($withParams->getQuery())->toContain('status=success');
});
it('handles fragment identifiers', function () {
$url = WhatwgUrl::parse('https://example.com/page#section');
expect($url->getFragment())->toBe('section');
});
it('normalizes paths for browser compatibility', function () {
$url = WhatwgUrl::parse('https://example.com/./path/../other');
expect($url->getPath())->toBe('/other');
});
});
describe('deep link use cases', function () {
it('handles custom schemes', function () {
$url = WhatwgUrl::parse('myapp://open/profile?user_id=123');
expect($url->getScheme())->toBe('myapp');
expect($url->getPath())->toContain('profile');
expect($url->getQuery())->toContain('user_id=123');
});
});
});