Version 1.0.0

This commit is contained in:
2025-07-13 00:10:45 +03:00
commit 42da8af363
35 changed files with 3468 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\Url\Tests\Builder;
use Diffhead\PHP\Url\Builder\HostRelative;
use Diffhead\PHP\Url\Port;
use Diffhead\PHP\Url\Scheme;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\TestCase;
#[CoversClass(HostRelative::class)]
#[CoversMethod(HostRelative::class, 'build')]
class HostRelativeTest extends TestCase
{
public function testUrlBuilding(): void
{
$hostname = 'www.google.com';
$scheme = Scheme::Https;
$port = Port::WebSecure;
$path = '/api/users';
$query = ['active' => true, 'page' => 2, 'perPage' => 100];
$builder = new HostRelative($hostname, $scheme->value, $port->value);
$url = $builder->build($path, $query);
$this->assertSame($hostname, $url->hostname());
$this->assertSame($scheme->value, $url->scheme());
$this->assertSame($port->value, $url->port());
$this->assertSame($path, $url->path());
$this->assertEquals($query, $url->parameters());
}
}

View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\Url\Tests\Builder;
use Diffhead\PHP\Url\Builder\ReplaceAttributes;
use Diffhead\PHP\Url\Url;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\TestCase;
use TypeError;
#[CoversClass(ReplaceAttributes::class)]
#[CoversMethod(ReplaceAttributes::class, 'build')]
class ReplaceAttributesTest extends TestCase
{
public function testReplacementOnEmptyData(): void
{
$origin = Url::create('http', 'example.com', '/path', 80, ['a' => '1']);
$builder = new ReplaceAttributes($origin);
$replaced = $builder->build();
$this->assertSame('http', $replaced->scheme());
$this->assertSame('example.com', $replaced->hostname());
$this->assertSame('/path', $replaced->path());
$this->assertSame(80, $replaced->port());
$this->assertSame(['a' => '1'], $replaced->parameters());
}
public function testPartiallyReplacement(): void
{
$origin = Url::create('http', 'example.com', '/path', 80, ['a' => '1']);
$builder = new ReplaceAttributes($origin);
$replaced = $builder->build('new-example.com', [
'scheme' => 'https',
'port' => 443,
]);
$this->assertSame('https', $replaced->scheme());
$this->assertSame('new-example.com', $replaced->hostname());
$this->assertSame('/path', $replaced->path());
$this->assertSame(443, $replaced->port());
$this->assertSame(['a' => '1'], $replaced->parameters());
}
public function testFullReplacement(): void
{
$origin = Url::create('http', 'example.com', '/path', 80, ['a' => '1']);
$builder = new ReplaceAttributes($origin);
$replaced = $builder->build('new-example.com', [
'scheme' => 'https',
'path' => '/new-path',
'port' => 443,
'parameters' => ['b' => '2'],
]);
$this->assertSame('https', $replaced->scheme());
$this->assertSame('new-example.com', $replaced->hostname());
$this->assertSame('/new-path', $replaced->path());
$this->assertSame(443, $replaced->port());
$this->assertSame(['b' => '2'], $replaced->parameters());
}
public function testInvalidParametersTypes(): void
{
$this->expectException(TypeError::class);
$origin = Url::create('http', 'example.com', '/path', 80, ['a' => '1']);
$builder = new ReplaceAttributes($origin);
$builder->build('new-example.com', [
'scheme' => 123,
]);
}
}

36
tests/Dto/ReplaceTest.php Normal file
View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\Url\Tests\Dto;
use Diffhead\PHP\Url\Dto\Replace;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\TestCase;
#[CoversClass(Replace::class)]
#[CoversMethod(Replace::class, 'scheme')]
#[CoversMethod(Replace::class, 'hostname')]
#[CoversMethod(Replace::class, 'path')]
#[CoversMethod(Replace::class, 'port')]
#[CoversMethod(Replace::class, 'parameters')]
class ReplaceTest extends TestCase
{
public function testReplaceDtoProperlyInitializes(): void
{
$replace = new Replace(
scheme: 'https',
hostname: 'example.com',
path: '/path',
port: 443,
parameters: ['param' => 'value'],
);
$this->assertSame('https', $replace->scheme());
$this->assertSame('example.com', $replace->hostname());
$this->assertSame('/path', $replace->path());
$this->assertSame(443, $replace->port());
$this->assertSame(['param' => 'value'], $replace->parameters());
}
}

