Merge pull request #14417 from civicrm/5.14
authorSeamus Lee <seamuslee001@gmail.com>
Mon, 3 Jun 2019 10:38:55 +0000 (20:38 +1000)
committerGitHub <noreply@github.com>
Mon, 3 Jun 2019 10:38:55 +0000 (20:38 +1000)
5.14

1  2 
CRM/Contact/BAO/Query.php
tests/phpunit/CRM/Contact/SelectorTest.php
tests/phpunit/api/v3/ContactTest.php

index 229a8de20fe478813dea2af9024911513ceef2d0,b56ac41f5247c7de29ebdbb26a2c566e94de3079..f2588a8b9ba159cdb7e5ea48c9b8c05bfe411a23
@@@ -213,28 -213,28 +213,28 @@@ class CRM_Contact_BAO_Query 
    /**
     * Are we in search mode.
     *
 -   * @var boolean
 +   * @var bool
     */
    public $_search = TRUE;
  
    /**
     * Should we skip permission checking.
     *
 -   * @var boolean
 +   * @var bool
     */
    public $_skipPermission = FALSE;
  
    /**
     * Should we skip adding of delete clause.
     *
 -   * @var boolean
 +   * @var bool
     */
    public $_skipDeleteClause = FALSE;
  
    /**
     * Are we in strict mode (use equality over LIKE)
     *
 -   * @var boolean
 +   * @var bool
     */
    public $_strict = FALSE;
  
    /**
     * Should we only search on primary location.
     *
 -   * @var boolean
 +   * @var bool
     */
    public $_primaryLocation = TRUE;
  
    /**
     * Are contact ids part of the query.
     *
 -   * @var boolean
 +   * @var bool
     */
    public $_includeContactIds = FALSE;
  
    /**
     * Should we use the smart group cache.
     *
 -   * @var boolean
 +   * @var bool
     */
    public $_smartGroupCache = TRUE;
  
     * Should we enable the distinct clause, used if we are including
     * more than one group
     *
 -   * @var boolean
 +   * @var bool
     */
    public $_useDistinct = FALSE;
  
     */
    protected $_rangeCache = [];
    /**
 -   * Set to true when $this->relationship is run to avoid adding twice
 -   * @var Boolean
 +   * Set to true when $this->relationship is run to avoid adding twice.
 +   *
 +   * @var bool
     */
    protected $_relationshipValuesAdded = FALSE;
  
    /**
 -   * Set to the name of the temp table if one has been created
 -   * @var String
 +   * Set to the name of the temp table if one has been created.
 +   *
 +   * @var string
     */
    public static $_relationshipTempTable = NULL;
  
     * @param null $displayRelationshipType
     * @param string $operator
     * @param string $apiEntity
 -   * @param bool|NULL $primaryLocationOnly
 +   * @param bool|null $primaryLocationOnly
     */
    public function __construct(
      $params = NULL, $returnProperties = NULL, $fields = NULL,
      if (array_key_exists('civicrm_membership', $this->_whereTables)) {
        $component = 'membership';
      }
 -    if (isset($component)) {
 -      // @todo should be if (isset($component && !$this->_skipPermission)
 +    if (isset($component) && !$this->_skipPermission) {
 +      // Unit test coverage in api_v3_FinancialTypeACLTest::testGetACLContribution.
        CRM_Financial_BAO_FinancialType::buildPermissionedClause($this->_whereClause, $component);
      }
  
      }
  
      self::filterCountryFromValuesIfStateExists($formValues);
 -
 +    // We shouldn't have to whitelist fields to not hack but here we are, for now.
 +    $nonLegacyDateFields = ['participant_register_date_relative'];
      // Handle relative dates first
      foreach (array_keys($formValues) as $id) {
 -      if (preg_match('/_date_relative$/', $id) ||
 +      if (
 +        !in_array($id, $nonLegacyDateFields) && (
 +        preg_match('/_date_relative$/', $id) ||
          $id == 'event_relative' ||
          $id == 'case_from_relative' ||
 -        $id == 'case_to_relative' ||
 -        $id == 'participant_relative'
 +        $id == 'case_to_relative')
        ) {
          if ($id == 'event_relative') {
            $fromRange = 'event_start_date_low';
            $toRange = 'event_end_date_high';
          }
 -        elseif ($id == 'participant_relative') {
 -          $fromRange = 'participant_register_date_low';
 -          $toRange = 'participant_register_date_high';
 -        }
          elseif ($id == 'case_from_relative') {
            $fromRange = 'case_from_start_date_low';
            $toRange = 'case_from_start_date_high';
        ) {
          self::convertCustomRelativeFields($formValues, $params, $values, $id);
        }
 -      elseif (preg_match('/_date_relative$/', $id) ||
 -        $id == 'event_relative' ||
 -        $id == 'case_from_relative' ||
 -        $id == 'case_to_relative' ||
 -        $id == 'participant_relative'
 +      elseif (
 +        !in_array($id, $nonLegacyDateFields) && (
 +          preg_match('/_date_relative$/', $id) ||
 +          $id == 'event_relative' ||
 +          $id == 'case_from_relative' ||
 +          $id == 'case_to_relative')
        ) {
          // Already handled in previous loop
          continue;
          $regularGroupIDs[] = trim($id);
        }
      }
 +    $hasNonSmartGroups = count($regularGroupIDs);
  
      $isNotOp = ($op == 'NOT IN' || $op == '!=');
  
 -    $statii = [];
 -    $gcsValues = $this->getWhereValues('group_contact_status', $grouping);
 -    if ($gcsValues &&
 -      is_array($gcsValues[2])
 -    ) {
 -      foreach ($gcsValues[2] as $k => $v) {
 -        if ($v) {
 -          $statii[] = "'" . CRM_Utils_Type::escape($k, 'String') . "'";
 -        }
 -      }
 -    }
 -    else {
 -      $statii[] = "'Added'";
 -    }
 +    $statusJoinClause = $this->getGroupStatusClause($grouping);
      $groupClause = [];
 -    if (count($regularGroupIDs) || empty($value)) {
 +    if ($hasNonSmartGroups || empty($value)) {
        // include child groups IDs if any
        $childGroupIds = (array) CRM_Contact_BAO_Group::getChildGroupIds($regularGroupIDs);
        foreach ($childGroupIds as $key => $id) {
        }
        $groupClause[] = "( {$clause} )";
  
 -      if ($statii) {
 -        $joinClause[] = "{$gcTable}.status IN (" . implode(', ', $statii) . ")";
 +      if ($statusJoinClause) {
 +        $joinClause[] = "{$gcTable}.$statusJoinClause";
        }
        $this->_tables[$gcTable] = $this->_whereTables[$gcTable] = " LEFT JOIN civicrm_group_contact {$gcTable} ON (" . implode(' AND ', $joinClause) . ")";
      }
      list($qillop, $qillVal) = CRM_Contact_BAO_Query::buildQillForFieldValue('CRM_Contact_DAO_Group', 'id', $value, $op);
      $this->_qill[$grouping][] = ts("Group(s) %1 %2", [1 => $qillop, 2 => $qillVal]);
      if (strpos($op, 'NULL') === FALSE) {
 -      $this->_qill[$grouping][] = ts("Group Status %1", [1 => implode(' ' . ts('or') . ' ', $statii)]);
 +      $this->_qill[$grouping][] = ts("Group Status %1", [1 => implode(' ' . ts('or') . ' ', $this->getSelectedGroupStatuses($grouping))]);
      }
    }
  
@@@ -4941,7 -4952,7 +4941,7 @@@ civicrm_relationship.start_date > {$tod
    public function alphabetQuery() {
      $sqlParts = $this->getSearchSQLParts(NULL, NULL, NULL, FALSE, FALSE, TRUE);
      $query = "SELECT DISTINCT LEFT(contact_a.sort_name, 1) as sort_name
-       {$this->_simpleFromClause}
+       {$sqlParts['from']}
        {$sqlParts['where']}
        {$sqlParts['having']}
        GROUP BY sort_name
     * @return CRM_Core_DAO
     */
    public function getCachedContacts($cids, $includeContactIds) {
 +    CRM_Core_DAO::disableFullGroupByMode();
      CRM_Utils_Type::validateAll($cids, 'Positive');
      $this->_includeContactIds = $includeContactIds;
      $onlyDeleted = in_array(['deleted_contacts', '=', '1', '0', '0'], $this->_params);
      $limit = '';
      $query = "$select $from $where $groupBy $order $limit";
  
 -    return CRM_Core_DAO::executeQuery($query);
 +    $result = CRM_Core_DAO::executeQuery($query);
 +    CRM_Core_DAO::reenableFullGroupByMode();
 +    return $result;
    }
  
    /**
      }
      else {
        // create temp table with contact ids
-       $tableName = CRM_Core_DAO::createTempTableName('civicrm_transform', TRUE);
  
-       $sql = "CREATE TEMPORARY TABLE $tableName ( contact_id int primary key) ENGINE=HEAP";
-       CRM_Core_DAO::executeQuery($sql);
+       $tableName = CRM_Utils_SQL_TempTable::build()->createWithColumns('contact_id int primary key')->setMemory(TRUE)->getName();
  
        $sql = "
  REPLACE INTO $tableName ( contact_id )
@@@ -6489,9 -6495,7 +6487,9 @@@ AND   displayRelType.is_active = 
     * @param string $where
     * @param string $from
     *
 +   *
     * @return array
 +   * @throws \CRM_Core_Exception
     */
    protected function addBasicStatsToSummary(&$summary, $where, $from) {
      $summary['total']['count'] = 0;
      ];
    }
  
 +  /**
 +   * Get the clause for group status.
 +   *
 +   * @param int $grouping
 +   *
 +   * @return string
 +   */
 +  protected function getGroupStatusClause($grouping) {
 +    $statuses = $this->getSelectedGroupStatuses($grouping);
 +    return "status IN (" . implode(', ', $statuses) . ")";
 +  }
 +
 +  /**
 +   * Get an array of the statuses that have been selected.
 +   *
 +   * @param string $grouping
 +   *
 +   * @return array
 +   */
 +  protected function getSelectedGroupStatuses($grouping) {
 +    $statuses = [];
 +    $gcsValues = $this->getWhereValues('group_contact_status', $grouping);
 +    if ($gcsValues &&
 +      is_array($gcsValues[2])
 +    ) {
 +      foreach ($gcsValues[2] as $k => $v) {
 +        if ($v) {
 +          $statuses[] = "'" . CRM_Utils_Type::escape($k, 'String') . "'";
 +        }
 +      }
 +    }
 +    else {
 +      $statuses[] = "'Added'";
 +    }
 +    return $statuses;
 +  }
 +
  }
index fcf065fb8c01593989fac19d729dee1888b4bad9,7723201522e85eb8fa3b26b50abfa1be0014be8d..aae974183e1082d13984d1a2111015059031d00e
@@@ -66,13 -66,15 +66,15 @@@ class CRM_Contact_SelectorTest extends 
        $dataSet['context']
      );
      $queryObject = $selector->getQueryObject();
+     // Make sure there is no fail on alphabet query.
+     $selector->alphabetQuery()->fetchAll();
      $sql = $queryObject->query();
      $this->wrangleDefaultClauses($dataSet['expected_query']);
      foreach ($dataSet['expected_query'] as $index => $queryString) {
        $this->assertEquals($this->strWrangle($queryString), $this->strWrangle($sql[$index]));
      }
      // Ensure that search builder return individual contact as per criteria
-     if (!empty($dataSet['context'] == 'builder')) {
+     if ($dataSet['context'] == 'builder') {
        $contactID = $this->individualCreate(['first_name' => 'James', 'last_name' => 'Bond']);
        if ('Search builder behaviour for Activity' == $dataSet['description']) {
          $this->callAPISuccess('Activity', 'create', [
          ]);
          $rows = $selector->getRows(CRM_Core_Action::VIEW, 0, 50, '');
          $this->assertEquals(1, count($rows));
 +
 +        CRM_Core_DAO::reenableFullGroupByMode();
 +        $rows = $selector->getRows(CRM_Core_Action::VIEW, 0, 50, '');
 +
          $sortChar = $selector->alphabetQuery()->fetchAll();
          // sort name is stored in '<last_name>, <first_name>' format, as per which the first character would be B of Bond
          $this->assertEquals('B', $sortChar[0]['sort_name']);
          $this->assertEquals($contactID, key($rows));
 +
 +        CRM_Core_DAO::reenableFullGroupByMode();
 +        $selector->getQueryObject()->getCachedContacts([$contactID], FALSE);
        }
      }
    }
      // build cache key and use to it to fetch prev-next cache record
      $cacheKey = 'civicrm search ' . $key;
      $contacts = CRM_Utils_SQL_Select::from('civicrm_prevnext_cache')
 -      ->select(['entity_id1', 'cacheKey'])
 -      ->where("cacheKey = @key")
 +      ->select(['entity_id1', 'cachekey'])
 +      ->where("cachekey = @key")
        ->param('key', $cacheKey)
        ->execute()
        ->fetchAll();
      // check the prevNext record matches
      $expectedEntry = [
        'entity_id1' => $contactID,
 -      'cacheKey' => $cacheKey,
 +      'cachekey' => $cacheKey,
      ];
      $this->checkArrayEquals($contacts[0], $expectedEntry);
    }
            ),
          ),
        ),
