- 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.
238 lines
8.7 KiB
PHP
238 lines
8.7 KiB
PHP
<?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×tamp=1234567890');
|
|
|
|
$canonical = $withAuth->toString();
|
|
|
|
expect($canonical)->toContain('signature=abc123');
|
|
expect($canonical)->toContain('timestamp=1234567890');
|
|
});
|
|
});
|
|
});
|