Convert api4 helper functionality to a trait & make available
authorEileen McNaughton <emcnaughton@wikimedia.org>
Tue, 24 Jan 2023 21:52:30 +0000 (10:52 +1300)
committerEileen McNaughton <emcnaughton@wikimedia.org>
Tue, 24 Jan 2023 22:19:30 +0000 (11:19 +1300)
Civi/Test/Api4TestTrait.php [new file with mode: 0644]
ext/afform/core/tests/phpunit/Civi/Afform/AfformContactSummaryTest.php
tests/phpunit/api/v4/Api4TestBase.php

diff --git a/Civi/Test/Api4TestTrait.php b/Civi/Test/Api4TestTrait.php
new file mode 100644 (file)
index 0000000..eaded65
--- /dev/null
@@ -0,0 +1,306 @@
+<?php
+
+namespace Civi\Test;
+
+use Civi\Api4\Generic\Result;
+use Civi\Api4\Utils\CoreUtil;
+
+/**
+ * Class Api3TestTrait
+ *
+ * @package Civi\Test
+ *
+ * This trait defines a number of helper functions for testing APIv4.
+ *
+ * This trait is intended for use with PHPUnit-based test cases.
+ */
+trait Api4TestTrait {
+
+  /**
+   * Inserts a test record, supplying all required values if not provided.
+   *
+   * Test records will be automatically deleted during tearDown.
+   *
+   * @param string $entityName
+   * @param array $values
+   * @return array|null
+   * @throws \CRM_Core_Exception
+   * @throws \Civi\API\Exception\NotImplementedException
+   */
+  public function createTestRecord(string $entityName, array $values = []): ?array {
+    return $this->saveTestRecords($entityName, ['records' => [$values]])->single();
+  }
+
+  /**
+   * Saves one or more test records, supplying default values.
+   *
+   * Test records will be automatically deleted during tearDown.
+   *
+   * @param string $entityName
+   * @param array $saveParams
+   * @return \Civi\Api4\Generic\Result
+   * @throws \CRM_Core_Exception
+   * @throws \Civi\API\Exception\NotImplementedException
+   */
+  public function saveTestRecords(string $entityName, array $saveParams): Result {
+    $saveParams += [
+      'checkPermissions' => FALSE,
+      'defaults' => [],
+    ];
+    $idField = CoreUtil::getIdFieldName($entityName);
+    foreach ($saveParams['records'] as &$record) {
+      $record += $saveParams['defaults'];
+      if (empty($record[$idField])) {
+        $this->getRequiredValuesToCreate($entityName, $record);
+      }
+    }
+    // Unset for clarity as it leaks from the foreach & is a reference.
+    unset($record);
+    $saved = civicrm_api4($entityName, 'save', $saveParams);
+    foreach ($saved as $item) {
+      $this->testRecords[] = [$entityName, [[$idField, '=', $item[$idField]]]];
+    }
+    return $saved;
+  }
+
+  /**
+   * Generate some random lowercase letters
+   * @param int $len
+   * @return string
+   */
+  protected function randomLetters(int $len = 10): string {
+    return \CRM_Utils_String::createRandom($len, implode('', range('a', 'z')));
+  }
+
+  /**
+   * Get the required fields for the api entity + action.
+   *
+   * @param string $entity
+   * @param array $values
+   *
+   * @return array
+   * @throws \CRM_Core_Exception
+   */
+  public function getRequiredValuesToCreate(string $entity, array &$values = []): array {
+    $requiredFields = civicrm_api4($entity, 'getfields', [
+      'action' => 'create',
+      'loadOptions' => TRUE,
+      'where' => [
+        ['type', 'IN', ['Field', 'Extra']],
+        ['OR',
+          [
+            ['required', '=', TRUE],
+            // Include conditionally-required fields only if they don't create a circular FK reference
+            ['AND', [['required_if', 'IS NOT EMPTY'], ['fk_entity', '!=', $entity]]],
+          ],
+        ],
+        ['default_value', 'IS EMPTY'],
+        ['readonly', 'IS EMPTY'],
+      ],
+    ], 'name');
+
+    $extraValues = [];
+    foreach ($requiredFields as $fieldName => $requiredField) {
+      if (!isset($values[$fieldName])) {
+        $extraValues[$fieldName] = $this->getRequiredValue($requiredField);
+      }
+    }
+
+    // Hack in some extra per-entity values that couldn't be determined by metadata.
+    // Try to keep this to a minimum and improve metadata as a first-resort.
+
+    switch ($entity) {
+      case 'UFField':
+        $extraValues['field_name'] = 'activity_campaign_id';
+        break;
+
+      case 'Translation':
+        $extraValues['entity_table'] = 'civicrm_msg_template';
+        $extraValues['entity_field'] = 'msg_subject';
+        $extraValues['entity_id'] = $this->getFkID('MessageTemplate');
+        break;
+
+      case 'Case':
+        $extraValues['creator_id'] = $this->getFkID('Contact');
+        break;
+
+      case 'CaseContact':
+        // Prevent "already exists" error from using an existing contact id
+        $extraValues['contact_id'] = $this->createTestRecord('Contact')['id'];
+        break;
+
+      case 'CaseType':
+        $extraValues['definition'] = [
+          'activityTypes' => [
+            [
+              'name' => 'Open Case',
+              'max_instances' => 1,
+            ],
+            [
+              'name' => 'Follow up',
+            ],
+          ],
+          'activitySets' => [
+            [
+              'name' => 'standard_timeline',
+              'label' => 'Standard Timeline',
+              'timeline' => 1,
+              'activityTypes' => [
+                [
+                  'name' => 'Open Case',
+                  'status' => 'Completed',
+                ],
+                [
+                  'name' => 'Follow up',
+                  'reference_activity' => 'Open Case',
+                  'reference_offset' => 3,
+                  'reference_select' => 'newest',
+                ],
+              ],
+            ],
+          ],
+          'timelineActivityTypes' => [
+            [
+              'name' => 'Open Case',
+              'status' => 'Completed',
+            ],
+            [
+              'name' => 'Follow up',
+              'reference_activity' => 'Open Case',
+              'reference_offset' => 3,
+              'reference_select' => 'newest',
+            ],
+          ],
+          'caseRoles' => [
+            [
+              'name' => 'Parent of',
+              'creator' => 1,
+              'manager' => 1,
+            ],
+          ],
+        ];
+        break;
+    }
+
+    $values += $extraValues;
+    return $values;
+  }
+
+  /**
+   * Attempt to get a value using field option, defaults, FKEntity, or a random
+   * value based on the data type.
+   *
+   * @param array $field
+   *
+   * @return mixed
+   * @throws \CRM_Core_Exception
+   */
+  private function getRequiredValue(array $field) {
+    if (!empty($field['options'])) {
+      return key($field['options']);
+    }
+    if (!empty($field['fk_entity'])) {
+      return $this->getFkID($field['fk_entity']);
+    }
+    if (isset($field['default_value'])) {
+      return $field['default_value'];
+    }
+    if ($field['name'] === 'contact_id') {
+      return $this->getFkID('Contact');
+    }
+    if ($field['name'] === 'entity_id') {
+      // What could possibly go wrong with this?
+      switch ($field['table_name'] ?? NULL) {
+        case 'civicrm_financial_item':
+          return $this->getFkID(\Civi\Api4\Service\Spec\Provider\FinancialItemCreationSpecProvider::DEFAULT_ENTITY);
+
+        default:
+          return $this->getFkID('Contact');
+      }
+    }
+
+    $randomValue = $this->getRandomValue($field['data_type']);
+
+    if ($randomValue) {
+      return $randomValue;
+    }
+
+    throw new \CRM_Core_Exception('Could not provide default value');
+  }
+
+  protected function deleteTestRecords(): void {
+    // Delete all test records in reverse order to prevent fk constraints
+    foreach (array_reverse($this->testRecords) as $record) {
+      $params = ['checkPermissions' => FALSE, 'where' => $record[1]];
+
+      // Set useTrash param if it exists
+      $entityClass = CoreUtil::getApiClass($record[0]);
+      $deleteAction = $entityClass::delete();
+      if (property_exists($deleteAction, 'useTrash')) {
+        $params['useTrash'] = FALSE;
+      }
+
+      civicrm_api4($record[0], 'delete', $params);
+    }
+  }
+
+  /**
+   * Get an ID for the appropriate entity.
+   *
+   * @param string $fkEntity
+   *
+   * @return int
+   *
+   * @throws \CRM_Core_Exception
+   */
+  private function getFkID(string $fkEntity): int {
+    $params = ['checkPermissions' => FALSE];
+    // Be predictable about what type of contact we select
+    if ($fkEntity === 'Contact') {
+      $params['where'] = [['contact_type', '=', 'Individual']];
+    }
+    $entityList = civicrm_api4($fkEntity, 'get', $params);
+    // If no existing entities, create one
+    if ($entityList->count() < 1) {
+      return $this->createTestRecord($fkEntity)['id'];
+    }
+
+    return $entityList->last()['id'];
+  }
+
+  /**
+   * @param $dataType
+   *
+   * @return int|null|string
+   *
+   * @noinspection PhpUnhandledExceptionInspection
+   * @noinspection PhpDocMissingThrowsInspection
+   */
+  private function getRandomValue($dataType) {
+    switch ($dataType) {
+      case 'Boolean':
+        return TRUE;
+
+      case 'Integer':
+        return random_int(1, 2000);
+
+      case 'String':
+        return $this->randomLetters();
+
+      case 'Text':
+        return $this->randomLetters(100);
+
+      case 'Money':
+        return sprintf('%d.%2d', random_int(0, 2000), random_int(10, 99));
+
+      case 'Date':
+        return '20100102';
+
+      case 'Timestamp':
+        return 'now';
+    }
+
+    return NULL;
+  }
+
+}
index ab2aca9b4ffd3f5b6cc88efec109672adb16311e..aa8817b792c9f91496f719b6d6c513d2ae33be71 100644 (file)
@@ -1,16 +1,19 @@
 <?php
 namespace Civi\Afform;
 