86
tests/FacadeTest.php Normal file
View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\Url\Tests;
use Diffhead\PHP\Url\Dto\Replace;
use Diffhead\PHP\Url\Facade;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\TestCase;
#[CoversClass(Facade::class)]
#[CoversMethod(Facade::class, 'parse')]
#[CoversMethod(Facade::class, 'toRfc3986String')]
#[CoversMethod(Facade::class, 'replace')]
class FacadeTest extends TestCase
{
public function testParsingOnEmptyString(): void
{
$url = Facade::parse('');
$this->assertSame('', $url->scheme());
$this->assertSame('', $url->hostname());
$this->assertSame('', $url->path());
$this->assertSame(0, $url->port());
$this->assertSame([], $url->parameters());
}
public function testParsingOnProperlyWebUrl(): void
{
$url = Facade::parse('https://example.com:8080/path/to/resource?param1=value1&param2=value2');
$this->assertSame('https', $url->scheme());
$this->assertSame('example.com', $url->hostname());
$this->assertSame('/path/to/resource', $url->path());
$this->assertSame(8080, $url->port());
$this->assertSame(['param1' => 'value1', 'param2' => 'value2'], $url->parameters());
}
public function testParsingOnDsn(): void
{
$url = Facade::parse('mysql://user:password@localhost:3306/database_name');
$this->assertSame('mysql', $url->scheme());
$this->assertSame('localhost', $url->hostname());
$this->assertSame('/database_name', $url->path());
$this->assertSame(3306, $url->port());
$this->assertSame([], $url->parameters());
}
public function testStringConversionToRfc3986(): void
{
$originUrl = 'https://example.com:8080/path/to/resource?param1=value1&param2=value2';
$url = Facade::parse($originUrl);
$convertedUrl = Facade::toRfc3986String($url);
$this->assertSame($originUrl, $convertedUrl);
}
public function testUrlAttributesReplacement(): void
{
$raw = 'http://example.com/path?param=oldValue';
$origin = Facade::parse($raw);
$replace = new Replace(
scheme: 'https',
hostname: 'new-example.com',
path: '/new-path',
port: 443,
parameters: ['param' => 'newValue', 'addedParam' => 'addedValue'],
);
$replaced = Facade::replace($origin, $replace);
$this->assertSame('https', $replaced->scheme());
$this->assertSame('new-example.com', $replaced->hostname());
$this->assertSame('/new-path', $replaced->path());
$this->assertSame(443, $replaced->port());
$this->assertSame(
['param' => 'newValue', 'addedParam' => 'addedValue'],
$replaced->parameters()
);
}
}

118
tests/ParserTest.php Normal file
View File

@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\Url\Tests;
use Diffhead\PHP\Url\Exception\UrlNotContainsHostname;
use Diffhead\PHP\Url\Exception\UrlNotContainsPath;
use Diffhead\PHP\Url\Exception\UrlNotContainsPort;
use Diffhead\PHP\Url\Exception\UrlNotContainsQuery;
use Diffhead\PHP\Url\Exception\UrlNotContainsScheme;
use Diffhead\PHP\Url\Parser;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\TestCase;
#[CoversClass(Parser::class)]
#[CoversMethod(Parser::class, 'getScheme')]
#[CoversMethod(Parser::class, 'getHostname')]
#[CoversMethod(Parser::class, 'getPath')]
#[CoversMethod(Parser::class, 'getPort')]
#[CoversMethod(Parser::class, 'getParameters')]
class ParserTest extends TestCase
{
public function testExistingSchemeParsing(): void
{
$this->assertSame('http', $this->getParser('http://example.info')->getScheme());
$this->assertSame('https', $this->getParser('https://example.info')->getScheme());
}
public function testEmptySchemeParsing(): void
{
$this->expectException(UrlNotContainsScheme::class);
$this->getParser('example.info')->getScheme();
}
public function testExistingHostnameParsing(): void
{
$this->assertSame('example.info', $this->getParser('example.info')->getHostname());
$this->assertSame('example.info', $this->getParser('https://example.info')->getHostname());
}
public function testEmptyHostnameParsing(): void
{
$this->expectException(UrlNotContainsHostname::class);
$this->getParser('https:///api/webhooks/event')->getHostname();
}
public function testExistingPortParsing(): void
{
$this->assertSame(80, $this->getParser('localhost:80')->getPort());
$this->assertSame(443, $this->getParser('https://google.com:443/index')->getPort());
}
public function testEmptyPortParsing(): void
{
$this->expectException(UrlNotContainsPort::class);
$this->getParser('localhost')->getPort();
}
public function testExistingPathParsing(): void
{
$this->assertSame('/', $this->getParser('https://google.com/')->getPath());
$this->assertSame('/api/endpoint', $this->getParser('https://something.org/api/endpoint')->getPath());
$this->assertSame('/api/endpoint', $this->getParser('something.org:1234/api/endpoint')->getPath());
}
public function testEmptyPathParsing(): void
{
$this->expectException(UrlNotContainsPath::class);
$this->getParser('https://google.com')->getPath();
}
public function testExistingFlatParametersParsing(): void
{
$url = 'https://example.com?parameter=value&secondParameter=secondValue&a=100';
$parser = $this->getParser($url);
$urlParameters = [
'parameter' => 'value',
'secondParameter' => 'secondValue',
'a' => '100'
];
$this->assertEquals($urlParameters, $parser->getParameters());
}
public function testExistingNestedParametersParsing(): void
{
$url = 'https://example.com/search?query=book&filters%5Bprice%5D%5Bmin%5D=10&filters%5Bprice%5D%5Bmax%5D=100&filters%5Bcategories%5D%5B%5D=fiction&filters%5Bcategories%5D%5B%5D=history';
$parser = $this->getParser($url);
$urlParameters = [
'query' => 'book',
'price' => [
'min' => '10',
'max' => '100'
],
'categories' => [
'fiction',
'history'
]
];
$this->assertEquals($urlParameters, $parser->getParameters());
}
public function testNonExistingParametersParsing(): void
{
$this->expectException(UrlNotContainsQuery::class);
$this->getParser('https://google.com')->getParameters();
}
private function getParser(string $url): Parser
{
return new Parser($url);
}
}

