Add initial test to the new extension
authoreileen <emcnaughton@wikimedia.org>
Fri, 16 Oct 2020 06:11:41 +0000 (19:11 +1300)
committereileen <emcnaughton@wikimedia.org>
Tue, 20 Oct 2020 19:51:19 +0000 (08:51 +1300)
I made a small change to support the test - causing Membership.create not to set the
stattus_id if it is specified.

ext/contributioncancelactions/phpunit.xml.dist [new file with mode: 0644]
ext/contributioncancelactions/tests/phpunit/IPNCancelTest.php [new file with mode: 0644]
ext/contributioncancelactions/tests/phpunit/bootstrap.php [new file with mode: 0644]

diff --git a/ext/contributioncancelactions/phpunit.xml.dist b/ext/contributioncancelactions/phpunit.xml.dist
new file mode 100644 (file)
index 0000000..fc8f870
--- /dev/null
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+<phpunit backupGlobals="false" backupStaticAttributes="false" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" bootstrap="tests/phpunit/bootstrap.php">
+  <testsuites>
+    <testsuite name="My Test Suite">
+      <directory>./tests/phpunit</directory>
+    </testsuite>
+  </testsuites>
+  <filter>
+    <whitelist>
+      <directory suffix=".php">./</directory>
+    </whitelist>
+  </filter>
+  <listeners>
+    <listener class="Civi\Test\CiviTestListener">
+      <arguments/>
+    </listener>
+  </listeners>
+</phpunit>
diff --git a/ext/contributioncancelactions/tests/phpunit/IPNCancelTest.php b/ext/contributioncancelactions/tests/phpunit/IPNCancelTest.php
new file mode 100644 (file)
index 0000000..9f80876
--- /dev/null
@@ -0,0 +1,204 @@
+<?php
+
+use CRM_Contributioncancelactions_ExtensionUtil as E;
+use Civi\Test\HeadlessInterface;
+use Civi\Test\HookInterface;
+use Civi\Test\TransactionalInterface;
+use Civi\Api4\Contact;
+use Civi\Api4\MembershipType;
+
+/**
+ * FIXME - Add test description.
+ *
+ * Tips:
+ *  - With HookInterface, you may implement CiviCRM hooks directly in the test class.
+ *    Simply create corresponding functions (e.g. "hook_civicrm_post(...)" or similar).
+ *  - With TransactionalInterface, any data changes made by setUp() or test****() functions will
+ *    rollback automatically -- as long as you don't manipulate schema or truncate tables.
+ *    If this test needs to manipulate schema or truncate tables, then either:
+ *       a. Do all that using setupHeadless() and Civi\Test.
+ *       b. Disable TransactionalInterface, and handle all setup/teardown yourself.
+ *
+ * @group headless
+ */
+class IPNCancelTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, HookInterface, TransactionalInterface {
+
+  use \Civi\Test\Api3TestTrait;
+
+  /**
+   * Created ids.
+   *
+   * @var array
+   */
+  protected $ids = [];
+
+  /**
+   * The setupHeadless function runs at the start of each test case, right before
+   * the headless environment reboots.
+   *
+   * It should perform any necessary steps required for putting the database
+   * in a consistent baseline -- such as loading schema and extensions.
+   *
+   * The utility `\Civi\Test::headless()` provides a number of helper functions
+   * for managing this setup, and it includes optimizations to avoid redundant
+   * setup work.
+   *
+   * @see \Civi\Test
+   */
+  public function setUpHeadless() {
+    // Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile().
+    // See: https://docs.civicrm.org/dev/en/latest/testing/phpunit/#civitest
+    return \Civi\Test::headless()
+      ->installMe(__DIR__)
+      ->apply();
+  }
+
+  /**
+   * Test that a cancel from paypal pro results in an order being cancelled.
+   *
+   * @throws \API_Exception
+   * @throws \CRM_Core_Exception
+   * @throws \Civi\API\Exception\UnauthorizedException
+   */
+  public function testPaypalProCancel() {
+    $this->createMembershipOrder();
+    $membership = $this->callAPISuccessGetSingle('Membership', []);
+    $membership['owner_membership_id'] = $membership['id'];
+    $membership['contact_id'] = Contact::create()->setValues(['first_name' => 'Bugs', 'last_name' => 'Bunny'])->execute()->first()['id'];
+    unset($membership['id']);
+    $this->callAPISuccess('Membership', 'create', $membership);
+
+    $ipn = new CRM_Core_Payment_PayPalProIPN([
+      'rp_invoice_id' => http_build_query([
+        'b' => $this->ids['Contribution'][0],
+        'm' => 'contribute',
+        'i' => 'zyx',
+        'c' => $this->ids['contact'][0],
+      ]),
+      'mc_gross' => 200,
+      'payment_status' => 'Refunded',
+      'processor_id' => $this->createPaymentProcessor(),
+    ]);
+    $ipn->main();
+    $this->callAPISuccessGetSingle('Contribution', ['contribution_status_id' => 'Cancelled']);
+    $this->callAPISuccessGetCount('Membership', ['status_id' => 'Cancelled'], 2);
+  }
+
+  /**
+   * Create an order with more than one membership.
+   *
+   * @throws \API_Exception
+   * @throws \CRM_Core_Exception
+   * @throws \Civi\API\Exception\UnauthorizedException
+   */
+  protected function createMembershipOrder() {
+    $this->ids['contact'][0] = Civi\Api4\Contact::create()->setValues(['first_name' => 'Brer', 'last_name' => 'Rabbit'])->execute()->first()['id'];
+    $this->createMembershipType();
+    $priceFieldID = $this->callAPISuccessGetValue('price_field', [
+      'return' => 'id',
+      'label' => 'Membership Amount',
+      'options' => ['limit' => 1, 'sort' => 'id DESC'],
+    ]);
+    $generalPriceFieldValueID = $this->callAPISuccessGetValue('price_field_value', [
+      'return' => 'id',
+      'label' => 'General',
+      'options' => ['limit' => 1, 'sort' => 'id DESC'],
+    ]);
+
+    $orderID = $this->callAPISuccess('Order', 'create', [
+      'financial_type_id' => 'Member Dues',
+      'contact_id' => $this->ids['contact'][0],
+      'is_test' => 0,
+      'payment_instrument_id' => 'Credit card',
+      'receive_date' => '2019-07-25 07:34:23',
+      'invoice_id' => 'zyx',
+      'line_items' => [
+        [
+          'params' => [
+            'contact_id' => $this->ids['contact'][0],
+            'source' => 'Payment',
+            'membership_type_id' => 'General',
+            // This is interim needed while we improve the BAO - if the test passes without it it can go!
+            'skipStatusCal' => TRUE,
+          ],
+          'line_item' => [
+            [
+              'label' => 'General',
+              'qty' => 1,
+              'unit_price' => 200,
+              'line_total' => 200,
+              'financial_type_id' => 1,
+              'entity_table' => 'civicrm_membership',
+              'price_field_id' => $priceFieldID,
+              'price_field_value_id' => $generalPriceFieldValueID,
+            ],
+          ],
+        ],
+      ],
+    ])['id'];
+    $this->ids['Contribution'][0] = $orderID;
+  }
+
+  /**
+   * Create the general membership type.
+   *
+   * @throws \API_Exception
+   * @throws \Civi\API\Exception\UnauthorizedException
+   */
+  protected function createMembershipType(): void {
+    MembershipType::create()->setValues([
+      'name' => 'General',
+      'duration_unit' => 'year',
+      'duration_interval' => 1,
+      'period_type' => 'rolling',
+      'member_of_contact_id' => 1,
+      'domain_id' => 1,
+      'financial_type_id' => 2,
+      'is_active' => 1,
+      'sequential' => 1,
+      'visibility' => 'Public',
+    ])->execute();
+  }
+
+  /**
+   * Create a payment processor.
+   *
+   * @param array $params
+   *
+   * @return int
+   * @throws \CRM_Core_Exception
+   */
+  public function createPaymentProcessor($params = []) {
+    $params = array_merge([
+      'name' => 'demo',
+      'domain_id' => CRM_Core_Config::domainID(),
+      'payment_processor_type_id' => 'PayPal',
+      'is_active' => 1,
+      'is_default' => 0,
+      'is_test' => 1,
+      'user_name' => 'sunil._1183377782_biz_api1.webaccess.co.in',
+      'password' => '1183377788',
+      'signature' => 'APixCoQ-Zsaj-u3IH7mD5Do-7HUqA9loGnLSzsZga9Zr-aNmaJa3WGPH',
+      'url_site' => 'https://www.sandbox.paypal.com/',
+      'url_api' => 'https://api-3t.sandbox.paypal.com/',
+      'url_button' => 'https://www.paypal.com/en_US/i/btn/btn_xpressCheckout.gif',
+      'class_name' => 'Payment_PayPalImpl',
+      'billing_mode' => 3,
+      'financial_type_id' => 1,
+      'financial_account_id' => 12,
+      // Credit card = 1 so can pass 'by accident'.
+      'payment_instrument_id' => 'Debit Card',
+    ], $params);
+    if (!is_numeric($params['payment_processor_type_id'])) {
+      // really the api should handle this through getoptions but it's not exactly api call so lets just sort it
+      //here
+      $params['payment_processor_type_id'] = $this->callAPISuccess('payment_processor_type', 'getvalue', [
+        'name' => $params['payment_processor_type_id'],
+        'return' => 'id',
+      ], 'integer');
+    }
+    $result = $this->callAPISuccess('payment_processor', 'create', $params);
+    return (int) $result['id'];
+  }
+
+}
diff --git a/ext/contributioncancelactions/tests/phpunit/bootstrap.php b/ext/contributioncancelactions/tests/phpunit/bootstrap.php
new file mode 100644 (file)
index 0000000..a5b4925
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+
+ini_set('memory_limit', '2G');
+ini_set('safe_mode', 0);
+// phpcs:disable
+eval(cv('php:boot --level=classloader', 'phpcode'));
+// phpcs:enable
+// Allow autoloading of PHPUnit helper classes in this extension.
+$loader = new \Composer\Autoload\ClassLoader();
+$loader->add('CRM_', __DIR__);
+$loader->add('Civi\\', __DIR__);
+$loader->add('api_', __DIR__);
+$loader->add('api\\', __DIR__);
+$loader->register();
+
+/**
+ * Call the "cv" command.
+ *
+ * @param string $cmd
+ *   The rest of the command to send.
+ * @param string $decode
+ *   Ex: 'json' or 'phpcode'.
+ * @return string
+ *   Response output (if the command executed normally).
+ * @throws \RuntimeException
+ *   If the command terminates abnormally.
+ */
+function cv($cmd, $decode = 'json') {
+  $cmd = 'cv ' . $cmd;
+  $descriptorSpec = array(0 => array("pipe", "r"), 1 => array("pipe", "w"), 2 => STDERR);
+  $oldOutput = getenv('CV_OUTPUT');
+  putenv("CV_OUTPUT=json");
+
+  // Execute `cv` in the original folder. This is a work-around for
+  // phpunit/codeception, which seem to manipulate PWD.
+  $cmd = sprintf('cd %s; %s', escapeshellarg(getenv('PWD')), $cmd);
+
+  $process = proc_open($cmd, $descriptorSpec, $pipes, __DIR__);
+  putenv("CV_OUTPUT=$oldOutput");
+  fclose($pipes[0]);
+  $result = stream_get_contents($pipes[1]);
+  fclose($pipes[1]);
+  if (proc_close($process) !== 0) {
+    throw new RuntimeException("Command failed ($cmd):\n$result");
+  }
+  switch ($decode) {
+    case 'raw':
+      return $result;
+
+    case 'phpcode':
+      // If the last output is /*PHPCODE*/, then we managed to complete execution.
+      if (substr(trim($result), 0, 12) !== "/*BEGINPHP*/" || substr(trim($result), -10) !== "/*ENDPHP*/") {
+        throw new \RuntimeException("Command failed ($cmd):\n$result");
+      }
+      return $result;
+
+    case 'json':
+      return json_decode($result, 1);
+
+    default:
+      throw new RuntimeException("Bad decoder format ($decode)");
+  }
+}