-// Hopefully temporry workaround for loading core test classes
-require_once __DIR__ . '/../../../../../../../tests/phpunit/api/v4/Api4TestBase.php';
-
 use Civi\Api4\Afform;
+use Civi\Test;
+use Civi\Test\Api4TestTrait;
 use Civi\Test\CiviEnvBuilder;
+use Civi\Test\HeadlessInterface;
+use PHPUnit\Framework\TestCase;
 
 /**
  * @group headless
  */
-class AfformContactSummaryTest extends \api\v4\Api4TestBase {
+class AfformContactSummaryTest extends TestCase implements HeadlessInterface {
+
+  use Api4TestTrait;
 
   private $formNames = [
     'contact_summary_test1',
@@ -21,12 +24,12 @@ class AfformContactSummaryTest extends \api\v4\Api4TestBase {
   ];
 
   public function setUpHeadless(): CiviEnvBuilder {
-    return \Civi\Test::headless()->installMe(__DIR__)->install('org.civicrm.search_kit')->apply();
+    return Test::headless()->installMe(__DIR__)->install('org.civicrm.search_kit')->apply();
   }
 
   public function tearDown(): void {
     Afform::revert(FALSE)->addWhere('name', 'IN', $this->formNames)->execute();
-    parent::tearDown();
+    $this->deleteTestRecords();
   }
 
   public function testAfformContactSummaryTab(): void {
index 3cec8ce020d0084da63c68dda510776d93bed74d..11f8800fe03d1c07229a84c7eb32ae3f6bf45369 100644 (file)
 
 namespace api\v4;
 
-use Civi\Api4\Generic\Result;
 use Civi\Api4\UFMatch;
-use Civi\Api4\Utils\CoreUtil;
+use Civi\Test\Api4TestTrait;
 use Civi\Test\CiviEnvBuilder;
 use Civi\Test\HeadlessInterface;
+use PHPUnit\Framework\TestCase;
 
 /**
  * @group headless
  */
-class Api4TestBase extends \PHPUnit\Framework\TestCase implements HeadlessInterface {
+class Api4TestBase extends TestCase implements HeadlessInterface {
+
+  use Api4TestTrait;
 
   /**
    * Records created which will be deleted during tearDown
@@ -62,19 +64,7 @@ class Api4TestBase extends \PHPUnit\Framework\TestCase implements HeadlessInterf
     $implements = class_implements($this);
     // If not created in a transaction, test records must be deleted
     if (!in_array('Civi\Test\TransactionalInterface', $implements, TRUE)) {
-      // Delete all test records in reverse order to prevent fk constraints
-      foreach (array_reverse($this->testRecords) as $record) {
-        $params = ['checkPermissions' => FALSE, 'where' => $record[1]];
-
-        // Set useTrash param if it exists
-        $entityClass = CoreUtil::getApiClass($record[0]);
-        $deleteAction = $entityClass::delete();
-        if (property_exists($deleteAction, 'useTrash')) {
-          $params['useTrash'] = FALSE;
-        }
-
-        civicrm_api4($record[0], 'delete', $params);
-      }
+      $this->deleteTestRecords();
     }
   }
 
@@ -120,273 +110,4 @@ class Api4TestBase extends \PHPUnit\Framework\TestCase implements HeadlessInterf
     return $contactID;
   }
 
-  /**
-   * Inserts a test record, supplying all required values if not provided.
-   *
-   * Test records will be automatically deleted during tearDown.
-   *
-   * @param string $entityName
-   * @param array $values
-   * @return array|null
-   * @throws \CRM_Core_Exception
-   * @throws \Civi\API\Exception\NotImplementedException
-   */
-  public function createTestRecord(string $entityName, array $values = []): ?array {
-    return $this->saveTestRecords($entityName, ['records' => [$values]])->single();
-  }
-
-  /**
-   * Saves one or more test records, supplying default values.
-   *
-   * Test records will be automatically deleted during tearDown.
-   *
-   * @param string $entityName
-   * @param array $saveParams
-   * @return \Civi\Api4\Generic\Result
-   * @throws \CRM_Core_Exception
-   * @throws \Civi\API\Exception\NotImplementedException
-   */
-  public function saveTestRecords(string $entityName, array $saveParams): Result {
-    $saveParams += [
-      'checkPermissions' => FALSE,
-      'defaults' => [],
-    ];
-    $idField = CoreUtil::getIdFieldName($entityName);
-    foreach ($saveParams['records'] as &$record) {
-      $record += $saveParams['defaults'];
-      if (empty($record[$idField])) {
-        $this->getRequiredValuesToCreate($entityName, $record);
-      }
-    }
-    $saved = civicrm_api4($entityName, 'save', $saveParams);
-    foreach ($saved as $item) {
-      $this->testRecords[] = [$entityName, [[$idField, '=', $item[$idField]]]];
-    }
-    return $saved;
-  }
-
-  /**
-   * Generate some random lowercase letters
-   * @param int $len
-   * @return string
-   */
-  public function randomLetters(int $len = 10): string {
-    return \CRM_Utils_String::createRandom($len, implode('', range('a', 'z')));
-  }
-
-  /**
-   * Get the required fields for the api entity + action.
-   *
-   * @param string $entity
-   * @param array $values
-   *
-   * @return array
-   * @throws \CRM_Core_Exception
-   */
-  public function getRequiredValuesToCreate(string $entity, array &$values = []): array {
-    $requiredFields = civicrm_api4($entity, 'getfields', [
-      'action' => 'create',
-      'loadOptions' => TRUE,
-      'where' => [
-        ['type', 'IN', ['Field', 'Extra']],
-        ['OR',
-          [
-            ['required', '=', TRUE],
-            // Include conditionally-required fields only if they don't create a circular FK reference
-            ['AND', [['required_if', 'IS NOT EMPTY'], ['fk_entity', '!=', $entity]]],
-          ],
-        ],
-        ['default_value', 'IS EMPTY'],
-        ['readonly', 'IS EMPTY'],
-      ],
-    ], 'name');
-
-    $extraValues = [];
-    foreach ($requiredFields as $fieldName => $requiredField) {
-      if (!isset($values[$fieldName])) {
-        $extraValues[$fieldName] = $this->getRequiredValue($requiredField);
-      }
-    }
-
-    // Hack in some extra per-entity values that couldn't be determined by metadata.
-    // Try to keep this to a minimum and improve metadata as a first-resort.
-
-    switch ($entity) {
-      case 'UFField':
-        $extraValues['field_name'] = 'activity_campaign_id';
-        break;
-
-      case 'Translation':
-        $extraValues['entity_table'] = 'civicrm_msg_template';
-        $extraValues['entity_field'] = 'msg_subject';
-        $extraValues['entity_id'] = $this->getFkID('MessageTemplate');
-        break;
-
-      case 'Case':
-        $extraValues['creator_id'] = $this->getFkID('Contact');
-        break;
-
-      case 'CaseContact':
-        // Prevent "already exists" error from using an existing contact id
-        $extraValues['contact_id'] = $this->createTestRecord('Contact')['id'];
-        break;
-
-      case 'CaseType':
-        $extraValues['definition'] = [
-          'activityTypes' => [
-            [
-              'name' => 'Open Case',
-              'max_instances' => 1,
-            ],
-            [
-              'name' => 'Follow up',
-            ],
-          ],
-          'activitySets' => [
-            [
-              'name' => 'standard_timeline',
-              'label' => 'Standard Timeline',
-              'timeline' => 1,
-              'activityTypes' => [
-                [
-                  'name' => 'Open Case',
-                  'status' => 'Completed',
-                ],
-                [
-                  'name' => 'Follow up',
-                  'reference_activity' => 'Open Case',
-                  'reference_offset' => 3,
-                  'reference_select' => 'newest',
-                ],
-              ],
-            ],
-          ],
-          'timelineActivityTypes' => [
-            [
-              'name' => 'Open Case',
-              'status' => 'Completed',
-            ],
-            [
-              'name' => 'Follow up',
-              'reference_activity' => 'Open Case',
-              'reference_offset' => 3,
-              'reference_select' => 'newest',
-            ],
-          ],
-          'caseRoles' => [
-            [
-              'name' => 'Parent of',
-              'creator' => 1,
-              'manager' => 1,
-            ],
-          ],
-        ];
-        break;
-    }
-
-    $values += $extraValues;
-    return $values;
-  }
-
-  /**
-   * Attempt to get a value using field option, defaults, FKEntity, or a random
-   * value based on the data type.
-   *
-   * @param array $field
-   *
-   * @return mixed
-   * @throws \CRM_Core_Exception
-   */
-  private function getRequiredValue(array $field) {
-    if (!empty($field['options'])) {
-      return key($field['options']);
-    }
-    if (!empty($field['fk_entity'])) {
-      return $this->getFkID($field['fk_entity']);
-    }
-    if (isset($field['default_value'])) {
-      return $field['default_value'];
-    }
-    if ($field['name'] === 'contact_id') {
-      return $this->getFkID('Contact');
-    }
-    if ($field['name'] === 'entity_id') {
-      // What could possibly go wrong with this?
-      switch ($field['table_name'] ?? NULL) {
-        case 'civicrm_financial_item':
-          return $this->getFkID(\Civi\Api4\Service\Spec\Provider\FinancialItemCreationSpecProvider::DEFAULT_ENTITY);
-
-        default:
-          return $this->getFkID('Contact');
-      }
-    }
-
-    $randomValue = $this->getRandomValue($field['data_type']);
-
-    if ($randomValue) {
-      return $randomValue;
-    }
-
-    throw new \CRM_Core_Exception('Could not provide default value');
-  }
-
-  /**
-   * Get an ID for the appropriate entity.
-   *
-   * @param string $fkEntity
-   *
-   * @return int
-   *
-   * @throws \CRM_Core_Exception
-   */
-  private function getFkID(string $fkEntity): int {
-    $params = ['checkPermissions' => FALSE];
-    // Be predictable about what type of contact we select
-    if ($fkEntity === 'Contact') {
-      $params['where'] = [['contact_type', '=', 'Individual']];
-    }
-    $entityList = civicrm_api4($fkEntity, 'get', $params);
-    // If no existing entities, create one
-    if ($entityList->count() < 1) {
-      return $this->createTestRecord($fkEntity)['id'];
-    }
-
-    return $entityList->last()['id'];
-  }
-
-  /**
-   * @param $dataType
-   *
-   * @return int|null|string
-   *
-   * @noinspection PhpUnhandledExceptionInspection
-   * @noinspection PhpDocMissingThrowsInspection
-   */
-  private function getRandomValue($dataType) {
-    switch ($dataType) {
-      case 'Boolean':
-        return TRUE;
-
-      case 'Integer':
-        return random_int(1, 2000);
-
-      case 'String':
-        return $this->randomLetters();
-
-      case 'Text':
-        return $this->randomLetters(100);
-
-      case 'Money':
-        return sprintf('%d.%2d', random_int(0, 2000), random_int(10, 99));
-
-      case 'Date':
-        return '20100102';
-
-      case 'Timestamp':
-        return 'now';
-    }
-
-    return NULL;
-  }
-
 }