From 6cd96970d0db98b61a5aec1404f05f3c295e72fd Mon Sep 17 00:00:00 2001 From: Benjamin Eckel Date: Wed, 19 Feb 2020 12:13:00 -0600 Subject: [PATCH 01/10] Mock Client --- composer.json | 1 + lib/recurly/base_client.php | 37 +++++------ lib/recurly/http.php | 32 ++++++++++ lib/recurly/utils.php | 19 ++++++ tests/BaseClient_Test.php | 27 ++++++++ tests/RecurlyError_Test.php | 23 +++---- tests/mock_client.php | 76 +++++++++++++++++++++++ tests/recurly/resources/test_resource.php | 11 ++++ 8 files changed, 192 insertions(+), 34 deletions(-) create mode 100644 lib/recurly/http.php create mode 100644 lib/recurly/utils.php create mode 100644 tests/BaseClient_Test.php create mode 100644 tests/mock_client.php diff --git a/composer.json b/composer.json index df977f81..cfca978d 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0", + "mockery/mockery": "^1.3", "phpcompatibility/php-compatibility": "^9.3", "phpstan/phpstan": "^0.12.11", "phpunit/phpunit": "^8", diff --git a/lib/recurly/base_client.php b/lib/recurly/base_client.php index 454cc312..3a0ca3c5 100644 --- a/lib/recurly/base_client.php +++ b/lib/recurly/base_client.php @@ -6,6 +6,7 @@ abstract class BaseClient { private $_baseUrl = 'https://v3.recurly.com'; private $_api_key; + public $_http; /** * Constructor @@ -15,6 +16,7 @@ abstract class BaseClient public function __construct(string $api_key) { $this->_api_key = $api_key; + $this->_http = new Http; } /** @@ -56,22 +58,13 @@ private function _getResponse(string $method, string $path, ?array $body = [], ? { $request = new \Recurly\Request($method, $path, $body, $params); - $options = array( - 'http' => array( - 'ignore_errors' => true, // Allows for returning error bodies - 'method' => $method, - 'header' => $this->_headers(), - 'content' => isset($body) && !empty($body) ? json_encode($body) : null - ) - ); - - $context = stream_context_create($options); + $body = isset($body) && !empty($body) ? json_encode($body) : null; $url = $this->_buildPath($path, $params); - $result = file_get_contents($url, false, $context); + list($result, $response_header) = $this->_http->execute($method, $url, $body, $this->_headers()); // TODO: The $request should be added to the $response $response = new \Recurly\Response($result); - $response->setHeaders($http_response_header); + $response->setHeaders($response_header); return $response; } @@ -142,20 +135,18 @@ protected function interpolatePath(string $path, array $options = []): string /** * Generates headers to be sent with the HTTP request * - * @return string String representation of the HTTP headers + * @return array Array representation of the HTTP headers */ - private function _headers(): string + private function _headers(): array { - $php_version = phpversion(); - $client_version = \Recurly\Version::CURRENT; - $auth_token = base64_encode("{$this->_api_key}:"); - $headers = array( - "User-Agent: Recurly/{$client_version}; php {$php_version}", - "Authorization: Basic {$auth_token}", - "Accept: application/vnd.recurly.{$this->apiVersion()}", - "Content-Type: application/json", + $auth_token = Utils::encodeApiKey($this->_api_key); + $agent = Utils::getUserAgent(); + return array( + "User-Agent" => $agent, + "Authorization" => "Basic {$auth_token}", + "Accept" => "application/vnd.recurly.{$this->apiVersion()}", + "Content-Type" => "application/json", ); - return join("\r\n", $headers); } /** diff --git a/lib/recurly/http.php b/lib/recurly/http.php new file mode 100644 index 00000000..49f98248 --- /dev/null +++ b/lib/recurly/http.php @@ -0,0 +1,32 @@ + true + ]; + + public function __construct() + { + + } + + public function execute($method, $url, $body, $headers) + { + $options = array_replace(self::$_default_options, [ + 'method' => $method, + 'content' => $body, + ]); + $headers_str = ""; + foreach ($headers as $k => $v) + { + $headers_str .= "$k: $v\r\n"; + } + $options['header'] = $headers_str; + $context = stream_context_create(['http' => $options]); + $result = file_get_contents($url, false, $context); + return array($result, $http_response_header); + } +} \ No newline at end of file diff --git a/lib/recurly/utils.php b/lib/recurly/utils.php new file mode 100644 index 00000000..23af7c4c --- /dev/null +++ b/lib/recurly/utils.php @@ -0,0 +1,19 @@ +client = MockClient::create(); + } + + public function testGetResource200(): void + { + $resource = $this->client->getResource("iexist"); + $this->assertEquals($resource->getId(), "iexist"); + } + + public function testGetResource404(): void + { + $this->expectException(\Recurly\Errors\NotFound::class); + $this->client->getResource("idontexist"); + } +} \ No newline at end of file diff --git a/tests/RecurlyError_Test.php b/tests/RecurlyError_Test.php index 9fc72d73..e6a89818 100644 --- a/tests/RecurlyError_Test.php +++ b/tests/RecurlyError_Test.php @@ -103,16 +103,17 @@ public function testApiErrorClass(): void "message" => "The error message" ) ); - - $response = new \Recurly\Response(json_encode($data)); - $response->setHeaders(array( - 'HTTP/1.1 500 Internal Server Error', - 'Content-Type: application/json' - )); - $result = \Recurly\RecurlyError::fromResponse($response); - $this->assertInstanceOf( - \Recurly\Resources\ErrorMayHaveTransaction::class, - $result->getApiError() - ); } + + // $response = new \Recurly\Response(json_encode($data)); + // $response->setHeaders(array( + // 'HTTP/1.1 500 Internal Server Error', + // 'Content-Type: application/json' + // )); + // $result = \Recurly\RecurlyError::fromResponse($response); + // $this->assertInstanceOf( + // \Recurly\Resources\ErrorMayHaveTransaction::class, + // $result->getApiError() + // ); + // } } \ No newline at end of file diff --git a/tests/mock_client.php b/tests/mock_client.php new file mode 100644 index 00000000..12826b29 --- /dev/null +++ b/tests/mock_client.php @@ -0,0 +1,76 @@ +interpolatePath("/resources/{resource_id}", ['resource_id' => $resource_id]); + return $this->makeRequest('GET', $path, null, null); + } + + public static function create() + { + $client = new MockClient("apikey"); + $http = Mockery::mock(); + + // mock getResource 200 OK + $url = "https://v3.recurly.com/resources/iexist"; + $result = '{"id": "iexist", "object": "test_resource"}'; + $resp_header = self::_generateRespHeader("200 OK"); + $http->allows()->execute( + "GET", $url, NULL, self::_expectedHeaders())->andReturns(array($result, $resp_header)); + + // mock getResource 404 Not Found + $url = "https://v3.recurly.com/resources/idontexist"; + $result = "{\"error\":{\"type\":\"not_found\",\"message\":\"Couldn't find Resource with id = idontexist\",\"params\":[{\"param\":\"resource_id\"}]}}"; + $resp_header = self::_generateRespHeader("404 Not Found"); + $http->allows()->execute( + "GET", $url, NULL, self::_expectedHeaders())->andReturns(array($result, $resp_header)); + + $client->_http = $http; + return $client; + } + + private static function _generateRespHeader($status): array + { + return [ + "HTTP/1.1 $status", + "Date: Wed, 19 Feb 2020 17:52:05 GMT", + "Content-Type: application/json; charset=utf-8", + "Recurly-Version: recurly.v2019-10-10", + "X-RateLimit-Limit: 2000", + "X-RateLimit-Remaining: 1996", + "X-RateLimit-Reset: 1582135020", + //"ETag: W/"9fa8e3452e9d6369c2c88004b3de81b4"" + //"Cache-Control: max-age=0, private, must-revalidate" + "X-Request-Id: 567a17af7875e3ba-ATL", + //"CF-Cache-Status: DYNAMIC", + //"Expect-CT: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"" + //"Strict-Transport-Security: max-age=15552000; includeSubDomains; preload" + "Server: cloudflare", + "CF-RAY: 567a17af7875e3ba-ATL" + ]; + } + + private static function _expectedHeaders(): array + { + $auth_token = Utils::encodeApiKey("apikey"); + $agent = Utils::getUserAgent(); + return [ + "User-Agent" => $agent, + "Authorization" => "Basic {$auth_token}", + "Accept" => "application/vnd.recurly.v2999-01-01", + "Content-Type" => "application/json", + ]; + } +} diff --git a/tests/recurly/resources/test_resource.php b/tests/recurly/resources/test_resource.php index 87e8152b..46652718 100644 --- a/tests/recurly/resources/test_resource.php +++ b/tests/recurly/resources/test_resource.php @@ -4,6 +4,7 @@ class TestResource extends \Recurly\RecurlyResource { + private $_id; private $_object; private $_name; private $_single_child; @@ -15,6 +16,16 @@ class TestResource extends \Recurly\RecurlyResource 'setStringArray' => 'string', ); + public function getId(): string + { + return $this->_id; + } + + public function setId(string $value): void + { + $this->_id = $value; + } + public function getObject(): string { return $this->_object; From 3ada0cc41be1864f0f512f103c8069a522da4281 Mon Sep 17 00:00:00 2001 From: Benjamin Eckel Date: Wed, 19 Feb 2020 13:20:22 -0600 Subject: [PATCH 02/10] HttpAdapter --- lib/recurly/base_client.php | 2 +- lib/recurly/{http.php => http_adapter.php} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename lib/recurly/{http.php => http_adapter.php} (97%) diff --git a/lib/recurly/base_client.php b/lib/recurly/base_client.php index 3a0ca3c5..2de88670 100644 --- a/lib/recurly/base_client.php +++ b/lib/recurly/base_client.php @@ -16,7 +16,7 @@ abstract class BaseClient public function __construct(string $api_key) { $this->_api_key = $api_key; - $this->_http = new Http; + $this->_http = new HttpAdapter; } /** diff --git a/lib/recurly/http.php b/lib/recurly/http_adapter.php similarity index 97% rename from lib/recurly/http.php rename to lib/recurly/http_adapter.php index 49f98248..27cd7d1e 100644 --- a/lib/recurly/http.php +++ b/lib/recurly/http_adapter.php @@ -2,7 +2,7 @@ namespace Recurly; -class Http +class HttpAdapter { private static $_default_options = [ 'ignore_errors' => true From 41e4eacc33e03ee51f41538687e0199b50b67e4a Mon Sep 17 00:00:00 2001 From: Benjamin Eckel Date: Thu, 20 Feb 2020 09:54:10 -0600 Subject: [PATCH 03/10] use scenario apprroach --- lib/recurly/base_client.php | 2 +- lib/recurly/recurly_traits.php | 13 ++++++++ tests/BaseClient_Test.php | 44 ++++++++++++++++++++++++++- tests/mock_client.php | 54 ++++++++++++++++++++++------------ 4 files changed, 93 insertions(+), 20 deletions(-) diff --git a/lib/recurly/base_client.php b/lib/recurly/base_client.php index 2de88670..98e93447 100644 --- a/lib/recurly/base_client.php +++ b/lib/recurly/base_client.php @@ -6,7 +6,7 @@ abstract class BaseClient { private $_baseUrl = 'https://v3.recurly.com'; private $_api_key; - public $_http; + protected $_http; /** * Constructor diff --git a/lib/recurly/recurly_traits.php b/lib/recurly/recurly_traits.php index a134ea50..378d16ca 100644 --- a/lib/recurly/recurly_traits.php +++ b/lib/recurly/recurly_traits.php @@ -5,6 +5,19 @@ trait RecurlyTraits { + private static $_client_version = \Recurly\Version::CURRENT; + + public static function getUserAgent(): string + { + $php_version = phpversion(); + return "Recurly/" . self::$_client_version . "; php " . $php_version; + } + + public static function encodeApiKey($key): string + { + return base64_encode($key); + } + /** * Capitalizes all the words in the $input. * diff --git a/tests/BaseClient_Test.php b/tests/BaseClient_Test.php index 9629c914..c32fbc5b 100644 --- a/tests/BaseClient_Test.php +++ b/tests/BaseClient_Test.php @@ -10,18 +10,60 @@ final class BaseClientTest extends RecurlyTestCase public function setUp(): void { parent::setUp(); - $this->client = MockClient::create(); + $this->client = new MockClient(); + } + + public function tearDown(): void + { + $this->client->clearScenarios(); } public function testGetResource200(): void { + $url = "https://v3.recurly.com/resources/iexist"; + $result = '{"id": "iexist", "object": "test_resource"}'; + $this->client->addScenario("GET", $url, NULL, $result, "200 OK"); + $resource = $this->client->getResource("iexist"); $this->assertEquals($resource->getId(), "iexist"); } public function testGetResource404(): void { + $url = "https://v3.recurly.com/resources/idontexist"; + $result = "{\"error\":{\"type\":\"not_found\",\"message\":\"Couldn't find Resource with id = idontexist\",\"params\":[{\"param\":\"resource_id\"}]}}"; + $this->client->addScenario("GET", $url, NULL, $result, "404 Not Found"); + $this->expectException(\Recurly\Errors\NotFound::class); $this->client->getResource("idontexist"); } + + public function testCreateResource201(): void + { + $url = "https://v3.recurly.com/resources/"; + $result = '{"id": "created", "object": "test_resource", "name": "valid"}'; + $body = '{"name":"valid"}'; + $this->client->addScenario("POST", $url, $body, $result, "201 Created"); + $resource = $this->client->createResource([ "name" => "valid" ]); + $this->assertEquals($resource->getId(), "created"); + } + + public function testCreateResource422(): void + { + $url = "https://v3.recurly.com/resources/"; + $result = "{\"error\":{\"type\":\"validation\",\"message\":\"Name is invalid\",\"params\":[{\"param\":\"name\",\"message\":\"is invalid\"}]}}"; + $body = '{"name":"invalid"}'; + $this->client->addScenario("POST", $url, $body, $result, "422 Unprocessable Entity"); + + $this->expectException(\Recurly\Errors\Validation::class); + $resource = $this->client->createResource([ "name" => "invalid" ]); + } + + public function testDeleteResource(): void + { + $url = "https://v3.recurly.com/resources/iexist"; + $result = ""; + $this->client->addScenario("DELETE", $url, NULL, $result, "204 No Content"); + $empty = $this->client->deleteResource("iexist"); + } } \ No newline at end of file diff --git a/tests/mock_client.php b/tests/mock_client.php index 12826b29..5a825970 100644 --- a/tests/mock_client.php +++ b/tests/mock_client.php @@ -7,6 +7,13 @@ class MockClient extends BaseClient { + + public function __construct() + { + parent::__construct("apikey"); + $this->_http = Mockery::mock(); + } + protected function apiVersion(): string { return "v2999-01-01"; @@ -18,27 +25,38 @@ public function getResource(string $resource_id): TestResource return $this->makeRequest('GET', $path, null, null); } - public static function create() + public function createResource(array $body): TestResource { - $client = new MockClient("apikey"); - $http = Mockery::mock(); + $path = $this->interpolatePath("/resources/", []); + return $this->makeRequest('POST', $path, $body, null); + } - // mock getResource 200 OK - $url = "https://v3.recurly.com/resources/iexist"; - $result = '{"id": "iexist", "object": "test_resource"}'; - $resp_header = self::_generateRespHeader("200 OK"); - $http->allows()->execute( - "GET", $url, NULL, self::_expectedHeaders())->andReturns(array($result, $resp_header)); + public function updateResource(string $resource_id, array $body): TestResource + { + $path = $this->interpolatePath("/resources/{resource_id}", ['resource_id' => $resource_id]); + return $this->makeRequest('PUT', $path, $body, null); + } - // mock getResource 404 Not Found - $url = "https://v3.recurly.com/resources/idontexist"; - $result = "{\"error\":{\"type\":\"not_found\",\"message\":\"Couldn't find Resource with id = idontexist\",\"params\":[{\"param\":\"resource_id\"}]}}"; - $resp_header = self::_generateRespHeader("404 Not Found"); - $http->allows()->execute( - "GET", $url, NULL, self::_expectedHeaders())->andReturns(array($result, $resp_header)); + public function deleteResource(string $resource_id): \Recurly\EmptyResource + { + $path = $this->interpolatePath("/resources/{resource_id}", ['resource_id' => $resource_id]); + return $this->makeRequest('DELETE', $path, null, null); + } - $client->_http = $http; - return $client; + public function addScenario($method, $url, $body, $result, $status): void + { + $resp_header = self::_generateRespHeader($status); + $this->_http->allows()->execute( + $method, + $url, + $body, + self::_expectedHeaders() + )->andReturns(array($result, $resp_header)); + } + + public function clearScenarios(): void + { + $this->_http = Mockery::mock(); } private static function _generateRespHeader($status): array @@ -47,7 +65,7 @@ private static function _generateRespHeader($status): array "HTTP/1.1 $status", "Date: Wed, 19 Feb 2020 17:52:05 GMT", "Content-Type: application/json; charset=utf-8", - "Recurly-Version: recurly.v2019-10-10", + "Recurly-Version: recurly.v2999-01-01", "X-RateLimit-Limit: 2000", "X-RateLimit-Remaining: 1996", "X-RateLimit-Reset: 1582135020", From 30fc38a1d4f577d83ffd26122693f124db46e50d Mon Sep 17 00:00:00 2001 From: Benjamin Eckel Date: Fri, 21 Feb 2020 13:51:54 -0600 Subject: [PATCH 04/10] Refactor to use phpunit, traits --- composer.json | 1 - lib/recurly/base_client.php | 7 ++++--- lib/recurly/http_adapter.php | 7 +++++++ lib/recurly/recurly_traits.php | 9 +++------ lib/recurly/utils.php | 19 ------------------- tests/BaseClient_Test.php | 4 ++-- tests/mock_client.php | 15 +++++++++------ 7 files changed, 25 insertions(+), 37 deletions(-) delete mode 100644 lib/recurly/utils.php diff --git a/composer.json b/composer.json index cfca978d..df977f81 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,6 @@ }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0", - "mockery/mockery": "^1.3", "phpcompatibility/php-compatibility": "^9.3", "phpstan/phpstan": "^0.12.11", "phpunit/phpunit": "^8", diff --git a/lib/recurly/base_client.php b/lib/recurly/base_client.php index 98e93447..408fd542 100644 --- a/lib/recurly/base_client.php +++ b/lib/recurly/base_client.php @@ -4,6 +4,8 @@ abstract class BaseClient { + use RecurlyTraits; + private $_baseUrl = 'https://v3.recurly.com'; private $_api_key; protected $_http; @@ -58,7 +60,6 @@ private function _getResponse(string $method, string $path, ?array $body = [], ? { $request = new \Recurly\Request($method, $path, $body, $params); - $body = isset($body) && !empty($body) ? json_encode($body) : null; $url = $this->_buildPath($path, $params); list($result, $response_header) = $this->_http->execute($method, $url, $body, $this->_headers()); @@ -139,8 +140,8 @@ protected function interpolatePath(string $path, array $options = []): string */ private function _headers(): array { - $auth_token = Utils::encodeApiKey($this->_api_key); - $agent = Utils::getUserAgent(); + $auth_token = self::encodeApiKey($this->_api_key); + $agent = self::getUserAgent(); return array( "User-Agent" => $agent, "Authorization" => "Basic {$auth_token}", diff --git a/lib/recurly/http_adapter.php b/lib/recurly/http_adapter.php index 27cd7d1e..31d2d23b 100644 --- a/lib/recurly/http_adapter.php +++ b/lib/recurly/http_adapter.php @@ -13,8 +13,15 @@ public function __construct() } + /** + * @param string $method HTTP method to use + * @param string $url Fully qualified URL + * @param array $body The request body + * @param array $headers HTTP headers + */ public function execute($method, $url, $body, $headers) { + $body = empty($body) ? NULL : json_encode($body); $options = array_replace(self::$_default_options, [ 'method' => $method, 'content' => $body, diff --git a/lib/recurly/recurly_traits.php b/lib/recurly/recurly_traits.php index 378d16ca..bf4770df 100644 --- a/lib/recurly/recurly_traits.php +++ b/lib/recurly/recurly_traits.php @@ -4,16 +4,13 @@ trait RecurlyTraits { - - private static $_client_version = \Recurly\Version::CURRENT; - - public static function getUserAgent(): string + protected static function getUserAgent(): string { $php_version = phpversion(); - return "Recurly/" . self::$_client_version . "; php " . $php_version; + return "Recurly/" . \Recurly\Version::CURRENT . "; php " . $php_version; } - public static function encodeApiKey($key): string + protected static function encodeApiKey($key): string { return base64_encode($key); } diff --git a/lib/recurly/utils.php b/lib/recurly/utils.php deleted file mode 100644 index 23af7c4c..00000000 --- a/lib/recurly/utils.php +++ /dev/null @@ -1,19 +0,0 @@ - "valid" ]; $this->client->addScenario("POST", $url, $body, $result, "201 Created"); $resource = $this->client->createResource([ "name" => "valid" ]); $this->assertEquals($resource->getId(), "created"); @@ -52,7 +52,7 @@ public function testCreateResource422(): void { $url = "https://v3.recurly.com/resources/"; $result = "{\"error\":{\"type\":\"validation\",\"message\":\"Name is invalid\",\"params\":[{\"param\":\"name\",\"message\":\"is invalid\"}]}}"; - $body = '{"name":"invalid"}'; + $body = [ "name" => "invalid" ]; $this->client->addScenario("POST", $url, $body, $result, "422 Unprocessable Entity"); $this->expectException(\Recurly\Errors\Validation::class); diff --git a/tests/mock_client.php b/tests/mock_client.php index 5a825970..b359bcd3 100644 --- a/tests/mock_client.php +++ b/tests/mock_client.php @@ -4,14 +4,17 @@ use Recurly\Resources\TestResource; use Recurly\BaseClient; use Recurly\Utils; +use PHPUnit\Framework\MockObject\Generator; +use Recurly\HttpAdapter; class MockClient extends BaseClient { + use Recurly\RecurlyTraits; public function __construct() { parent::__construct("apikey"); - $this->_http = Mockery::mock(); + $this->_http = (new Generator())->getMock(HttpAdapter::class); } protected function apiVersion(): string @@ -46,17 +49,17 @@ public function deleteResource(string $resource_id): \Recurly\EmptyResource public function addScenario($method, $url, $body, $result, $status): void { $resp_header = self::_generateRespHeader($status); - $this->_http->allows()->execute( + $this->_http->method('execute')->with( $method, $url, $body, self::_expectedHeaders() - )->andReturns(array($result, $resp_header)); + )->willReturn(array($result, $resp_header)); } public function clearScenarios(): void { - $this->_http = Mockery::mock(); + $this->_http = (new Generator())->getMock(HttpAdapter::class); } private static function _generateRespHeader($status): array @@ -82,8 +85,8 @@ private static function _generateRespHeader($status): array private static function _expectedHeaders(): array { - $auth_token = Utils::encodeApiKey("apikey"); - $agent = Utils::getUserAgent(); + $auth_token = self::encodeApiKey("apikey"); + $agent = self::getUserAgent(); return [ "User-Agent" => $agent, "Authorization" => "Basic {$auth_token}", From 755e44b184c0e7fb696e8b2332dbef9f5a7395d3 Mon Sep 17 00:00:00 2001 From: Benjamin Eckel Date: Fri, 21 Feb 2020 13:56:17 -0600 Subject: [PATCH 05/10] Add back err test --- tests/RecurlyError_Test.php | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/tests/RecurlyError_Test.php b/tests/RecurlyError_Test.php index e6a89818..9fc72d73 100644 --- a/tests/RecurlyError_Test.php +++ b/tests/RecurlyError_Test.php @@ -103,17 +103,16 @@ public function testApiErrorClass(): void "message" => "The error message" ) ); - } - // $response = new \Recurly\Response(json_encode($data)); - // $response->setHeaders(array( - // 'HTTP/1.1 500 Internal Server Error', - // 'Content-Type: application/json' - // )); - // $result = \Recurly\RecurlyError::fromResponse($response); - // $this->assertInstanceOf( - // \Recurly\Resources\ErrorMayHaveTransaction::class, - // $result->getApiError() - // ); - // } + $response = new \Recurly\Response(json_encode($data)); + $response->setHeaders(array( + 'HTTP/1.1 500 Internal Server Error', + 'Content-Type: application/json' + )); + $result = \Recurly\RecurlyError::fromResponse($response); + $this->assertInstanceOf( + \Recurly\Resources\ErrorMayHaveTransaction::class, + $result->getApiError() + ); + } } \ No newline at end of file From aa97a93565ebaef57d03ea236f023f6285af0a1f Mon Sep 17 00:00:00 2001 From: Benjamin Eckel Date: Fri, 21 Feb 2020 14:40:12 -0600 Subject: [PATCH 06/10] More cases --- lib/recurly/base_client.php | 2 +- tests/BaseClient_Test.php | 43 ++++++++++++++++++++++++++++++++++++- tests/mock_client.php | 6 ++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/lib/recurly/base_client.php b/lib/recurly/base_client.php index 408fd542..5cde1cba 100644 --- a/lib/recurly/base_client.php +++ b/lib/recurly/base_client.php @@ -108,7 +108,7 @@ public function pagerCount(string $path, ?array $params = []): \Recurly\Response */ private function _buildPath(string $path, ?array $params): string { - if (isset($params)) { + if (isset($params) && !empty($params)) { return $this->_baseUrl . $path . '?' . http_build_query($params); } else { return $this->_baseUrl . $path; diff --git a/tests/BaseClient_Test.php b/tests/BaseClient_Test.php index 2728263e..ce9a6c65 100644 --- a/tests/BaseClient_Test.php +++ b/tests/BaseClient_Test.php @@ -59,11 +59,52 @@ public function testCreateResource422(): void $resource = $this->client->createResource([ "name" => "invalid" ]); } - public function testDeleteResource(): void + public function testDeleteResource204(): void { $url = "https://v3.recurly.com/resources/iexist"; $result = ""; $this->client->addScenario("DELETE", $url, NULL, $result, "204 No Content"); $empty = $this->client->deleteResource("iexist"); } + + public function testUpdateResource200(): void + { + $url = "https://v3.recurly.com/resources/iexist"; + $result = '{"id": "iexist", "object": "test_resource", "name": "newname"}'; + $body = [ "name" => "newname" ]; + $this->client->addScenario("PUT", $url, $body, $result, "200 OK"); + + $resource = $this->client->updateResource("iexist", $body); + $this->assertEquals($resource->getName(), "newname"); + } + + public function testListResources200(): void + { + $url = "https://v3.recurly.com/resources"; + $result = '{ "object": "list", "has_more": false, "next": null, "data": [{"id": "iexist", "object": "test_resource", "name": "newname"}]}'; + $this->client->addScenario("GET", $url, NULL, $result, "200 OK"); + + $resources = $this->client->listResources(); + $count = 0; + foreach($resources as $resource) { + $count = $count + 1; + $this->assertEquals($resource->getId(), "iexist"); + } + $this->assertEquals($count, 1); + } + + public function testListResourcesWithParams200(): void + { + $url = "https://v3.recurly.com/resources?limit=1"; + $result = '{ "object": "list", "has_more": false, "next": null, "data": [{"id": "iexist", "object": "test_resource", "name": "newname"}]}'; + $this->client->addScenario("GET", $url, NULL, $result, "200 OK"); + + $resources = $this->client->listResources([ "limit" => 1 ]); + $count = 0; + foreach($resources as $resource) { + $count = $count + 1; + $this->assertEquals($resource->getId(), "iexist"); + } + $this->assertEquals($count, 1); + } } \ No newline at end of file diff --git a/tests/mock_client.php b/tests/mock_client.php index b359bcd3..f65f67af 100644 --- a/tests/mock_client.php +++ b/tests/mock_client.php @@ -22,6 +22,12 @@ protected function apiVersion(): string return "v2999-01-01"; } + public function listResources(array $options = []): \Recurly\Pager + { + $path = $this->interpolatePath("/resources", []); + return new \Recurly\Pager($this, $path, $options); + } + public function getResource(string $resource_id): TestResource { $path = $this->interpolatePath("/resources/{resource_id}", ['resource_id' => $resource_id]); From 47de3993ea322628b5f0ec1b840e6816696ca7a8 Mon Sep 17 00:00:00 2001 From: Benjamin Eckel Date: Fri, 21 Feb 2020 15:15:52 -0600 Subject: [PATCH 07/10] Remove setApiUrl as its not needed --- lib/recurly/base_client.php | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/lib/recurly/base_client.php b/lib/recurly/base_client.php index 5cde1cba..6562e723 100644 --- a/lib/recurly/base_client.php +++ b/lib/recurly/base_client.php @@ -149,23 +149,4 @@ private function _headers(): array "Content-Type" => "application/json", ); } - - /** - * Method to override the default Recurly API URL. - * This is primarily for Recurly testing - * - * @param string $url The replacement URL to use - * - * @return void - */ - public function setApiUrl(string $url): void - { - echo "[SECURITY WARNING] setApiUrl is for testing only and not supported in production." . PHP_EOL; - if (getenv("RECURLY_INSECURE") == "true") { - $this->_baseUrl = $url; - } else { - echo "ApiUrl not changed. To change, set the environment variable RECURLY_INSECURE to true" . PHP_EOL; - } - } - } From addfc5657652a7838b2ea432fa1611df90073b62 Mon Sep 17 00:00:00 2001 From: Benjamin Eckel Date: Fri, 21 Feb 2020 15:20:49 -0600 Subject: [PATCH 08/10] Add some docs for HttpAdapter --- lib/recurly/http_adapter.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/recurly/http_adapter.php b/lib/recurly/http_adapter.php index 31d2d23b..4ec87411 100644 --- a/lib/recurly/http_adapter.php +++ b/lib/recurly/http_adapter.php @@ -2,17 +2,18 @@ namespace Recurly; +/** + * This class abstracts away all the PHP-level HTTP + * code. This allows us to easily mock out the HTTP + * calls in BaseClient by injecting a mocked version of + * this adapter. + */ class HttpAdapter { private static $_default_options = [ 'ignore_errors' => true ]; - public function __construct() - { - - } - /** * @param string $method HTTP method to use * @param string $url Fully qualified URL From dc48da36d01e036abc92a7062215b2c387422998 Mon Sep 17 00:00:00 2001 From: Benjamin Eckel Date: Fri, 21 Feb 2020 15:26:51 -0600 Subject: [PATCH 09/10] remove mocked headers --- tests/mock_client.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/mock_client.php b/tests/mock_client.php index f65f67af..667b4000 100644 --- a/tests/mock_client.php +++ b/tests/mock_client.php @@ -78,12 +78,7 @@ private static function _generateRespHeader($status): array "X-RateLimit-Limit: 2000", "X-RateLimit-Remaining: 1996", "X-RateLimit-Reset: 1582135020", - //"ETag: W/"9fa8e3452e9d6369c2c88004b3de81b4"" - //"Cache-Control: max-age=0, private, must-revalidate" "X-Request-Id: 567a17af7875e3ba-ATL", - //"CF-Cache-Status: DYNAMIC", - //"Expect-CT: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"" - //"Strict-Transport-Security: max-age=15552000; includeSubDomains; preload" "Server: cloudflare", "CF-RAY: 567a17af7875e3ba-ATL" ]; From 959ccd059cd166104ef161f8418ebd5cae9c6a06 Mon Sep 17 00:00:00 2001 From: Doug Miller Date: Fri, 21 Feb 2020 16:40:45 -0600 Subject: [PATCH 10/10] Fixing phpcs complaints --- lib/recurly/base_client.php | 6 +++--- lib/recurly/http_adapter.php | 31 ++++++++++++++++++------------- lib/recurly/recurly_traits.php | 14 +++++++++++++- tests/mock_client.php | 6 +++--- 4 files changed, 37 insertions(+), 20 deletions(-) diff --git a/lib/recurly/base_client.php b/lib/recurly/base_client.php index 6562e723..78d0dabe 100644 --- a/lib/recurly/base_client.php +++ b/lib/recurly/base_client.php @@ -8,7 +8,7 @@ abstract class BaseClient private $_baseUrl = 'https://v3.recurly.com'; private $_api_key; - protected $_http; + protected $http; /** * Constructor @@ -18,7 +18,7 @@ abstract class BaseClient public function __construct(string $api_key) { $this->_api_key = $api_key; - $this->_http = new HttpAdapter; + $this->http = new HttpAdapter; } /** @@ -61,7 +61,7 @@ private function _getResponse(string $method, string $path, ?array $body = [], ? $request = new \Recurly\Request($method, $path, $body, $params); $url = $this->_buildPath($path, $params); - list($result, $response_header) = $this->_http->execute($method, $url, $body, $this->_headers()); + list($result, $response_header) = $this->http->execute($method, $url, $body, $this->_headers()); // TODO: The $request should be added to the $response $response = new \Recurly\Response($result); diff --git a/lib/recurly/http_adapter.php b/lib/recurly/http_adapter.php index 4ec87411..ae7c9480 100644 --- a/lib/recurly/http_adapter.php +++ b/lib/recurly/http_adapter.php @@ -1,13 +1,13 @@ $method, 'content' => $body, - ]); + ] + ); $headers_str = ""; - foreach ($headers as $k => $v) - { + foreach ($headers as $k => $v) { $headers_str .= "$k: $v\r\n"; } $options['header'] = $headers_str; diff --git a/lib/recurly/recurly_traits.php b/lib/recurly/recurly_traits.php index bf4770df..4e5f00bd 100644 --- a/lib/recurly/recurly_traits.php +++ b/lib/recurly/recurly_traits.php @@ -4,13 +4,25 @@ trait RecurlyTraits { + /** + * Generates User-Agent for API requests + * + * @return string Recurly client User-Agent string + */ protected static function getUserAgent(): string { $php_version = phpversion(); return "Recurly/" . \Recurly\Version::CURRENT . "; php " . $php_version; } - protected static function encodeApiKey($key): string + /** + * Base64 encodes the API key + * + * @param string $key The API key to encode + * + * @return string base64 encoded API key + */ + protected static function encodeApiKey(string $key): string { return base64_encode($key); } diff --git a/tests/mock_client.php b/tests/mock_client.php index 667b4000..f0f371ec 100644 --- a/tests/mock_client.php +++ b/tests/mock_client.php @@ -14,7 +14,7 @@ class MockClient extends BaseClient public function __construct() { parent::__construct("apikey"); - $this->_http = (new Generator())->getMock(HttpAdapter::class); + $this->http = (new Generator())->getMock(HttpAdapter::class); } protected function apiVersion(): string @@ -55,7 +55,7 @@ public function deleteResource(string $resource_id): \Recurly\EmptyResource public function addScenario($method, $url, $body, $result, $status): void { $resp_header = self::_generateRespHeader($status); - $this->_http->method('execute')->with( + $this->http->method('execute')->with( $method, $url, $body, @@ -65,7 +65,7 @@ public function addScenario($method, $url, $body, $result, $status): void public function clearScenarios(): void { - $this->_http = (new Generator())->getMock(HttpAdapter::class); + $this->http = (new Generator())->getMock(HttpAdapter::class); } private static function _generateRespHeader($status): array