diff --git a/.gitignore b/.gitignore index f7d3b34..c687625 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,4 @@ composer.lock composer.phar vendor reports -coverage.xml +coverage diff --git a/composer.json b/composer.json index 446e477..3763ff8 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,8 @@ "phpunit/phpunit": "^8.5 || ^9.5", "friendsofphp/php-cs-fixer": "^2.19.3 || ^3.9.5", "phpstan/phpstan": "1.11.0", - "vlucas/phpdotenv": "5.5.0" + "vlucas/phpdotenv": "5.5.0", + "brain/monkey": "2.6.2" }, "autoload": { "classmap": ["lib/omise/"] @@ -44,7 +45,8 @@ }, "scripts": { "test:unit": "TEST_TYPE=unit ./vendor/bin/phpunit --testdox", - "test:coverage": "XDEBUG_MODE=coverage TEST_TYPE=unit ./vendor/bin/phpunit --coverage-clover=coverage.xml", + "test:coverage:html": "XDEBUG_MODE=coverage TEST_TYPE=unit ./vendor/bin/phpunit --coverage-html=coverage", + "test:coverage:xml": "XDEBUG_MODE=coverage TEST_TYPE=unit ./vendor/bin/phpunit --coverage-clover=coverage/coverage.xml", "fix": "vendor/bin/php-cs-fixer fix -vvv --diff --dry-run --allow-risky=yes --using-cache=no", "analyse": "vendor/bin/phpstan analyse lib tests", "phpstan": "vendor/bin/phpstan analyse lib tests" diff --git a/lib/Omise.php b/lib/Omise.php index 8b20b37..4c02da7 100644 --- a/lib/Omise.php +++ b/lib/Omise.php @@ -3,6 +3,8 @@ // Cores and utilities. require_once __DIR__ . '/omise/res/obj/OmiseObject.php'; require_once __DIR__ . '/omise/res/OmiseApiResource.php'; +require_once __DIR__ . '/omise/http/OmiseHttpExecutorInterface.php'; +require_once __DIR__ . '/omise/http/OmiseHttpExecutor.php'; // Errors require_once __DIR__ . '/omise/exception/OmiseExceptions.php'; diff --git a/lib/omise/http/OmiseHttpExecutor.php b/lib/omise/http/OmiseHttpExecutor.php new file mode 100644 index 0000000..32701d1 --- /dev/null +++ b/lib/omise/http/OmiseHttpExecutor.php @@ -0,0 +1,87 @@ +genOptions($requestMethod, $key . ':', $params)); + + // Make a request or thrown an exception. + if (($result = curl_exec($ch)) === false) { + $error = curl_error($ch); + curl_close($ch); + + throw new Exception($error); + } + + // Close. + curl_close($ch); + + return $result; + } + + /** + * Creates an option for php-curl from the given request method and parameters in an associative array. + * + * @param string $requestMethod + * @param array $params + * + * @return array + */ + private function genOptions($requestMethod, $userpwd, $params) + { + $user_agent = 'OmisePHP/' . OMISE_PHP_LIB_VERSION . ' PHP/' . PHP_VERSION; + $omise_api_version = defined('OMISE_API_VERSION') ? OMISE_API_VERSION : null; + + $options = [ + // Set the HTTP version to 1.1. + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + // Set the request method. + CURLOPT_CUSTOMREQUEST => $requestMethod, + // Make php-curl returns the data as string. + CURLOPT_RETURNTRANSFER => true, + // Do not include the header in the output. + CURLOPT_HEADER => false, + // Track the header request string and set the referer on redirect. + CURLINFO_HEADER_OUT => true, + CURLOPT_AUTOREFERER => true, + // Make HTTP error code above 400 an error. + // CURLOPT_FAILONERROR => true, + // Time before the request is aborted. + CURLOPT_TIMEOUT => $this->OMISE_TIMEOUT, + // Time before the request is aborted when attempting to connect. + CURLOPT_CONNECTTIMEOUT => $this->OMISE_CONNECTTIMEOUT, + // Authentication. + CURLOPT_USERPWD => $userpwd + ]; + + // Config Omise API Version + if ($omise_api_version) { + $options += [CURLOPT_HTTPHEADER => ['Omise-Version: ' . $omise_api_version]]; + + $user_agent .= ' OmiseAPI/' . $omise_api_version; + } + + // Config UserAgent + if (defined('OMISE_USER_AGENT_SUFFIX')) { + $options += [CURLOPT_USERAGENT => $user_agent . ' ' . OMISE_USER_AGENT_SUFFIX]; + } else { + $options += [CURLOPT_USERAGENT => $user_agent]; + } + + // Also merge POST parameters with the option. + if (is_array($params) && count($params) > 0) { + $http_query = http_build_query($params); + $http_query = preg_replace('/%5B\d+%5D/simU', '%5B%5D', $http_query); + + $options += [CURLOPT_POSTFIELDS => $http_query]; + } + + return $options; + } +} diff --git a/lib/omise/http/OmiseHttpExecutorInterface.php b/lib/omise/http/OmiseHttpExecutorInterface.php new file mode 100644 index 0000000..3594703 --- /dev/null +++ b/lib/omise/http/OmiseHttpExecutorInterface.php @@ -0,0 +1,16 @@ +_executeTest($url, $requestMethod, $key, $params); - } else { - $result = $this->_executeCurl($url, $requestMethod, $key, $params); - } + $result = self::$httpExecutor->execute($url, $requestMethod, $key, $params); // Decode the JSON response as an associative array. $array = json_decode($result, true); @@ -210,147 +209,6 @@ protected static function isValidAPIResponse($array) return $array && count($array) && isset($array['object']); } - /** - * @param string $url - * @param string $requestMethod - * @param array $params - * - * @throws OmiseException - * - * @return string - */ - private function _executeTest($url, $requestMethod, $key, $params = null) - { - // Extract only hostname and URL path without trailing slash. - $parsed = parse_url($url); - $request_url = $parsed['host'] . rtrim($parsed['path'], '/'); - - // Convert query string into filename friendly format. - if (!empty($parsed['query'])) { - $query = base64_encode($parsed['query']); - $query = str_replace(['+', '/', '='], ['-', '_', ''], $query); - $request_url = $request_url . '-' . $query; - } - - // Finally. - $request_url = dirname(__FILE__) . '/../../../tests/fixtures/' . $request_url . '-' . strtolower($requestMethod) . '.json'; - - // Make a request from Curl if json file was not exists. - if (!file_exists($request_url)) { - // Get a directory that's file should contain. - $request_dir = explode('/', $request_url); - unset($request_dir[count($request_dir) - 1]); - $request_dir = implode('/', $request_dir); - - // Create directory if it not exists. - if (!file_exists($request_dir)) { - mkdir($request_dir, 0777, true); - } - - $result = $this->_executeCurl($url, $requestMethod, $key, $params); - - $f = fopen($request_url, 'w'); - if ($f) { - fwrite($f, $result); - - fclose($f); - } - } else { // Or get response from json file. - $result = file_get_contents($request_url); - } - - return $result; - } - - /** - * @param string $url - * @param string $requestMethod - * @param array $params - * - * @throws OmiseException - * - * @return string - */ - private function _executeCurl($url, $requestMethod, $key, $params = null) - { - $ch = curl_init($url); - - curl_setopt_array($ch, $this->genOptions($requestMethod, $key . ':', $params)); - - // Make a request or thrown an exception. - if (($result = curl_exec($ch)) === false) { - $error = curl_error($ch); - curl_close($ch); - - throw new Exception($error); - } - - // Close. - curl_close($ch); - - return $result; - } - - /** - * Creates an option for php-curl from the given request method and parameters in an associative array. - * - * @param string $requestMethod - * @param array $params - * - * @return array - */ - private function genOptions($requestMethod, $userpwd, $params) - { - $user_agent = 'OmisePHP/' . OMISE_PHP_LIB_VERSION . ' PHP/' . PHP_VERSION; - $omise_api_version = defined('OMISE_API_VERSION') ? OMISE_API_VERSION : null; - - $options = [ - // Set the HTTP version to 1.1. - CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, - // Set the request method. - CURLOPT_CUSTOMREQUEST => $requestMethod, - // Make php-curl returns the data as string. - CURLOPT_RETURNTRANSFER => true, - // Do not include the header in the output. - CURLOPT_HEADER => false, - // Track the header request string and set the referer on redirect. - CURLINFO_HEADER_OUT => true, - CURLOPT_AUTOREFERER => true, - // Make HTTP error code above 400 an error. - // CURLOPT_FAILONERROR => true, - // Time before the request is aborted. - CURLOPT_TIMEOUT => $this->OMISE_TIMEOUT, - // Time before the request is aborted when attempting to connect. - CURLOPT_CONNECTTIMEOUT => $this->OMISE_CONNECTTIMEOUT, - // Authentication. - CURLOPT_USERPWD => $userpwd - ]; - - // Config Omise API Version - if ($omise_api_version) { - $options += [CURLOPT_HTTPHEADER => ['Omise-Version: ' . $omise_api_version]]; - - $user_agent .= ' OmiseAPI/' . $omise_api_version; - } - - // Config UserAgent - if (defined('OMISE_USER_AGENT_SUFFIX')) { - $options += [CURLOPT_USERAGENT => $user_agent . ' ' . OMISE_USER_AGENT_SUFFIX]; - } else { - $options += [CURLOPT_USERAGENT => $user_agent]; - } - - // Also merge POST parameters with the option. - if (is_array($params) && count($params) > 0) { - $http_query = http_build_query($params); - $http_query = preg_replace('/%5B\d+%5D/simU', '%5B%5D', $http_query); - - $options += [CURLOPT_POSTFIELDS => $http_query]; - } - - return $options; - } - /** * Checks whether the resource has been destroyed. * diff --git a/phpunit.xml b/phpunit.xml index 33b5acf..b641a8f 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,6 +1,6 @@ - ./tests/InitTest.php - ./tests/OmiseExceptionTest.php - ./tests/ClassExistsTest.php - ./tests/AccountTest.php - ./tests/BalanceTest.php - ./tests/CapabilityTest.php - ./tests/ChargeTest.php - ./tests/DisputeTest.php - ./tests/EventTest.php - ./tests/ForexTest.php - ./tests/LinkTest.php - ./tests/OccurrenceTest.php - ./tests/ReceiptTest.php - ./tests/TransferTest.php - ./tests/RecipientTest.php - ./tests/RefundTest.php - ./tests/SchedulerTest.php - ./tests/SearchTest.php - ./tests/SourceTest.php - ./tests/TokenTest.php - ./tests/TransactionTest.php - ./tests/CardTest.php - ./tests/CustomerTest.php + ./tests/unit/InitTest.php + ./tests/unit/ClassExistsTest.php + + ./tests/unit/http/OmiseHttpExecutorTest.php + ./tests/unit/exception/OmiseExceptionTest.php + + ./tests/unit/AccountTest.php + ./tests/unit/BalanceTest.php + ./tests/unit/CapabilityTest.php + ./tests/unit/ChargeTest.php + ./tests/unit/DisputeTest.php + ./tests/unit/EventTest.php + ./tests/unit/ForexTest.php + ./tests/unit/LinkTest.php + ./tests/unit/OccurrenceTest.php + ./tests/unit/ReceiptTest.php + ./tests/unit/TransferTest.php + ./tests/unit/RecipientTest.php + ./tests/unit/RefundTest.php + ./tests/unit/SchedulerTest.php + ./tests/unit/SearchTest.php + ./tests/unit/SourceTest.php + ./tests/unit/TokenTest.php + ./tests/unit/TransactionTest.php + ./tests/unit/CardTest.php + ./tests/unit/CustomerTest.php diff --git a/tests/UnitTestCase.php b/tests/UnitTestCase.php new file mode 100644 index 0000000..d117f85 --- /dev/null +++ b/tests/UnitTestCase.php @@ -0,0 +1,22 @@ +execute($url, $requestMethod, $key, $params); + + $f = fopen($request_url, 'w'); + if ($f) { + fwrite($f, $result); + + fclose($f); + } + } else { // Or get response from json file. + $result = file_get_contents($request_url); + } + + return $result; + } +} diff --git a/tests/patchwork.json b/tests/patchwork.json new file mode 100644 index 0000000..14ec87d --- /dev/null +++ b/tests/patchwork.json @@ -0,0 +1,9 @@ +{ + "redefinable-internals": [ + "curl_close", + "curl_error", + "curl_exec", + "curl_init", + "curl_setopt_array" + ] +} diff --git a/tests/AccountTest.php b/tests/unit/AccountTest.php similarity index 100% rename from tests/AccountTest.php rename to tests/unit/AccountTest.php diff --git a/tests/BalanceTest.php b/tests/unit/BalanceTest.php similarity index 100% rename from tests/BalanceTest.php rename to tests/unit/BalanceTest.php diff --git a/tests/CapabilityTest.php b/tests/unit/CapabilityTest.php similarity index 100% rename from tests/CapabilityTest.php rename to tests/unit/CapabilityTest.php diff --git a/tests/CardTest.php b/tests/unit/CardTest.php similarity index 100% rename from tests/CardTest.php rename to tests/unit/CardTest.php diff --git a/tests/ChargeTest.php b/tests/unit/ChargeTest.php similarity index 100% rename from tests/ChargeTest.php rename to tests/unit/ChargeTest.php diff --git a/tests/ClassExistsTest.php b/tests/unit/ClassExistsTest.php similarity index 93% rename from tests/ClassExistsTest.php rename to tests/unit/ClassExistsTest.php index 0ad1774..39f31ca 100644 --- a/tests/ClassExistsTest.php +++ b/tests/unit/ClassExistsTest.php @@ -45,5 +45,7 @@ public function testResourceClassesExists() $this->assertTrue(class_exists('OmiseVaultResource')); $this->assertTrue(class_exists('OmiseObject')); $this->assertTrue(class_exists('OmiseException')); + $this->assertTrue(class_exists('OmiseHttpExecutor')); + $this->assertTrue(interface_exists('OmiseHttpExecutorInterface')); } } diff --git a/tests/CustomerTest.php b/tests/unit/CustomerTest.php similarity index 100% rename from tests/CustomerTest.php rename to tests/unit/CustomerTest.php diff --git a/tests/DisputeTest.php b/tests/unit/DisputeTest.php similarity index 100% rename from tests/DisputeTest.php rename to tests/unit/DisputeTest.php diff --git a/tests/EventTest.php b/tests/unit/EventTest.php similarity index 100% rename from tests/EventTest.php rename to tests/unit/EventTest.php diff --git a/tests/ForexTest.php b/tests/unit/ForexTest.php similarity index 100% rename from tests/ForexTest.php rename to tests/unit/ForexTest.php diff --git a/tests/InitTest.php b/tests/unit/InitTest.php similarity index 100% rename from tests/InitTest.php rename to tests/unit/InitTest.php diff --git a/tests/LinkTest.php b/tests/unit/LinkTest.php similarity index 100% rename from tests/LinkTest.php rename to tests/unit/LinkTest.php diff --git a/tests/OccurrenceTest.php b/tests/unit/OccurrenceTest.php similarity index 100% rename from tests/OccurrenceTest.php rename to tests/unit/OccurrenceTest.php diff --git a/tests/ReceiptTest.php b/tests/unit/ReceiptTest.php similarity index 100% rename from tests/ReceiptTest.php rename to tests/unit/ReceiptTest.php diff --git a/tests/RecipientTest.php b/tests/unit/RecipientTest.php similarity index 100% rename from tests/RecipientTest.php rename to tests/unit/RecipientTest.php diff --git a/tests/RefundTest.php b/tests/unit/RefundTest.php similarity index 100% rename from tests/RefundTest.php rename to tests/unit/RefundTest.php diff --git a/tests/SchedulerTest.php b/tests/unit/SchedulerTest.php similarity index 100% rename from tests/SchedulerTest.php rename to tests/unit/SchedulerTest.php diff --git a/tests/SearchTest.php b/tests/unit/SearchTest.php similarity index 100% rename from tests/SearchTest.php rename to tests/unit/SearchTest.php diff --git a/tests/SourceTest.php b/tests/unit/SourceTest.php similarity index 100% rename from tests/SourceTest.php rename to tests/unit/SourceTest.php diff --git a/tests/TokenTest.php b/tests/unit/TokenTest.php similarity index 100% rename from tests/TokenTest.php rename to tests/unit/TokenTest.php diff --git a/tests/TransactionTest.php b/tests/unit/TransactionTest.php similarity index 100% rename from tests/TransactionTest.php rename to tests/unit/TransactionTest.php diff --git a/tests/TransferTest.php b/tests/unit/TransferTest.php similarity index 100% rename from tests/TransferTest.php rename to tests/unit/TransferTest.php diff --git a/tests/_index.php b/tests/unit/_index.php similarity index 87% rename from tests/_index.php rename to tests/unit/_index.php index f759672..9736f2a 100644 --- a/tests/_index.php +++ b/tests/unit/_index.php @@ -18,7 +18,11 @@ define('OMISE_SECRET_KEY', $secretKey); define('OMISE_API_VERSION', '2019-05-29'); -include __DIR__ . '/traits/ChargeTrait.php'; +include __DIR__ . '/../traits/ChargeTrait.php'; +include __DIR__ . '/../helpers/HttpTestExecutor.php'; +include __DIR__ . '/../UnitTestCase.php'; + +define('OMISE_HTTP_TEST_EXECUTOR', new HttpTestExecutor()); /** * this function is created to debug easily diff --git a/tests/OmiseExceptionTest.php b/tests/unit/exception/OmiseExceptionTest.php similarity index 100% rename from tests/OmiseExceptionTest.php rename to tests/unit/exception/OmiseExceptionTest.php diff --git a/tests/unit/http/OmiseHttpExecutorTest.php b/tests/unit/http/OmiseHttpExecutorTest.php new file mode 100644 index 0000000..de66b65 --- /dev/null +++ b/tests/unit/http/OmiseHttpExecutorTest.php @@ -0,0 +1,121 @@ + [12345], + 'string' => 'value', + ]; + + $expectedUserAgent = sprintf('OmisePHP/2.18.0 PHP/%s OmiseAPI/2019-05-29', PHP_VERSION); + $expectedCurlOpts = [ + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => $requestMethod, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADER => false, + CURLINFO_HEADER_OUT => true, + CURLOPT_AUTOREFERER => true, + CURLOPT_TIMEOUT => 60, + CURLOPT_CONNECTTIMEOUT => 30, + CURLOPT_USERPWD => $key . ':', + CURLOPT_HTTPHEADER => ['Omise-Version: 2019-05-29'], + CURLOPT_USERAGENT => $expectedUserAgent, + CURLOPT_POSTFIELDS => 'numbers%5B%5D=12345&string=value', + ]; + + $this->mockCurl( + $url, + function () { + return '{"success":1}'; + }, + function ($data) use ($expectedCurlOpts) { + return json_encode($expectedCurlOpts) === json_encode($data); + } + ); + + $result = (new OmiseHttpExecutor())->execute($url, $requestMethod, $key, $params); + + $this->assertEquals($result, '{"success":1}'); + } + + public function execute_http_request_provider() + { + return [ + ['GET'], + ['POST'], + ['PATCH'], + ['DELETE'], + ]; + } + + public function test_execute_http_request_with_custom_agent_suffix() + { + define('OMISE_USER_AGENT_SUFFIX', 'MyApp'); + $url = 'https://www.omise.co'; + + $expectedUserAgent = sprintf('OmisePHP/2.18.0 PHP/%s OmiseAPI/2019-05-29 MyApp', PHP_VERSION); + $this->mockCurl( + $url, + function () { + return '{"success":1}'; + }, + function ($data) use ($expectedUserAgent) { + return $data[CURLOPT_USERAGENT] === $expectedUserAgent; + } + ); + + $result = (new OmiseHttpExecutor())->execute($url, 'GET', 'pkey_xxx'); + + $this->assertEquals($result, '{"success":1}'); + } + + public function test_execute_http_request_returns_failed_response() + { + $url = 'https://www.omise.co'; + + Functions\expect('curl_error')->once()->andReturn('Request Timeout'); + $this->mockCurl( + $url, + function () { + return false; + }, + function ($data) { + return true; + } + ); + + $this->expectExceptionMessage('Request Timeout'); + + (new OmiseHttpExecutor())->execute($url, 'GET', 'pkey_xxx'); + } + + private function mockCurl($url, $curlFn, $assertCurlOptsFn) + { + Functions\expect('curl_init')->once()->with($url); + Functions\expect('curl_setopt_array') + ->once() + ->with(Mockery::any(), Mockery::on($assertCurlOptsFn)); + Functions\expect('curl_exec')->andReturnUsing($curlFn); + Functions\expect('curl_close')->once(); + } +}