3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
13 * Test class for CRM_Contact_BAO_GroupContact BAO
18 class CRM_Contact_BAO_GroupContactCacheTest
extends CiviUnitTestCase
{
21 * Manually add and remove contacts from a smart group.
23 public function testManualAddRemove() {
24 list($group, $living, $deceased) = $this->setupSmartGroup();
27 $this->callAPISuccess('group_contact', 'create', [
28 'contact_id' => $living[0]->id
,
29 'group_id' => $group->id
,
32 CRM_Contact_BAO_GroupContactCache
::load($group, TRUE);
33 $this->assertCacheMatches(
34 [$deceased[0]->id
, $deceased[1]->id
, $deceased[2]->id
, $living[0]->id
],
39 $this->callAPISuccess('group_contact', 'create', [
40 'contact_id' => $deceased[0]->id
,
41 'group_id' => $group->id
,
42 'status' => 'Removed',
45 CRM_Contact_BAO_GroupContactCache
::load($group, TRUE);
46 $this->assertCacheMatches(
57 * Allow removing contact from a parent group even if contact is in a child group. (CRM-8858).
59 public function testRemoveFromParentSmartGroup() {
60 // Create smart group $parent
62 'name' => 'Deceased Contacts',
63 'title' => 'Deceased Contacts',
65 'formValues' => ['is_deceased' => 1],
67 $parent = CRM_Contact_BAO_Group
::createSmartGroup($params);
68 $this->registerTestObjects([$parent]);
70 // Create group $child in $parent
72 'name' => 'Child Group',
73 'title' => 'Child Group',
75 'parents' => [$parent->id
=> 1],
77 $child = CRM_Contact_BAO_Group
::create($params);
78 $this->registerTestObjects([$child]);
80 // Create $c1, $c2, $c3
81 $deceased = $this->createTestObject('CRM_Contact_DAO_Contact', ['is_deceased' => 1], 3);
83 // Add $c1, $c2, $c3 to $child
84 foreach ($deceased as $contact) {
85 $this->callAPISuccess('group_contact', 'create', [
86 'contact_id' => $contact->id
,
87 'group_id' => $child->id
,
91 CRM_Contact_BAO_GroupContactCache
::load($parent, TRUE);
92 $this->assertCacheMatches(
93 [$deceased[0]->id
, $deceased[1]->id
, $deceased[2]->id
],
97 // Remove $c1 from $parent
98 $this->callAPISuccess('group_contact', 'create', [
99 'contact_id' => $deceased[0]->id
,
100 'group_id' => $parent->id
,
101 'status' => 'Removed',
104 // Assert $c1 not in $parent
105 CRM_Contact_BAO_GroupContactCache
::load($parent, TRUE);
106 $this->assertCacheMatches(
114 // Assert $c1 still in $child
115 $this->assertDBQuery(1,
116 'select count(*) from civicrm_group_contact where group_id=%1 and contact_id=%2 and status=%3',
118 1 => [$child->id
, 'Integer'],
119 2 => [$deceased[0]->id
, 'Integer'],
120 3 => ['Added', 'String'],
126 * Assert that the cache for a group contains exactly the listed contacts.
128 * @param array $expectedContactIds
130 * @param int $groupId
132 public function assertCacheMatches($expectedContactIds, $groupId) {
133 $sql = 'SELECT contact_id FROM civicrm_group_contact_cache WHERE group_id = %1';
134 $params = [1 => [$groupId, 'Integer']];
135 $dao = CRM_Core_DAO
::executeQuery($sql, $params);
136 $actualContactIds = [];
137 while ($dao->fetch()) {
138 $actualContactIds[] = $dao->contact_id
;
141 sort($expectedContactIds);
142 sort($actualContactIds);
143 $this->assertEquals($expectedContactIds, $actualContactIds);
147 * Test the opportunistic refresh cache function does not touch non-expired entries.
149 public function testOpportunisticRefreshCacheNoChangeIfNotExpired() {
150 list($group, $living, $deceased) = $this->setupSmartGroup();
151 $this->callAPISuccess('Contact', 'create', ['id' => $deceased[0]->id
, 'is_deceased' => 0]);
152 $this->assertCacheMatches(
153 [$deceased[0]->id
, $deceased[1]->id
, $deceased[2]->id
],
156 CRM_Contact_BAO_GroupContactCache
::opportunisticCacheFlush();
158 $this->assertCacheNotRefreshed($deceased, $group);
162 * Test the opportunistic refresh cache function does refresh stale entries.
164 public function testOpportunisticRefreshChangeIfCacheDateFieldStale() {
165 list($group, $living, $deceased) = $this->setupSmartGroup();
166 $this->callAPISuccess('Contact', 'create', ['id' => $deceased[0]->id
, 'is_deceased' => 0]);
167 CRM_Core_DAO
::executeQuery('UPDATE civicrm_group SET cache_date = DATE_SUB(NOW(), INTERVAL 7 MINUTE) WHERE id = ' . $group->id
);
169 Civi
::$statics['CRM_Contact_BAO_GroupContactCache']['is_refresh_init'] = FALSE;
171 CRM_Contact_BAO_GroupContactCache
::opportunisticCacheFlush();
173 $this->assertCacheRefreshed($group);
177 * Test the opportunistic refresh cache function does refresh expired entries if mode is deterministic.
179 public function testOpportunisticRefreshNoChangeWithDeterministicSetting() {
180 list($group, $living, $deceased) = $this->setupSmartGroup();
181 $this->callAPISuccess('Setting', 'create', ['smart_group_cache_refresh_mode' => 'deterministic']);
182 $this->callAPISuccess('Contact', 'create', ['id' => $deceased[0]->id
, 'is_deceased' => 0]);
183 $this->makeCacheStale($group);
184 CRM_Contact_BAO_GroupContactCache
::opportunisticCacheFlush();
185 $this->assertCacheNotRefreshed($deceased, $group);
186 $this->callAPISuccess('Setting', 'create', ['smart_group_cache_refresh_mode' => 'opportunistic']);
190 * Test the deterministic cache function refreshes with the deterministic setting.
192 public function testDeterministicRefreshChangeWithDeterministicSetting() {
193 list($group, $living, $deceased) = $this->setupSmartGroup();
194 $this->callAPISuccess('Setting', 'create', ['smart_group_cache_refresh_mode' => 'deterministic']);
195 $this->callAPISuccess('Contact', 'create', ['id' => $deceased[0]->id
, 'is_deceased' => 0]);
196 $this->makeCacheStale($group);
197 CRM_Contact_BAO_GroupContactCache
::deterministicCacheFlush();
198 $this->assertCacheRefreshed($group);
199 $this->callAPISuccess('Setting', 'create', ['smart_group_cache_refresh_mode' => 'opportunistic']);
203 * Test the deterministic cache function refresh doesn't mess up non-expired.
205 public function testDeterministicRefreshChangeDoesNotTouchNonExpired() {
206 list($group, $living, $deceased) = $this->setupSmartGroup();
207 $this->callAPISuccess('Setting', 'create', ['smart_group_cache_refresh_mode' => 'deterministic']);
208 $this->callAPISuccess('Contact', 'create', ['id' => $deceased[0]->id
, 'is_deceased' => 0]);
209 CRM_Contact_BAO_GroupContactCache
::deterministicCacheFlush();
210 $this->assertCacheNotRefreshed($deceased, $group);
211 $this->callAPISuccess('Setting', 'create', ['smart_group_cache_refresh_mode' => 'opportunistic']);
215 * Test the deterministic cache function refreshes with the opportunistic setting.
217 * (hey it's an opportunity!).
219 public function testDeterministicRefreshChangeWithOpportunisticSetting() {
220 list($group, $living, $deceased) = $this->setupSmartGroup();
221 $this->callAPISuccess('Setting', 'create', ['smart_group_cache_refresh_mode' => 'opportunistic']);
222 $this->callAPISuccess('Contact', 'create', ['id' => $deceased[0]->id
, 'is_deceased' => 0]);
223 $this->makeCacheStale($group);
224 CRM_Contact_BAO_GroupContactCache
::deterministicCacheFlush();
225 $this->assertCacheRefreshed($group);
229 * Test the api job wrapper around the deterministic refresh works.
231 public function testJobWrapper() {
232 list($group, $living, $deceased) = $this->setupSmartGroup();
233 $this->callAPISuccess('Setting', 'create', ['smart_group_cache_refresh_mode' => 'opportunistic']);
234 $this->callAPISuccess('Contact', 'create', ['id' => $deceased[0]->id
, 'is_deceased' => 0]);
235 $this->makeCacheStale($group);
236 $this->callAPISuccess('Job', 'group_cache_flush', []);
237 $this->assertCacheRefreshed($group);
240 // *** Everything below this should be moved to parent class ****
244 * (DAO_Name => array(int)) List of items to garbage-collect during tearDown
246 private $_testObjects;
249 * Tears down the fixture, for example, closes a network connection.
251 * This method is called after a test is executed.
253 protected function tearDown(): void
{
255 $this->deleteTestObjects();
259 * This is a wrapper for CRM_Core_DAO::createTestObject which tracks created entities.
261 * @see CRM_Core_DAO::createTestObject
263 * @param string $daoName
264 * @param array $params
265 * @param int $numObjects
266 * @param bool $createOnly
268 * @return array|NULL|object
270 public function createTestObject($daoName, $params = [], $numObjects = 1, $createOnly = FALSE) {
271 $objects = CRM_Core_DAO
::createTestObject($daoName, $params, $numObjects, $createOnly);
272 if (is_array($objects)) {
273 $this->registerTestObjects($objects);
276 $this->registerTestObjects([$objects]);
282 * Register test objects.
284 * @param array $objects
285 * DAO or BAO objects.
287 public function registerTestObjects($objects) {
288 foreach ($objects as $object) {
289 $daoName = preg_replace('/_BAO_/', '_DAO_', get_class($object));
290 $this->_testObjects
[$daoName][] = $object->id
;
295 * Delete test objects.
297 * Note: You might argue that the FK relations between test
298 * objects could make this problematic; however, it should
299 * behave intuitively as long as we mentally split our
300 * test-objects between the "manual/primary records"
301 * and the "automatic/secondary records"
303 public function deleteTestObjects() {
304 foreach ($this->_testObjects
as $daoName => $daoIds) {
305 foreach ($daoIds as $daoId) {
306 CRM_Core_DAO
::deleteTestObjects($daoName, ['id' => $daoId]);
309 $this->_testObjects
= [];
313 * Set up a smart group testing scenario.
317 protected function setupSmartGroup() {
319 'name' => 'Deceased Contacts',
320 'title' => 'Deceased Contacts',
322 'formValues' => ['is_deceased' => 1],
324 $group = CRM_Contact_BAO_Group
::createSmartGroup($params);
325 $this->registerTestObjects([$group]);
327 // Create contacts $y1, $y2, $y3 which do match $g; create $n1, $n2, $n3 which do not match $g
328 $living = $this->createTestObject('CRM_Contact_DAO_Contact', ['is_deceased' => 0], 3);
329 $deceased = $this->createTestObject('CRM_Contact_DAO_Contact', ['is_deceased' => 1], 3);
330 $this->assertEquals(3, count($deceased));
331 $this->assertEquals(3, count($living));
333 // Assert: $g cache has exactly $y1, $y2, $y3
334 CRM_Contact_BAO_GroupContactCache
::load($group, TRUE);
336 $this->assertCacheMatches(
337 [$deceased[0]->id
, $deceased[1]->id
, $deceased[2]->id
],
340 // Reload the group so we have the cache_date & refresh_date.
341 return [$group, $living, $deceased];
350 protected function assertCacheNotRefreshed($deceased, $group) {
351 $this->assertCacheMatches(
352 [$deceased[0]->id
, $deceased[1]->id
, $deceased[2]->id
],
355 $afterGroup = $this->callAPISuccessGetSingle('Group', ['id' => $group->id
]);
356 $this->assertEquals($group->cache_date
, $afterGroup['cache_date']);
360 * Make the cache for the group stale, resetting it to before the timeout period.
362 * @param CRM_Contact_BAO_Group $group
364 protected function makeCacheStale(&$group) {
365 CRM_Core_DAO
::executeQuery('UPDATE civicrm_group SET cache_date = DATE_SUB(NOW(), INTERVAL 7 MINUTE) WHERE id = ' . $group->id
);
366 unset($group->cache_date
);
368 Civi
::$statics['CRM_Contact_BAO_GroupContactCache']['is_refresh_init'] = FALSE;
376 protected function assertCacheRefreshed($group) {
377 $this->assertCacheMatches(
382 $afterGroup = $this->callAPISuccessGetSingle('Group', ['id' => $group->id
]);
383 $this->assertTrue(empty($afterGroup['cache_date']), 'cache date should not be set as the cache is not built');
387 * Test Smart group search
389 public function testSmartGroupSearchBuilder() {
390 $returnProperties = [
392 'contact_sub_type' => 1,
396 list($group, $living, $deceased) = $this->setupSmartGroup();
399 'name' => 'Living Contacts',
400 'title' => 'Living Contacts',
402 'formValues' => ['is_deceased' => 0],
404 $group2 = CRM_Contact_BAO_Group
::createSmartGroup($params);
406 //Filter on smart group with =, !=, IN and NOT IN operator.
407 $params = [['group', '=', $group2->id
, 1, 0]];
408 $query = new CRM_Contact_BAO_Query(
409 $params, $returnProperties,
410 NULL, FALSE, FALSE, CRM_Contact_BAO_Query
::MODE_CONTACTS
,
414 $ids = $query->searchQuery(0, 0, NULL,
418 $key = $query->getGroupCacheTableKeys()[0];
419 $expectedWhere = "civicrm_group_contact_cache_{$key}.group_id IN (\"{$group2->id}\")";
420 $this->assertContains($expectedWhere, $query->_whereClause
);
421 $this->_assertContactIds($query, "group_id = {$group2->id}");
423 $params = [['group', '!=', $group->id
, 1, 0]];
424 $query = new CRM_Contact_BAO_Query(
425 $params, $returnProperties,
426 NULL, FALSE, FALSE, CRM_Contact_BAO_Query
::MODE_CONTACTS
,
430 $key = $query->getGroupCacheTableKeys()[0];
431 //Assert if proper where clause is present.
432 $expectedWhere = "civicrm_group_contact_{$key}.group_id != {$group->id} AND civicrm_group_contact_cache_{$key}.group_id IS NULL OR ( civicrm_group_contact_cache_{$key}.contact_id NOT IN (SELECT contact_id FROM civicrm_group_contact_cache cgcc WHERE cgcc.group_id IN ( {$group->id} ) ) )";
433 $this->assertContains($expectedWhere, $query->_whereClause
);
434 $this->_assertContactIds($query, "group_id != {$group->id}");
436 $params = [['group', 'IN', [$group->id
, $group2->id
], 1, 0]];
437 $query = new CRM_Contact_BAO_Query(
438 $params, $returnProperties,
439 NULL, FALSE, FALSE, CRM_Contact_BAO_Query
::MODE_CONTACTS
,
443 $key = $query->getGroupCacheTableKeys()[0];
444 $expectedWhere = "civicrm_group_contact_cache_{$key}.group_id IN (\"{$group->id}\", \"{$group2->id}\")";
445 $this->assertContains($expectedWhere, $query->_whereClause
);
446 $this->_assertContactIds($query, "group_id IN ({$group->id}, {$group2->id})");
448 $params = [['group', 'NOT IN', [$group->id
], 1, 0]];
449 $query = new CRM_Contact_BAO_Query(
450 $params, $returnProperties,
451 NULL, FALSE, FALSE, CRM_Contact_BAO_Query
::MODE_CONTACTS
,
455 $key = $query->getGroupCacheTableKeys()[0];
456 $expectedWhere = "civicrm_group_contact_{$key}.group_id NOT IN ( {$group->id} ) AND civicrm_group_contact_cache_{$key}.group_id IS NULL OR ( civicrm_group_contact_cache_{$key}.contact_id NOT IN (SELECT contact_id FROM civicrm_group_contact_cache cgcc WHERE cgcc.group_id IN ( {$group->id} ) ) )";
457 $this->assertContains($expectedWhere, $query->_whereClause
);
458 $this->_assertContactIds($query, "group_id NOT IN ({$group->id})");
459 $this->callAPISuccess('group', 'delete', ['id' => $group->id
]);
460 $this->callAPISuccess('group', 'delete', ['id' => $group2->id
]);
463 public function testMultipleGroupWhereClause() {
464 $returnProperties = [
466 'contact_sub_type' => 1,
470 list($group, $living, $deceased) = $this->setupSmartGroup();
473 'name' => 'Living Contacts',
474 'title' => 'Living Contacts',
476 'formValues' => ['is_deceased' => 0],
478 $group2 = CRM_Contact_BAO_Group
::createSmartGroup($params);
480 //Filter on smart group with =, !=, IN and NOT IN operator.
481 $params = [['group', '=', $group2->id
, 1, 0], ['group', '=', $group->id
, 1, 0]];
482 $query = new CRM_Contact_BAO_Query(
483 $params, $returnProperties,
484 NULL, FALSE, FALSE, CRM_Contact_BAO_Query
::MODE_CONTACTS
,
488 $ids = $query->searchQuery(0, 0, NULL,
492 $key1 = $query->getGroupCacheTableKeys()[0];
493 $key2 = $query->getGroupCacheTableKeys()[1];
494 $expectedWhere = 'civicrm_group_contact_cache_' . $key1 . '.group_id IN ("' . $group2->id
. '") ) ) AND ( ( civicrm_group_contact_cache_' . $key2 . '.group_id IN ("' . $group->id
. '")';
495 $this->assertContains($expectedWhere, $query->_whereClause
);
496 // Check that we have 3 joins to the group contact cache 1 for each of the group where clauses and 1 for the fact we are returning groups in the select.
497 $expectedFrom1 = 'LEFT JOIN civicrm_group_contact_cache civicrm_group_contact_cache_' . $key1 . ' ON contact_a.id = civicrm_group_contact_cache_' . $key1 . '.contact_id';
498 $this->assertContains($expectedFrom1, $query->_fromClause
);
499 $expectedFrom2 = 'LEFT JOIN civicrm_group_contact_cache civicrm_group_contact_cache_' . $key2 . ' ON contact_a.id = civicrm_group_contact_cache_' . $key2 . '.contact_id';
500 $this->assertContains($expectedFrom2, $query->_fromClause
);
501 $expectedFrom3 = 'LEFT JOIN civicrm_group_contact_cache ON contact_a.id = civicrm_group_contact_cache.contact_id';
502 $this->assertContains($expectedFrom3, $query->_fromClause
);
506 * Check if contact ids are fetched correctly.
508 * @param object $query
509 * @param string $groupWhereClause
511 public function _assertContactIds($query, $groupWhereClause) {
512 $contactIds = explode(',', $query->searchQuery(0, 0, NULL,
516 $expectedContactIds = [];
517 $groupDAO = CRM_Core_DAO
::executeQuery("SELECT contact_id FROM civicrm_group_contact_cache WHERE {$groupWhereClause}");
518 while ($groupDAO->fetch()) {
519 $expectedContactIds[] = $groupDAO->contact_id
;
521 $this->assertEquals(sort($expectedContactIds), sort($contactIds));