diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 6e8578c..1327a09 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -14,6 +14,7 @@ jobs: fail-fast: false matrix: php-version: ['8.1', '8.2', '8.3', '8.4', '8.5'] + guzzle: ['^7', '^8'] steps: - name: Checkout code @@ -26,7 +27,9 @@ jobs: coverage: xdebug - name: Install dependencies - run: composer install --no-interaction --no-progress --prefer-dist + run: | + composer require "guzzlehttp/guzzle:${{ matrix.guzzle }}" --no-update --no-interaction + composer update --no-interaction --no-progress --prefer-dist - name: Run PHPUnit with coverage run: vendor/bin/phpunit --coverage-clover coverage.xml diff --git a/Tests/Unit/Model/PayOrderTest.php b/Tests/Unit/Model/PayOrderTest.php index 46735d1..5349430 100644 --- a/Tests/Unit/Model/PayOrderTest.php +++ b/Tests/Unit/Model/PayOrderTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Tests\Unit; +namespace Tests\Unit\Model; use PayNL\Sdk\Model\Amount; use PayNL\Sdk\Model\Pay\PayOrder; diff --git a/Tests/Unit/Model/PayStatusTest.php b/Tests/Unit/Model/PayStatusTest.php index 634a7c0..1fba6ff 100644 --- a/Tests/Unit/Model/PayStatusTest.php +++ b/Tests/Unit/Model/PayStatusTest.php @@ -2,6 +2,8 @@ declare(strict_types=1); +namespace Tests\Unit\Model; + use PayNL\Sdk\Model\Pay\PayStatus; use PHPUnit\Framework\TestCase; diff --git a/Tests/Unit/Model/PayloadTest.php b/Tests/Unit/Model/PayloadTest.php index d158655..00e5eb0 100644 --- a/Tests/Unit/Model/PayloadTest.php +++ b/Tests/Unit/Model/PayloadTest.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace Tests\Unit; +namespace Tests\Unit\Model; use PayNL\Sdk\Model\Pay\PayLoad; use PHPUnit\Framework\TestCase; -class PayLoadTest extends TestCase +class PayloadTest extends TestCase { public function testLegacyPayloadMapping(): void { diff --git a/Tests/Unit/OrderRequests/OrderApproveRequestTest.php b/Tests/Unit/OrderRequests/OrderApproveRequestTest.php index 367bd9c..aa21369 100644 --- a/Tests/Unit/OrderRequests/OrderApproveRequestTest.php +++ b/Tests/Unit/OrderRequests/OrderApproveRequestTest.php @@ -1,6 +1,6 @@ getProperty('optimize'); - $optimizeProperty->setAccessible(true); $this->assertEquals([ 'flow' => 'fastCheckout', @@ -43,7 +42,6 @@ public function testSetReturnUrl(): void $reflection = new \ReflectionClass($request); $returnUrlProperty = $reflection->getProperty('returnUrl'); - $returnUrlProperty->setAccessible(true); $this->assertEquals('https://example.com/return', $returnUrlProperty->getValue($request)); } @@ -58,7 +56,6 @@ public function testSetAmount(): void $reflection = new \ReflectionClass($request); $amountProperty = $reflection->getProperty('amount'); - $amountProperty->setAccessible(true); $this->assertEquals(12345, $amountProperty->getValue($request)); } @@ -73,10 +70,8 @@ public function testSetAmountUsingObject(): void $reflection = new \ReflectionClass($request); $amountProperty = $reflection->getProperty('amount'); - $amountProperty->setAccessible(true); $currencyProperty = $reflection->getProperty('currency'); - $currencyProperty->setAccessible(true); $this->assertEquals(12345, $amountProperty->getValue($request)); $this->assertEquals('EUR', $currencyProperty->getValue($request)); @@ -94,7 +89,6 @@ public function testSetReference(): void $reflection = new \ReflectionClass($request); $referenceProperty = $reflection->getProperty('reference'); - $referenceProperty->setAccessible(true); $this->assertEquals('Order123', $referenceProperty->getValue($request)); } @@ -124,9 +118,7 @@ public function testSetNotification(): void $reflection = new \ReflectionClass($request); $notificationTypeProperty = $reflection->getProperty('notificationType'); - $notificationTypeProperty->setAccessible(true); $notificationRecipientProperty = $reflection->getProperty('notificationRecipient'); - $notificationRecipientProperty->setAccessible(true); $this->assertEquals('email', $notificationTypeProperty->getValue($request)); $this->assertEquals('test@example.com', $notificationRecipientProperty->getValue($request)); @@ -170,7 +162,6 @@ public function testSetCustomer(): void $reflection = new \ReflectionClass($request); $customerProperty = $reflection->getProperty('customer'); - $customerProperty->setAccessible(true); $this->assertSame($mockCustomer, $customerProperty->getValue($request)); } @@ -187,7 +178,6 @@ public function testSetOrder(): void $reflection = new \ReflectionClass($request); $orderProperty = $reflection->getProperty('order'); - $orderProperty->setAccessible(true); $this->assertSame($mockOrder, $orderProperty->getValue($request)); } @@ -204,7 +194,6 @@ public function testSetStats(): void $reflection = new \ReflectionClass($request); $statsProperty = $reflection->getProperty('stats'); - $statsProperty->setAccessible(true); $this->assertSame($mockStats, $statsProperty->getValue($request)); } diff --git a/Tests/Unit/OrderRequests/OrderStatusRequestTest.php b/Tests/Unit/OrderRequests/OrderStatusRequestTest.php index a46acaf..7536c9c 100644 --- a/Tests/Unit/OrderRequests/OrderStatusRequestTest.php +++ b/Tests/Unit/OrderRequests/OrderStatusRequestTest.php @@ -1,6 +1,6 @@ setServiceId('SL-1234-5678'); $reflection = new \ReflectionClass($request); $serviceIdProperty = $reflection->getProperty('serviceId'); - $serviceIdProperty->setAccessible(true); $this->assertEquals('SL-1234-5678', $serviceIdProperty->getValue($request)); } @@ -77,7 +76,6 @@ public function testSetCardNumber(): void $request->setCardNumber('1234-5678-9012-3456'); $reflection = new \ReflectionClass($request); $cardNumberProperty = $reflection->getProperty('cardNumber'); - $cardNumberProperty->setAccessible(true); $this->assertEquals('1234-5678-9012-3456', $cardNumberProperty->getValue($request)); } @@ -93,7 +91,6 @@ public function testSetPinCode(): void $request->setPinCode('123456'); $reflection = new \ReflectionClass($request); $pinCodeProperty = $reflection->getProperty('pinCode'); - $pinCodeProperty->setAccessible(true); $this->assertEquals('123456', $pinCodeProperty->getValue($request)); } @@ -109,7 +106,6 @@ public function testSetPointOfInteraction(): void $request->setPointOfInteraction('ON_THE_MOVE'); $reflection = new \ReflectionClass($request); $poiProperty = $reflection->getProperty('pointOfInteraction'); - $poiProperty->setAccessible(true); $this->assertEquals('ON_THE_MOVE', $poiProperty->getValue($request)); } diff --git a/Tests/Unit/OrderRequests/VoucherPaymentRequestTest.php b/Tests/Unit/OrderRequests/VoucherPaymentRequestTest.php index 8ccdcab..b828c45 100644 --- a/Tests/Unit/OrderRequests/VoucherPaymentRequestTest.php +++ b/Tests/Unit/OrderRequests/VoucherPaymentRequestTest.php @@ -1,6 +1,6 @@ setCardNumber('1234-5678-9012-3456'); $reflection = new \ReflectionClass($request); $cardNumberProperty = $reflection->getProperty('cardNumber'); - $cardNumberProperty->setAccessible(true); $this->assertEquals('1234-5678-9012-3456', $cardNumberProperty->getValue($request)); } @@ -78,7 +77,6 @@ public function testSetPinCode(): void $request->setPinCode('123456'); $reflection = new \ReflectionClass($request); $pinCodeProperty = $reflection->getProperty('pinCode'); - $pinCodeProperty->setAccessible(true); $this->assertEquals('123456', $pinCodeProperty->getValue($request)); } @@ -94,7 +92,6 @@ public function testSetPointOfInteraction(): void $request->setPointOfInteraction('ON_THE_MOVE'); $reflection = new \ReflectionClass($request); $poiProperty = $reflection->getProperty('pointOfInteraction'); - $poiProperty->setAccessible(true); $this->assertEquals('ON_THE_MOVE', $poiProperty->getValue($request)); } diff --git a/Tests/Unit/Request/AbstractRequestExecuteTest.php b/Tests/Unit/Request/AbstractRequestExecuteTest.php new file mode 100644 index 0000000..fa31974 --- /dev/null +++ b/Tests/Unit/Request/AbstractRequestExecuteTest.php @@ -0,0 +1,65 @@ + 'application/json'], '{"id":"1"}'), + ]); + $client = new Client([ + 'handler' => HandlerStack::create($mock), + 'base_uri' => 'https://rest.pay.nl/v2/', + ]); + + $request = new Request('orders', Request::METHOD_GET); + $request->applyClient($client, 'https://rest.pay.nl/v2/'); + + $response = new Response(); + $request->execute($response); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('{"id":"1"}', $response->getRawBody()); + + $lastRequest = $mock->getLastRequest(); + $this->assertNotNull($lastRequest); + $this->assertSame('GET', $lastRequest->getMethod()); + $this->assertSame('https://rest.pay.nl/v2/orders', (string)$lastRequest->getUri()); + } + + public function testExecuteUsesAbsoluteUriWhenUrlOptionIsSet(): void + { + $mock = new MockHandler([ + new GuzzleResponse(200, ['Content-Type' => 'application/json'], '{"ok":true}'), + ]); + $client = new Client([ + 'handler' => HandlerStack::create($mock), + 'base_uri' => 'https://rest.pay.nl/v2/', + ]); + + $request = new Request('orders', Request::METHOD_POST, [], ['url' => 'https://failover.pay.nl']); + $request->applyClient($client, 'https://rest.pay.nl/v2/'); + + $response = new Response(); + $request->execute($response); + + $this->assertSame(200, $response->getStatusCode()); + + $lastRequest = $mock->getLastRequest(); + $this->assertNotNull($lastRequest); + $this->assertSame('POST', $lastRequest->getMethod()); + $this->assertSame('https://failover.pay.nl/orders', (string)$lastRequest->getUri()); + } +} diff --git a/Tests/Unit/Response/VoucherInfoResponseTest.php b/Tests/Unit/Response/VoucherInfoResponseTest.php index 9b2d4b8..3038153 100644 --- a/Tests/Unit/Response/VoucherInfoResponseTest.php +++ b/Tests/Unit/Response/VoucherInfoResponseTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Tests\Unit; +namespace Tests\Unit\Response; use PayNL\Sdk\Model\Amount; use PayNL\Sdk\Model\Response\VoucherInfoResponse; diff --git a/Tests/Unit/Util/ExchangeResponseTest.php b/Tests/Unit/Util/ExchangeResponseTest.php index a71126f..ef6537a 100644 --- a/Tests/Unit/Util/ExchangeResponseTest.php +++ b/Tests/Unit/Util/ExchangeResponseTest.php @@ -2,6 +2,8 @@ declare(strict_types=1); +namespace Tests\Unit\Util; + use PayNL\Sdk\Util\ExchangeResponse; use PHPUnit\Framework\TestCase; @@ -11,7 +13,6 @@ private function getProperty(object $object, string $name): mixed { $ref = new \ReflectionClass($object); $prop = $ref->getProperty($name); - $prop->setAccessible(true); return $prop->getValue($object); } diff --git a/Tests/Unit/Util/ExchangeTest.php b/Tests/Unit/Util/ExchangeTest.php index e8b7b8a..534548d 100644 --- a/Tests/Unit/Util/ExchangeTest.php +++ b/Tests/Unit/Util/ExchangeTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Tests\Unit; +namespace Tests\Unit\Util; use PayNL\Sdk\Util\Exchange; use PayNL\Sdk\Util\ExchangeResponse; @@ -25,7 +25,6 @@ private function setHeaders(Exchange $exchange, array $headers): void { $refClass = new ReflectionClass(Exchange::class); $prop = $refClass->getProperty('headers'); - $prop->setAccessible(true); $prop->setValue($exchange, $headers); } @@ -107,6 +106,22 @@ public function testIsSignExchangeFalseWhenSignatureMethodMissingOrNotHmac(): vo $this->assertFalse($exchange->isSignExchange()); } + public function testIsSignExchangeReadsSignatureMethodFromServer(): void + { + $previous = $_SERVER['HTTP_SIGNATURE_METHOD'] ?? null; + $_SERVER['HTTP_SIGNATURE_METHOD'] = 'HMAC'; + + try { + $this->assertTrue($this->createExchange()->isSignExchange()); + } finally { + if ($previous === null) { + unset($_SERVER['HTTP_SIGNATURE_METHOD']); + } else { + $_SERVER['HTTP_SIGNATURE_METHOD'] = $previous; + } + } + } + public function testCheckSignExchangeReturnsFalseWhenNotSigningExchange(): void { $exchange = $this->createExchange(); diff --git a/Tests/Unit/Util/PayCacheTest.php b/Tests/Unit/Util/PayCacheTest.php index 70188a2..8bf6937 100644 --- a/Tests/Unit/Util/PayCacheTest.php +++ b/Tests/Unit/Util/PayCacheTest.php @@ -2,6 +2,8 @@ declare(strict_types=1); +namespace Tests\Unit\Util; + use PayNL\Sdk\Util\PayCache; use PHPUnit\Framework\TestCase; @@ -11,7 +13,6 @@ private function invokeMethod(object $object, string $method, array $args = []): { $refClass = new \ReflectionClass($object); $refMethod = $refClass->getMethod($method); - $refMethod->setAccessible(true); return $refMethod->invokeArgs($object, $args); } diff --git a/Tests/Unit/Util/VatFunctionsTest.php b/Tests/Unit/Util/VatFunctionsTest.php index a8abbc0..60c60e4 100644 --- a/Tests/Unit/Util/VatFunctionsTest.php +++ b/Tests/Unit/Util/VatFunctionsTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Tests\Unit; +namespace Tests\Unit\Util; use PHPUnit\Framework\TestCase; diff --git a/composer.json b/composer.json index de8ee31..764a3bc 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "php": "^8.1", "ext-curl": "*", "ext-json": "*", - "guzzlehttp/guzzle": "^7", + "guzzlehttp/guzzle": "^7 || ^8", "psr/container": "^2 || ^1", "psr/http-message": "^2 || ^1" }, @@ -46,7 +46,7 @@ }, "autoload-dev": { "psr-4": { - "Tests\\": "tests/" + "Tests\\": "Tests/" } }, "minimum-stability": "dev", @@ -62,8 +62,10 @@ }, "scripts": { "test": [ + "@phpunit", "@style-check" ], + "phpunit": "vendor/bin/phpunit", "style-check": [ "@phpstan", "@phpcs" diff --git a/phpcs.xml b/phpcs.xml index 2c11b4e..b6106db 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -17,7 +17,7 @@ - --> + diff --git a/src/Api/Api.php b/src/Api/Api.php index e36e56c..d03f6bc 100644 --- a/src/Api/Api.php +++ b/src/Api/Api.php @@ -101,7 +101,11 @@ protected function setAuthAdapter(AuthAdapterInterface $adapter): self public function doHandle(RequestInterface $request, Response $response): Response { $format = $request->getFormat(); - $request->applyClient($this->getClient()); + if ($request instanceof AbstractRequest) { + $request->applyClient($this->getClient(), $this->getConfiguredBaseUri()); + } else { + $request->applyClient($this->getClient()); + } # Apply the correct headers based on the formats set on request and response object # and also add the authentication header which is based on the authentication adapter @@ -126,4 +130,21 @@ public function doHandle(RequestInterface $request, Response $response): Respons return $response; } + + /** + * @return string + */ + private function getConfiguredBaseUri(): string + { + $api = $this->getOption('api'); + if (!is_array($api)) { + return ''; + } + + $url = rtrim((string)($api['url'] ?? ''), '/'); + $version = $api['version'] ?? ''; + $pathVersion = $version === '' ? '' : '/v' . $version; + + return $url . $pathVersion . '/'; + } } diff --git a/src/Request/AbstractRequest.php b/src/Request/AbstractRequest.php index c28cb6c..993d67a 100644 --- a/src/Request/AbstractRequest.php +++ b/src/Request/AbstractRequest.php @@ -27,7 +27,7 @@ }; use GuzzleHttp\Client; use GuzzleHttp\Exception\GuzzleException; -use GuzzleHttp\Exception\RequestException; +use GuzzleHttp\Psr7\Request as Psr7Request; /** * Class AbstractRequest @@ -82,6 +82,11 @@ abstract class AbstractRequest implements */ protected $client; + /** + * @var string + */ + protected $clientBaseUri = ''; + /** * @var array */ @@ -333,11 +338,13 @@ public function setBody(mixed $body): self /** * @param Client $client + * @param string $baseUri * @return $this */ - public function applyClient(Client $client): self + public function applyClient(Client $client, string $baseUri = ''): self { $this->client = $client; + $this->clientBaseUri = $baseUri; return $this; } @@ -435,28 +442,35 @@ public function execute(Response $response): void $filters = $this->getFilters(); $uri = trim($uri . '?' . implode('&', $filters), '?'); - $url = $this->getOption('url'); + $overrideUrl = $this->getOption('url'); + $baseUri = is_string($overrideUrl) && $overrideUrl !== '' + ? $overrideUrl + : (string)$this->clientBaseUri; $this->dumpPreStringAdvanced($this->getBody(), 'Request body', 400); + $requestBody = ''; + $curlRequest = ''; + $rawBody = ''; + $body = ''; + $statusCode = 500; + try { $guzzleClient = $this->getClient(); if (false === ($guzzleClient instanceof Client)) { throw new RuntimeException('No HTTP client found', 500); } - if (!empty($url)) { - $guzzleConfig = $guzzleClient->getConfig(); - # This also will override the version - $guzzleConfig['base_uri'] = $url; - $guzzleClient = new Client($guzzleConfig); - } $requestBody = $this->getBody(); + $method = strtoupper($this->getMethod()); + $requestUri = $uri; + if (is_string($overrideUrl) && $overrideUrl !== '') { + $requestUri = rtrim($overrideUrl, '/') . '/' . $uri; + } - # Create a Guzzle PSR 7 Request - $guzzleRequest = new \GuzzleHttp\Psr7\Request($this->getMethod(), $uri, $this->getHeaders(), $requestBody); + $guzzleRequest = new Psr7Request($method, $requestUri, $this->getHeaders(), $requestBody); - $curlRequest = 'curl -X ' . $this->getMethod() . ' ' . $guzzleClient->getConfig('base_uri') . $uri; + $curlRequest = 'curl -X ' . $method . ' ' . rtrim((string)$baseUri, '/') . '/' . $uri; foreach ($this->getHeaders() as $headerfield => $headervalue) { $curlRequest .= ' -H "' . $headerfield . ': ' . $headervalue . '"'; } @@ -464,39 +478,18 @@ public function execute(Response $response): void $curlRequest .= empty($requestBody) ? '' : ' -d \'' . $requestBody . '\''; $this->dumpPreString($curlRequest, 'Curl request'); - $this->dumpPreString(rtrim((string)$guzzleClient->getConfig('base_uri'), '/') . '/' . $guzzleRequest->getUri(), 'Requested URL'); + $this->dumpPreString(rtrim((string)$baseUri, '/') . '/' . $guzzleRequest->getUri(), 'Requested URL'); $this->dumpPreString(implode(PHP_EOL, array_map(static function ($item, $key) { return "{$key}: {$item}"; }, $this->getHeaders(), array_keys($this->getHeaders()))), 'Headers'); - $guzzleResponse = $guzzleClient->send($guzzleRequest); - + $guzzleResponse = $guzzleClient->send($guzzleRequest, ['http_errors' => false]); $rawBody = $guzzleResponse->getBody()->getContents(); - $statusCode = $guzzleResponse->getStatusCode(); $body = $rawBody; - } catch (RequestException $re) { - $errorMessages = ''; - $rawBody = $re->getMessage(); - - if (null !== $re->getResponse()) { - $guzzleExceptionBody = $re->getResponse()->getBody(); - $size = $guzzleExceptionBody->isSeekable() === true ? (int)$guzzleExceptionBody->getSize() : 0; - - if (0 < $size) { - $content = $guzzleExceptionBody->read($size); - $guzzleExceptionBody->rewind(); - - $errorMessages = $content; - } - - $rawBody = $errorMessages; - } - $statusCode = $re->getCode(); - $body = ''; - if ('' !== $errorMessages) { - $body = $this->getErrorsString($response->getFormat(), (int)$statusCode, $errorMessages); + if ($statusCode >= 400 && $rawBody !== '') { + $body = $this->getErrorsString($response->getFormat(), $statusCode, $rawBody); } } catch (GuzzleException | ExceptionInterface $e) { $statusCode = $e->getCode() ?: 500; @@ -505,7 +498,7 @@ public function execute(Response $response): void } if (function_exists('displayPayRequest')) { - displayPayRequest($guzzleClient->getConfig('base_uri') . $uri, $requestBody ?? '', $rawBody, $curlRequest ?? ''); + displayPayRequest(rtrim((string)$baseUri, '/') . '/' . $uri, $requestBody, $rawBody, $curlRequest); } $response->setStatusCode($statusCode)->setRawBody($rawBody)->setBody($body); diff --git a/src/Util/Exchange.php b/src/Util/Exchange.php index b74073a..f66924e 100644 --- a/src/Util/Exchange.php +++ b/src/Util/Exchange.php @@ -451,7 +451,24 @@ public function isSignExchange(): bool private function getRequestHeaders(): bool|array|string { if (empty($this->headers)) { - $this->headers = array_change_key_case(getallheaders()); + $headers = []; + foreach ($_SERVER as $name => $value) { + if (!is_string($name)) { + continue; + } + if (str_starts_with($name, 'HTTP_')) { + $headers[str_replace('_', '-', substr($name, 5))] = $value; + continue; + } + if ($name === 'CONTENT_TYPE') { + $headers['Content-Type'] = $value; + continue; + } + if ($name === 'CONTENT_LENGTH') { + $headers['Content-Length'] = $value; + } + } + $this->headers = array_change_key_case($headers); } return $this->headers; }