diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d75618..b9497f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [Unreleased] +### Added +- service url scheme and host validation +- service url with port support + +### Changed +- PSR-18 HTTP client related exceptions namespace moved + ## [3.0.10] - 2024-01-16 ### Added - PHP-8.3 support diff --git a/src/Curl/Exception/ClientException.php b/src/Curl/Exception/ClientException.php index 4da31c7..0d2726a 100644 --- a/src/Curl/Exception/ClientException.php +++ b/src/Curl/Exception/ClientException.php @@ -4,27 +4,14 @@ namespace Smsapi\Client\Curl\Exception; -use Exception; -use Psr\Http\Client\ClientExceptionInterface; -use Psr\Http\Message\RequestInterface; +use Smsapi\Client\Infrastructure\HttpClient\ClientException as HttpClientException; /** * @api + * @deprecated + * @see HttpClientException */ -class ClientException extends Exception implements ClientExceptionInterface +class ClientException extends HttpClientException { - private $request; - public static function withRequest(string $message, RequestInterface $request): self - { - $exception = new self($message); - $exception->request = $request; - - return $exception; - } - - public function getRequest(): RequestInterface - { - return $this->request; - } } \ No newline at end of file diff --git a/src/Curl/Exception/NetworkException.php b/src/Curl/Exception/NetworkException.php index a4b7421..9d1729a 100644 --- a/src/Curl/Exception/NetworkException.php +++ b/src/Curl/Exception/NetworkException.php @@ -4,12 +4,14 @@ namespace Smsapi\Client\Curl\Exception; -use Psr\Http\Client\NetworkExceptionInterface; +use Smsapi\Client\Infrastructure\HttpClient\NetworkException as HttpClientNetworkException; /** * @api + * @deprecated + * @see HttpClientNetworkException */ -class NetworkException extends ClientException implements NetworkExceptionInterface +class NetworkException extends HttpClientNetworkException { } \ No newline at end of file diff --git a/src/Curl/Exception/RequestException.php b/src/Curl/Exception/RequestException.php index 2ea1aa5..0b96f1e 100644 --- a/src/Curl/Exception/RequestException.php +++ b/src/Curl/Exception/RequestException.php @@ -4,12 +4,14 @@ namespace Smsapi\Client\Curl\Exception; -use Psr\Http\Client\RequestExceptionInterface; +use Smsapi\Client\Infrastructure\HttpClient\RequestException as HttpClientRequestException; /** * @api + * @deprecated + * @see HttpClientRequestException */ -class RequestException extends ClientException implements RequestExceptionInterface +class RequestException extends HttpClientRequestException { } \ No newline at end of file diff --git a/src/Curl/HttpClient.php b/src/Curl/HttpClient.php index b2f9af7..7bfadcb 100644 --- a/src/Curl/HttpClient.php +++ b/src/Curl/HttpClient.php @@ -8,8 +8,8 @@ use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; -use Smsapi\Client\Curl\Exception\NetworkException; -use Smsapi\Client\Curl\Exception\RequestException; +use Smsapi\Client\Infrastructure\HttpClient\NetworkException; +use Smsapi\Client\Infrastructure\HttpClient\RequestException; /** * @internal @@ -34,7 +34,13 @@ public function sendRequest(RequestInterface $request): ResponseInterface private function prepareRequestHttpClient(RequestInterface $request) { - $url = sprintf("%s://%s%s", $request->getUri()->getScheme(), $request->getUri()->getHost(), $request->getRequestTarget()); + $url = strtr("{scheme}://{host}{port}{path}", [ + '{scheme}' => $request->getUri()->getScheme(), + '{host}' => $request->getUri()->getHost(), + '{port}' => $request->getUri()->getPort() ? ':' . $request->getUri()->getPort() : '', + '{path}' => $request->getRequestTarget() + ]); + $httpClient = curl_init($url); if ($httpClient === false) { diff --git a/src/Infrastructure/HttpClient/ClientException.php b/src/Infrastructure/HttpClient/ClientException.php new file mode 100644 index 0000000..a17f164 --- /dev/null +++ b/src/Infrastructure/HttpClient/ClientException.php @@ -0,0 +1,30 @@ +request = $request; + + return $exception; + } + + public function getRequest(): RequestInterface + { + return $this->request; + } +} \ No newline at end of file diff --git a/src/Infrastructure/HttpClient/Decorator/BaseUriDecorator.php b/src/Infrastructure/HttpClient/Decorator/BaseUriDecorator.php index 75b50db..79cc2fc 100644 --- a/src/Infrastructure/HttpClient/Decorator/BaseUriDecorator.php +++ b/src/Infrastructure/HttpClient/Decorator/BaseUriDecorator.php @@ -7,6 +7,7 @@ use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; +use Smsapi\Client\Infrastructure\HttpClient\RequestException; /** * @internal @@ -33,14 +34,20 @@ private function prependBaseUri(RequestInterface $request): RequestInterface { $uri = $request->getUri(); + if (!filter_var($this->baseUri, FILTER_VALIDATE_URL)) { + throw RequestException::withRequest("Invalid Base URI", $request); + } + $baseUriParts = parse_url($this->baseUri); $scheme = $baseUriParts['scheme'] ?? ''; $host = $baseUriParts['host'] ?? ''; + $port = $baseUriParts['port'] ?? null; $basePath = $baseUriParts['path'] ?? ''; $basePath = rtrim($basePath, '/'); $uri = $uri->withPath($basePath . '/' . $uri->getPath()); + $uri = $uri->withPort($port); $uri = $uri->withHost($host); $uri = $uri->withScheme($scheme); diff --git a/src/Infrastructure/HttpClient/NetworkException.php b/src/Infrastructure/HttpClient/NetworkException.php new file mode 100644 index 0000000..8464188 --- /dev/null +++ b/src/Infrastructure/HttpClient/NetworkException.php @@ -0,0 +1,15 @@ +lastSentRequest = $request; + + return new Response(); + } + + public function getLastSentRequest(): RequestInterface + { + return $this->lastSentRequest; + } +} \ No newline at end of file diff --git a/tests/SmsapiClientIntegrationTestCase.php b/tests/SmsapiClientIntegrationTestCase.php index a3a5337..09cc561 100644 --- a/tests/SmsapiClientIntegrationTestCase.php +++ b/tests/SmsapiClientIntegrationTestCase.php @@ -27,10 +27,6 @@ public static function prepare() $apiUri = Config::get('API URI'); - if (!filter_var($apiUri, FILTER_VALIDATE_URL)) { - throw new RuntimeException('Invalid API URI'); - } - $smsapiHttpClient = new SmsapiHttpClient(); $serviceName = Config::get('Service name'); diff --git a/tests/Unit/Infrastructure/HttpClient/Decorator/BaseUriDecoratorTest.php b/tests/Unit/Infrastructure/HttpClient/Decorator/BaseUriDecoratorTest.php new file mode 100644 index 0000000..0e538bd --- /dev/null +++ b/tests/Unit/Infrastructure/HttpClient/Decorator/BaseUriDecoratorTest.php @@ -0,0 +1,162 @@ +sendRequestToAnyEndpoint($decorator); + + $this->assertEquals($expectedRequestUri, (string)$sentRequestSpy->getLastSentRequest()->getUri()); + $this->assertEquals($expectedRequestSchema, $sentRequestSpy->getLastSentRequest()->getUri()->getScheme()); + } + + /** + * @test + * @testWith + * ["example.com"] + * ["example.com/base/"] + * ["example.com:80"] + * ["example.com:80/base/"] + * ["any://"] + * ["any:///"] + * ["any://:80/"] + */ + public function dont_send_request_without_base_schema_or_host(string $baseUri) + { + $sentRequestSpy = new HttpClientRequestSpy(); + $decorator = new BaseUriDecorator($sentRequestSpy, $baseUri); + + $this->expectException(RequestException::class); + $this->expectExceptionMessage('Invalid Base URI'); + $this->sendRequestToAnyEndpoint($decorator); + } + + /** + * @test + * @testWith + * ["any://example.com", "any://example.com/endpoint", "example.com"] + * ["any://example.com:80", "any://example.com:80/endpoint", "example.com"] + * ["any://example", "any://example/endpoint", "example"] + * ["any://example:80", "any://example:80/endpoint", "example"] + */ + public function send_request_with_base_host(string $baseUri, string $expectedRequestUri, string $expectedRequestHost) + { + $sentRequestSpy = new HttpClientRequestSpy(); + $decorator = new BaseUriDecorator($sentRequestSpy, $baseUri); + + $this->sendRequestToAnyEndpoint($decorator); + + $this->assertEquals($expectedRequestUri, (string)$sentRequestSpy->getLastSentRequest()->getUri()); + $this->assertEquals($expectedRequestHost, $sentRequestSpy->getLastSentRequest()->getUri()->getHost()); + } + + /** + * @test + * @testWith + * ["any://example.com:80", "any://example.com:80/endpoint", "80"] + * ["any://example:80", "any://example:80/endpoint", "80"] + */ + public function send_request_with_base_port(string $baseUri, string $expectedRequestUri, string $expectedRequestPort) + { + $sentRequestSpy = new HttpClientRequestSpy(); + $decorator = new BaseUriDecorator($sentRequestSpy, $baseUri); + + $this->sendRequestToAnyEndpoint($decorator); + + $this->assertEquals($expectedRequestUri, (string)$sentRequestSpy->getLastSentRequest()->getUri()); + $this->assertEquals($expectedRequestPort, $sentRequestSpy->getLastSentRequest()->getUri()->getPort()); + } + + /** + * @test + * @testWith + * ["any://example.com", "any://example.com/endpoint"] + * ["any://example", "any://example/endpoint"] + * ["any://example.com/base", "any://example.com/base/endpoint"] + * ["any://example/base", "any://example/base/endpoint"] + */ + public function send_request_without_base_port(string $baseUri, string $expectedRequestUri) + { + $sentRequestSpy = new HttpClientRequestSpy(); + $decorator = new BaseUriDecorator($sentRequestSpy, $baseUri); + + $this->sendRequestToAnyEndpoint($decorator); + + $this->assertEquals($expectedRequestUri, (string)$sentRequestSpy->getLastSentRequest()->getUri()); + $this->assertEquals('', $sentRequestSpy->getLastSentRequest()->getUri()->getPort()); + } + + /** + * @test + * @testWith + * ["any://example.com/base", "any://example.com/base/endpoint", "/base/endpoint"] + * ["any://example.com:80/base/", "any://example.com:80/base/endpoint", "/base/endpoint"] + * ["any://example:80/base", "any://example:80/base/endpoint", "/base/endpoint"] + * ["any://example.com/base/", "any://example.com/base/endpoint", "/base/endpoint"] + * ["any://example/base/", "any://example/base/endpoint", "/base/endpoint"] + * ["any://example.com:80/base/", "any://example.com:80/base/endpoint", "/base/endpoint"] + * ["any://example:80/base", "any://example:80/base/endpoint", "/base/endpoint"] + */ + public function send_request_with_base_path(string $baseUri, string $expectedRequestUri, string $expectedRequestPath) + { + $sentRequestSpy = new HttpClientRequestSpy(); + $decorator = new BaseUriDecorator($sentRequestSpy, $baseUri); + + $this->sendRequestToAnyEndpoint($decorator); + + $this->assertEquals($expectedRequestUri, (string)$sentRequestSpy->getLastSentRequest()->getUri()); + $this->assertEquals($expectedRequestPath, $sentRequestSpy->getLastSentRequest()->getUri()->getPath()); + } + + /** + * @test + * @testWith + * ["any://example.com", "any://example.com/endpoint", "/endpoint"] + * ["any://example", "any://example/endpoint", "/endpoint"] + * ["any://example.com:80", "any://example.com:80/endpoint", "/endpoint"] + * ["any://example:80", "any://example:80/endpoint", "/endpoint"] + * ["any://example.com/", "any://example.com/endpoint", "/endpoint"] + * ["any://example/", "any://example/endpoint", "/endpoint"] + * ["any://example.com:80/", "any://example.com:80/endpoint", "/endpoint"] + * ["any://example:80/", "any://example:80/endpoint", "/endpoint"] + */ + public function send_request_without_base_path(string $baseUri, string $expectedRequestUri, string $expectedRequestPath) + { + $sentRequestSpy = new HttpClientRequestSpy(); + $decorator = new BaseUriDecorator($sentRequestSpy, $baseUri); + + $this->sendRequestToAnyEndpoint($decorator); + + $this->assertEquals($expectedRequestUri, (string)$sentRequestSpy->getLastSentRequest()->getUri()); + $this->assertEquals($expectedRequestPath, $sentRequestSpy->getLastSentRequest()->getUri()->getPath()); + } + + private function sendRequestToAnyEndpoint(BaseUriDecorator $decorator) + { + $request = new Request('ANY', 'endpoint'); + $decorator->sendRequest($request); + } +} \ No newline at end of file