From 9fa25593b2889e291390644082cbab16cacb3376 Mon Sep 17 00:00:00 2001 From: eileen Date: Tue, 2 Jun 2020 00:09:45 +1200 Subject: [PATCH] [REF] Convert one of the http calls in Authorize.net class to use guzzle. This achieves the following 1) addresses a cause of intermittent test failures 2) starts the process of modelling how payment processors should be written in a best-practice, tested way in core 3) stops us interacting with A.net during tests 4) starts to reduce curl dependency --- CRM/Core/Payment/AuthorizeNet.php | 59 +++--- Civi/Test/GuzzleTestTrait.php | 191 ++++++++++++++++++ .../CRM/Core/Payment/AuthorizeNetTest.php | 112 ++++++++-- 3 files changed, 324 insertions(+), 38 deletions(-) create mode 100644 Civi/Test/GuzzleTestTrait.php diff --git a/CRM/Core/Payment/AuthorizeNet.php b/CRM/Core/Payment/AuthorizeNet.php index 52657cf221..7f0c4f4fd2 100644 --- a/CRM/Core/Payment/AuthorizeNet.php +++ b/CRM/Core/Payment/AuthorizeNet.php @@ -32,6 +32,25 @@ class CRM_Core_Payment_AuthorizeNet extends CRM_Core_Payment { protected $_params = []; + /** + * @var GuzzleHttp\Client + */ + protected $guzzleClient; + + /** + * @return \GuzzleHttp\Client + */ + public function getGuzzleClient(): \GuzzleHttp\Client { + return $this->guzzleClient ?? new \GuzzleHttp\Client(); + } + + /** + * @param \GuzzleHttp\Client $guzzleClient + */ + public function setGuzzleClient(\GuzzleHttp\Client $guzzleClient) { + $this->guzzleClient = $guzzleClient; + } + /** * We only need one instance of this object. So we use the singleton * pattern and cache the instance in this variable @@ -98,6 +117,8 @@ class CRM_Core_Payment_AuthorizeNet extends CRM_Core_Payment { * * @return array * the result in a nice formatted array (or an error object) + * + * @throws \Civi\Payment\Exception\PaymentProcessorException */ public function doDirectPayment(&$params) { if (!defined('CURLOPT_SSLCERT')) { @@ -316,30 +337,22 @@ class CRM_Core_Payment_AuthorizeNet extends CRM_Core_Payment { $template->assign('billingCountry', $this->_getParam('country')); $arbXML = $template->fetch('CRM/Contribute/Form/Contribution/AuthorizeNetARB.tpl'); - // submit to authorize.net - $submit = curl_init($this->_paymentProcessor['url_recur']); - if (!$submit) { - return self::error(9002, 'Could not initiate connection to payment gateway'); - } - curl_setopt($submit, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($submit, CURLOPT_HTTPHEADER, ["Content-Type: text/xml"]); - curl_setopt($submit, CURLOPT_HEADER, 1); - curl_setopt($submit, CURLOPT_POSTFIELDS, $arbXML); - curl_setopt($submit, CURLOPT_POST, 1); - curl_setopt($submit, CURLOPT_SSL_VERIFYPEER, Civi::settings()->get('verifySSL')); - - $response = curl_exec($submit); - - if (!$response) { - return self::error(curl_errno($submit), curl_error($submit)); - } - - curl_close($submit); - $responseFields = $this->_ParseArbReturn($response); - - if ($responseFields['resultCode'] == 'Error') { - return self::error($responseFields['code'], $responseFields['text']); + // Submit to authorize.net + $response = $this->getGuzzleClient()->post($this->_paymentProcessor['url_recur'], [ + 'headers' => [ + 'Content-Type' => 'text/xml; charset=UTF8', + ], + 'body' => $arbXML, + 'curl' => [ + CURLOPT_RETURNTRANSFER => TRUE, + CURLOPT_SSL_VERIFYPEER => Civi::settings()->get('verifySSL'), + ], + ]); + $responseFields = $this->_ParseArbReturn((string) $response->getBody()); + + if ($responseFields['resultCode'] === 'Error') { + throw new PaymentProcessorException($responseFields['code'], $responseFields['text']); } // update recur processor_id with subscriptionId diff --git a/Civi/Test/GuzzleTestTrait.php b/Civi/Test/GuzzleTestTrait.php new file mode 100644 index 0000000000..c372d3eee9 --- /dev/null +++ b/Civi/Test/GuzzleTestTrait.php @@ -0,0 +1,191 @@ +guzzleClient; + } + + /** + * @param \GuzzleHttp\Client $guzzleClient + */ + public function setGuzzleClient($guzzleClient) { + $this->guzzleClient = $guzzleClient; + } + + /** + * @return array + */ + public function getContainer() { + return $this->container; + } + + /** + * @param array $container + */ + public function setContainer($container) { + $this->container = $container; + } + + /** + * @return mixed + */ + public function getBaseUri() { + return $this->baseUri; + } + + /** + * @param mixed $baseUri + */ + public function setBaseUri($baseUri) { + $this->baseUri = $baseUri; + } + + /** + * @return \GuzzleHttp\Handler\MockHandler + */ + public function getMockHandler() { + return $this->mockHandler; + } + + /** + * @param \GuzzleHttp\Handler\MockHandler $mockHandler + */ + public function setMockHandler($mockHandler) { + $this->mockHandler = $mockHandler; + } + + /** + * @param $responses + */ + protected function createMockHandler($responses) { + $mocks = []; + foreach ($responses as $response) { + $mocks[] = new Response(200, [], $response); + } + $this->setMockHandler(new MockHandler($mocks)); + } + + /** + * @param $files + */ + protected function createMockHandlerForFiles($files) { + $body = []; + foreach ($files as $file) { + $body[] = trim(file_get_contents(__DIR__ . $file)); + } + $this->createMockHandler($body); + } + + /** + * Set up a guzzle client with a history container. + * + * After you have run the requests you can inspect $this->container + * for the outgoing requests and incoming responses. + * + * If $this->mock is defined then no outgoing http calls will be made + * and the responses configured on the handler will be returned instead + * of replies from a remote provider. + */ + protected function setUpClientWithHistoryContainer() { + $this->container = []; + $history = Middleware::history($this->container); + $handler = HandlerStack::create($this->getMockHandler()); + $handler->push($history); + $this->guzzleClient = new Client(['base_uri' => $this->baseUri, 'handler' => $handler]); + } + + /** + * Get the bodies of the requests sent via Guzzle. + * + * @return array + */ + protected function getRequestBodies() { + $requests = []; + foreach ($this->getContainer() as $guzzle) { + $requests[] = (string) $guzzle['request']->getBody(); + } + return $requests; + } + + /** + * Get the bodies of the requests sent via Guzzle. + * + * @return array + */ + protected function getRequestHeaders() { + $requests = []; + foreach ($this->getContainer() as $guzzle) { + $requests[] = $guzzle['request']->getHeaders(); + } + return $requests; + } + + /** + * Get the bodies of the requests sent via Guzzle. + * + * @return array + */ + protected function getRequestUrls() { + $requests = []; + foreach ($this->getContainer() as $guzzle) { + $requests[] = (string) $guzzle['request']->getUri(); + } + return $requests; + } + + /** + * Get the bodies of the responses returned via Guzzle. + * + * @return array + */ + protected function getResponseBodies() { + $responses = []; + foreach ($this->getContainer() as $guzzle) { + $responses[] = (string) $guzzle['response']->getBody(); + } + return $responses; + } + +} diff --git a/tests/phpunit/CRM/Core/Payment/AuthorizeNetTest.php b/tests/phpunit/CRM/Core/Payment/AuthorizeNetTest.php index 8ad69d1efd..4065e734f6 100644 --- a/tests/phpunit/CRM/Core/Payment/AuthorizeNetTest.php +++ b/tests/phpunit/CRM/Core/Payment/AuthorizeNetTest.php @@ -15,6 +15,13 @@ */ class CRM_Core_Payment_AuthorizeNetTest extends CiviUnitTestCase { + use \Civi\Test\GuzzleTestTrait; + + /** + * @var \CRM_Core_Payment_AuthorizeNet + */ + protected $processor; + public function setUp() { parent::setUp(); $this->_paymentProcessorID = $this->paymentProcessorAuthorizeNetCreate(); @@ -37,12 +44,15 @@ class CRM_Core_Payment_AuthorizeNetTest extends CiviUnitTestCase { * Test works but not both due to some form of caching going on in the SmartySingleton */ public function testCreateSingleNowDated() { - $firstName = 'John_' . substr(sha1(rand()), 0, 7) . uniqid(); - $lastName = 'Smith_' . substr(sha1(rand()), 0, 7) . uniqid(); - $nameParams = ['first_name' => $firstName, 'last_name' => $lastName]; + $this->createMockHandler([$this->getExpectedResponse()]); + $this->setUpClientWithHistoryContainer(); + $this->processor->setGuzzleClient($this->getGuzzleClient()); + $firstName = 'John'; + $lastName = 'Smith'; + $nameParams = ['first_name' => 'John', 'last_name' => $lastName]; $contactId = $this->individualCreate($nameParams); - $invoiceID = sha1(rand()); + $invoiceID = 123456; $amount = rand(100, 1000) . '.00'; $recur = $this->callAPISuccess('ContributionRecur', 'create', [ @@ -76,7 +86,7 @@ class CRM_Core_Payment_AuthorizeNetTest extends CiviUnitTestCase { 'qfKey' => '08ed21c7ca00a1f7d32fff2488596ef7_4454', 'hidden_CreditCard' => 1, 'billing_first_name' => $firstName, - 'billing_middle_name' => "", + 'billing_middle_name' => '', 'billing_last_name' => $lastName, 'billing_street_address-5' => '8 Hobbitton Road', 'billing_city-5' => 'The Shire', @@ -96,14 +106,14 @@ class CRM_Core_Payment_AuthorizeNetTest extends CiviUnitTestCase { 'installments' => 12, 'financial_type_id' => $this->_financialTypeId, 'is_email_receipt' => 1, - 'from_email_address' => "{$firstName}.{$lastName}@example.com", + 'from_email_address' => 'john.smith@example.com', 'receive_date' => date('Ymd'), 'receipt_date_time' => '', 'payment_processor_id' => $this->_paymentProcessorID, 'price_set_id' => '', 'total_amount' => $amount, 'currency' => 'USD', - 'source' => "Mordor", + 'source' => 'Mordor', 'soft_credit_to' => '', 'soft_contact_id' => '', 'billing_state_province-5' => 'IL', @@ -116,20 +126,20 @@ class CRM_Core_Payment_AuthorizeNetTest extends CiviUnitTestCase { 'amount' => 7, 'amount_level' => 0, 'currencyID' => 'USD', - 'pcp_display_in_roll' => "", - 'pcp_roll_nickname' => "", - 'pcp_personal_note' => "", - 'non_deductible_amount' => "", - 'fee_amount' => "", - 'net_amount' => "", + 'pcp_display_in_roll' => '', + 'pcp_roll_nickname' => '', + 'pcp_personal_note' => '', + 'non_deductible_amount' => '', + 'fee_amount' => '', + 'net_amount' => '', 'invoiceID' => $invoiceID, - 'contribution_page_id' => "", + 'contribution_page_id' => '', 'thankyou_date' => NULL, 'honor_contact_id' => NULL, 'first_name' => $firstName, 'middle_name' => '', 'last_name' => $lastName, - 'street_address' => '8 Hobbiton Road' . uniqid(), + 'street_address' => '8 Hobbiton Road', 'city' => 'The Shire', 'state_province' => 'IL', 'postal_code' => 5010, @@ -160,6 +170,17 @@ class CRM_Core_Payment_AuthorizeNetTest extends CiviUnitTestCase { $message = ''; $result = $this->processor->cancelSubscription($message, ['subscriptionId' => $subscriptionID]); $this->assertTrue($result, 'Failed to cancel subscription with Authorize.'); + + $requests = $this->getRequestBodies(); + $this->assertEquals($this->getExpectedRequest(date('Y-m-d')), $requests[0]); + $header = $this->getRequestHeaders()[0]; + $this->assertEquals(['apitest.authorize.net'], $header['Host']); + $this->assertEquals(['text/xml; charset=UTF8'], $header['Content-Type']); + + $this->assertEquals([ + CURLOPT_RETURNTRANSFER => TRUE, + CURLOPT_SSL_VERIFYPEER => Civi::settings()->get('verifySSL'), + ], $this->container[0]['options']['curl']); } /** @@ -320,4 +341,65 @@ class CRM_Core_Payment_AuthorizeNetTest extends CiviUnitTestCase { } } + /** + * Get the content that we expect to see sent out. + * + * @param string $startDate + * + * @return string + */ + public function getExpectedRequest($startDate) { + return ' + + + 4y5BfuW7jm + 4cAmW927n8uLf5J8 + + 123456 + + + + 1 + months + + ' . $startDate . ' + 12 + + 7 + + + 4444333322221111 + 2025-09 + + + + 1 + + + 3 + John.Smith@example.com + + + John + Smith +
8 Hobbiton Road
+ The Shire + IL + 5010 + US +
+
+
+'; + } + + /** + * Get a successful response to setting up a recurring. + * + * @return string + */ + public function getExpectedResponse() { + return '8d468ca1b1dd5c2b56c7OkI00001Successful.663205215120232801512027350'; + } + } -- 2.25.1