View File

@@ -0,0 +1,183 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\Url\Tests\Serializer;
use Diffhead\PHP\Url\Port;
use Diffhead\PHP\Url\Scheme;
use Diffhead\PHP\Url\Serializer\RFC3986;
use Diffhead\PHP\Url\Url;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\TestCase;
#[CoversClass(RFC3986::class)]
#[CoversMethod(RFC3986::class, 'toString')]
class RFC3986Test extends TestCase
{
public function testSerializeHttpUrlWithDefaultPort(): void
{
$url = new Url(
Scheme::Http->value,
'localhost',
'/api/endpoint',
Port::Web->value,
[
'isActive' => true,
'page' => 1,
'perPage' => 100
]
);
$this->assertSame(
'http://localhost/api/endpoint?isActive=1&page=1&perPage=100',
$this->getSerializer()->toString($url)
);
}
public function serializeHttpUrlWithNonDefaultPort(): void
{
$url = new Url(
Scheme::Http->value,
'localhost',
'/api/endpoint',
Port::WebSecure->value,
[
'isActive' => true,
'page' => 1,
'perPage' => 100
]
);
$this->assertSame(
'http://localhost:443/api/endpoint?isActive=1&page=1&perPage=100',
$this->getSerializer()->toString($url)
);
}
public function serializeHttpsUrlWithDefaultPort(): void
{
$url = new Url(
Scheme::Https->value,
'localhost',
'/api/endpoint',
Port::WebSecure->value,
[
'isActive' => true,
'page' => 1,
'perPage' => 100
]
);
$this->assertSame(
'https://localhost/api/endpoint?isActive=1&page=1&perPage=100',
$this->getSerializer()->toString($url)
);
}
public function serializeHttpsUrlWithNonDefaultPort(): void
{
$url = new Url(
Scheme::Https->value,
'localhost',
'/api/endpoint',
Port::MySql->value,
[
'isActive' => true,
'page' => 1,
'perPage' => 100
]
);
$this->assertSame(
'https://localhost:3306/api/endpoint?isActive=1&page=1&perPage=100',
$this->getSerializer()->toString($url)
);
}
public function testSerializeWebSocketUrlWithDefaultPort(): void
{
$url = new Url(
Scheme::WebSocket->value,
'localhost',
'/api/endpoint',
Port::Web->value,
[
'isActive' => true,
'page' => 1,
'perPage' => 100
]
);
$this->assertSame(
'ws://localhost/api/endpoint?isActive=1&page=1&perPage=100',
$this->getSerializer()->toString($url)
);
}
public function serializeWebSocketUrlWithNonDefaultPort(): void
{
$url = new Url(
Scheme::Http->value,
'localhost',
'/api/endpoint',
Port::WebSecure->value,
[
'isActive' => true,
'page' => 1,
'perPage' => 100
]
);
$this->assertSame(
'ws://localhost:443/api/endpoint?isActive=1&page=1&perPage=100',
$this->getSerializer()->toString($url)
);
}
public function serializeWebSocketSecureUrlWithDefaultPort(): void
{
$url = new Url(
Scheme::WebSocketSecure->value,
'localhost',
'/api/endpoint',
Port::WebSecure->value,
[
'isActive' => true,
'page' => 1,
'perPage' => 100
]
);
$this->assertSame(
'wss://localhost/api/endpoint?isActive=1&page=1&perPage=100',
$this->getSerializer()->toString($url)
);
}
public function serializeWebSocketSecureUrlWithNonDefaultPort(): void
{
$url = new Url(
Scheme::WebSocketSecure->value,
'localhost',
'/api/endpoint',
Port::Web->value,
[
'isActive' => true,
'page' => 1,
'perPage' => 100
]
);
$this->assertSame(
'wss://localhost:80/api/endpoint?isActive=1&page=1&perPage=100',
$this->getSerializer()->toString($url)
);
}
private function getSerializer(): RFC3986
{
return new RFC3986();
}
}

