Merge pull request #16746 from eileenmcnaughton/fatal
[civicrm-core.git] / tests / phpunit / CRM / Core / ManagedEntitiesTest.php
1 <?php
2
3 /**
4 * Class CRM_Core_ManagedEntitiesTest
5 * @group headless
6 */
7 class CRM_Core_ManagedEntitiesTest extends CiviUnitTestCase {
8 /**
9 * @var \Civi\API\Kernel
10 */
11 protected $apiKernel;
12
13 /**
14 * @var \Civi\API\Provider\AdhocProvider
15 */
16 protected $adhocProvider;
17
18 /**
19 * @var String[]
20 */
21 protected $modules;
22
23 protected $fixtures;
24
25 public function setUp() {
26 $this->useTransaction(TRUE);
27 parent::setUp();
28 $this->modules = [
29 'one' => new CRM_Core_Module('com.example.one', TRUE),
30 'two' => new CRM_Core_Module('com.example.two', TRUE),
31 ];
32
33 // Testing on drupal-demo fails because some extensions have mgd ents.
34 CRM_Core_DAO::singleValueQuery('DELETE FROM civicrm_managed');
35
36 $this->fixtures['com.example.one-foo'] = [
37 'module' => 'com.example.one',
38 'name' => 'foo',
39 'entity' => 'CustomSearch',
40 'params' => [
41 'version' => 3,
42 'class_name' => 'CRM_Example_One_Foo',
43 'is_reserved' => 1,
44 ],
45 ];
46 $this->fixtures['com.example.one-bar'] = [
47 'module' => 'com.example.one',
48 'name' => 'bar',
49 'entity' => 'CustomSearch',
50 'params' => [
51 'version' => 3,
52 'class_name' => 'CRM_Example_One_Bar',
53 'is_reserved' => 1,
54 ],
55 ];
56 $this->fixtures['com.example.one-CustomGroup'] = [
57 'module' => 'com.example.one',
58 'name' => 'CustomGroup',
59 'entity' => 'CustomGroup',
60 'params' => [
61 'version' => 3,
62 'name' => 'test_custom_group',
63 'title' => 'Test custom group',
64 'extends' => 'Individual',
65 ],
66 ];
67 $this->fixtures['com.example.one-CustomField'] = [
68 'module' => 'com.example.one',
69 'name' => 'CustomField',
70 'entity' => 'CustomField',
71 'params' => [
72 'version' => 3,
73 'name' => 'test_custom_field',
74 'label' => 'Test custom field',
75 'custom_group_id' => 'test_custom_group',
76 'data_type' => 'String',
77 'html_type' => 'Text',
78 ],
79 ];
80
81 $this->fixtures['com.example.one-Job'] = [
82 'module' => 'com.example.one',
83 'name' => 'Job',
84 'entity' => 'Job',
85 'params' => [
86 'version' => 3,
87 'name' => 'test_job',
88 'run_frequency' => 'Daily',
89 'api_entity' => 'Job',
90 'api_action' => 'Get',
91 'parameters' => '',
92 ],
93 ];
94
95 $this->apiKernel = \Civi::service('civi_api_kernel');
96 $this->adhocProvider = new \Civi\API\Provider\AdhocProvider(3, 'CustomSearch');
97 $this->apiKernel->registerApiProvider($this->adhocProvider);
98 }
99
100 public function tearDown() {
101 parent::tearDown();
102 \Civi::reset();
103 }
104
105 /**
106 * Set up an active module and, over time, the hook implementation changes
107 * to (1) create 'foo' entity, (2) create 'bar' entity', (3) remove 'foo'
108 * entity
109 */
110 public function testAddRemoveEntitiesModule_UpdateAlways_DeleteAlways() {
111 $decls = [];
112
113 // create first managed entity ('foo')
114 $decls[] = $this->fixtures['com.example.one-foo'];
115 $me = new CRM_Core_ManagedEntities($this->modules, $decls);
116 $me->reconcile();
117 $foo = $me->get('com.example.one', 'foo');
118 $this->assertEquals('CRM_Example_One_Foo', $foo['name']);
119 $this->assertDBQuery(1, 'SELECT count(*) FROM civicrm_option_value WHERE name = "CRM_Example_One_Foo"');
120
121 // later on, hook returns an extra managed entity ('bar')
122 $decls[] = $this->fixtures['com.example.one-bar'];
123 $me = new CRM_Core_ManagedEntities($this->modules, $decls);
124 $me->reconcile();
125 $foo = $me->get('com.example.one', 'foo');
126 $this->assertEquals('CRM_Example_One_Foo', $foo['name']);
127 $this->assertDBQuery(1, 'SELECT count(*) FROM civicrm_option_value WHERE name = "CRM_Example_One_Foo"');
128 $bar = $me->get('com.example.one', 'bar');
129 $this->assertEquals('CRM_Example_One_Bar', $bar['name']);
130 $this->assertDBQuery(1, 'SELECT count(*) FROM civicrm_option_value WHERE name = "CRM_Example_One_Bar"');
131
132 // and then hook changes its mind, removing 'foo' (first of two entities)
133 unset($decls[0]);
134 $me = new CRM_Core_ManagedEntities($this->modules, $decls);
135 $me->reconcile();
136 $foo = $me->get('com.example.one', 'foo');
137 $this->assertTrue($foo === NULL);
138 $this->assertDBQuery(0, 'SELECT count(*) FROM civicrm_option_value WHERE name = "CRM_Example_One_Foo"');
139 $bar = $me->get('com.example.one', 'bar');
140 $this->assertEquals('CRM_Example_One_Bar', $bar['name']);
141 $this->assertDBQuery(1, 'SELECT count(*) FROM civicrm_option_value WHERE name = "CRM_Example_One_Bar"');
142
143 // and then hook changes its mind, removing 'bar' (the last remaining entity)
144 unset($decls[1]);
145 $me = new CRM_Core_ManagedEntities($this->modules, $decls);
146 $me->reconcile();
147 $foo = $me->get('com.example.one', 'foo');
148 $this->assertTrue($foo === NULL);
149 $this->assertDBQuery(0, 'SELECT count(*) FROM civicrm_option_value WHERE name = "CRM_Example_One_Foo"');
150 $bar = $me->get('com.example.one', 'bar');
151 $this->assertTrue($bar === NULL);
152 $this->assertDBQuery(0, 'SELECT count(*) FROM civicrm_option_value WHERE name = "CRM_Example_One_Bar"');
153 }
154
155 /**
156 * Set up an active module with one managed-entity and, over
157 * time, the content of the entity changes
158 */
159 public function testModifyDeclaration_UpdateAlways() {
160 $decls = [];
161
162 // create first managed entity ('foo')
163 $decls[] = $this->fixtures['com.example.one-foo'];
164 $me = new CRM_Core_ManagedEntities($this->modules, $decls);
165 $me->reconcile();
166 $foo = $me->get('com.example.one', 'foo');
167 $this->assertEquals('CRM_Example_One_Foo', $foo['name']);
168 $this->assertDBQuery(1, 'SELECT count(*) FROM civicrm_option_value WHERE name = "CRM_Example_One_Foo"');
169
170 // later on, hook specification changes
171 $decls[0]['params']['class_name'] = 'CRM_Example_One_Foobar';
172 $me = new CRM_Core_ManagedEntities($this->modules, $decls);
173 $me->reconcile();
174 $foo2 = $me->get('com.example.one', 'foo');
175 $this->assertEquals('CRM_Example_One_Foobar', $foo2['name']);
176 $this->assertDBQuery(0, 'SELECT count(*) FROM civicrm_option_value WHERE name = "CRM_Example_One_Foo"');
177 $this->assertDBQuery(1, 'SELECT count(*) FROM civicrm_option_value WHERE name = "CRM_Example_One_FooBar"');
178 $this->assertEquals($foo['id'], $foo2['id']);
179 }
180
181 /**
182 * Set up an active module with one managed-entity and, over
183 * time, the content of the entity changes
184 */
185 public function testModifyDeclaration_UpdateNever() {
186 $decls = [];
187
188 // create first managed entity ('foo')
189 $decls[] = array_merge($this->fixtures['com.example.one-foo'], [
190 // Policy is to never update after initial creation
191 'update' => 'never',
192 ]);
193 $me = new CRM_Core_ManagedEntities($this->modules, $decls);
194 $me->reconcile();
195 $foo = $me->get('com.example.one', 'foo');
196 $this->assertEquals('CRM_Example_One_Foo', $foo['name']);
197 $this->assertDBQuery(1, 'SELECT count(*) FROM civicrm_option_value WHERE name = "CRM_Example_One_Foo"');
198
199 // later on, hook specification changes
200 $decls[0]['params']['class_name'] = 'CRM_Example_One_Foobar';
201 $me = new CRM_Core_ManagedEntities($this->modules, $decls);
202 $me->reconcile();
203 $foo2 = $me->get('com.example.one', 'foo');
204 $this->assertEquals('CRM_Example_One_Foo', $foo2['name']);
205 $this->assertDBQuery(1, 'SELECT count(*) FROM civicrm_option_value WHERE name = "CRM_Example_One_Foo"');
206 $this->assertDBQuery(0, 'SELECT count(*) FROM civicrm_option_value WHERE name = "CRM_Example_One_FooBar"');
207 $this->assertEquals($foo['id'], $foo2['id']);
208 }
209
210 /**
211 * Set up an active module with one managed-entity using the
212 * policy "cleanup=>never". When the managed-entity goes away,
213 * ensure that the policy is followed (ie the entity is not
214 * deleted).
215 */
216 public function testRemoveDeclaration_CleanupNever() {
217 $decls = [];
218
219 // create first managed entity ('foo')
220 $decls[] = array_merge($this->fixtures['com.example.one-foo'], [
221 'cleanup' => 'never',
222 ]);
223 $me = new CRM_Core_ManagedEntities($this->modules, $decls);
224 $me->reconcile();
225 $foo = $me->get('com.example.one', 'foo');
226 $this->assertEquals('CRM_Example_One_Foo', $foo['name']);
227 $this->assertDBQuery(1, 'SELECT count(*) FROM civicrm_option_value WHERE name = "CRM_Example_One_Foo"');
228
229 // later on, entity definition disappears; but we decide not to do any cleanup (per policy)
230 $decls = [];
231 $me = new CRM_Core_ManagedEntities($this->modules, $decls);
232 $me->reconcile();
233 $foo2 = $me->get('com.example.one', 'foo');
234 $this->assertEquals('CRM_Example_One_Foo', $foo2['name']);
235 $this->assertDBQuery(1, 'SELECT count(*) FROM civicrm_option_value WHERE name = "CRM_Example_One_Foo"');
236 $this->assertEquals($foo['id'], $foo2['id']);
237 }
238
239 /**
240 * Set up an active module with one managed-entity using the
241 * policy "cleanup=>never". When the managed-entity goes away,
242 * ensure that the policy is followed (ie the entity is not
243 * deleted).
244 */
245 public function testRemoveDeclaration_CleanupUnused() {
246 $decls = [];
247
248 // create first managed entity ('foo')
249 $decls[] = array_merge($this->fixtures['com.example.one-foo'], [
250 'cleanup' => 'unused',
251 ]);
252 $me = new CRM_Core_ManagedEntities($this->modules, $decls);
253 $me->reconcile();
254 $foo = $me->get('com.example.one', 'foo');
255 $this->assertEquals('CRM_Example_One_Foo', $foo['name']);
256 $this->assertDBQuery(1, 'SELECT count(*) FROM civicrm_option_value WHERE name = "CRM_Example_One_Foo"');
257
258 // Override 'getrefcount' ==> The refcount is 1
259 $this->adhocProvider->addAction('getrefcount', 'access CiviCRM', function ($apiRequest) {
260 return civicrm_api3_create_success([
261 [
262 'name' => 'mock',
263 'type' => 'mock',
264 'count' => 1,
265 ],
266 ]);
267 });
268
269 // Later on, entity definition disappears; but we decide not to do any cleanup (per policy)
270 $decls = [];
271 $me = new CRM_Core_ManagedEntities($this->modules, $decls);
272 $me->reconcile();
273 $foo2 = $me->get('com.example.one', 'foo');
274 $this->assertEquals('CRM_Example_One_Foo', $foo2['name']);
275 $this->assertDBQuery(1, 'SELECT count(*) FROM civicrm_option_value WHERE name = "CRM_Example_One_Foo"');
276 $this->assertEquals($foo['id'], $foo2['id']);
277
278 // Override 'getrefcount' ==> The refcount is 0
279 $this->adhocProvider->addAction('getrefcount', 'access CiviCRM', function ($apiRequest) {
280 return civicrm_api3_create_success([]);
281 });
282
283 // The entity definition disappeared and there's no reference; we decide to cleanup (per policy)
284 $decls = [];
285 $me = new CRM_Core_ManagedEntities($this->modules, $decls);
286 $me->reconcile();
287 $foo3 = $me->get('com.example.one', 'foo');
288 $this->assertDBQuery(0, 'SELECT count(*) FROM civicrm_option_value WHERE name = "CRM_Example_One_Foo"');
289 $this->assertTrue($foo3 === NULL);
290 }
291
292 /**
293 * Setup an active module with a malformed entity declaration.
294 */
295 public function testInvalidDeclarationModule() {
296 // create first managed entity ('foo')
297 $decls = [];
298 $decls[] = [
299 // erroneous
300 'module' => 'com.example.unknown',
301 'name' => 'foo',
302 'entity' => 'CustomSearch',
303 'params' => [
304 'version' => 3,
305 'class_name' => 'CRM_Example_One_Foo',
306 'is_reserved' => 1,
307 ],
308 ];
309 $me = new CRM_Core_ManagedEntities($this->modules, $decls);
310 try {
311 $me->reconcile();
312 $this->fail('Expected exception when using invalid declaration');
313 }
314 catch (Exception $e) {
315 // good
316 }
317 }
318
319 /**
320 * Setup an active module with a malformed entity declaration.
321 */
322 public function testMissingName() {
323 // create first managed entity ('foo')
324 $decls = [];
325 $decls[] = [
326 'module' => 'com.example.unknown',
327 // erroneous
328 'name' => NULL,
329 'entity' => 'CustomSearch',
330 'params' => [
331 'version' => 3,
332 'class_name' => 'CRM_Example_One_Foo',
333 'is_reserved' => 1,
334 ],
335 ];
336 $me = new CRM_Core_ManagedEntities($this->modules, $decls);
337 try {
338 $me->reconcile();
339 $this->fail('Expected exception when using invalid declaration');
340 }
341 catch (Exception $e) {
342 // good
343 }
344 }
345
346 /**
347 * Setup an active module with a malformed entity declaration.
348 */
349 public function testMissingEntity() {
350 // create first managed entity ('foo')
351 $decls = [];
352 $decls[] = [
353 'module' => 'com.example.unknown',
354 'name' => 'foo',
355 // erroneous
356 'entity' => NULL,
357 'params' => [
358 'version' => 3,
359 'class_name' => 'CRM_Example_One_Foo',
360 'is_reserved' => 1,
361 ],
362 ];
363 $me = new CRM_Core_ManagedEntities($this->modules, $decls);
364 try {
365 $me->reconcile();
366 $this->fail('Expected exception when using invalid declaration');
367 }
368 catch (Exception $e) {
369 // good
370 }
371 }
372
373 /**
374 * Setup an active module with an entity -- then disable and re-enable the
375 * module
376 */
377 public function testDeactivateReactivateModule() {
378 $manager = CRM_Extension_System::singleton()->getManager();
379
380 // create first managed entity ('foo')
381 $decls = [];
382 $decls[] = $this->fixtures['com.example.one-foo'];
383 // Mock the contextual process info that would be added by CRM_Extension_Manager::install
384 $manager->setProcessesForTesting(['com.example.one' => ['install']]);
385 $me = new CRM_Core_ManagedEntities($this->modules, $decls);
386 $me->reconcile();
387 $foo = $me->get('com.example.one', 'foo');
388 $this->assertEquals(1, $foo['is_active']);
389 $this->assertEquals('CRM_Example_One_Foo', $foo['name']);
390 $this->assertDBQuery(1, 'SELECT is_active FROM civicrm_option_value WHERE name = "CRM_Example_One_Foo"');
391
392 // now deactivate module, which has empty decls and which cascades to managed object
393 $this->modules['one']->is_active = FALSE;
394 // Mock the contextual process info that would be added by CRM_Extension_Manager::disable
395 $manager->setProcessesForTesting(['com.example.one' => ['disable']]);
396 $me = new CRM_Core_ManagedEntities($this->modules, []);
397 $me->reconcile();
398 $foo = $me->get('com.example.one', 'foo');
399 $this->assertEquals(0, $foo['is_active']);
400 $this->assertEquals('CRM_Example_One_Foo', $foo['name']);
401 $this->assertDBQuery(0, 'SELECT is_active FROM civicrm_option_value WHERE name = "CRM_Example_One_Foo"');
402
403 // and reactivate module, which again provides decls and which cascades to managed object
404 $this->modules['one']->is_active = TRUE;
405 // Mock the contextual process info that would be added by CRM_Extension_Manager::enable
406 $manager->setProcessesForTesting(['com.example.one' => ['enable']]);
407 $me = new CRM_Core_ManagedEntities($this->modules, $decls);
408 $me->reconcile();
409 $foo = $me->get('com.example.one', 'foo');
410 $this->assertEquals(1, $foo['is_active']);
411 $this->assertEquals('CRM_Example_One_Foo', $foo['name']);
412 $this->assertDBQuery(1, 'SELECT is_active FROM civicrm_option_value WHERE name = "CRM_Example_One_Foo"');
413
414 // Special case: Job entities.
415 //
416 // First we repeat the above steps, but adding the context that
417 // CRM_Extension_Manager adds when installing/enabling extensions.
418 //
419 // The behaviour should be as above.
420 $decls = [$this->fixtures['com.example.one-Job']];
421 // Mock the contextual process info that would be added by CRM_Extension_Manager::install
422 $manager->setProcessesForTesting(['com.example.one' => ['install']]);
423 $me = new CRM_Core_ManagedEntities($this->modules, $decls);
424 $me->reconcile();
425 $job = $me->get('com.example.one', 'Job');
426 $this->assertEquals(1, $job['is_active']);
427 $this->assertEquals('test_job', $job['name']);
428 $this->assertDBQuery(1, 'SELECT is_active FROM civicrm_job WHERE name = "test_job"');
429 // Reset context.
430 $manager->setProcessesForTesting([]);
431
432 // now deactivate module, which has empty decls and which cascades to managed object
433 $this->modules['one']->is_active = FALSE;
434 // Mock the contextual process info that would be added by CRM_Extension_Manager::disable
435 $manager->setProcessesForTesting(['com.example.one' => ['disable']]);
436 $me = new CRM_Core_ManagedEntities($this->modules, []);
437 $me->reconcile();
438 $job = $me->get('com.example.one', 'Job');
439 $this->assertEquals(0, $job['is_active']);
440 $this->assertEquals('test_job', $job['name']);
441 $this->assertDBQuery(0, 'SELECT is_active FROM civicrm_job WHERE name = "test_job"');
442
443 // and reactivate module, which again provides decls and which cascades to managed object
444 $this->modules['one']->is_active = TRUE;
445 $me = new CRM_Core_ManagedEntities($this->modules, $decls);
446 // Mock the contextual process info that would be added by CRM_Extension_Manager::enable
447 $manager->setProcessesForTesting(['com.example.one' => ['enable']]);
448 $me->reconcile();
449 $job = $me->get('com.example.one', 'Job');
450 $this->assertEquals(1, $job['is_active']);
451 $this->assertEquals('test_job', $job['name']);
452 $this->assertDBQuery(1, 'SELECT is_active FROM civicrm_job WHERE name = "test_job"');
453
454 // Currently: module enabled, job enabled.
455 // Test that if we now manually disable the job, calling reconcile in a
456 // normal flush situation does NOT re-enable it.
457 // ... manually disable job.
458 $this->callAPISuccess('Job', 'create', ['id' => $job['id'], 'is_active' => 0]);
459
460 // ... now call reconcile in the context of a normal flush operation.
461 // Mock the contextual process info - there would not be any
462 $manager->setProcessesForTesting([]);
463 $me = new CRM_Core_ManagedEntities($this->modules, $decls);
464 $me->reconcile();
465 $job = $me->get('com.example.one', 'Job');
466 $this->assertEquals(0, $job['is_active'], "Job that was manually set inactive should not have been set active again, but it was.");
467 $this->assertDBQuery(0, 'SELECT is_active FROM civicrm_job WHERE name = "test_job"');
468
469 // Now call reconcile again, but in the context of the job's extension being installed/enabled. This should re-enable the job.
470 foreach (['enable', 'install'] as $process) {
471 // Manually disable the job
472 $this->callAPISuccess('Job', 'create', ['id' => $job['id'], 'is_active' => 0]);
473 // Mock the contextual process info that would be added by CRM_Extension_Manager::enable
474 $manager->setProcessesForTesting(['com.example.one' => [$process]]);
475 $me = new CRM_Core_ManagedEntities($this->modules, $decls);
476 $me->reconcile();
477 $job = $me->get('com.example.one', 'Job');
478 $this->assertEquals(1, $job['is_active']);
479 $this->assertEquals('test_job', $job['name']);
480 $this->assertDBQuery(1, 'SELECT is_active FROM civicrm_job WHERE name = "test_job"');
481 }
482
483 // Reset context.
484 $manager->setProcessesForTesting([]);
485 }
486
487 /**
488 * Setup an active module with an entity -- then entirely uninstall the
489 * module
490 */
491 public function testUninstallModule() {
492 // create first managed entity ('foo')
493 $decls = [];
494 $decls[] = $this->fixtures['com.example.one-foo'];
495 $me = new CRM_Core_ManagedEntities($this->modules, $decls);
496 $me->reconcile();
497 $foo = $me->get('com.example.one', 'foo');
498 $this->assertEquals('CRM_Example_One_Foo', $foo['name']);
499 $this->assertDBQuery(1, 'SELECT count(*) FROM civicrm_option_value WHERE name = "CRM_Example_One_Foo"');
500
501 // then destroy module; note that decls go away
502 unset($this->modules['one']);
503 $me = new CRM_Core_ManagedEntities($this->modules, []);
504 $me->reconcile();
505 $fooNew = $me->get('com.example.one', 'foo');
506 $this->assertTrue(NULL === $fooNew);
507 $this->assertDBQuery(0, 'SELECT count(*) FROM civicrm_option_value WHERE name = "CRM_Example_One_Foo"');
508 }
509
510 public function testDependentEntitiesUninstallCleanly() {
511
512 // Install a module with two dependent managed entities
513 $decls = [];
514 $decls[] = $this->fixtures['com.example.one-CustomGroup'];
515 $decls[] = $this->fixtures['com.example.one-CustomField'];
516 $me = new CRM_Core_ManagedEntities($this->modules, $decls);
517 $me->reconcile();
518
519 // Uninstall the module
520 unset($this->modules['one']);
521 $me = new CRM_Core_ManagedEntities($this->modules, []);
522 $me->reconcile();
523
524 // Ensure that no managed entities remain in the civicrm_managed
525 $this->assertDBQuery(0, 'SELECT count(*) FROM civicrm_managed');
526
527 // Ensure that com.example.one-CustomGroup is deleted
528 $this->assertDBQuery(0, 'SELECT count(*) FROM civicrm_custom_group WHERE name = "test_custom_group"');
529
530 // Ensure that com.example.one-CustomField is deleted
531 $this->assertDBQuery(0, 'SELECT count(*) FROM civicrm_custom_field WHERE name = "test_custom_field"');
532
533 }
534
535 }