3 +--------------------------------------------------------------------+
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2019 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
29 * Test class for CRM_Contact_BAO_GroupContact BAO
34 class CRM_Contact_BAO_GroupContactCacheTest
extends CiviUnitTestCase
{
37 * Manually add and remove contacts from a smart group.
39 public function testManualAddRemove() {
40 list($group, $living, $deceased) = $this->setupSmartGroup();
43 $this->callAPISuccess('group_contact', 'create', array(
44 'contact_id' => $living[0]->id
,
45 'group_id' => $group->id
,
48 CRM_Contact_BAO_GroupContactCache
::load($group, TRUE);
49 $this->assertCacheMatches(
50 array($deceased[0]->id
, $deceased[1]->id
, $deceased[2]->id
, $living[0]->id
),
55 $this->callAPISuccess('group_contact', 'create', array(
56 'contact_id' => $deceased[0]->id
,
57 'group_id' => $group->id
,
58 'status' => 'Removed',
61 CRM_Contact_BAO_GroupContactCache
::load($group, TRUE);
62 $this->assertCacheMatches(
73 * Allow removing contact from a parent group even if contact is in a child group. (CRM-8858).
75 public function testRemoveFromParentSmartGroup() {
76 // Create smart group $parent
78 'name' => 'Deceased Contacts',
79 'title' => 'Deceased Contacts',
81 'formValues' => array('is_deceased' => 1),
83 $parent = CRM_Contact_BAO_Group
::createSmartGroup($params);
84 $this->registerTestObjects(array($parent));
86 // Create group $child in $parent
88 'name' => 'Child Group',
89 'title' => 'Child Group',
91 'parents' => array($parent->id
=> 1),
93 $child = CRM_Contact_BAO_Group
::create($params);
94 $this->registerTestObjects(array($child));
96 // Create $c1, $c2, $c3
97 $deceased = $this->createTestObject('CRM_Contact_DAO_Contact', array('is_deceased' => 1), 3);
99 // Add $c1, $c2, $c3 to $child
100 foreach ($deceased as $contact) {
101 $this->callAPISuccess('group_contact', 'create', array(
102 'contact_id' => $contact->id
,
103 'group_id' => $child->id
,
107 CRM_Contact_BAO_GroupContactCache
::load($parent, TRUE);
108 $this->assertCacheMatches(
109 array($deceased[0]->id
, $deceased[1]->id
, $deceased[2]->id
),
113 // Remove $c1 from $parent
114 $this->callAPISuccess('group_contact', 'create', array(
115 'contact_id' => $deceased[0]->id
,
116 'group_id' => $parent->id
,
117 'status' => 'Removed',
120 // Assert $c1 not in $parent
121 CRM_Contact_BAO_GroupContactCache
::load($parent, TRUE);
122 $this->assertCacheMatches(
130 // Assert $c1 still in $child
131 $this->assertDBQuery(1,
132 'select count(*) from civicrm_group_contact where group_id=%1 and contact_id=%2 and status=%3',
134 1 => array($child->id
, 'Integer'),
135 2 => array($deceased[0]->id
, 'Integer'),
136 3 => array('Added', 'String'),
142 * Assert that the cache for a group contains exactly the listed contacts.
144 * @param array $expectedContactIds
146 * @param int $groupId
148 public function assertCacheMatches($expectedContactIds, $groupId) {
149 $sql = 'SELECT contact_id FROM civicrm_group_contact_cache WHERE group_id = %1';
150 $params = array(1 => array($groupId, 'Integer'));
151 $dao = CRM_Core_DAO
::executeQuery($sql, $params);
152 $actualContactIds = array();
153 while ($dao->fetch()) {
154 $actualContactIds[] = $dao->contact_id
;
157 sort($expectedContactIds);
158 sort($actualContactIds);
159 $this->assertEquals($expectedContactIds, $actualContactIds);
163 * Test the opportunistic refresh cache function does not touch non-expired entries.
165 public function testOpportunisticRefreshCacheNoChangeIfNotExpired() {
166 list($group, $living, $deceased) = $this->setupSmartGroup();
167 $this->callAPISuccess('Contact', 'create', array('id' => $deceased[0]->id
, 'is_deceased' => 0));
168 $this->assertCacheMatches(
169 array($deceased[0]->id
, $deceased[1]->id
, $deceased[2]->id
),
172 CRM_Contact_BAO_GroupContactCache
::opportunisticCacheFlush();
174 $this->assertCacheNotRefreshed($deceased, $group);
178 * Test the opportunistic refresh cache function does refresh stale entries.
180 public function testOpportunisticRefreshChangeIfCacheDateFieldStale() {
181 list($group, $living, $deceased) = $this->setupSmartGroup();
182 $this->callAPISuccess('Contact', 'create', array('id' => $deceased[0]->id
, 'is_deceased' => 0));
183 CRM_Core_DAO
::executeQuery('UPDATE civicrm_group SET cache_date = DATE_SUB(NOW(), INTERVAL 7 MINUTE) WHERE id = ' . $group->id
);
185 Civi
::$statics['CRM_Contact_BAO_GroupContactCache']['is_refresh_init'] = FALSE;
187 CRM_Contact_BAO_GroupContactCache
::opportunisticCacheFlush();
189 $this->assertCacheRefreshed($group);
193 * Test the opportunistic refresh cache function does refresh expired entries if mode is deterministic.
195 public function testOpportunisticRefreshNoChangeWithDeterministicSetting() {
196 list($group, $living, $deceased) = $this->setupSmartGroup();
197 $this->callAPISuccess('Setting', 'create', array('smart_group_cache_refresh_mode' => 'deterministic'));
198 $this->callAPISuccess('Contact', 'create', array('id' => $deceased[0]->id
, 'is_deceased' => 0));
199 $this->makeCacheStale($group);
200 CRM_Contact_BAO_GroupContactCache
::opportunisticCacheFlush();
201 $this->assertCacheNotRefreshed($deceased, $group);
202 $this->callAPISuccess('Setting', 'create', array('smart_group_cache_refresh_mode' => 'opportunistic'));
206 * Test the deterministic cache function refreshes with the deterministic setting.
208 public function testDeterministicRefreshChangeWithDeterministicSetting() {
209 list($group, $living, $deceased) = $this->setupSmartGroup();
210 $this->callAPISuccess('Setting', 'create', array('smart_group_cache_refresh_mode' => 'deterministic'));
211 $this->callAPISuccess('Contact', 'create', array('id' => $deceased[0]->id
, 'is_deceased' => 0));
212 $this->makeCacheStale($group);
213 CRM_Contact_BAO_GroupContactCache
::deterministicCacheFlush();
214 $this->assertCacheRefreshed($group);
215 $this->callAPISuccess('Setting', 'create', array('smart_group_cache_refresh_mode' => 'opportunistic'));
219 * Test the deterministic cache function refresh doesn't mess up non-expired.
221 public function testDeterministicRefreshChangeDoesNotTouchNonExpired() {
222 list($group, $living, $deceased) = $this->setupSmartGroup();
223 $this->callAPISuccess('Setting', 'create', array('smart_group_cache_refresh_mode' => 'deterministic'));
224 $this->callAPISuccess('Contact', 'create', array('id' => $deceased[0]->id
, 'is_deceased' => 0));
225 CRM_Contact_BAO_GroupContactCache
::deterministicCacheFlush();
226 $this->assertCacheNotRefreshed($deceased, $group);
227 $this->callAPISuccess('Setting', 'create', array('smart_group_cache_refresh_mode' => 'opportunistic'));
231 * Test the deterministic cache function refreshes with the opportunistic setting.
233 * (hey it's an opportunity!).
235 public function testDeterministicRefreshChangeWithOpportunisticSetting() {
236 list($group, $living, $deceased) = $this->setupSmartGroup();
237 $this->callAPISuccess('Setting', 'create', array('smart_group_cache_refresh_mode' => 'opportunistic'));
238 $this->callAPISuccess('Contact', 'create', array('id' => $deceased[0]->id
, 'is_deceased' => 0));
239 $this->makeCacheStale($group);
240 CRM_Contact_BAO_GroupContactCache
::deterministicCacheFlush();
241 $this->assertCacheRefreshed($group);
245 * Test the api job wrapper around the deterministic refresh works.
247 public function testJobWrapper() {
248 list($group, $living, $deceased) = $this->setupSmartGroup();
249 $this->callAPISuccess('Setting', 'create', array('smart_group_cache_refresh_mode' => 'opportunistic'));
250 $this->callAPISuccess('Contact', 'create', array('id' => $deceased[0]->id
, 'is_deceased' => 0));
251 $this->makeCacheStale($group);
252 $this->callAPISuccess('Job', 'group_cache_flush', array());
253 $this->assertCacheRefreshed($group);
256 // *** Everything below this should be moved to parent class ****
259 * @var array(DAO_Name => array(int)) List of items to garbage-collect during tearDown
261 private $_testObjects;
264 * Sets up the fixture, for example, opens a network connection.
266 * This method is called before a test is executed.
268 protected function setUp() {
269 $this->_testObjects
= array();
274 * Tears down the fixture, for example, closes a network connection.
276 * This method is called after a test is executed.
278 protected function tearDown() {
280 $this->deleteTestObjects();
284 * This is a wrapper for CRM_Core_DAO::createTestObject which tracks created entities.
286 * @see CRM_Core_DAO::createTestObject
288 * @param string $daoName
289 * @param array $params
290 * @param int $numObjects
291 * @param bool $createOnly
293 * @return array|NULL|object
295 public function createTestObject($daoName, $params = array(), $numObjects = 1, $createOnly = FALSE) {
296 $objects = CRM_Core_DAO
::createTestObject($daoName, $params, $numObjects, $createOnly);
297 if (is_array($objects)) {
298 $this->registerTestObjects($objects);
301 $this->registerTestObjects(array($objects));
307 * Register test objects.
309 * @param array $objects
310 * DAO or BAO objects.
312 public function registerTestObjects($objects) {
313 foreach ($objects as $object) {
314 $daoName = preg_replace('/_BAO_/', '_DAO_', get_class($object));
315 $this->_testObjects
[$daoName][] = $object->id
;
320 * Delete test objects.
322 * Note: You might argue that the FK relations between test
323 * objects could make this problematic; however, it should
324 * behave intuitively as long as we mentally split our
325 * test-objects between the "manual/primary records"
326 * and the "automatic/secondary records"
328 public function deleteTestObjects() {
329 foreach ($this->_testObjects
as $daoName => $daoIds) {
330 foreach ($daoIds as $daoId) {
331 CRM_Core_DAO
::deleteTestObjects($daoName, array('id' => $daoId));
334 $this->_testObjects
= array();
338 * Set up a smart group testing scenario.
342 protected function setupSmartGroup() {
344 'name' => 'Deceased Contacts',
345 'title' => 'Deceased Contacts',
347 'formValues' => array('is_deceased' => 1),
349 $group = CRM_Contact_BAO_Group
::createSmartGroup($params);
350 $this->registerTestObjects(array($group));
352 // Create contacts $y1, $y2, $y3 which do match $g; create $n1, $n2, $n3 which do not match $g
353 $living = $this->createTestObject('CRM_Contact_DAO_Contact', array('is_deceased' => 0), 3);
354 $deceased = $this->createTestObject('CRM_Contact_DAO_Contact', array('is_deceased' => 1), 3);
355 $this->assertEquals(3, count($deceased));
356 $this->assertEquals(3, count($living));
358 // Assert: $g cache has exactly $y1, $y2, $y3
359 CRM_Contact_BAO_GroupContactCache
::load($group, TRUE);
361 $this->assertCacheMatches(
362 array($deceased[0]->id
, $deceased[1]->id
, $deceased[2]->id
),
365 // Reload the group so we have the cache_date & refresh_date.
366 return array($group, $living, $deceased);
375 protected function assertCacheNotRefreshed($deceased, $group) {
376 $this->assertCacheMatches(
377 array($deceased[0]->id
, $deceased[1]->id
, $deceased[2]->id
),
380 $afterGroup = $this->callAPISuccessGetSingle('Group', array('id' => $group->id
));
381 $this->assertEquals($group->cache_date
, $afterGroup['cache_date']);
385 * Make the cache for the group stale, resetting it to before the timeout period.
387 * @param CRM_Contact_BAO_Group $group
389 protected function makeCacheStale(&$group) {
390 CRM_Core_DAO
::executeQuery('UPDATE civicrm_group SET cache_date = DATE_SUB(NOW(), INTERVAL 7 MINUTE) WHERE id = ' . $group->id
);
391 unset($group->cache_date
);
393 Civi
::$statics['CRM_Contact_BAO_GroupContactCache']['is_refresh_init'] = FALSE;
401 protected function assertCacheRefreshed($group) {
402 $this->assertCacheMatches(
407 $afterGroup = $this->callAPISuccessGetSingle('Group', array('id' => $group->id
));
408 $this->assertTrue(empty($afterGroup['cache_date']), 'refresh date should not be set as the cache is not built');
409 $this->assertTrue(empty($afterGroup['refresh_date']), 'refresh date should not be set as the cache is not built');
413 * Test Smart group search
415 public function testSmartGroupSearchBuilder() {
416 $returnProperties = array(
418 'contact_sub_type' => 1,
422 list($group, $living, $deceased) = $this->setupSmartGroup();
425 'name' => 'Living Contacts',
426 'title' => 'Living Contacts',
428 'formValues' => array('is_deceased' => 0),
430 $group2 = CRM_Contact_BAO_Group
::createSmartGroup($params);
432 //Filter on smart group with =, !=, IN and NOT IN operator.
433 $params = array(array('group', '=', $group2->id
, 1, 0));
434 $query = new CRM_Contact_BAO_Query(
435 $params, $returnProperties,
436 NULL, FALSE, FALSE, CRM_Contact_BAO_Query
::MODE_CONTACTS
,
440 $ids = $query->searchQuery(0, 0, NULL,
444 $key = $query->getGroupCacheTableKeys()[0];
445 $expectedWhere = "civicrm_group_contact_cache_{$key}.group_id IN (\"{$group2->id}\")";
446 $this->assertContains($expectedWhere, $query->_whereClause
);
447 $this->_assertContactIds($query, "group_id = {$group2->id}");
449 $params = array(array('group', '!=', $group->id
, 1, 0));
450 $query = new CRM_Contact_BAO_Query(
451 $params, $returnProperties,
452 NULL, FALSE, FALSE, CRM_Contact_BAO_Query
::MODE_CONTACTS
,
456 $key = $query->getGroupCacheTableKeys()[0];
457 //Assert if proper where clause is present.
458 $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} ) ) )";
459 $this->assertContains($expectedWhere, $query->_whereClause
);
460 $this->_assertContactIds($query, "group_id != {$group->id}");
462 $params = array(array('group', 'IN', array($group->id
, $group2->id
), 1, 0));
463 $query = new CRM_Contact_BAO_Query(
464 $params, $returnProperties,
465 NULL, FALSE, FALSE, CRM_Contact_BAO_Query
::MODE_CONTACTS
,
469 $key = $query->getGroupCacheTableKeys()[0];
470 $expectedWhere = "civicrm_group_contact_cache_{$key}.group_id IN (\"{$group->id}\", \"{$group2->id}\")";
471 $this->assertContains($expectedWhere, $query->_whereClause
);
472 $this->_assertContactIds($query, "group_id IN ({$group->id}, {$group2->id})");
474 $params = array(array('group', 'NOT IN', array($group->id
), 1, 0));
475 $query = new CRM_Contact_BAO_Query(
476 $params, $returnProperties,
477 NULL, FALSE, FALSE, CRM_Contact_BAO_Query
::MODE_CONTACTS
,
481 $key = $query->getGroupCacheTableKeys()[0];
482 $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} ) ) )";
483 $this->assertContains($expectedWhere, $query->_whereClause
);
484 $this->_assertContactIds($query, "group_id NOT IN ({$group->id})");
485 $this->callAPISuccess('group', 'delete', ['id' => $group->id
]);
486 $this->callAPISuccess('group', 'delete', ['id' => $group2->id
]);
490 public function testMultipleGroupWhereClause() {
491 $returnProperties = array(
493 'contact_sub_type' => 1,
497 list($group, $living, $deceased) = $this->setupSmartGroup();
500 'name' => 'Living Contacts',
501 'title' => 'Living Contacts',
503 'formValues' => array('is_deceased' => 0),
505 $group2 = CRM_Contact_BAO_Group
::createSmartGroup($params);
507 //Filter on smart group with =, !=, IN and NOT IN operator.
508 $params = array(array('group', '=', $group2->id
, 1, 0), array('group', '=', $group->id
, 1, 0));
509 $query = new CRM_Contact_BAO_Query(
510 $params, $returnProperties,
511 NULL, FALSE, FALSE, CRM_Contact_BAO_Query
::MODE_CONTACTS
,
515 $ids = $query->searchQuery(0, 0, NULL,
519 $key1 = $query->getGroupCacheTableKeys()[0];
520 $key2 = $query->getGroupCacheTableKeys()[1];
521 $expectedWhere = 'civicrm_group_contact_cache_' . $key1 . '.group_id IN ("' . $group2->id
. '") ) ) AND ( ( civicrm_group_contact_cache_' . $key2 . '.group_id IN ("' . $group->id
. '")';
522 $this->assertContains($expectedWhere, $query->_whereClause
);
523 // 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.
524 $expectedFrom1 = 'LEFT JOIN civicrm_group_contact_cache civicrm_group_contact_cache_' . $key1 . ' ON contact_a.id = civicrm_group_contact_cache_' . $key1 . '.contact_id';
525 $this->assertContains($expectedFrom1, $query->_fromClause
);
526 $expectedFrom2 = 'LEFT JOIN civicrm_group_contact_cache civicrm_group_contact_cache_' . $key2 . ' ON contact_a.id = civicrm_group_contact_cache_' . $key2 . '.contact_id';
527 $this->assertContains($expectedFrom2, $query->_fromClause
);
528 $expectedFrom3 = 'LEFT JOIN civicrm_group_contact_cache ON contact_a.id = civicrm_group_contact_cache.contact_id';
529 $this->assertContains($expectedFrom3, $query->_fromClause
);
533 * Check if contact ids are fetched correctly.
535 * @param object $query
536 * @param string $groupWhereClause
538 public function _assertContactIds($query, $groupWhereClause) {
539 $contactIds = explode(',', $query->searchQuery(0, 0, NULL,
543 $expectedContactIds = array();
544 $groupDAO = CRM_Core_DAO
::executeQuery("SELECT contact_id FROM civicrm_group_contact_cache WHERE {$groupWhereClause}");
545 while ($groupDAO->fetch()) {
546 $expectedContactIds[] = $groupDAO->contact_id
;
548 $this->assertEquals(sort($expectedContactIds), sort($contactIds));