Merge pull request #8302 from colemanw/CRM-18306
[civicrm-core.git] / tests / phpunit / CRM / Dedupe / MergerTest.php
1 <?php
2
3 /**
4 * Class CRM_Dedupe_DedupeMergerTest
5 * @group headless
6 */
7 class CRM_Dedupe_MergerTest extends CiviUnitTestCase {
8
9 protected $_groupId;
10 protected $_contactIds = array();
11
12 public function createDupeContacts() {
13 // create a group to hold contacts, so that dupe checks don't consider any other contacts in the DB
14 $params = array(
15 'name' => 'Test Dupe Merger Group',
16 'title' => 'Test Dupe Merger Group',
17 'domain_id' => 1,
18 'is_active' => 1,
19 'visibility' => 'Public Pages',
20 'version' => 3,
21 );
22 // TODO: This is not an API test!!
23 $result = civicrm_api('group', 'create', $params);
24 $this->_groupId = $result['id'];
25
26 // contact data set
27
28 // make dupe checks based on based on following contact sets:
29 // FIRST - LAST - EMAIL
30 // ---------------------------------
31 // robin - hood - robin@example.com
32 // robin - hood - robin@example.com
33 // robin - hood - hood@example.com
34 // robin - dale - robin@example.com
35 // little - dale - dale@example.com
36 // little - dale - dale@example.com
37 // will - dale - dale@example.com
38 // will - dale - will@example.com
39 // will - dale - will@example.com
40 $params = array(
41 array(
42 'first_name' => 'robin',
43 'last_name' => 'hood',
44 'email' => 'robin@example.com',
45 'contact_type' => 'Individual',
46 ),
47 array(
48 'first_name' => 'robin',
49 'last_name' => 'hood',
50 'email' => 'robin@example.com',
51 'contact_type' => 'Individual',
52 ),
53 array(
54 'first_name' => 'robin',
55 'last_name' => 'hood',
56 'email' => 'hood@example.com',
57 'contact_type' => 'Individual',
58 ),
59 array(
60 'first_name' => 'robin',
61 'last_name' => 'dale',
62 'email' => 'robin@example.com',
63 'contact_type' => 'Individual',
64 ),
65 array(
66 'first_name' => 'little',
67 'last_name' => 'dale',
68 'email' => 'dale@example.com',
69 'contact_type' => 'Individual',
70 ),
71 array(
72 'first_name' => 'little',
73 'last_name' => 'dale',
74 'email' => 'dale@example.com',
75 'contact_type' => 'Individual',
76 ),
77 array(
78 'first_name' => 'will',
79 'last_name' => 'dale',
80 'email' => 'dale@example.com',
81 'contact_type' => 'Individual',
82 ),
83 array(
84 'first_name' => 'will',
85 'last_name' => 'dale',
86 'email' => 'will@example.com',
87 'contact_type' => 'Individual',
88 ),
89 array(
90 'first_name' => 'will',
91 'last_name' => 'dale',
92 'email' => 'will@example.com',
93 'contact_type' => 'Individual',
94 ),
95 );
96
97 $count = 1;
98 foreach ($params as $param) {
99 $param['version'] = 3;
100 $contact = civicrm_api('contact', 'create', $param);
101 $this->_contactIds[$count++] = $contact['id'];
102
103 $grpParams = array(
104 'contact_id' => $contact['id'],
105 'group_id' => $this->_groupId,
106 'version' => 3,
107 );
108 $res = civicrm_api('group_contact', 'create', $grpParams);
109 }
110 }
111
112 /**
113 * Delete all created contacts.
114 */
115 public function deleteDupeContacts() {
116 foreach ($this->_contactIds as $contactId) {
117 $this->contactDelete($contactId);
118 }
119
120 // delete dupe group
121 $params = array('id' => $this->_groupId, 'version' => 3);
122 civicrm_api('group', 'delete', $params);
123 }
124
125 /**
126 * Test the batch merge.
127 */
128 public function testBatchMergeSelectedDuplicates() {
129 $this->createDupeContacts();
130
131 // verify that all contacts have been created separately
132 $this->assertEquals(count($this->_contactIds), 9, 'Check for number of contacts.');
133
134 $dao = new CRM_Dedupe_DAO_RuleGroup();
135 $dao->contact_type = 'Individual';
136 $dao->name = 'IndividualSupervised';
137 $dao->is_default = 1;
138 $dao->find(TRUE);
139
140 $foundDupes = CRM_Dedupe_Finder::dupesInGroup($dao->id, $this->_groupId);
141
142 // -------------------------------------------------------------------------
143 // Name and Email (reserved) Matches ( 3 pairs )
144 // --------------------------------------------------------------------------
145 // robin - hood - robin@example.com
146 // robin - hood - robin@example.com
147 // little - dale - dale@example.com
148 // little - dale - dale@example.com
149 // will - dale - will@example.com
150 // will - dale - will@example.com
151 // so 3 pairs for - first + last + mail
152 $this->assertEquals(count($foundDupes), 3, 'Check Individual-Supervised dupe rule for dupesInGroup().');
153
154 // Run dedupe finder as the browser would
155 $_SERVER['REQUEST_METHOD'] = 'GET'; //avoid invalid key error
156 $object = new CRM_Contact_Page_DedupeFind();
157 $object->set('gid', $this->_groupId);
158 $object->set('rgid', $dao->id);
159 $object->set('action', CRM_Core_Action::UPDATE);
160 @$object->run();
161
162 // Retrieve pairs from prev next cache table
163 $select = array('pn.is_selected' => 'is_selected');
164 $cacheKeyString = "merge Individual_{$dao->id}_{$this->_groupId}";
165 $pnDupePairs = CRM_Core_BAO_PrevNextCache::retrieve($cacheKeyString, NULL, NULL, 0, 0, $select);
166
167 $this->assertEquals(count($foundDupes), count($pnDupePairs), 'Check number of dupe pairs in prev next cache.');
168
169 // mark first two pairs as selected
170 CRM_Core_DAO::singleValueQuery("UPDATE civicrm_prevnext_cache SET is_selected = 1 WHERE id IN ({$pnDupePairs[0]['prevnext_id']}, {$pnDupePairs[1]['prevnext_id']})");
171
172 $pnDupePairs = CRM_Core_BAO_PrevNextCache::retrieve($cacheKeyString, NULL, NULL, 0, 0, $select);
173 $this->assertEquals($pnDupePairs[0]['is_selected'], 1, 'Check if first record in dupe pairs is marked as selected.');
174 $this->assertEquals($pnDupePairs[0]['is_selected'], 1, 'Check if second record in dupe pairs is marked as selected.');
175
176 // batch merge selected dupes
177 $result = CRM_Dedupe_Merger::batchMerge($dao->id, $this->_groupId, 'safe', TRUE, 5, 1);
178 $this->assertEquals(count($result['merged']), 2, 'Check number of merged pairs.');
179
180 // retrieve pairs from prev next cache table
181 $pnDupePairs = CRM_Core_BAO_PrevNextCache::retrieve($cacheKeyString, NULL, NULL, 0, 0, $select);
182 $this->assertEquals(count($pnDupePairs), 1, 'Check number of remaining dupe pairs in prev next cache.');
183
184 $this->deleteDupeContacts();
185 }
186
187 /**
188 * Test the batch merge.
189 */
190 public function testBatchMergeAllDuplicates() {
191 $this->createDupeContacts();
192
193 // verify that all contacts have been created separately
194 $this->assertEquals(count($this->_contactIds), 9, 'Check for number of contacts.');
195
196 $dao = new CRM_Dedupe_DAO_RuleGroup();
197 $dao->contact_type = 'Individual';
198 $dao->name = 'IndividualSupervised';
199 $dao->is_default = 1;
200 $dao->find(TRUE);
201
202 $foundDupes = CRM_Dedupe_Finder::dupesInGroup($dao->id, $this->_groupId);
203
204 // -------------------------------------------------------------------------
205 // Name and Email (reserved) Matches ( 3 pairs )
206 // --------------------------------------------------------------------------
207 // robin - hood - robin@example.com
208 // robin - hood - robin@example.com
209 // little - dale - dale@example.com
210 // little - dale - dale@example.com
211 // will - dale - will@example.com
212 // will - dale - will@example.com
213 // so 3 pairs for - first + last + mail
214 $this->assertEquals(count($foundDupes), 3, 'Check Individual-Supervised dupe rule for dupesInGroup().');
215
216 // Run dedupe finder as the browser would
217 $_SERVER['REQUEST_METHOD'] = 'GET'; //avoid invalid key error
218 $object = new CRM_Contact_Page_DedupeFind();
219 $object->set('gid', $this->_groupId);
220 $object->set('rgid', $dao->id);
221 $object->set('action', CRM_Core_Action::UPDATE);
222 @$object->run();
223
224 // Retrieve pairs from prev next cache table
225 $select = array('pn.is_selected' => 'is_selected');
226 $cacheKeyString = "merge Individual_{$dao->id}_{$this->_groupId}";
227 $pnDupePairs = CRM_Core_BAO_PrevNextCache::retrieve($cacheKeyString, NULL, NULL, 0, 0, $select);
228
229 $this->assertEquals(count($foundDupes), count($pnDupePairs), 'Check number of dupe pairs in prev next cache.');
230
231 // batch merge all dupes
232 $result = CRM_Dedupe_Merger::batchMerge($dao->id, $this->_groupId, 'safe', TRUE, 5, 2);
233 $this->assertEquals(count($result['merged']), 3, 'Check number of merged pairs.');
234
235 // retrieve pairs from prev next cache table
236 $pnDupePairs = CRM_Core_BAO_PrevNextCache::retrieve($cacheKeyString, NULL, NULL, 0, 0, $select);
237 $this->assertEquals(count($pnDupePairs), 0, 'Check number of remaining dupe pairs in prev next cache.');
238
239 $this->deleteDupeContacts();
240 }
241
242 /**
243 * The goal of this function is to test that all required tables are returned.
244 */
245 public function testGetCidRefs() {
246 $this->entityCustomGroupWithSingleFieldCreate(__FUNCTION__, 'Contacts');
247 $this->assertEquals(array_merge($this->getStaticCIDRefs(), $this->getHackedInCIDRef()), CRM_Dedupe_Merger::cidRefs());
248 $this->assertEquals(array_merge($this->getCalculatedCIDRefs(), $this->getHackedInCIDRef()), CRM_Dedupe_Merger::cidRefs());
249 }
250
251 /**
252 * Get the list of not-really-cid-refs that are currently hacked in.
253 *
254 * This is hacked into getCIDs function.
255 *
256 * @return array
257 */
258 public function getHackedInCIDRef() {
259 return array(
260 'civicrm_entity_tag' => array(
261 0 => 'entity_id',
262 ),
263 );
264 }
265
266 /**
267 * Get the list of tables that refer to the CID.
268 *
269 * This is a statically maintained (in this test list).
270 *
271 * There is also a check against an automated list but having both seems to add extra stability to me. They do
272 * not change often.
273 */
274 public function getStaticCIDRefs() {
275 return array(
276 'civicrm_acl_cache' => array(
277 0 => 'contact_id',
278 ),
279 'civicrm_acl_contact_cache' => array(
280 0 => 'user_id',
281 1 => 'contact_id',
282 ),
283 'civicrm_action_log' => array(
284 0 => 'contact_id',
285 ),
286 'civicrm_activity_contact' => array(
287 0 => 'contact_id',
288 ),
289 'civicrm_address' => array(
290 0 => 'contact_id',
291 ),
292 'civicrm_batch' => array(
293 0 => 'created_id',
294 1 => 'modified_id',
295 ),
296 'civicrm_campaign' => array(
297 0 => 'created_id',
298 1 => 'last_modified_id',
299 ),
300 'civicrm_case_contact' => array(
301 0 => 'contact_id',
302 ),
303 'civicrm_contact' => array(
304 0 => 'primary_contact_id',
305 1 => 'employer_id',
306 ),
307 'civicrm_contribution' => array(
308 0 => 'contact_id',
309 ),
310 'civicrm_contribution_page' => array(
311 0 => 'created_id',
312 ),
313 'civicrm_contribution_recur' => array(
314 0 => 'contact_id',
315 ),
316 'civicrm_contribution_soft' => array(
317 0 => 'contact_id',
318 ),
319 'civicrm_custom_group' => array(
320 0 => 'created_id',
321 ),
322 'civicrm_dashboard_contact' => array(
323 0 => 'contact_id',
324 ),
325 'civicrm_dedupe_exception' => array(
326 0 => 'contact_id1',
327 1 => 'contact_id2',
328 ),
329 'civicrm_domain' => array(
330 0 => 'contact_id',
331 ),
332 'civicrm_email' => array(
333 0 => 'contact_id',
334 ),
335 'civicrm_event' => array(
336 0 => 'created_id',
337 ),
338 'civicrm_event_carts' => array(
339 0 => 'user_id',
340 ),
341 'civicrm_financial_account' => array(
342 0 => 'contact_id',
343 ),
344 'civicrm_financial_item' => array(
345 0 => 'contact_id',
346 ),
347 'civicrm_grant' => array(
348 0 => 'contact_id',
349 ),
350 'civicrm_group' => array(
351 0 => 'created_id',
352 1 => 'modified_id',
353 ),
354 'civicrm_group_contact' => array(
355 0 => 'contact_id',
356 ),
357 'civicrm_group_contact_cache' => array(
358 0 => 'contact_id',
359 ),
360 'civicrm_group_organization' => array(
361 0 => 'organization_id',
362 ),
363 'civicrm_im' => array(
364 0 => 'contact_id',
365 ),
366 'civicrm_log' => array(
367 0 => 'modified_id',
368 ),
369 'civicrm_mailing' => array(
370 0 => 'created_id',
371 1 => 'scheduled_id',
372 2 => 'approver_id',
373 ),
374 'civicrm_mailing_abtest' => array(
375 0 => 'created_id',
376 ),
377 'civicrm_mailing_event_queue' => array(
378 0 => 'contact_id',
379 ),
380 'civicrm_mailing_event_subscribe' => array(
381 0 => 'contact_id',
382 ),
383 'civicrm_mailing_recipients' => array(
384 0 => 'contact_id',
385 ),
386 'civicrm_membership' => array(
387 0 => 'contact_id',
388 ),
389 'civicrm_membership_log' => array(
390 0 => 'modified_id',
391 ),
392 'civicrm_membership_type' => array(
393 0 => 'member_of_contact_id',
394 ),
395 'civicrm_note' => array(
396 0 => 'contact_id',
397 ),
398 'civicrm_openid' => array(
399 0 => 'contact_id',
400 ),
401 'civicrm_participant' => array(
402 0 => 'contact_id',
403 1 => 'transferred_to_contact_id', //CRM-16761
404 ),
405 'civicrm_payment_token' => array(
406 0 => 'contact_id',
407 1 => 'created_id',
408 ),
409 'civicrm_pcp' => array(
410 0 => 'contact_id',
411 ),
412 'civicrm_phone' => array(
413 0 => 'contact_id',
414 ),
415 'civicrm_pledge' => array(
416 0 => 'contact_id',
417 ),
418 'civicrm_print_label' => array(
419 0 => 'created_id',
420 ),
421 'civicrm_relationship' => array(
422 0 => 'contact_id_a',
423 1 => 'contact_id_b',
424 ),
425 'civicrm_report_instance' => array(
426 0 => 'created_id',
427 1 => 'owner_id',
428 ),
429 'civicrm_setting' => array(
430 0 => 'contact_id',
431 1 => 'created_id',
432 ),
433 'civicrm_subscription_history' => array(
434 0 => 'contact_id',
435 ),
436 'civicrm_survey' => array(
437 0 => 'created_id',
438 1 => 'last_modified_id',
439 ),
440 'civicrm_tag' => array(
441 0 => 'created_id',
442 ),
443 'civicrm_uf_group' => array(
444 0 => 'created_id',
445 ),
446 'civicrm_uf_match' => array(
447 0 => 'contact_id',
448 ),
449 'civicrm_value_testgetcidref_1' => array(
450 0 => 'entity_id',
451 ),
452 'civicrm_website' => array(
453 0 => 'contact_id',
454 ),
455 );
456 }
457
458 /**
459 * Get a list of CIDs that is calculated off the schema.
460 *
461 * Note this is an expensive and table locking query. Should be safe in tests though.
462 */
463 public function getCalculatedCIDRefs() {
464 $cidRefs = array();
465 $sql = "
466 SELECT
467 table_name,
468 column_name
469 FROM information_schema.key_column_usage
470 WHERE
471 referenced_table_schema = database() AND
472 referenced_table_name = 'civicrm_contact' AND
473 referenced_column_name = 'id';
474 ";
475 $dao = CRM_Core_DAO::executeQuery($sql);
476 while ($dao->fetch()) {
477 $cidRefs[$dao->table_name][] = $dao->column_name;
478 }
479 // Do specific re-ordering changes to make this the same as the ref validated one.
480 // The above query orders by FK alphabetically.
481 // There might be cleverer ways to do this but it shouldn't change much.
482 $cidRefs['civicrm_contact'][0] = 'primary_contact_id';
483 $cidRefs['civicrm_contact'][1] = 'employer_id';
484 $cidRefs['civicrm_acl_contact_cache'][0] = 'user_id';
485 $cidRefs['civicrm_acl_contact_cache'][1] = 'contact_id';
486 $cidRefs['civicrm_mailing'][0] = 'created_id';
487 $cidRefs['civicrm_mailing'][1] = 'scheduled_id';
488 $cidRefs['civicrm_mailing'][2] = 'approver_id';
489 return $cidRefs;
490 }
491
492 }