[REF] Convert one of the http calls in Authorize.net class to use guzzle.
authoreileen <emcnaughton@wikimedia.org>
Mon, 1 Jun 2020 12:09:45 +0000 (00:09 +1200)
committereileen <emcnaughton@wikimedia.org>
Mon, 1 Jun 2020 19:46:55 +0000 (07:46 +1200)
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
Civi/Test/GuzzleTestTrait.php [new file with mode: 0644]
tests/phpunit/CRM/Core/Payment/AuthorizeNetTest.php

index 52657cf22117d8a59c44579100f28bfaa5f54bcf..7f0c4f4fd2aece9b2ae0dcbc3be4f7fdd4092205 100644 (file)
@@ -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 (file)
index 0000000..c372d3e
--- /dev/null
@@ -0,0 +1,191 @@
+<?php
+
+namespace Civi\Test;
+
+use GuzzleHttp\Handler\MockHandler;
+use GuzzleHttp\HandlerStack;
+use GuzzleHttp\Psr7\Response;
+use GuzzleHttp\Middleware;
+use GuzzleHttp\Client;
+
+/**
+ * Class GuzzleTestTrait
+ *
+ * This trait defines a number of helper functions for testing guzzle.
+ */
+trait GuzzleTestTrait {
+  /**
+   * @var \GuzzleHttp\Client
+   */
+  protected $guzzleClient;
+
+  /**
+   * Array containing guzzle history of requests and responses.
+   *
+   * @var array
+   */
+  protected $container;
+
+  /**
+   * Mockhandler to simulate guzzle requests.
+   *
+   * @var \GuzzleHttp\Handler\MockHandler
+   */
+  protected $mockHandler;
+
+  /**
+   * The url to mock-interact with.
+   *
+   * @var string
+   */
+  protected $baseUri;
+
+  /**
+   * @return \GuzzleHttp\Client
+   */
+  public function getGuzzleClient() {
+    return $this->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;
+  }
+
+}
index 8ad69d1efd23083385b8b042fca7f35d2bd00a4e..4065e734f678b5e7f10ad6dc50dc6e0668a160a8 100644 (file)
  */
 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 '<?xml version="1.0" encoding="utf-8"?>
+<ARBCreateSubscriptionRequest xmlns="AnetApi/xml/v1/schema/AnetApiSchema.xsd">
+  <merchantAuthentication>
+    <name>4y5BfuW7jm</name>
+    <transactionKey>4cAmW927n8uLf5J8</transactionKey>
+  </merchantAuthentication>
+  <refId>123456</refId>
+  <subscription>
+        <paymentSchedule>
+      <interval>
+        <length>1</length>
+        <unit>months</unit>
+      </interval>
+      <startDate>' . $startDate . '</startDate>
+      <totalOccurrences>12</totalOccurrences>
+    </paymentSchedule>
+    <amount>7</amount>
+    <payment>
+      <creditCard>
+        <cardNumber>4444333322221111</cardNumber>
+        <expirationDate>2025-09</expirationDate>
+      </creditCard>
+    </payment>
+      <order>
+     <invoiceNumber>1</invoiceNumber>
+        </order>
+       <customer>
+      <id>3</id>
+      <email>John.Smith@example.com</email>
+    </customer>
+    <billTo>
+      <firstName>John</firstName>
+      <lastName>Smith</lastName>
+      <address>8 Hobbiton Road</address>
+      <city>The Shire</city>
+      <state>IL</state>
+      <zip>5010</zip>
+      <country>US</country>
+    </billTo>
+  </subscription>
+</ARBCreateSubscriptionRequest>
+';
+  }
+
+  /**
+   * Get a successful response to setting up a recurring.
+   *
+   * @return string
+   */
+  public function getExpectedResponse() {
+    return '<?xml version="1.0" encoding="utf-8"?><ARBCreateSubscriptionResponse xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="AnetApi/xml/v1/schema/AnetApiSchema.xsd"><refId>8d468ca1b1dd5c2b56c7</refId><messages><resultCode>Ok</resultCode><message><code>I00001</code><text>Successful.</text></message></messages><subscriptionId>6632052</subscriptionId><profile><customerProfileId>1512023280</customerProfileId><customerPaymentProfileId>1512027350</customerPaymentProfileId></profile></ARBCreateSubscriptionResponse>';
+  }
+
 }