55
tests/UrlTest.php Normal file
View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\Url\Tests;
use Diffhead\PHP\Url\Port;
use Diffhead\PHP\Url\Scheme;
use Diffhead\PHP\Url\Url;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\TestCase;
#[CoversClass(Url::class)]
#[CoversMethod(Url::class, 'scheme')]
#[CoversMethod(Url::class, 'hostname')]
#[CoversMethod(Url::class, 'path')]
#[CoversMethod(Url::class, 'port')]
#[CoversMethod(Url::class, 'parameters')]
class UrlTest extends TestCase
{
public function testInitialization(): void
{
$url = new Url(
Scheme::FileTransferOverSsh->value,
'file.storage.info',
'/home/user/Downloads',
Port::FileTransferOverSsh->value,
['file' => 'something.txt']
);
$this->assertSame(Scheme::FileTransferOverSsh->value, $url->scheme());
$this->assertSame('file.storage.info', $url->hostname());
$this->assertSame('/home/user/Downloads', $url->path());
$this->assertSame(Port::FileTransferOverSsh->value, $url->port());
$this->assertEquals(['file' => 'something.txt'], $url->parameters());
}
public function testStaticInitialization(): void
{
$url = Url::create(
Scheme::WebSocketSecure->value,
'localhost',
'/api/endpoint',
Port::WebSecure->value,
['token' => 'token1234']
);
$this->assertSame(Scheme::WebSocketSecure->value, $url->scheme());
$this->assertSame('localhost', $url->hostname());
$this->assertSame('/api/endpoint', $url->path());
$this->assertSame(Port::WebSecure->value, $url->port());
$this->assertEquals(['token' => 'token1234'], $url->parameters());
}
}

108
tests/UtilTest.php Normal file
View File

@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace Diffhead\PHP\Url\Tests;
use Diffhead\PHP\Url\Port;
use Diffhead\PHP\Url\Scheme;
use Diffhead\PHP\Url\Url;
use Diffhead\PHP\Url\Util;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\TestCase;
#[CoversClass(Util::class)]
#[CoversMethod(Util::class, 'isDefaultHttpUrl')]
#[CoversMethod(Util::class, 'isDefaultWebSocketUrl')]
class UtilTest extends TestCase
{
public function testHttpHttpsWithDefaultPortsTesting(): void
{
$httpWith80Port = new Url(
Scheme::Http->value,
'localhost',
'/',
Port::Web->value,
[]
);
$httpsWith443Port = new Url(
Scheme::Https->value,
'localhost',
'/',
Port::WebSecure->value,
[]
);
$this->assertTrue(Util::isDefaultHttpUrl($httpWith80Port));
$this->assertTrue(Util::isDefaultHttpUrl($httpsWith443Port));
}
public function testHttpHttpsWithNonDefaultPortsTesting(): void
{
$httpWith443Port = new Url(
Scheme::Http->value,
'localhost',
'/',
Port::WebSecure->value,
[]
);
$httpsWith80Port = new Url(
Scheme::Https->value,
'localhost',
'/',
Port::Web->value,
[]
);
$this->assertFalse(Util::isDefaultHttpUrl($httpWith443Port));
$this->assertFalse(Util::isDefaultHttpUrl($httpsWith80Port));
}
public function testWebSocketWithDefaultPortsTesting(): void
{
$webSocketWith80Port = new Url(
Scheme::WebSocket->value,
'localhost',
'/',
Port::Web->value,
[]
);
$webSocketSecureWith443Port = new Url(
Scheme::WebSocketSecure->value,
'localhost',
'/',
Port::WebSecure->value,
[]
);
$this->assertTrue(Util::isDefaultWebSocketUrl($webSocketWith80Port));
$this->assertTrue(Util::isDefaultWebSocketUrl($webSocketSecureWith443Port));
}
public function testWebSocketWithNonDefaultPortsTesting(): void
{
$webSocketWith443Port = new Url(
Scheme::WebSocket->value,
'localhost',
'/',
Port::WebSecure->value,
[]
);
$webSocketSecureWith80Port = new Url(
Scheme::WebSocketSecure->value,
'localhost',
'/',
Port::Web->value,
[]
);
$this->assertFalse(Util::isDefaultWebSocketUrl($webSocketWith443Port));
$this->assertFalse(Util::isDefaultWebSocketUrl($webSocketSecureWith80Port));
}
}