diff --git a/.gitignore b/.gitignore index c1c45d22..9229ded4 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ composer.lock .phpunit.result.cache /build .vscode + +/coverage diff --git a/composer.json b/composer.json index 82f48b68..d9b37ad2 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,8 @@ "ext-curl": "*", "ext-json": "*", "ext-mbstring": "*", - "guzzlehttp/guzzle":"^7" + "psr/http-client": "^1.0", + "psr/http-message": "^2.0" }, "require-dev": { "phpunit/phpunit": "^9", diff --git a/src/Contracts/HttpClientInterface.php b/src/Contracts/HttpClientInterface.php index 5c9a7eef..8e0cc63f 100644 --- a/src/Contracts/HttpClientInterface.php +++ b/src/Contracts/HttpClientInterface.php @@ -3,7 +3,7 @@ namespace Transbank\Contracts; use Psr\Http\Message\ResponseInterface; -use GuzzleHttp\Exception\GuzzleException; +use Transbank\Utils\Curl\Exceptions\CurlRequestException; interface HttpClientInterface { @@ -13,7 +13,7 @@ interface HttpClientInterface * @param array|null $payload * @param array|null $options * - * @throws GuzzleException + * @throws CurlRequestException * * @return ResponseInterface */ diff --git a/src/PatpassComercio/Inscription.php b/src/PatpassComercio/Inscription.php index bea2c1df..923c806f 100644 --- a/src/PatpassComercio/Inscription.php +++ b/src/PatpassComercio/Inscription.php @@ -17,7 +17,7 @@ use Transbank\Utils\RequestServiceTrait; use Transbank\Contracts\RequestService; use Transbank\PatpassComercio\Options; -use GuzzleHttp\Exception\GuzzleException; +use Transbank\Utils\Curl\Exceptions\CurlRequestException; class Inscription { @@ -63,7 +63,7 @@ public function __construct( * @param string $city * * @throws InscriptionStartException - * @throws GuzzleException + * @throws CurlRequestException * * @return InscriptionStartResponse */ @@ -123,7 +123,7 @@ public function start( * @param string $token * * @throws InscriptionStatusException - * @throws GuzzleException + * @throws CurlRequestException * * @return InscriptionStatusResponse */ diff --git a/src/Utils/Curl/Client.php b/src/Utils/Curl/Client.php new file mode 100644 index 00000000..bec68986 --- /dev/null +++ b/src/Utils/Curl/Client.php @@ -0,0 +1,95 @@ +timeout = $timeout; + } + public function sendRequest(RequestInterface $request): ResponseInterface + { + $curl = curl_init(); + + if (!$curl) { + throw new CurlRequestException('Unable to initialize cURL session.'); + } + + curl_setopt_array($curl, [ + CURLOPT_URL => (string) $request->getUri(), + CURLOPT_CUSTOMREQUEST => $request->getMethod(), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_HTTP_VERSION => $this->getCurlHttpVersion($request->getProtocolVersion()), + CURLOPT_TIMEOUT => $this->timeout, + CURLOPT_CONNECTTIMEOUT => $this->timeout, + ]); + + $headers = []; + foreach ($request->getHeaders() as $name => $value) { + $headers[] = "$name: $value"; + } + curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); + + $body = (string) $request->getBody(); + if (!empty($body)) { + curl_setopt($curl, CURLOPT_POSTFIELDS, $body); + } + + $response = curl_exec($curl); + if ($response === false) { + if (is_resource($curl)) { + curl_close($curl); + } + throw new CurlRequestException(curl_error($curl), curl_errno($curl)); + } + + $headerSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE); + $statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); + $rawHeaders = substr($response, 0, $headerSize); + $body = substr($response, $headerSize); + + curl_close($curl); + + $headers = $this->parseHeaders($rawHeaders); + + return new Response($statusCode, $headers, $body); + } + + private function parseHeaders(string $rawHeaders): array + { + $headers = []; + $lines = explode("\r\n", $rawHeaders); + + foreach ($lines as $line) { + if (strpos($line, ':') !== false) { + list($name, $value) = explode(': ', $line, 2); + $headers[$name][] = $value; + } + } + + return $headers; + } + + private function getCurlHttpVersion(string $protocol): int + { + $protocol = trim($protocol); + $curlHttpVersion = [ + '1.0' => CURL_HTTP_VERSION_1_0, + '1.1' => CURL_HTTP_VERSION_1_1, + '2.0' => CURL_HTTP_VERSION_2_0, + '3.0' => defined('CURL_HTTP_VERSION_3') ? CURL_HTTP_VERSION_3 : CURL_HTTP_VERSION_NONE + ]; + + return $curlHttpVersion[$protocol] ?? CURL_HTTP_VERSION_NONE; + } +} diff --git a/src/Utils/Curl/Exceptions/CurlRequestException.php b/src/Utils/Curl/Exceptions/CurlRequestException.php new file mode 100644 index 00000000..eeeb9235 --- /dev/null +++ b/src/Utils/Curl/Exceptions/CurlRequestException.php @@ -0,0 +1,24 @@ +code}]: {$this->message} in {$this->file} on line {$this->line}\n"; + } +} diff --git a/src/Utils/Curl/Exceptions/StreamException.php b/src/Utils/Curl/Exceptions/StreamException.php new file mode 100644 index 00000000..3421a4f8 --- /dev/null +++ b/src/Utils/Curl/Exceptions/StreamException.php @@ -0,0 +1,19 @@ + 'application/json', + 'User-Agent' => 'SDK-PHP/' . $installedVersion, + ]; + + $givenHeaders = $options['headers'] ?? []; + $headers = array_merge($baseHeaders, $givenHeaders); + if (!$payload) { + $payload = null; + } + if (is_array($payload)) { + $payload = json_encode($payload); + } + + $requestTimeout = $options['timeout'] ?? 0; + + $request = new Request($method, $url, $headers, $payload); + $client = new Client($requestTimeout); + return $client->sendRequest($request); + } +} diff --git a/src/Utils/Curl/MessageTrait.php b/src/Utils/Curl/MessageTrait.php new file mode 100644 index 00000000..bb137204 --- /dev/null +++ b/src/Utils/Curl/MessageTrait.php @@ -0,0 +1,63 @@ +protocolVersion; + } + + public function withProtocolVersion($version): static + { + $new = clone $this; + $new->protocolVersion = $version; + return $new; + } + + public function getHeaders(): array + { + return $this->headers; + } + + public function hasHeader($name): bool + { + return isset($this->headers[$name]); + } + + public function getHeader($name): array + { + return isset($this->headers[$name]) ? [$this->headers[$name]] : []; + } + + public function getHeaderLine($name): string + { + return implode(',', $this->getHeader($name)); + } + + public function withoutHeader($name): static + { + $new = clone $this; + unset($new->headers[$name]); + return $new; + } + + public function getBody(): StreamInterface + { + return $this->body; + } + + public function withBody(StreamInterface $body): static + { + $new = clone $this; + $new->body = $body; + return $new; + } +} diff --git a/src/Utils/Curl/Request.php b/src/Utils/Curl/Request.php new file mode 100644 index 00000000..4e86e25b --- /dev/null +++ b/src/Utils/Curl/Request.php @@ -0,0 +1,92 @@ +method = $method; + $this->uri = is_string($uri) ? new Uri($uri) : $uri; + $this->body = $this->createBody($body); + $this->protocolVersion = $protocolVersion; + $this->headers = $headers; + } + + public function getRequestTarget(): string + { + if ($this->requestTarget === '') { + $this->requestTarget = $this->uri->getPath() ?: '/'; + if ($this->uri->getQuery()) { + $this->requestTarget .= '?' . $this->uri->getQuery(); + } + } + + return $this->requestTarget; + } + + public function withRequestTarget($requestTarget): RequestInterface + { + $new = clone $this; + $new->requestTarget = $requestTarget; + return $new; + } + + public function getMethod(): string + { + return $this->method; + } + + public function withMethod($method): RequestInterface + { + $new = clone $this; + $new->method = $method; + return $new; + } + + public function getUri(): UriInterface + { + return $this->uri; + } + + public function withUri(UriInterface $uri, $preserveHost = false): RequestInterface + { + $new = clone $this; + $new->uri = $uri; + + if (!$preserveHost || !$this->hasHeader('Host')) { + $new->headers['Host'] = [$uri->getHost()]; + } + + return $new; + } + + public function withHeader($name, $value): RequestInterface + { + return clone $this; + } + + public function withAddedHeader($name, $value): RequestInterface + { + return clone $this; + } + + private function createBody($body = ''): StreamInterface + { + $resource = fopen('php://temp', 'rw+'); + if (!empty($body)) { + fwrite($resource, $body); + } + + return new Stream($resource); + } +} diff --git a/src/Utils/Curl/Response.php b/src/Utils/Curl/Response.php new file mode 100644 index 00000000..3148e78d --- /dev/null +++ b/src/Utils/Curl/Response.php @@ -0,0 +1,73 @@ + 'OK', + 201 => 'Created', + 204 => 'No Content', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 403 => 'Forbidden', + 404 => 'Not Found', + 500 => 'Internal Server Error', + ]; + + public function __construct(int $statusCode = 200, array $headers = [], StreamInterface|string|null $body = null) + { + $this->statusCode = $statusCode; + $this->headers = $headers; + $this->reasonPhrase = self::PHRASES[$statusCode] ?? ''; + $this->body = $this->createBody($body); + } + + public function getStatusCode(): int + { + return $this->statusCode; + } + + public function withStatus($code, $reasonPhrase = ''): ResponseInterface + { + $new = clone $this; + $new->statusCode = $code; + $new->reasonPhrase = $reasonPhrase !== '' ? $reasonPhrase : (self::PHRASES[$code] ?? ''); + return $new; + } + + public function getReasonPhrase(): string + { + return $this->reasonPhrase; + } + + public function withHeader($name, $value): ResponseInterface + { + $new = clone $this; + $new->headers[$name] = (array) $value; + return $new; + } + + public function withAddedHeader($name, $value): ResponseInterface + { + $new = clone $this; + $new->headers[$name][] = $value; + return $new; + } + + private function createBody($body = ''): StreamInterface + { + $resource = fopen('php://temp', 'rw+'); + if (!empty($body)) { + fwrite($resource, $body); + } + + return new Stream($resource); + } +} diff --git a/src/Utils/Curl/Stream.php b/src/Utils/Curl/Stream.php new file mode 100644 index 00000000..59709cf4 --- /dev/null +++ b/src/Utils/Curl/Stream.php @@ -0,0 +1,171 @@ +resource = $resource; + } + + public function __toString(): string + { + try { + if ($this->isSeekable()) { + $this->rewind(); + } + return stream_get_contents($this->resource); + } catch (\Exception $e) { + return ''; + } + } + + public function close(): void + { + if ($this->resource) { + fclose($this->resource); + $this->resource = null; + } + } + + public function detach() + { + $resource = $this->resource; + $this->resource = null; + return $resource; + } + + public function getSize(): ?int + { + if ($this->resource) { + $stats = fstat($this->resource); + return $stats['size'] ?? null; + } + return null; + } + + public function tell(): int + { + if ($this->resource === null) { + throw new StreamException('Stream is not open.'); + } + + $position = ftell($this->resource); + if ($position === false) { + throw new StreamException('Unable to determine stream position.'); + } + + return $position; + } + + public function eof(): bool + { + return $this->resource ? feof($this->resource) : true; + } + + public function isSeekable(): bool + { + return $this->resource ? (bool)stream_get_meta_data($this->resource)['seekable'] : false; + } + + public function seek($offset, $whence = SEEK_SET): void + { + if (!$this->isSeekable()) { + throw new StreamException('Stream is not seekable.'); + } + + if (fseek($this->resource, $offset, $whence) === -1) { + throw new StreamException('Unable to seek in stream.'); + } + } + + public function rewind(): void + { + $this->seek(0); + } + + public function isWritable(): bool + { + if (!$this->resource) { + return false; + } + + $mode = stream_get_meta_data($this->resource)['mode']; + return strpbrk($mode, 'w+') !== false; + } + + public function write($string): int + { + if (!$this->isWritable()) { + throw new StreamException('Stream is not writable.'); + } + + $result = fwrite($this->resource, $string); + if ($result === false) { + throw new StreamException('Unable to write to stream.'); + } + + return $result; + } + + public function isReadable(): bool + { + if (!$this->resource) { + return false; + } + + $mode = stream_get_meta_data($this->resource)['mode']; + return strpbrk($mode, 'r+') !== false; + } + + public function read($length): string + { + if (!$this->isReadable()) { + throw new StreamException('Stream is not readable.'); + } + + $result = fread($this->resource, $length); + if ($result === false) { + throw new StreamException('Unable to read from stream.'); + } + + return $result; + } + + public function getContents(): string + { + if (!$this->resource) { + throw new StreamException('Stream is not open.'); + } + + $contents = stream_get_contents($this->resource); + if ($contents === false) { + throw new StreamException('Unable to read stream contents.'); + } + + return $contents; + } + + public function getMetadata($key = null) + { + if (!$this->resource) { + return null; + } + + $meta = stream_get_meta_data($this->resource); + if ($key === null) { + return $meta; + } + + return $meta[$key] ?? null; + } +} diff --git a/src/Utils/Curl/Uri.php b/src/Utils/Curl/Uri.php new file mode 100644 index 00000000..912f58d9 --- /dev/null +++ b/src/Utils/Curl/Uri.php @@ -0,0 +1,167 @@ +parseUri($uri); + } + } + + private function parseUri(string $uri): void + { + $parts = parse_url($uri); + if ($parts === false) { + throw new \InvalidArgumentException("Invalid URI: $uri"); + } + + $this->scheme = $parts['scheme'] ?? ''; + $this->userInfo = isset($parts['user']) ? $parts['user'] : ''; + if (isset($parts['pass'])) { + $this->userInfo .= ':' . $parts['pass']; + } + $this->host = $parts['host'] ?? ''; + $this->port = $parts['port'] ?? null; + $this->path = $parts['path'] ?? ''; + $this->query = $parts['query'] ?? ''; + $this->fragment = $parts['fragment'] ?? ''; + } + + public function getScheme(): string + { + return $this->scheme; + } + + public function withScheme($scheme): UriInterface + { + $new = clone $this; + $new->scheme = $scheme; + return $new; + } + + public function getAuthority(): string + { + $authority = $this->host; + if ($this->userInfo) { + $authority = $this->userInfo . '@' . $authority; + } + if ($this->port !== null) { + $authority .= ':' . $this->port; + } + return $authority; + } + + public function getUserInfo(): string + { + return $this->userInfo; + } + + public function withUserInfo($user, $password = null): UriInterface + { + $new = clone $this; + $new->userInfo = $user; + if ($password !== null) { + $new->userInfo .= ':' . $password; + } + return $new; + } + + public function getHost(): string + { + return $this->host; + } + + public function withHost($host): UriInterface + { + $new = clone $this; + $new->host = $host; + return $new; + } + + public function getPort(): ?int + { + return $this->port; + } + + public function withPort($port): UriInterface + { + $new = clone $this; + $new->port = $port; + return $new; + } + + public function getPath(): string + { + return $this->path; + } + + public function withPath($path): UriInterface + { + $new = clone $this; + $new->path = $path; + return $new; + } + + public function getQuery(): string + { + return $this->query; + } + + public function withQuery($query): UriInterface + { + $new = clone $this; + $new->query = $query; + return $new; + } + + public function getFragment(): string + { + return $this->fragment; + } + + public function withFragment($fragment): UriInterface + { + $new = clone $this; + $new->fragment = $fragment; + return $new; + } + + public function __toString(): string + { + $uri = ''; + + if ($this->scheme) { + $uri .= $this->scheme . '://'; + } + + $authority = $this->getAuthority(); + if ($authority) { + $uri .= $authority; + } + + $uri .= $this->path; + + if ($this->query) { + $uri .= '?' . $this->query; + } + + if ($this->fragment) { + $uri .= '#' . $this->fragment; + } + + return $uri; + } +} diff --git a/src/Utils/HttpClient.php b/src/Utils/HttpClient.php deleted file mode 100644 index 490e1795..00000000 --- a/src/Utils/HttpClient.php +++ /dev/null @@ -1,87 +0,0 @@ - 'application/json', - 'User-Agent' => 'SDK-PHP/' . $installedVersion, - ]; - - $givenHeaders = $options['headers'] ?? []; - $headers = array_merge($baseHeaders, $givenHeaders); - if (!$payload) { - $payload = null; - } - if (is_array($payload)) { - $payload = json_encode($payload); - } - - $requestTimeout = $options['timeout'] ?? 0; - - return $this->sendGuzzleRequest($method, $url, $headers, $payload, $requestTimeout); - } - - /** - * Sends a Guzzle request. - * - * @param string $method - * @param string $url - * @param array $headers - * @param string|null $payload - * @param int $timeout - * - * @throws GuzzleException - * - * @return ResponseInterface - */ - protected function sendGuzzleRequest( - string $method, - string $url, - array $headers, - string|null $payload, - int $timeout - ): ResponseInterface { - $request = new Request($method, $url, $headers, $payload); - - $client = new Client([ - 'http_errors' => false, - 'timeout' => $timeout, - 'read_timeout' => $timeout, - 'connect_timeout' => $timeout, - ]); - - return $client->send($request); - } -} diff --git a/src/Utils/HttpClientRequestService.php b/src/Utils/HttpClientRequestService.php index 24b1741d..2c896af7 100644 --- a/src/Utils/HttpClientRequestService.php +++ b/src/Utils/HttpClientRequestService.php @@ -5,9 +5,10 @@ use Transbank\Contracts\HttpClientInterface; use Transbank\Contracts\RequestService; use Transbank\Utils\TransbankApiRequest; +use Transbank\Utils\Curl\HttpCurlClient; use Transbank\Webpay\Exceptions\WebpayRequestException; use Transbank\Webpay\Options; -use GuzzleHttp\Exception\GuzzleException; +use Transbank\Utils\Curl\Exceptions\CurlRequestException; use Psr\Http\Message\ResponseInterface; class HttpClientRequestService implements RequestService @@ -29,7 +30,7 @@ class HttpClientRequestService implements RequestService public function __construct(HttpClientInterface|null $httpClient = null) { - $this->setHttpClient($httpClient ?? new HttpClient()); + $this->setHttpClient($httpClient ?? new HttpCurlClient()); } /** @@ -56,7 +57,7 @@ public function setHttpClient(HttpClientInterface $httpClient): void * @param array $payload * @param Options $options * - * @throws GuzzleException + * @throws CurlRequestException * @throws WebpayRequestException * * @return array diff --git a/src/Webpay/Oneclick/MallInscription.php b/src/Webpay/Oneclick/MallInscription.php index 3c2d5e27..a69533e5 100644 --- a/src/Webpay/Oneclick/MallInscription.php +++ b/src/Webpay/Oneclick/MallInscription.php @@ -10,7 +10,7 @@ use Transbank\Webpay\Oneclick\Responses\InscriptionDeleteResponse; use Transbank\Webpay\Oneclick\Responses\InscriptionFinishResponse; use Transbank\Webpay\Oneclick\Responses\InscriptionStartResponse; -use GuzzleHttp\Exception\GuzzleException; +use Transbank\Utils\Curl\Exceptions\CurlRequestException; class MallInscription { @@ -28,7 +28,7 @@ class MallInscription * @return InscriptionStartResponse * * @throws InscriptionStartException - * @throws GuzzleException + * @throws CurlRequestException */ public function start(string $username, string $email, string $responseUrl): InscriptionStartResponse { @@ -63,7 +63,7 @@ public function start(string $username, string $email, string $responseUrl): Ins * @return InscriptionFinishResponse * * @throws InscriptionFinishException - * @throws GuzzleException + * @throws CurlRequestException */ public function finish(string $token): InscriptionFinishResponse { @@ -93,7 +93,7 @@ public function finish(string $token): InscriptionFinishResponse * @return InscriptionDeleteResponse * * @throws InscriptionDeleteException - * @throws GuzzleException + * @throws CurlRequestException */ public function delete(string $tbkUser, string $username): InscriptionDeleteResponse { diff --git a/src/Webpay/Oneclick/MallTransaction.php b/src/Webpay/Oneclick/MallTransaction.php index a5ead788..51a8734c 100644 --- a/src/Webpay/Oneclick/MallTransaction.php +++ b/src/Webpay/Oneclick/MallTransaction.php @@ -12,7 +12,7 @@ use Transbank\Webpay\Oneclick\Responses\MallTransactionCaptureResponse; use Transbank\Webpay\Oneclick\Responses\MallTransactionRefundResponse; use Transbank\Webpay\Oneclick\Responses\MallTransactionStatusResponse; -use GuzzleHttp\Exception\GuzzleException; +use Transbank\Utils\Curl\Exceptions\CurlRequestException; class MallTransaction { @@ -31,7 +31,7 @@ class MallTransaction * @return MallTransactionAuthorizeResponse * * @throws MallTransactionAuthorizeException - * @throws GuzzleException + * @throws CurlRequestException */ public function authorize( string $userName, @@ -74,7 +74,7 @@ public function authorize( * @return MallTransactionCaptureResponse * * @throws MallTransactionCaptureException - * @throws GuzzleException + * @throws CurlRequestException */ public function capture( string $childCommerceCode, @@ -114,7 +114,7 @@ public function capture( * @return MallTransactionStatusResponse * * @throws MallTransactionStatusException - * @throws GuzzleException + * @throws CurlRequestException */ public function status(string $buyOrder): MallTransactionStatusResponse { @@ -146,7 +146,7 @@ public function status(string $buyOrder): MallTransactionStatusResponse * @return MallTransactionRefundResponse * * @throws MallRefundTransactionException - * @throws GuzzleException + * @throws CurlRequestException */ public function refund( string $buyOrder, diff --git a/src/Webpay/TransaccionCompleta/MallTransaction.php b/src/Webpay/TransaccionCompleta/MallTransaction.php index bfe2445a..f17b2508 100644 --- a/src/Webpay/TransaccionCompleta/MallTransaction.php +++ b/src/Webpay/TransaccionCompleta/MallTransaction.php @@ -21,7 +21,7 @@ use Transbank\Webpay\TransaccionCompleta\Responses\MallTransactionStatusResponse; use Transbank\Webpay\TransaccionCompleta\Responses\MallTransactionCaptureResponse; use Transbank\Utils\InteractsWithWebpayApi; -use GuzzleHttp\Exception\GuzzleException; +use Transbank\Utils\Curl\Exceptions\CurlRequestException; use Transbank\Webpay\Exceptions\WebpayRequestException; class MallTransaction @@ -224,7 +224,7 @@ public function status(string $token) * @param int|float $captureAmount * * @throws MallTransactionCaptureException - * @throws GuzzleException + * @throws CurlRequestException * * @return MallTransactionCaptureResponse */ diff --git a/src/Webpay/TransaccionCompleta/Transaction.php b/src/Webpay/TransaccionCompleta/Transaction.php index 2721317b..6d27ef59 100644 --- a/src/Webpay/TransaccionCompleta/Transaction.php +++ b/src/Webpay/TransaccionCompleta/Transaction.php @@ -16,7 +16,7 @@ use Transbank\Webpay\TransaccionCompleta\Responses\TransactionCaptureResponse; use Transbank\Utils\InteractsWithWebpayApi; use Transbank\Webpay\Exceptions\WebpayRequestException; -use GuzzleHttp\Exception\GuzzleException; +use Transbank\Utils\Curl\Exceptions\CurlRequestException; /** * Class Transaction. @@ -42,7 +42,7 @@ class Transaction * @param string|null $cvv * * @throws TransactionCreateException - * @throws GuzzleException + * @throws CurlRequestException * * @return TransactionCreateResponse */ @@ -83,7 +83,7 @@ public function create( * @param int $installmentsNumber * * @throws TransactionInstallmentsException - * @throws GuzzleException + * @throws CurlRequestException * * @return TransactionInstallmentsResponse */ @@ -119,7 +119,7 @@ public function installments( * @param bool|null $gracePeriod * * @throws TransactionCommitException - * @throws GuzzleException + * @throws CurlRequestException * * @return TransactionCommitResponse */ @@ -157,7 +157,7 @@ public function commit( * @param int|float $amount * * @throws TransactionRefundException - * @throws GuzzleException + * @throws CurlRequestException * * @return TransactionRefundResponse */ @@ -188,7 +188,7 @@ public function refund(string $token, int|float $amount) * @param string $token * * @throws TransactionStatusException - * @throws GuzzleException + * @throws CurlRequestException * * @return TransactionStatusResponse */ @@ -218,7 +218,7 @@ public function status($token) * @param int|float $captureAmount * * @throws TransactionCaptureException - * @throws GuzzleException + * @throws CurlRequestException * * @return TransactionCaptureResponse */ diff --git a/src/Webpay/WebpayPlus/MallTransaction.php b/src/Webpay/WebpayPlus/MallTransaction.php index 491bb048..8f20dd8f 100644 --- a/src/Webpay/WebpayPlus/MallTransaction.php +++ b/src/Webpay/WebpayPlus/MallTransaction.php @@ -16,7 +16,7 @@ use Transbank\Webpay\WebpayPlus\Responses\MallTransactionCreateResponse; use Transbank\Webpay\WebpayPlus\Responses\MallTransactionRefundResponse; use Transbank\Webpay\WebpayPlus\Responses\MallTransactionStatusResponse; -use GuzzleHttp\Exception\GuzzleException; +use Transbank\Utils\Curl\Exceptions\CurlRequestException; class MallTransaction { @@ -36,7 +36,7 @@ class MallTransaction * @param array $details * * @throws MallTransactionCreateException - * @throws GuzzleException + * @throws CurlRequestException * * @return MallTransactionCreateResponse */ @@ -72,7 +72,7 @@ public function create(string $buyOrder, string $sessionId, string $returnUrl, a * @param string $token * * @throws MallTransactionCommitException - * @throws GuzzleException + * @throws CurlRequestException * * @return MallTransactionCommitResponse */ @@ -108,7 +108,7 @@ public function commit(string $token) * @param int|float $amount * * @throws MallTransactionRefundException - * @throws GuzzleException + * @throws CurlRequestException * * @return MallTransactionRefundResponse */ @@ -143,7 +143,7 @@ public function refund(string $token, string $buyOrder, string $childCommerceCod * @param string $token * * @throws MallTransactionStatusException - * @throws GuzzleException + * @throws CurlRequestException * * @return MallTransactionStatusResponse */ @@ -176,7 +176,7 @@ public function status(string $token) * @param int|float $captureAmount * * @throws MallTransactionCaptureException - * @throws GuzzleException + * @throws CurlRequestException * * @return MallTransactionCaptureResponse */ diff --git a/src/Webpay/WebpayPlus/Transaction.php b/src/Webpay/WebpayPlus/Transaction.php index 83ebca83..f4d0db6c 100644 --- a/src/Webpay/WebpayPlus/Transaction.php +++ b/src/Webpay/WebpayPlus/Transaction.php @@ -3,7 +3,7 @@ namespace Transbank\Webpay\WebpayPlus; use Transbank\Utils\InteractsWithWebpayApi; -use GuzzleHttp\Exception\GuzzleException; +use Transbank\Utils\Curl\Exceptions\CurlRequestException; use Transbank\Webpay\Exceptions\WebpayRequestException; use Transbank\Webpay\WebpayPlus\Exceptions\TransactionCaptureException; use Transbank\Webpay\WebpayPlus\Exceptions\TransactionCommitException; @@ -43,7 +43,7 @@ class Transaction * @param string $returnUrl * * @throws TransactionCreateException - * @throws GuzzleException + * @throws CurlRequestException * * @return TransactionCreateResponse */ @@ -79,7 +79,7 @@ public function create( * @param string $token * * @throws TransactionCommitException - * @throws GuzzleException + * @throws CurlRequestException * * @return TransactionCommitResponse */ @@ -113,7 +113,7 @@ public function commit(string $token): TransactionCommitResponse * @param int|float $amount * * @throws TransactionRefundException - * @throws GuzzleException + * @throws CurlRequestException * * @return TransactionRefundResponse */ @@ -142,7 +142,7 @@ public function refund(string $token, int|float $amount): TransactionRefundRespo * @param string $token * * @throws TransactionStatusException - * @throws GuzzleException + * @throws CurlRequestException * * @return TransactionStatusResponse */ @@ -174,7 +174,7 @@ public function status(string $token): TransactionStatusResponse * @param int|float $captureAmount * * @throws TransactionCaptureException - * @throws GuzzleException + * @throws CurlRequestException * * @return TransactionCaptureResponse */ diff --git a/tests/RequestServiceTest.php b/tests/RequestServiceTest.php index 15548b0b..d9afd3bd 100644 --- a/tests/RequestServiceTest.php +++ b/tests/RequestServiceTest.php @@ -1,8 +1,8 @@ method('getTimeout') ->willReturn($timeOut); - $httpClientMock = $this->createMock(HttpClient::class); + $httpClientMock = $this->createMock(HttpCurlClient::class); $httpClientMock ->expects($this->once()) ->method('request') @@ -49,7 +49,7 @@ public function it_uses_the_base_url_provided_by_the_given_options() ->method('getApiBaseUrl') ->willReturn($expectedBaseUrl); - $httpClientMock = $this->createMock(HttpClient::class); + $httpClientMock = $this->createMock(HttpCurlClient::class); $httpClientMock ->expects($this->once()) ->method('request') @@ -64,7 +64,7 @@ public function it_uses_the_base_url_provided_by_the_given_options() public function it_returns_an_empty_array() { $options = new Options('ApiKey', 'commerceCode', Options::ENVIRONMENT_INTEGRATION); - $httpClientMock = $this->createMock(HttpClient::class); + $httpClientMock = $this->createMock(HttpCurlClient::class); $httpClientMock ->expects($this->once()) ->method('request') @@ -77,7 +77,7 @@ public function it_returns_an_empty_array() public function it_returns_an_api_request() { $options = new Options('ApiKey', 'commerceCode', Options::ENVIRONMENT_INTEGRATION); - $httpClientMock = $this->createMock(HttpClient::class); + $httpClientMock = $this->createMock(HttpCurlClient::class); $httpClientMock ->expects($this->once()) ->method('request') diff --git a/tests/Utils/Curl/CurlRequestExceptionTest.php b/tests/Utils/Curl/CurlRequestExceptionTest.php new file mode 100644 index 00000000..d97d4dd0 --- /dev/null +++ b/tests/Utils/Curl/CurlRequestExceptionTest.php @@ -0,0 +1,16 @@ +assertEquals(CurlRequestException::DEFAULT_MESSAGE, $exception->getMessage()); + $newException = new CurlRequestException('test Error Message', 404); + $this->assertTrue(str_contains($newException->__toString(), 'error code 404')); + } +} diff --git a/tests/Utils/Curl/RequestTest.php b/tests/Utils/Curl/RequestTest.php new file mode 100644 index 00000000..aefc5bfe --- /dev/null +++ b/tests/Utils/Curl/RequestTest.php @@ -0,0 +1,70 @@ +request = new Request('GET', 'https://www.transbank.cl:443/webpay/1.2/transactions/token?param1=123¶m2=222', [ + 'Accept' => 'text/plain', + 'api_key' => 'fakeApiKey' + ], null, '1.2'); + } + + /** @test */ + public function it_can_get_class_properties(): void + { + $this->assertEquals('/webpay/1.2/transactions/token?param1=123¶m2=222', $this->request->getRequestTarget()); + $this->assertEquals('1.2', $this->request->getProtocolVersion()); + $this->assertEquals('text/plain', $this->request->getHeader('Accept')[0]); + $this->assertEquals('text/plain', $this->request->getHeaderLine('Accept')); + $this->assertTrue($this->request->hasHeader('Accept')); + } + + /** @test */ + public function it_can_set_class_properties(): void + { + $newRequest = $this->request->withRequestTarget('/api/test/'); + $this->assertEquals('/webpay/1.2/transactions/token?param1=123¶m2=222', $this->request->getRequestTarget()); + $this->assertEquals('/api/test/', $newRequest->getRequestTarget()); + $this->assertNotSame($this->request, $newRequest); + + $newRequest = $this->request->withMethod('PUT'); + $this->assertEquals('GET', $this->request->getMethod()); + $this->assertEquals('PUT', $newRequest->getMethod()); + $this->assertNotSame($this->request, $newRequest); + + $uri = new Uri('https://www.transbank.cl:443/'); + $newRequest = $this->request->withUri($uri); + $this->assertEquals($uri, $newRequest->getUri()); + $this->assertNotSame($newRequest, $this->request); + + $newRequest = $this->request->withProtocolVersion('1.1'); + $this->assertEquals('1.1', $newRequest->getProtocolVersion()); + $this->assertNotSame($newRequest, $this->request); + + $newRequest = $this->request->withHeader('testHeader', 'testValue'); + $this->assertNotSame($newRequest, $this->request); + + $newRequest = $this->request->withAddedHeader('test', 'testValue'); + $this->assertNotSame($newRequest, $this->request); + + $newRequest = $this->request->withoutHeader('Accept'); + $this->assertEquals([], $newRequest->getHeader('Accept')); + $this->assertNotSame($newRequest, $this->request); + + + $resource = fopen('php://temp', 'rw+'); + fwrite($resource, 'testData'); + $newRequest = $this->request->withBody(new Stream($resource)); + $this->assertFalse($newRequest->getBody() == $this->request->getBody()); + $this->assertNotSame($newRequest, $this->request); + } +} diff --git a/tests/Utils/Curl/ResponseTest.php b/tests/Utils/Curl/ResponseTest.php new file mode 100644 index 00000000..b5cbbc30 --- /dev/null +++ b/tests/Utils/Curl/ResponseTest.php @@ -0,0 +1,62 @@ +response = new Response(200, [ + 'Accept' => 'text/plain', + 'api_key' => 'fakeApiKey' + ], null); + } + + /** @test */ + public function it_can_get_class_properties(): void + { + $this->assertEquals('', $this->response->getProtocolVersion()); + $this->assertEquals([ + 'Accept' => 'text/plain', + 'api_key' => 'fakeApiKey' + ], $this->response->getHeaders()); + $this->assertEquals('text/plain', $this->response->getHeader('Accept')[0]); + $this->assertEquals('text/plain', $this->response->getHeaderLine('Accept')); + $this->assertTrue($this->response->hasHeader('Accept')); + } + + /** @test */ + public function it_can_set_class_properties(): void + { + $newResponse = $this->response->withStatus(404, 'NotFound'); + $this->assertEquals(404, $newResponse->getStatusCode()); + $this->assertEquals('NotFound', $newResponse->getReasonPhrase()); + $this->assertNotSame($this->response, $newResponse); + + $newResponse = $this->response->withProtocolVersion('1.0'); + $this->assertEquals('1.0', $newResponse->getProtocolVersion()); + $this->assertNotSame($this->response, $newResponse); + + $newResponse = $this->response->withHeader('testHeader', 'testValue'); + $this->assertNotSame($newResponse, $this->response); + + $newResponse = $this->response->withAddedHeader('test', 'testValue'); + $this->assertNotSame($newResponse, $this->response); + + $newResponse = $this->response->withoutHeader('Accept'); + $this->assertEquals([], $newResponse->getHeader('Accept')); + $this->assertNotSame($newResponse, $this->response); + + + $resource = fopen('php://temp', 'rw+'); + fwrite($resource, 'testData'); + $newResponse = $this->response->withBody(new Stream($resource)); + $this->assertFalse($newResponse->getBody() == $this->response->getBody()); + $this->assertNotSame($newResponse, $this->response); + } +} diff --git a/tests/Utils/Curl/StreamTest.php b/tests/Utils/Curl/StreamTest.php new file mode 100644 index 00000000..16b1c756 --- /dev/null +++ b/tests/Utils/Curl/StreamTest.php @@ -0,0 +1,130 @@ +stream = new Stream($resource); + } + + /** @test */ + public function it_throws_exception_on_constructor_failure() + { + $this->expectException(InvalidArgumentException::class); + new Stream('no resource data'); + } + + /** @test */ + public function it_gets_empty_string_on_failure() + { + $mockStream = $this->getMockBuilder(Stream::class) + ->setConstructorArgs([fopen('php://temp', 'r')]) + ->onlyMethods(['isSeekable']) + ->getMock(); + $mockStream->method('isSeekable')->willThrowException(new Exception('test Exception')); + $result = (string) $mockStream; + $this->assertEquals('', $result); + } + + /** @test */ + public function it_can_close_resource() + { + $this->stream->close(); + $this->expectException(StreamException::class); + $this->stream->getContents(); + } + + /** @test */ + public function it_can_detach_resource() + { + $resource = $this->stream->detach(); + $this->assertIsResource($resource); + $this->expectException(StreamException::class); + $this->stream->getContents(); + } + + /** @test */ + public function it_can_get_size() + { + $this->assertGreaterThan(0, $this->stream->getSize()); + $this->stream->close(); + $this->assertNull($this->stream->getSize()); + } + + /** @test */ + public function it_can_tell_position() + { + $this->assertEquals(30, $this->stream->tell()); + $this->stream->close(); + + $this->expectException(StreamException::class); + $this->stream->tell(); + } + + /** @test */ + public function it_can_get_eof() + { + $this->assertFalse($this->stream->eof()); + } + + /** @test */ + public function it_can_get_seek() + { + $this->stream->close(); + $this->expectException(StreamException::class); + $this->stream->seek(0); + } + + /** @test */ + public function it_can_write_stream() + { + $this->assertTrue($this->stream->isWritable()); + $this->stream->write('testWrite'); + $this->stream->close(); + $this->assertFalse($this->stream->isWritable()); + $this->expectException(StreamException::class); + $this->stream->write('testWrite'); + } + + /** @test */ + public function it_can_check_readable_stream() + { + $this->assertTrue($this->stream->isReadable()); + $this->stream->close(); + $this->assertFalse($this->stream->isReadable()); + } + + /** @test */ + public function it_can_read_stream() + { + $this->stream->rewind(); + $this->assertEquals('this is a test data for stream', $this->stream->read($this->stream->getSize())); + $this->stream->close(); + $this->expectException(StreamException::class); + $this->stream->read(1); + } + + /** @test */ + public function it_can_get_content() + { + $this->stream->rewind(); + $this->assertEquals('this is a test data for stream', $this->stream->getContents()); + } + + /** @test */ + public function it_can_get_metadata() + { + $this->assertEquals('TEMP', $this->stream->getMetadata('stream_type')); + $this->assertIsArray($this->stream->getMetadata()); + $this->stream->close(); + $this->assertNull($this->stream->getMetadata()); + } +} diff --git a/tests/Utils/Curl/UriTest.php b/tests/Utils/Curl/UriTest.php new file mode 100644 index 00000000..4f513e1c --- /dev/null +++ b/tests/Utils/Curl/UriTest.php @@ -0,0 +1,68 @@ +uri = new Uri($this->exampleUri); + } + + /** @test */ + public function it_can_get_data() + { + $this->assertEquals('usuario:pass', $this->uri->getUserInfo()); + $this->assertEquals('https', $this->uri->getScheme()); + $this->assertEquals('usuario:pass@www.ejemplo.com:8080', $this->uri->getAuthority()); + $this->assertEquals(8080, $this->uri->getPort()); + $this->assertEquals('seccion2', $this->uri->getFragment()); + $this->assertEquals($this->exampleUri, $this->uri); + } + + /** @test */ + public function it_can_set_data() + { + $newUri = $this->uri->withScheme('http'); + $this->assertNotSame($newUri, $this->uri); + $this->assertEquals('http', $newUri->getScheme()); + $this->assertEquals('https', $this->uri->getScheme()); + + $newUri = $this->uri->withUserInfo('newUser', 'testPassword'); + $this->assertNotSame($newUri, $this->uri); + $this->assertEquals('newUser:testPassword', $newUri->getUserInfo()); + $this->assertEquals('usuario:pass', $this->uri->getUserInfo()); + + $newUri = $this->uri->withHost('www.new-host.cl'); + $this->assertNotSame($newUri, $this->uri); + $this->assertEquals('www.new-host.cl', $newUri->getHost()); + $this->assertEquals('www.ejemplo.com', $this->uri->getHost()); + + $newUri = $this->uri->withPort(9000); + $this->assertNotSame($newUri, $this->uri); + $this->assertEquals(9000, $newUri->getPort()); + $this->assertEquals(8080, $this->uri->getPort()); + + $newUri = $this->uri->withPath('/nueva/ruta'); + $this->assertNotSame($newUri, $this->uri); + $this->assertEquals('/nueva/ruta', $newUri->getPath()); + $this->assertEquals('/ruta/al/recurso', $this->uri->getPath()); + + $newUri = $this->uri->withQuery('nuevabusqueda=test&filtro=inactivo'); + $this->assertNotSame($newUri, $this->uri); + $this->assertEquals('nuevabusqueda=test&filtro=inactivo', $newUri->getQuery()); + $this->assertEquals('busqueda=phpunit&filtro=activo', $this->uri->getQuery()); + + $newUri = $this->uri->withFragment('newfragment'); + $this->assertNotSame($newUri, $this->uri); + $this->assertEquals('newfragment', $newUri->getFragment()); + $this->assertEquals('seccion2', $this->uri->getFragment()); + + $this->expectException(InvalidArgumentException::class); + $newUri = new Uri('http://:80'); + } +} diff --git a/tests/Webpay/WebpayPlus/WebpayPlusWithoutMocksTest.php b/tests/Webpay/WebpayPlus/WebpayPlusWithoutMocksTest.php index 0d4ed400..20a08166 100644 --- a/tests/Webpay/WebpayPlus/WebpayPlusWithoutMocksTest.php +++ b/tests/Webpay/WebpayPlus/WebpayPlusWithoutMocksTest.php @@ -10,6 +10,8 @@ use Transbank\Webpay\WebpayPlus\Exceptions\TransactionCreateException; use Transbank\Webpay\WebpayPlus\Transaction; use Transbank\Webpay\WebpayPlus\Responses\TransactionStatusResponse; +use Transbank\Utils\Curl\HttpCurlClient; +use Transbank\Utils\HttpClientRequestService; class WebpayPlusWithoutMocksTest extends TestCase { @@ -39,6 +41,8 @@ class WebpayPlusWithoutMocksTest extends TestCase */ public $options; + public $clientRequestService; + protected function setUp(): void { $this->amount = 1000; @@ -51,12 +55,14 @@ protected function setUp(): void WebpayPlus::INTEGRATION_COMMERCE_CODE, Options::ENVIRONMENT_INTEGRATION ); + $httpClient = new HttpCurlClient(); + $this->clientRequestService = new HttpClientRequestService($httpClient); } /** @test */ public function it_creates_a_real_transaction_with_options() { - $transaction = (new Transaction($this->options)); + $transaction = (new Transaction($this->options, $this->clientRequestService)); $transactionResult = $transaction->create( $this->buyOrder, $this->sessionId, @@ -74,7 +80,7 @@ public function testCreateTransactionWithIncorrectCredentialsShouldFail() $this->expectException(TransactionCreateException::class); $this->expectExceptionMessage('Not Authorized'); - $transaction = (new Transaction($options)); + $transaction = (new Transaction($options, $this->clientRequestService)); $transaction->create($this->buyOrder, $this->sessionId, $this->amount, $this->returnUrl); } @@ -107,7 +113,7 @@ public function it_can_get_the_status_of_a_transaction() /** @test */ public function it_can_not_commit_a_recently_created_transaction() { - $response = (new Transaction($this->options))->create( + $response = (new Transaction($this->options, $this->clientRequestService))->create( $this->buyOrder, $this->sessionId, $this->amount, @@ -127,7 +133,7 @@ public function it_can_not_capture_a_recently_created_transaction() WebpayPlus::INTEGRATION_DEFERRED_COMMERCE_CODE, Options::ENVIRONMENT_INTEGRATION ); - $response = (new Transaction($deferredOptions))->create( + $response = (new Transaction($deferredOptions, $this->clientRequestService))->create( $this->buyOrder, $this->sessionId, $this->amount, @@ -145,7 +151,7 @@ public function it_can_not_capture_a_transaction_with_simultaneous_capture_comme WebpayPlus::INTEGRATION_API_KEY, WebpayPlus::INTEGRATION_COMMERCE_CODE, Options::ENVIRONMENT_INTEGRATION - )); + ), $this->clientRequestService); $response = $transaction->create($this->buyOrder, $this->sessionId, $this->amount, $this->returnUrl); $this->expectException(TransactionCaptureException::class); $this->expectExceptionMessage('Operation not allowed'); @@ -157,7 +163,7 @@ public function it_can_not_capture_a_transaction_with_simultaneous_capture_comme /** @test */ public function it_returns_a_card_number_in_null_when_it_not_exists() { - $transaction = new Transaction($this->options); + $transaction = new Transaction($this->options, $this->clientRequestService); $createResponse = $transaction->create($this->buyOrder, $this->sessionId, $this->amount, $this->returnUrl); $statusResponse = $transaction->status($createResponse->getToken());