+       array(
+         array(
+           'description' => 'Test display relationships',
+           'class' => 'CRM_Contact_Selector',
+           'settings' => [],
+           'form_values' => ['display_relationship_type' => '1_b_a'],
+           'return_properties' => NULL,
+           'params' => [],
+           'context' => 'advanced',
+           'action' => CRM_Core_Action::NONE,
+           'includeContactIds' => NULL,
+           'searchDescendentGroups' => FALSE,
+           'expected_query' => array(
+             0 => 'SELECT contact_a.id as contact_id, contact_a.contact_type as `contact_type`, contact_a.contact_sub_type as `contact_sub_type`, contact_a.sort_name as `sort_name`, contact_a.display_name as `display_name`, contact_a.do_not_email as `do_not_email`, contact_a.do_not_phone as `do_not_phone`, contact_a.do_not_mail as `do_not_mail`, contact_a.do_not_sms as `do_not_sms`, contact_a.do_not_trade as `do_not_trade`, contact_a.is_opt_out as `is_opt_out`, contact_a.legal_identifier as `legal_identifier`, contact_a.external_identifier as `external_identifier`, contact_a.nick_name as `nick_name`, contact_a.legal_name as `legal_name`, contact_a.image_URL as `image_URL`, contact_a.preferred_communication_method as `preferred_communication_method`, contact_a.preferred_language as `preferred_language`, contact_a.preferred_mail_format as `preferred_mail_format`, contact_a.first_name as `first_name`, contact_a.middle_name as `middle_name`, contact_a.last_name as `last_name`, contact_a.prefix_id as `prefix_id`, contact_a.suffix_id as `suffix_id`, contact_a.formal_title as `formal_title`, contact_a.communication_style_id as `communication_style_id`, contact_a.job_title as `job_title`, contact_a.gender_id as `gender_id`, contact_a.birth_date as `birth_date`, contact_a.is_deceased as `is_deceased`, contact_a.deceased_date as `deceased_date`, contact_a.household_name as `household_name`, IF ( contact_a.contact_type = \'Individual\', NULL, contact_a.organization_name ) as organization_name, contact_a.sic_code as `sic_code`, contact_a.is_deleted as `contact_is_deleted`, IF ( contact_a.contact_type = \'Individual\', contact_a.organization_name, NULL ) as current_employer, civicrm_address.id as address_id, civicrm_address.street_address as `street_address`, civicrm_address.supplemental_address_1 as `supplemental_address_1`, civicrm_address.supplemental_address_2 as `supplemental_address_2`, civicrm_address.supplemental_address_3 as `supplemental_address_3`, civicrm_address.city as `city`, civicrm_address.postal_code_suffix as `postal_code_suffix`, civicrm_address.postal_code as `postal_code`, civicrm_address.geo_code_1 as `geo_code_1`, civicrm_address.geo_code_2 as `geo_code_2`, civicrm_address.state_province_id as state_province_id, civicrm_address.country_id as country_id, civicrm_phone.id as phone_id, civicrm_phone.phone_type_id as phone_type_id, civicrm_phone.phone as `phone`, civicrm_email.id as email_id, civicrm_email.email as `email`, civicrm_email.on_hold as `on_hold`, civicrm_im.id as im_id, civicrm_im.provider_id as provider_id, civicrm_im.name as `im`, civicrm_worldregion.id as worldregion_id, civicrm_worldregion.name as `world_region`',
+             2 => 'WHERE displayRelType.relationship_type_id = 1
+ AND   displayRelType.is_active = 1
+ AND (contact_a.is_deleted = 0)',
+           ),
+         ),
+       ),
      );
    }
  
     * @return string
     */
    public function strWrangle($string) {
-     return str_replace('  ', ' ', $string);
+     return trim(str_replace('  ', ' ', $string));
    }
  
    /**
index b09fcd5ed14bc3e00bd96de20300945a50e7b6c3,32301bbb1bb6a5f071403dc5e80b39bbd073b829..4788abef523b6730bac7cb68737834e5b228c2e9
@@@ -37,8 -37,6 +37,8 @@@
   * @group headless
   */
  class api_v3_ContactTest extends CiviUnitTestCase {
 +  use CRMTraits_Custom_CustomDataTrait;
 +
    public $DBResetRequired = FALSE;
    protected $_apiversion;
    protected $_entity;
    protected $_contactID;
    protected $_financialTypeId = 1;
  
 +
 +  /**
 +   * Entity to be extended.
 +   *
 +   * @var string
 +   */
 +  protected $entity = 'Contact';
 +
    /**
     * Test setup for every test.
     *
@@@ -64,6 -54,7 +64,6 @@@
    public function setUp() {
      // Connect to the database.
      parent::setUp();
 -    $this->_apiversion = 3;
      $this->_entity = 'contact';
      $this->_params = array(
        'first_name' => 'abc1',
@@@ -78,7 -69,6 +78,7 @@@
     * @throws \Exception
     */
    public function tearDown() {
 +    $this->_apiversion = 3;
      $this->callAPISuccess('Setting', 'create', array('includeOrderByClause' => TRUE));
      // truncate a few tables
      $tablesToTruncate = array(
@@@ -97,7 -87,6 +97,7 @@@
        'civicrm_group_contact',
        'civicrm_saved_search',
        'civicrm_group_contact_cache',
 +      'civicrm_prevnext_cache',
      );
  
      $this->quickCleanup($tablesToTruncate, TRUE);
     *
     * Verify that attempt to create individual contact with only
     * first and last names succeeds
 +   *
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testAddCreateIndividual() {
 +  public function testAddCreateIndividual($version) {
 +    $this->_apiversion = $version;
      $oldCount = CRM_Core_DAO::singleValueQuery('select count(*) from civicrm_contact');
      $params = array(
        'first_name' => 'abc1',
     * Test civicrm_contact_create.
     *
     * Verify that preferred language can be set.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testAddCreateIndividualWithPreferredLanguage() {
 +  public function testAddCreateIndividualWithPreferredLanguage($version) {
 +    $this->_apiversion = $version;
      $params = array(
        'first_name' => 'abc1',
        'contact_type' => 'Individual',
     * Test civicrm_contact_create with sub-types.
     *
     * Verify that sub-types are created successfully and not deleted by subsequent updates.
 +   *
 +   * v3 only - uses nonstandard syntax
     */
    public function testIndividualSubType() {
      $params = array(
  
    /**
     * Verify that we can retreive contacts of different sub types
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testGetMultipleContactSubTypes() {
 +  public function testGetMultipleContactSubTypes($version) {
 +    $this->_apiversion = $version;
  
      // This test presumes that there are no parents or students in the dataset
  
     * Test creating individual by name.
     *
     * Verify create individual contact with only first and last names succeeds.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testCreateNameIndividual() {
 +  public function testCreateNameIndividual($version) {
 +    $this->_apiversion = $version;
      $params = array(
        'first_name' => 'abc1',
        'contact_type' => 'Individual',
     * Test creating individual by display_name.
     *
     * Display name & sort name should be set.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testCreateDisplayNameIndividual() {
 +  public function testCreateDisplayNameIndividual($version) {
 +    $this->_apiversion = $version;
      $params = array(
        'display_name' => 'abc1',
        'contact_type' => 'Individual',
  
    /**
     * Test that name searches are case insensitive.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testGetNameVariantsCaseInsensitive() {
 +  public function testGetNameVariantsCaseInsensitive($version) {
 +    $this->_apiversion = $version;
      $this->callAPISuccess('contact', 'create', [
        'display_name' => 'Abc1',
        'contact_type' => 'Individual',
      $this->callAPISuccessGetSingle('Contact', ['display_name' => 'aBc1']);
      $this->callAPISuccessGetSingle('Contact', ['sort_name' => 'aBc1']);
      Civi::settings()->set('includeNickNameInName', TRUE);
 -    $this->callAPISuccessGetSingle('Contact', ['display_name' => 'aBc1']);
 +    $result = $this->callAPISuccessGetSingle('Contact', ['display_name' => 'aBc1']);
      $this->callAPISuccessGetSingle('Contact', ['sort_name' => 'aBc1']);
      Civi::settings()->set('includeNickNameInName', FALSE);
    }
     *
     * Verify that attempt to create household contact with only
     * household name succeeds
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testCreateNameHousehold() {
 +  public function testCreateNameHousehold($version) {
 +    $this->_apiversion = $version;
      $params = array(
        'household_name' => 'The abc Household',
        'contact_type' => 'Household',
     *
     * Verify that attempt to create organization contact with only
     * organization name succeeds.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testCreateNameOrganization() {
 +  public function testCreateNameOrganization($version) {
 +    $this->_apiversion = $version;
      $params = array(
        'organization_name' => 'The abc Organization',
        'contact_type' => 'Organization',
  
    /**
     * Check that permissions on API key are restricted (CRM-18112).
 +   *
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testCreateApiKey() {
 +  public function testCreateApiKey($version) {
 +    $this->_apiversion = $version;
      $config = CRM_Core_Config::singleton();
      $contactId = $this->individualCreate(array(
        'first_name' => 'A',
     * Note that the test is written on purpose without any
     * variables specific to participant so it can be replicated into other entities
     * and / or moved to the automated test suite
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testCreateWithCustom() {
 +  public function testCreateWithCustom($version) {
 +    $this->_apiversion = $version;
      $ids = $this->entityCustomGroupWithSingleFieldCreate(__FUNCTION__, __FILE__);
  
      $params = $this->_params;
  
    /**
     * CRM-14232 test preferred language set to site default if not passed.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testCreatePreferredLanguageUnset() {
 +  public function testCreatePreferredLanguageUnset($version) {
 +    $this->_apiversion = $version;
      $this->callAPISuccess('Contact', 'create', array(
        'first_name' => 'Snoop',
        'last_name' => 'Dog',
  
    /**
     * CRM-14232 test preferred language returns setting if not passed.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testCreatePreferredLanguageSet() {
 +  public function testCreatePreferredLanguageSet($version) {
 +    $this->_apiversion = $version;
      $this->callAPISuccess('Setting', 'create', array('contact_default_language' => 'fr_FR'));
      $this->callAPISuccess('Contact', 'create', array(
        'first_name' => 'Snoop',
  
    /**
     * CRM-14232 test preferred language returns setting if not passed where setting is NULL.
 +   * TODO: Api4
     */
    public function testCreatePreferredLanguageNull() {
      $this->callAPISuccess('Setting', 'create', array('contact_default_language' => 'null'));
  
    /**
     * CRM-14232 test preferred language returns setting if not passed where setting is NULL.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testCreatePreferredLanguagePassed() {
 +  public function testCreatePreferredLanguagePassed($version) {
 +    $this->_apiversion = $version;
      $this->callAPISuccess('Setting', 'create', array('contact_default_language' => 'null'));
      $this->callAPISuccess('Contact', 'create', array(
        'first_name' => 'Snoop',
     * Check deceased contacts are not retrieved.
     *
     * Note at time of writing the default is to return default. This should possibly be changed & test added.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testGetDeceasedRetrieved() {
 +  public function testGetDeceasedRetrieved($version) {
 +    $this->_apiversion = $version;
      $this->callAPISuccess($this->_entity, 'create', $this->_params);
      $c2 = $this->callAPISuccess($this->_entity, 'create', array(
        'first_name' => 'bb',
     * Test that we can retrieve contacts using array syntax.
     *
     * I.e 'id' => array('IN' => array('3,4')).
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testGetINIDArray() {
 +  public function testGetINIDArray($version) {
 +    $this->_apiversion = $version;
      $c1 = $this->callAPISuccess($this->_entity, 'create', $this->_params);
      $c2 = $this->callAPISuccess($this->_entity, 'create', array(
        'first_name' => 'bb',
  
    /**
     * Test that sort works - new syntax.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testGetSortNewSyntax() {
 +  public function testGetSortNewSyntax($version) {
 +    $this->_apiversion = $version;
      $c1 = $this->callAPISuccess($this->_entity, 'create', $this->_params);
      $c2 = $this->callAPISuccess($this->_entity, 'create', array(
        'first_name' => 'bb',
     * Test sort and limit for chained relationship get.
     *
     * https://issues.civicrm.org/jira/browse/CRM-15983
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testSortLimitChainedRelationshipGetCRM15983() {
 +  public function testSortLimitChainedRelationshipGetCRM15983($version) {
 +    $this->_apiversion = $version;
      // Some contact
      $create_result_1 = $this->callAPISuccess('contact', 'create', array(
        'first_name' => 'Jules',
  
    /**
     * Test apostrophe works in get & create.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testGetApostropheCRM10857() {
 +  public function testGetApostropheCRM10857($version) {
 +    $this->_apiversion = $version;
      $params = array_merge($this->_params, array('last_name' => "O'Connor"));
      $this->callAPISuccess($this->_entity, 'create', $params);
      $result = $this->callAPISuccess($this->_entity, 'getsingle', array(
     * Test between accepts zero.
     *
     * In the past it incorrectly required !empty.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testGetBetweenZeroWorks() {
 +  public function testGetBetweenZeroWorks($version) {
 +    $this->_apiversion = $version;
      $this->callAPISuccess($this->_entity, 'get', [
        'contact_id' => ['BETWEEN' => [0, 9]],
      ]);
  
    /**
     * Test retrieval by addressee id.
 +   * V3 only - the "skip_greeting_processing" param is not currently in v4
     */
    public function testGetByAddresseeID() {
      $individual1ID = $this->individualCreate([
     * Test for direction when chaining relationships.
     *
     * https://issues.civicrm.org/jira/browse/CRM-16084
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testDirectionChainingRelationshipsCRM16084() {
 +  public function testDirectionChainingRelationshipsCRM16084($version) {
 +    $this->_apiversion = $version;
      // Some contact, called Jules.
      $create_result_1 = $this->callAPISuccess('contact', 'create', array(
        'first_name' => 'Jules',
      $this->callAPISuccess('contact', 'delete', array(
        'id' => $create_result_2['id'],
      ));
 -    $this->callAPISuccess('contact', 'delete', array(
 -      'id' => $create_result_2['id'],
 -    ));
  
      // Assert.
      $this->assertEquals(1, $get_result['api.relationship.get']['count']);
     * Verify successful update of individual contact.
     */
    public function testUpdateIndividualWithAll() {
 -    // Insert a row in civicrm_contact creating individual contact.
 -    $op = new PHPUnit_Extensions_Database_Operation_Insert();
 -    $op->execute($this->_dbconn,
 -      $this->createXMLDataSet(
 -        dirname(__FILE__) . '/dataset/contact_ind.xml'
 -      )
 -    );
 +    $contactID = $this->individualCreate();
  
 -    $params = array(
 -      'id' => 23,
 +    $params = [
 +      'id' => $contactID,
        'first_name' => 'abcd',
        'contact_type' => 'Individual',
        'nick_name' => 'This is nickname first',
        'external_identifier' => '1928837465',
        'image_URL' => 'http://some.url.com/image.jpg',
        'home_url' => 'http://www.example.org',
 -
 -    );
 +    ];
  
      $this->callAPISuccess('Contact', 'Update', $params);
      $getResult = $this->callAPISuccess('Contact', 'Get', $params);
      //reducing this test partially back to api v2 level to get it through
      unset($params['home_url']);
      foreach ($params as $key => $value) {
 -      $this->assertEquals($value, $getResult['values'][23][$key]);
 +      $this->assertEquals($value, $getResult['values'][$contactID][$key]);
      }
 -    // Check updated civicrm_contact against expected.
 -    $expected = $this->createXMLDataSet(
 -      dirname(__FILE__) . '/dataset/contact_ind_upd.xml'
 -    );
 -    $actual = new PHPUnit_Extensions_Database_DataSet_QueryDataSet(
 -      $this->_dbconn
 -    );
 -    $actual->addTable('civicrm_contact');
 -    $expected->matches($actual);
    }
  
    /**
     * Verify successful update of organization contact.
 +   *
 +   * @throws \Exception
     */
    public function testUpdateOrganizationWithAll() {
 -    // Insert a row in civicrm_contact creating organization contact
 -    $op = new PHPUnit_Extensions_Database_Operation_Insert();
 -    $op->execute($this->_dbconn,
 -      $this->createXMLDataSet(
 -        dirname(__FILE__) . '/dataset/contact_org.xml'
 -      )
 -    );
 +    $contactID = $this->organizationCreate();
  
 -    $params = array(
 -      'id' => 24,
 +    $params = [
 +      'id' => $contactID,
        'organization_name' => 'WebAccess India Pvt Ltd',
        'legal_name' => 'WebAccess',
        'sic_code' => 'ABC12DEF',
        'contact_type' => 'Organization',
 -    );
 +    ];
  
      $this->callAPISuccess('Contact', 'Update', $params);
 -
 -    // Check updated civicrm_contact against expected.
 -    $expected = $this->createXMLDataSet(
 -      dirname(__FILE__) . '/dataset/contact_org_upd.xml'
 -    );
 -    $actual = new PHPUnit_Extensions_Database_DataSet_QueryDataSet(
 -      $this->_dbconn
 -    );
 -    $actual->addTable('civicrm_contact');
 -    $expected->matches($actual);
 +    $this->getAndCheck($params, $contactID, 'Contact');
    }
  
    /**
  
    /**
     * Verify successful update of household contact.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testUpdateHouseholdWithAll() {
 -    // Insert a row in civicrm_contact creating household contact
 -    $op = new PHPUnit_Extensions_Database_Operation_Insert();
 -    $op->execute($this->_dbconn,
 -      $this->createXMLDataSet(
 -        dirname(__FILE__) . '/dataset/contact_hld.xml'
 -      )
 -    );
 +  public function testUpdateHouseholdWithAll($version) {
 +    $this->_apiversion = $version;
 +    $contactID = $this->householdCreate();
  
 -    $params = array(
 -      'id' => 25,
 +    $params = [
 +      'id' => $contactID ,
        'household_name' => 'ABC household',
        'nick_name' => 'ABC House',
        'contact_type' => 'Household',
 -    );
 +    ];
  
      $result = $this->callAPISuccess('Contact', 'Update', $params);
  
 -    $expected = array(
 +    $expected = [
        'contact_type' => 'Household',
        'is_opt_out' => 0,
        'sort_name' => 'ABC household',
        'display_name' => 'ABC household',
        'nick_name' => 'ABC House',
 -    );
 +    ];
      $this->getAndCheck($expected, $result['id'], 'contact');
    }
  
     * Deliberately exclude contact_type as it should still cope using civicrm_api.
     *
     * CRM-7645.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testUpdateCreateWithID() {
 -    // Insert a row in civicrm_contact creating individual contact.
 -    $op = new PHPUnit_Extensions_Database_Operation_Insert();
 -    $op->execute($this->_dbconn,
 -      $this->createXMLDataSet(
 -        dirname(__FILE__) . '/dataset/contact_ind.xml'
 -      )
 -    );
 -
 -    $params = array(
 -      'id' => 23,
 +  public function testUpdateCreateWithID($version) {
 +    $this->_apiversion = $version;
 +    $contactID = $this->individualCreate();
 +    $this->callAPISuccess('Contact', 'Update', [
 +      'id' => $contactID,
        'first_name' => 'abcd',
        'last_name' => 'wxyz',
 -    );
 -    $this->callAPISuccess('Contact', 'Update', $params);
 +    ]);
    }
  
    /**
     * Test civicrm_contact_delete() with no contact ID.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testContactDeleteNoID() {
 +  public function testContactDeleteNoID($version) {
 +    $this->_apiversion = $version;
      $params = array(
        'foo' => 'bar',
      );
  
    /**
     * Test civicrm_contact_delete() with error.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testContactDeleteError() {
 +  public function testContactDeleteError($version) {
 +    $this->_apiversion = $version;
      $params = array('contact_id' => 999);
      $this->callAPIFailure('contact', 'delete', $params);
    }
  
    /**
     * Test civicrm_contact_delete().
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testContactDelete() {
 +  public function testContactDelete($version) {
 +    $this->_apiversion = $version;
      $contactID = $this->individualCreate();
      $params = array(
        'id' => $contactID,
  
    /**
     * Test civicrm_contact_get() return only first name.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testContactGetRetFirst() {
 +  public function testContactGetRetFirst($version) {
 +    $this->_apiversion = $version;
      $contact = $this->callAPISuccess('contact', 'create', $this->_params);
      $params = array(
        'contact_id' => $contact['id'],
     * Test civicrm_contact_get() return only first name & last name.
     *
     * Use comma separated string return with a space.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testContactGetReturnFirstLast() {
 +  public function testContactGetReturnFirstLast($version) {
 +    $this->_apiversion = $version;
      $contact = $this->callAPISuccess('contact', 'create', $this->_params);
      $params = array(
        'contact_id' => $contact['id'],
     * Test civicrm_contact_get() return only first name & last name.
     *
     * Use comma separated string return without a space
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testContactGetReturnFirstLastNoComma() {
 +  public function testContactGetReturnFirstLastNoComma($version) {
 +    $this->_apiversion = $version;
      $contact = $this->callAPISuccess('contact', 'create', $this->_params);
      $params = array(
        'contact_id' => $contact['id'],
     * Test civicrm_contact_getquick() with empty name param.
     */
    public function testContactGetQuick() {
 -    // Insert a row in civicrm_contact creating individual contact.
 -    $op = new PHPUnit_Extensions_Database_Operation_Insert();
 -    $op->execute($this->_dbconn,
 -      $this->createXMLDataSet(
 -        dirname(__FILE__) . '/dataset/contact_17.xml'
 -      )
 -    );
 -    $op->execute($this->_dbconn,
 -      $this->createXMLDataSet(
 -        dirname(__FILE__) . '/dataset/email_contact_17.xml'
 -      )
 -    );
 -    $params = array(
 -      'name' => "T",
 -    );
 +    $contactID = $this->individualCreate(['first_name' => 'Test', 'last_name' => 'Contact', 'email' => 'TestContact@example.com']);
  
 -    $result = $this->callAPISuccess('contact', 'getquick', $params);
 -    $this->assertEquals(17, $result['values'][0]['id']);
 -    $params = array(
 +    $result = $this->callAPISuccess('contact', 'getquick', ['name' => 'T']);
 +    $this->assertEquals($contactID, $result['values'][0]['id']);
 +    $params = [
        'name' => "TestContact@example.com",
        'field_name' => 'sort_name',
 -    );
 +    ];
      $result = $this->callAPISuccess('contact', 'getquick', $params);
 -    $this->assertEquals(17, $result['values'][0]['id']);
 +    $this->assertEquals($contactID, $result['values'][0]['id']);
    }
  
    /**
     * Test civicrm_contact_get) with empty params.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testContactGetEmptyParams() {
 -    $this->callAPISuccess('contact', 'get', array());
 +  public function testContactGetEmptyParams($version) {
 +    $this->_apiversion = $version;
 +    $this->callAPISuccess('contact', 'get', []);
    }
  
    /**
     * Test civicrm_contact_get(,true) with no matches.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testContactGetOldParamsNoMatches() {
 -    // Insert a row in civicrm_contact creating contact 17.
 -    $op = new PHPUnit_Extensions_Database_Operation_Insert();
 -    $op->execute($this->_dbconn,
 -      $this->createXMLDataSet(
 -        dirname(__FILE__) . '/dataset/contact_17.xml'
 -      )
 -    );
 -
 -    $params = array(
 -      'first_name' => 'Fred',
 -    );
 -    $result = $this->callAPISuccess('contact', 'get', $params);
 +  public function testContactGetOldParamsNoMatches($version) {
 +    $this->_apiversion = $version;
 +    $this->individualCreate();
 +    $result = $this->callAPISuccess('contact', 'get', ['first_name' => 'Fred']);
      $this->assertEquals(0, $result['count']);
    }
  
     * Test civicrm_contact_get(,true) with one match.
     */
    public function testContactGetOldParamsOneMatch() {
 -    // Insert a row in civicrm_contact creating contact 17
 -    $op = new PHPUnit_Extensions_Database_Operation_Insert();
 -    $op->execute($this->_dbconn,
 -      $this->createXMLDataSet(dirname(__FILE__) . '/dataset/contact_17.xml'
 -      )
 -    );
 +    $contactID = $this->individualCreate(['first_name' => 'Test', 'last_name' => 'Contact']);
  
 -    $params = array(
 -      'first_name' => 'Test',
 -    );
 -    $result = $this->callAPISuccess('contact', 'get', $params);
 -    $this->assertEquals(17, $result['values'][17]['contact_id']);
 -    $this->assertEquals(17, $result['id']);
 +    $result = $this->callAPISuccess('contact', 'get', ['first_name' => 'Test']);
 +    $this->assertEquals($contactID, $result['values'][$contactID]['contact_id']);
 +    $this->assertEquals($contactID, $result['id']);
    }
  
    /**
  
    /**
     * Ensure consistent return format for option group fields.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testSetPreferredCommunicationNull() {
 +  public function testSetPreferredCommunicationNull($version) {
 +    $this->_apiversion = $version;
      $contact = $this->callAPISuccess('contact', 'create', array_merge($this->_params, array(
        'preferred_communication_method' => array('Phone', 'SMS'),
      )));
     * Test creating multiple phones using chaining.
     *
     * @throws \Exception
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testCRM13252MultipleChainedPhones() {
 +  public function testCRM13252MultipleChainedPhones($version) {
 +    $this->_apiversion = $version;
      $contactID = $this->householdCreate();
      $this->callAPISuccessGetCount('phone', array('contact_id' => $contactID), 0);
      $params = array(
  
    /**
     * Verify attempt to create individual with chained arrays and sequential.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testGetIndividualWithChainedArraysAndSequential() {
 +  public function testGetIndividualWithChainedArraysAndSequential($version) {
 +    $this->_apiversion = $version;
      $ids = $this->entityCustomGroupWithSingleFieldCreate(__FUNCTION__, __FILE__);
      $params['custom_' . $ids['custom_field_id']] = "custom string";
  
    /**
     * Verify attempt to create individual with chained arrays and sequential.
     *
 -   *  See https://issues.civicrm.org/jira/browse/CRM-15815
 +   * @see https://issues.civicrm.org/jira/browse/CRM-15815
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testCreateIndividualWithChainedArrayAndSequential() {
 +  public function testCreateIndividualWithChainedArrayAndSequential($version) {
 +    $this->_apiversion = $version;
      $params = array(
        'sequential' => 1,
        'first_name' => 'abc5',
  
    /**
     * Test checks usage of $values to pick & choose inputs.
 +   *
 +   * Api3 Only - chaining syntax is too funky for v4 (assuming entityTag "entity_id" field will be filled by magic)
     */
    public function testChainingValuesCreate() {
      $description = "This demonstrates the usage of chained api functions.  Specifically it has one 'parent function' &
  
    /**
     * Test TrueFalse format - I couldn't come up with an easy way to get an error on Get.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testContactGetFormatIsSuccessTrue() {
 -    $this->createContactFromXML();
 +  public function testContactGetFormatIsSuccessTrue($version) {
 +    $this->_apiversion = $version;
 +    $contactID = $this->individualCreate(['first_name' => 'Test', 'last_name' => 'Contact']);
      $description = "This demonstrates use of the 'format.is_success' param.
      This param causes only the success or otherwise of the function to be returned as BOOLEAN";
      $subfile = "FormatIsSuccess_True";
 -    $params = array('id' => 17, 'format.is_success' => 1);
 +    $params = ['id' => $contactID, 'format.is_success' => 1];
      $result = $this->callAPIAndDocument('Contact', 'Get', $params, __FUNCTION__, __FILE__, $description, $subfile);
      $this->assertEquals(1, $result);
      $this->callAPISuccess('Contact', 'Delete', $params);
  
    /**
     * Test TrueFalse format.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testContactCreateFormatIsSuccessFalse() {
 +  public function testContactCreateFormatIsSuccessFalse($version) {
 +    $this->_apiversion = $version;
  
      $description = "This demonstrates use of the 'format.is_success' param.
      This param causes only the success or otherwise of the function to be returned as BOOLEAN";
     * Test long display names.
     *
     * CRM-21258
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testContactCreateLongDisplayName() {
 +  public function testContactCreateLongDisplayName($version) {
 +    $this->_apiversion = $version;
      $result = $this->callAPISuccess('Contact', 'Create', array(
        'first_name' => str_pad('a', 64, 'a'),
        'last_name' => str_pad('a', 64, 'a'),
     * Test that we can set the sort name via the api or alter it via a hook.
     *
     * As of writing this is being fixed for Organization & Household but it makes sense to do for individuals too.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testCreateAlterSortName() {
 +  public function testCreateAlterSortName($version) {
 +    $this->_apiversion = $version;
      $organizationID = $this->organizationCreate(['organization_name' => 'The Justice League', 'sort_name' => 'Justice League, The']);
      $organization = $this->callAPISuccessGetSingle('Contact', ['return' => ['sort_name', 'display_name'], 'id' => $organizationID]);
      $this->assertEquals('Justice League, The', $organization['sort_name']);
  
    /**
     * Test Single Entity format.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testContactGetSingleEntityArray() {
 -    $this->createContactFromXML();
 +  public function testContactGetSingleEntityArray($version) {
 +    $this->_apiversion = $version;
 +    $contactID = $this->individualCreate(['first_name' => 'Test', 'last_name' => 'Contact']);
      $description = "This demonstrates use of the 'format.single_entity_array' param.
        This param causes the only contact to be returned as an array without the other levels.
        It will be ignored if there is not exactly 1 result";
      $subfile = "GetSingleContact";
 -    $params = array('id' => 17);
 -    $result = $this->callAPIAndDocument('Contact', 'GetSingle', $params, __FUNCTION__, __FILE__, $description, $subfile);
 -    $this->assertEquals('Test Contact', $result['display_name']);
 -    $this->callAPISuccess('Contact', 'Delete', $params);
 +    $result = $this->callAPIAndDocument('Contact', 'GetSingle', ['id' => $contactID], __FUNCTION__, __FILE__, $description, $subfile);
 +    $this->assertEquals('Mr. Test Contact II', $result['display_name']);
 +    $this->callAPISuccess('Contact', 'Delete', ['id' => $contactID]);
    }
  
    /**
     * Test Single Entity format.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testContactGetFormatCountOnly() {
 -    $this->createContactFromXML();
 +  public function testContactGetFormatCountOnly($version) {
 +    $this->_apiversion = $version;
 +    $contactID = $this->individualCreate(['first_name' => 'Test', 'last_name' => 'Contact']);
      $description = "This demonstrates use of the 'getCount' action.
        This param causes the count of the only function to be returned as an integer.";
 -    $params = array('id' => 17);
 +    $params = ['id' => $contactID];
      $result = $this->callAPIAndDocument('Contact', 'GetCount', $params, __FUNCTION__, __FILE__, $description,
        'GetCountContact');
      $this->assertEquals('1', $result);
  
    /**
     * Test id only format.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testContactGetFormatIDOnly() {
 -    $this->createContactFromXML();
 +  public function testContactGetFormatIDOnly($version) {
 +    $this->_apiversion = $version;
 +    $contactID = $this->individualCreate(['first_name' => 'Test', 'last_name' => 'Contact']);
      $description = "This demonstrates use of the 'format.id_only' param.
        This param causes the id of the only entity to be returned as an integer.
        It will be ignored if there is not exactly 1 result";
      $subfile = "FormatOnlyID";
 -    $params = array('id' => 17, 'format.only_id' => 1);
 +    $params = ['id' => $contactID, 'format.only_id' => 1];
      $result = $this->callAPIAndDocument('Contact', 'Get', $params, __FUNCTION__, __FILE__, $description, $subfile);
 -    $this->assertEquals('17', $result);
 +    $this->assertEquals($contactID, $result);
      $this->callAPISuccess('Contact', 'Delete', $params);
    }
  
    /**
     * Test id only format.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testContactGetFormatSingleValue() {
 -    $this->createContactFromXML();
 +  public function testContactGetFormatSingleValue($version) {
 +    $this->_apiversion = $version;
 +    $contactID = $this->individualCreate(['first_name' => 'Test', 'last_name' => 'Contact']);
      $description = "This demonstrates use of the 'format.single_value' param.
        This param causes only a single value of the only entity to be returned as an string.
        It will be ignored if there is not exactly 1 result";
      $subFile = "FormatSingleValue";
 -    $params = array('id' => 17, 'return' => 'display_name');
 +    $params = ['id' => $contactID, 'return' => 'display_name'];
      $result = $this->callAPIAndDocument('Contact', 'getvalue', $params, __FUNCTION__, __FILE__, $description, $subFile);
 -    $this->assertEquals('Test Contact', $result);
 +    $this->assertEquals('Mr. Test Contact II', $result);
      $this->callAPISuccess('Contact', 'Delete', $params);
    }
  
    /**
     * Test that permissions are respected when creating contacts.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testContactCreationPermissions() {
 +  public function testContactCreationPermissions($version) {
 +    $this->_apiversion = $version;
      $params = array(
        'contact_type' => 'Individual',
        'first_name' => 'Foo',
      $config = CRM_Core_Config::singleton();
      $config->userPermissionClass->permissions = array('access CiviCRM');
      $result = $this->callAPIFailure('contact', 'create', $params);
 -    $this->assertEquals('API permission check failed for Contact/create call; insufficient permission: require access CiviCRM and add contacts', $result['error_message'], 'lacking permissions should not be enough to create a contact');
 +    $this->assertContains('failed', $result['error_message'], 'lacking permissions should not be enough to create a contact');
  
      $config->userPermissionClass->permissions = array('access CiviCRM', 'add contacts', 'import contacts');
      $this->callAPISuccess('contact', 'create', $params);
  
    /**
     * Test that delete with skip undelete respects permissions.
 +   * TODO: Api4
     */
    public function testContactDeletePermissions() {
      $contactID = $this->individualCreate();
  
    /**
     * Test update with check permissions set.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testContactUpdatePermissions() {
 +  public function testContactUpdatePermissions($version) {
 +    $this->_apiversion = $version;
      $params = array(
        'contact_type' => 'Individual',
        'first_name' => 'Foo',
      $this->callAPISuccess('contact', 'update', $params);
    }
  
 -  /**
 -   * Set up helper to create a contact.
 -   */
 -  public function createContactFromXML() {
 -    // Insert a row in civicrm_contact creating contact 17.
 -    $op = new PHPUnit_Extensions_Database_Operation_Insert();
 -    $op->execute($this->_dbconn,
 -      $this->createXMLDataSet(
 -        dirname(__FILE__) . '/dataset/contact_17.xml'
 -      )
 -    );
 -  }
 -
    /**
     * Test contact proximity api.
     */
  
    /**
     * Test the use of sql operators.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testSQLOperatorsOnContactAPI() {
 +  public function testSQLOperatorsOnContactAPI($version) {
 +    $this->_apiversion = $version;
      $this->individualCreate();
      $this->organizationCreate();
      $this->householdCreate();
  
    /**
     * CRM-14743 - test api respects search operators.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testGetModifiedDateByOperators() {
 +  public function testGetModifiedDateByOperators($version) {
 +    $this->_apiversion = $version;
      $preExistingContactCount = CRM_Core_DAO::singleValueQuery('select count(*) FROM civicrm_contact');
      $contact1 = $this->individualCreate();
      $sql = "UPDATE civicrm_contact SET created_date = '2012-01-01', modified_date = '2013-01-01' WHERE id = " . $contact1;
  
    /**
     * CRM-14743 - test api respects search operators.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testGetCreatedDateByOperators() {
 +  public function testGetCreatedDateByOperators($version) {
 +    $this->_apiversion = $version;
      $preExistingContactCount = CRM_Core_DAO::singleValueQuery('select count(*) FROM civicrm_contact');
      $contact1 = $this->individualCreate();
      $sql = "UPDATE civicrm_contact SET created_date = '2012-01-01' WHERE id = " . $contact1;
  
    }
  
 +  /**
 +   * Test merging 2 contacts with custom fields.
 +   *
 +   * @throws \Exception
 +   */
 +  public function testMergeCustomFields() {
 +    $contact1 = $this->individualCreate();
 +    // Not sure this is quite right but it does get it into the file table
 +    $file = $this->callAPISuccess('Attachment', 'create', [
 +      'name' => 'header.txt',
 +      'mime_type' => 'text/plain',
 +      'description' => 'My test description',
 +      'content' => 'My test content',
 +      'entity_table' => 'civicrm_contact',
 +      'entity_id' => $contact1,
 +    ]);
 +
 +    $this->createCustomGroupWithFieldsOfAllTypes();
 +    $fileField = $this->getCustomFieldName('file');
 +    $linkField = $this->getCustomFieldName('link');
 +    $dateField = $this->getCustomFieldName('select_date');
 +    $selectField = $this->getCustomFieldName('select_string');
 +    $countryField = $this->getCustomFieldName('country');
 +
 +    $countriesByName = array_flip(CRM_Core_PseudoConstant::country(FALSE, FALSE));
 +    $customFieldValues = [
 +      // @todo fix the fatal bug on this & uncomment - see dev/core#723
 +      $fileField => $file['id'],
 +      $linkField => 'http://example.org',
 +      $dateField => '2018-01-01 17:10:56',
 +      $selectField => 'G',
 +      // Currently broken.
 +      //$countryField => $countriesByName['New Zealand'],
 +    ];
 +    $this->callAPISuccess('Contact', 'create', array_merge([
 +      'id' => $contact1,
 +    ], $customFieldValues));
 +
 +    $contact2 = $this->individualCreate();
 +    $this->callAPISuccess('contact', 'merge', [
 +      'to_keep_id' => $contact2,
 +      'to_remove_id' => $contact1,
 +      'auto_flip' => FALSE,
 +    ]);
 +    $contact = $this->callAPISuccessGetSingle('Contact', ['id' => $contact2, 'return' => array_keys($customFieldValues)]);
 +    $this->assertEquals($contact2, CRM_Core_DAO::singleValueQuery('SELECT entity_id FROM civicrm_entity_file WHERE file_id = ' . $file['id']));
 +    foreach ($customFieldValues as $key => $value) {
 +      $this->assertEquals($value, $contact[$key]);
 +    }
 +  }
 +
    /**
     * Test retrieving merged contacts.
     *
  
    /**
     * CRM-21041 Test if 'communication style' is set to site default if not passed.
 +   * @param int $version
 +   * @dataProvider versionThreeAndFour
     */
 -  public function testCreateCommunicationStyleUnset() {
 +  public function testCreateCommunicationStyleUnset($version) {
 +    $this->_apiversion = $version;
      $this->callAPISuccess('Contact', 'create', array(
        'first_name' => 'John',
        'last_name' => 'Doe',
  
    /**
     * Test that creating a contact with various contact greetings works.
 +   * V3 Only.
     */
    public function testContactGreetingsCreate() {
      $contact = $this->callAPISuccess('Contact', 'create', array('first_name' => 'Alan', 'last_name' => 'MouseMouse', 'contact_type' => 'Individual'));
      ]);
    }
  
+   /**
+    * Test the related contacts filter.
+    *
+    * @throws \Exception
+    */
    public function testSmartGroupsForRelatedContacts() {
      $rtype1 = $this->callAPISuccess('relationship_type', 'create', array(
        "name_a_b" => uniqid() . " Child of",