From 782d9509f47f359560c121581332c658d8ca7d61 Mon Sep 17 00:00:00 2001 From: Darrick Servis Date: Sat, 30 Apr 2022 12:55:20 -0700 Subject: [PATCH] Allow dupeQuery to add query for table added via dupeQuery type supportedFields hook. Adding unit tests for hook_civicrm_dupeQuery. Fix coding standards. Add query option to dupeQuery hook. Fix typo. Provide custom test entity to test findDuplicates. Change contact type from Organization to Individual. Use CRM_Core_DAO::getReferencesToContactTable() and CRM_Core_DAO::getDynamicReferencesToTable('civicrm_contact') to find refs for the DedupeRule query. Simplify changes as much as possible. --- CRM/Dedupe/BAO/DedupeRule.php | 40 ++- .../phpunit/CRM/Dedupe/BAO/RuleGroupTest.php | 275 ++++++++++++++++++ 2 files changed, 292 insertions(+), 23 deletions(-) diff --git a/CRM/Dedupe/BAO/DedupeRule.php b/CRM/Dedupe/BAO/DedupeRule.php index 5cd159c7b2..ab12b8ffd2 100644 --- a/CRM/Dedupe/BAO/DedupeRule.php +++ b/CRM/Dedupe/BAO/DedupeRule.php @@ -39,6 +39,7 @@ class CRM_Dedupe_BAO_DedupeRule extends CRM_Dedupe_DAO_DedupeRule { * * @return string * SQL query performing the search + * or NULL if params is present and doesn't have and for a field. * * @throws \CRM_Core_Exception * @throws \CiviCRM_API3_Exception @@ -71,6 +72,9 @@ class CRM_Dedupe_BAO_DedupeRule extends CRM_Dedupe_DAO_DedupeRule { $innerJoinClauses[] = "t2.{$this->rule_field} <> ''"; } + $cidRefs = CRM_Core_DAO::getReferencesToContactTable(); + $eidRefs = CRM_Core_DAO::getDynamicReferencesToTable('civicrm_contact'); + switch ($this->rule_table) { case 'civicrm_contact': $id = 'id'; @@ -86,30 +90,20 @@ class CRM_Dedupe_BAO_DedupeRule extends CRM_Dedupe_DAO_DedupeRule { } break; - case 'civicrm_address': - case 'civicrm_email': - case 'civicrm_im': - case 'civicrm_openid': - case 'civicrm_phone': - case 'civicrm_website': - $id = 'contact_id'; - break; - - case 'civicrm_note': - $id = 'entity_id'; - if ($this->params) { - $where[] = "t1.entity_table = 'civicrm_contact'"; - } - else { - $where[] = "t1.entity_table = 'civicrm_contact'"; - $where[] = "t2.entity_table = 'civicrm_contact'"; - } - break; - default: - // custom data tables - if (preg_match('/^civicrm_value_/', $this->rule_table) || preg_match('/^custom_value_/', $this->rule_table)) { - $id = 'entity_id'; + if (array_key_exists($this->rule_table, $eidRefs)) { + $id = $eidRefs[$this->rule_table][0]; + $entity_table = $eidRefs[$this->rule_table][1]; + if ($this->params) { + $where[] = "t1.$entity_table = 'civicrm_contact'"; + } + else { + $where[] = "t1.$entity_table = 'civicrm_contact'"; + $where[] = "t2.$entity_table = 'civicrm_contact'"; + } + } + elseif (array_key_exists($this->rule_table, $cidRefs)) { + $id = $cidRefs[$this->rule_table][0]; } else { throw new CRM_Core_Exception("Unsupported rule_table for civicrm_dedupe_rule.id of {$this->id}"); diff --git a/tests/phpunit/CRM/Dedupe/BAO/RuleGroupTest.php b/tests/phpunit/CRM/Dedupe/BAO/RuleGroupTest.php index a716ab13e0..dd5cb947e2 100644 --- a/tests/phpunit/CRM/Dedupe/BAO/RuleGroupTest.php +++ b/tests/phpunit/CRM/Dedupe/BAO/RuleGroupTest.php @@ -1,11 +1,111 @@ [ + 'name' => 'id', + 'type' => CRM_Utils_Type::T_INT, + 'required' => TRUE, + 'where' => 'civicrm_dedupe_test_table.id', + 'table_name' => 'civicrm_dedupe_test_table', + 'entity' => 'TestEntity', + ], + 'contact_id' => [ + 'name' => 'contact_id', + 'type' => CRM_Utils_Type::T_INT, + 'where' => 'civicrm_dedupe_test_table.contact_id', + 'table_name' => 'civicrm_dedupe_test_table', + 'entity' => 'TestEntity', + 'FKClassName' => 'CRM_Contact_DAO_Contact', + ], + 'dedupe_test_field' => [ + 'name' => 'dedupe_test_field', + 'type' => CRM_Utils_Type::T_STRING, + 'maxlength' => 64, + 'size' => 8, + 'import' => TRUE, + 'where' => 'civicrm_dedupe_test_table.dedupe_test_field', + 'table_name' => 'civicrm_dedupe_test_table', + 'entity' => 'TestEntity', + ], + ]; + } + return Civi::$statics[__CLASS__]['fields']; + } + +} + /** * Class CRM_Dedupe_BAO_RuleGroupTest * @group headless */ class CRM_Dedupe_BAO_RuleGroupTest extends CiviUnitTestCase { + /** + * IDs of created contacts. + * + * @var array + */ + protected $contactIDs = []; + + /** + * ID of the group holding the contacts. + * + * @var int + */ + protected $groupID; + + /** + * @var \Civi\API\Kernel + */ + protected $apiKernel; + + /** + * @var \Civi\API\Provider\AdhocProvider + */ + protected $adhocProvider; + + /** + * Clean up after the test. + * + * @throws \CRM_Core_Exception + */ + public function tearDown(): void { + + foreach ($this->contactIDs as $contactId) { + $this->contactDelete($contactId); + } + if ($this->groupID) { + $this->callAPISuccess('group', 'delete', ['id' => $this->groupID]); + } + $this->quickCleanup(['civicrm_contact'], TRUE); + CRM_Core_DAO::executeQuery("DELETE r FROM civicrm_dedupe_rule_group rg INNER JOIN civicrm_dedupe_rule r ON rg.id = r.dedupe_rule_group_id WHERE rg.is_reserved = 0 AND used = 'General'"); + CRM_Core_DAO::executeQuery("DELETE FROM civicrm_dedupe_rule_group WHERE is_reserved = 0 AND used = 'General'"); + + parent::tearDown(); + } + /** * Test that sort_name is included in supported fields. * @@ -100,4 +200,179 @@ class CRM_Dedupe_BAO_RuleGroupTest extends CiviUnitTestCase { ], $fields); } + /** + * Test hook_dupeQuery match on custom entity field. + * + * @throws \CRM_Core_Exception + */ + public function testHookDupeQueryMatch() { + $this->hookClass->setHook('civicrm_dupeQuery', [$this, 'hook_civicrm_dupeQuery']); + + \CRM_Core_DAO_AllCoreTables::init(TRUE); + + \CRM_Core_DAO_AllCoreTables::registerEntityType('TestEntity', 'CRM_Dedupe_DAO_TestEntity', 'civicrm_dedupe_test_table'); + $this->apiKernel = \Civi::service('civi_api_kernel'); + $this->adhocProvider = new \Civi\API\Provider\AdhocProvider(3, 'TestEntity'); + $this->apiKernel->registerApiProvider($this->adhocProvider); + + //DedupeRule.php call this hook to get the type for the field. + $this->adhocProvider->addAction('getfields', 'access CiviCRM', function ($apiRequest) { + return [ + 'values' => [ + 'id' => [ + 'name' => 'id', + 'type' => CRM_Utils_Type::T_INT, + ], + 'contact_id' => [ + 'name' => 'contact_id', + 'type' => CRM_Utils_Type::T_INT, + ], + 'dedupe_test_field' => [ + 'name' => 'dedupe_test_field', + 'type' => CRM_Utils_Type::T_STRING, + ], + ], + ]; + }); + + CRM_Core_DAO::executeQuery('DROP TABLE IF EXISTS civicrm_dedupe_test_table'); + // Setup our custom enity table. + $sql = "CREATE TABLE `civicrm_dedupe_test_table` ( + `id` int(10) UNSIGNED NOT NULL COMMENT 'Unique ID', + `contact_id` int(10) UNSIGNED DEFAULT NULL, + `dedupe_test_field` varchar(64) DEFAULT NULL + )"; + + CRM_Core_DAO::executeQuery($sql); + + $sql = "ALTER TABLE `civicrm_dedupe_test_table` ADD INDEX `FK_civicrm_dedupe_test_table_contact_id` (`contact_id`);"; + + CRM_Core_DAO::executeQuery($sql); + + $params = [ + 'name' => 'Dupe Group', + 'title' => 'New Test Dupe Group', + 'domain_id' => 1, + 'is_active' => 1, + 'visibility' => 'Public Pages', + ]; + + $result = $this->callAPISuccess('group', 'create', $params); + $this->groupID = $result['id']; + + $params = [ + [ + 'first_name' => 'robin', + 'last_name' => 'hood', + 'contact_type' => 'Individual', + ], + [ + 'first_name' => 'bob', + 'last_name' => 'dobbs', + 'contact_type' => 'Individual', + ], + ]; + + $count = 1; + $contact_id; + foreach ($params as $param) { + $contact = $this->callAPISuccess('contact', 'create', $param); + $this->contactIDs[$count++] = $contact['id']; + + $grpParams = [ + 'contact_id' => $contact['id'], + 'group_id' => $this->groupID, + ]; + $this->callAPISuccess('group_contact', 'create', $grpParams); + + CRM_Core_DAO::executeQuery("INSERT INTO `civicrm_dedupe_test_table` (`id`, `contact_id`, `dedupe_test_field`) VALUES (" . $count . "," . $contact['id'] . ", 'duplicate');"); + $contact_id = $contact['id']; + } + + // verify that all contacts have been created separately + $this->assertEquals(count($this->contactIDs), 2, 'Check for number of contacts.'); + + // Create our RuleGroup with one rule. + $ruleGroup = $this->callAPISuccess('RuleGroup', 'create', [ + 'contact_type' => 'Individual', + 'threshold' => 10, + 'used' => 'General', + 'name' => 'TestRule', + 'title' => 'TestRule', + 'is_reserved' => 0, + ]); + + foreach (['dedupe_test_field'] as $field) { + $rules[$field] = $this->callAPISuccess('Rule', 'create', [ + 'dedupe_rule_group_id' => $ruleGroup['id'], + 'rule_weight' => 10, + 'rule_field' => $field, + 'rule_table' => 'civicrm_dedupe_test_table', + ]); + } + + // Test op supportedFields + $fields = CRM_Dedupe_BAO_DedupeRuleGroup::supportedFields('Individual'); + $this->assertEquals([ + 'dedupe_test_field' => 'Test Field', + ], $fields['civicrm_dedupe_test_table']); + + // Test rule finds a match. + $foundDupes = CRM_Dedupe_Finder::dupesInGroup($ruleGroup['id'], $this->groupID); + + $this->assertCount(1, $foundDupes); + + // Test rule finds no match. + CRM_Core_DAO::executeQuery("UPDATE `civicrm_dedupe_test_table` SET dedupe_test_field = 'not a duplicate' WHERE contact_id = $contact_id;"); + + $foundDupes = CRM_Dedupe_Finder::dupesInGroup($ruleGroup['id'], $this->groupID); + + $this->assertCount(0, $foundDupes); + + CRM_Core_DAO::executeQuery('DROP TABLE civicrm_dedupe_test_table'); + \CRM_Core_DAO_AllCoreTables::init(TRUE); + } + + /** + * Implements hook_civicrm_dupeQuery(). + * + * Locks in expected params + * + */ + public function hook_civicrm_dupeQuery($baoObject, $op, &$objectData) { + $this->assertContains($op, ['supportedFields', 'dedupeIndexes', 'query', 'table', 'threshold']); + switch ($op) { + case 'supportedFields': + $this->assertIsArray($objectData); + $this->assertNull($baoObject); + $objectData['Individual']['civicrm_dedupe_test_table'] = [ + 'dedupe_test_field' => 'Test Field', + ]; + break; + + case 'dedupeIndexes': + //Not tested. + break; + + case 'query': + $this->assertIsArray($objectData); + $this->assertInstanceOf(CRM_Dedupe_BAO_DedupeRule::class, $baoObject); + if ($baoObject->rule_table == 'civicrm_dedupe_test_table') { + $objectData = $baoObject->entitySql('contact_id'); + } + break; + + case 'table': + $this->assertIsArray($objectData); + $this->assertInstanceOf(CRM_Dedupe_BAO_DedupeRuleGroup::class, $baoObject); + break; + + case 'threshold': + $this->assertIsString($objectData); + $this->assertInstanceOf(CRM_Dedupe_BAO_DedupeRuleGroup::class, $baoObject); + break; + + } + } + } -- 2.25.1