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 * Class CRM_Extension_ManagerTest
32 class CRM_Extension_ManagerTest
extends CiviUnitTestCase
{
33 const TESTING_TYPE
= 'report';
34 const OTHER_TESTING_TYPE
= 'module';
36 public function setUp() {
38 list ($this->basedir
, $this->container
) = $this->_createContainer();
39 $this->mapper
= new CRM_Extension_Mapper($this->container
);
42 public function tearDown() {
47 * Install an extension with an invalid type name.
49 * @expectedException CRM_Extension_Exception
51 public function testInstallInvalidType() {
52 $mockFunction = $this->mockMethod
;
53 $testingTypeManager = $this->$mockFunction('CRM_Extension_Manager_Interface');
54 $testingTypeManager->expects($this->never())
55 ->method('onPreInstall');
56 $manager = $this->_createManager([
57 self
::OTHER_TESTING_TYPE
=> $testingTypeManager,
59 $manager->install(['test.foo.bar']);
63 * Install an extension with a valid type name.
65 * Note: We initially install two extensions but then toggle only
66 * the second. This controls for bad SQL queries which hit either
67 * "the first row" or "all rows".
69 public function testInstall_Disable_Uninstall() {
70 $mockFunction = $this->mockMethod
;
71 $testingTypeManager = $this->$mockFunction('CRM_Extension_Manager_Interface');
72 $manager = $this->_createManager([
73 self
::TESTING_TYPE
=> $testingTypeManager,
75 $this->assertEquals('uninstalled', $manager->getStatus('test.foo.bar'));
76 $this->assertEquals('uninstalled', $manager->getStatus('test.whiz.bang'));
79 ->expects($this->exactly(2))
80 ->method('onPreInstall');
82 ->expects($this->exactly(2))
83 ->method('onPostInstall');
84 $manager->install(['test.whiz.bang', 'test.foo.bar']);
85 $this->assertEquals('installed', $manager->getStatus('test.foo.bar'));
86 $this->assertEquals('installed', $manager->getStatus('test.whiz.bang'));
89 ->expects($this->once())
90 ->method('onPreDisable');
92 ->expects($this->once())
93 ->method('onPostDisable');
94 $manager->disable(['test.foo.bar']);
95 $this->assertEquals('disabled', $manager->getStatus('test.foo.bar'));
97 $this->assertEquals('installed', $manager->getStatus('test.whiz.bang'));
100 ->expects($this->once())
101 ->method('onPreUninstall');
103 ->expects($this->once())
104 ->method('onPostUninstall');
105 $manager->uninstall(['test.foo.bar']);
106 $this->assertEquals('uninstalled', $manager->getStatus('test.foo.bar'));
108 $this->assertEquals('installed', $manager->getStatus('test.whiz.bang'));
112 * This is the same as testInstall_Disable_Uninstall, but we also install and remove a dependency.
114 * @throws \CRM_Extension_Exception
116 public function test_InstallAuto_DisableDownstream_UninstallDownstream() {
117 $mockFunction = $this->mockMethod
;
118 $testingTypeManager = $this->$mockFunction('CRM_Extension_Manager_Interface');
119 $manager = $this->_createManager([
120 self
::TESTING_TYPE
=> $testingTypeManager,
122 $this->assertEquals('uninstalled', $manager->getStatus('test.foo.bar'));
123 $this->assertEquals('uninstalled', $manager->getStatus('test.foo.downstream'));
124 $this->assertEquals('uninstalled', $manager->getStatus('test.whiz.bang'));
126 $testingTypeManager->expects($this->exactly(2))->method('onPreInstall');
127 $testingTypeManager->expects($this->exactly(2))->method('onPostInstall');
128 $this->assertEquals(['test.foo.bar', 'test.foo.downstream'],
129 $manager->findInstallRequirements(['test.foo.downstream']));
131 $manager->findInstallRequirements(['test.foo.downstream']));
132 $this->assertEquals('installed', $manager->getStatus('test.foo.bar'));
133 $this->assertEquals('installed', $manager->getStatus('test.foo.downstream'));
134 $this->assertEquals('uninstalled', $manager->getStatus('test.whiz.bang'));
136 $testingTypeManager->expects($this->once())->method('onPreDisable');
137 $testingTypeManager->expects($this->once())->method('onPostDisable');
138 $this->assertEquals(['test.foo.downstream'],
139 $manager->findDisableRequirements(['test.foo.downstream']));
140 $manager->disable(['test.foo.downstream']);
141 $this->assertEquals('installed', $manager->getStatus('test.foo.bar'));
142 $this->assertEquals('disabled', $manager->getStatus('test.foo.downstream'));
143 $this->assertEquals('uninstalled', $manager->getStatus('test.whiz.bang'));
145 $testingTypeManager->expects($this->once())->method('onPreUninstall');
146 $testingTypeManager->expects($this->once())->method('onPostUninstall');
147 $manager->uninstall(['test.foo.downstream']);
148 $this->assertEquals('installed', $manager->getStatus('test.foo.bar'));
149 $this->assertEquals('uninstalled', $manager->getStatus('test.foo.downstream'));
150 $this->assertEquals('uninstalled', $manager->getStatus('test.whiz.bang'));
154 * This is the same as testInstallAuto_Twice
156 * @throws \CRM_Extension_Exception
158 public function testInstallAuto_Twice() {
159 $mockFunction = $this->mockMethod
;
160 $testingTypeManager = $this->$mockFunction('CRM_Extension_Manager_Interface');
161 $manager = $this->_createManager([
162 self
::TESTING_TYPE
=> $testingTypeManager,
164 $this->assertEquals('uninstalled', $manager->getStatus('test.foo.bar'));
165 $this->assertEquals('uninstalled', $manager->getStatus('test.foo.downstream'));
166 $this->assertEquals('uninstalled', $manager->getStatus('test.whiz.bang'));
168 $testingTypeManager->expects($this->exactly(2))->method('onPreInstall');
169 $testingTypeManager->expects($this->exactly(2))->method('onPostInstall');
170 $this->assertEquals(['test.foo.bar', 'test.foo.downstream'],
171 $manager->findInstallRequirements(['test.foo.downstream']));
173 $manager->findInstallRequirements(['test.foo.downstream']));
174 $this->assertEquals('installed', $manager->getStatus('test.foo.bar'));
175 $this->assertEquals('installed', $manager->getStatus('test.foo.downstream'));
176 $this->assertEquals('uninstalled', $manager->getStatus('test.whiz.bang'));
178 // And install a second time...
179 $testingTypeManager->expects($this->exactly(0))->method('onPreInstall');
180 $testingTypeManager->expects($this->exactly(0))->method('onPostInstall');
182 $manager->findInstallRequirements(['test.foo.downstream']));
183 $this->assertEquals('installed', $manager->getStatus('test.foo.bar'));
184 $this->assertEquals('installed', $manager->getStatus('test.foo.downstream'));
185 $this->assertEquals('uninstalled', $manager->getStatus('test.whiz.bang'));
188 public function test_InstallAuto_DisableUpstream() {
189 $mockFunction = $this->mockMethod
;
190 $testingTypeManager = $this->$mockFunction('CRM_Extension_Manager_Interface');
191 $manager = $this->_createManager([
192 self
::TESTING_TYPE
=> $testingTypeManager,
194 $this->assertEquals('uninstalled', $manager->getStatus('test.foo.bar'));
195 $this->assertEquals('uninstalled', $manager->getStatus('test.foo.downstream'));
196 $this->assertEquals('uninstalled', $manager->getStatus('test.whiz.bang'));
198 $testingTypeManager->expects($this->exactly(2))->method('onPreInstall');
199 $testingTypeManager->expects($this->exactly(2))->method('onPostInstall');
200 $this->assertEquals(['test.foo.bar', 'test.foo.downstream'],
201 $manager->findInstallRequirements(['test.foo.downstream']));
203 $manager->findInstallRequirements(['test.foo.downstream']));
204 $this->assertEquals('installed', $manager->getStatus('test.foo.bar'));
205 $this->assertEquals('installed', $manager->getStatus('test.foo.downstream'));
206 $this->assertEquals('uninstalled', $manager->getStatus('test.whiz.bang'));
208 $testingTypeManager->expects($this->never())->method('onPreDisable');
209 $testingTypeManager->expects($this->never())->method('onPostDisable');
210 $this->assertEquals(['test.foo.downstream', 'test.foo.bar'],
211 $manager->findDisableRequirements(['test.foo.bar']));
214 $manager->disable(['test.foo.bar']);
215 $this->fail('Expected disable to fail due to dependency');
217 catch (CRM_Extension_Exception
$e) {
218 $this->assertRegExp('/test.foo.downstream/', $e->getMessage());
222 $this->assertEquals('installed', $manager->getStatus('test.foo.bar'));
223 $this->assertEquals('installed', $manager->getStatus('test.foo.downstream'));
224 $this->assertEquals('uninstalled', $manager->getStatus('test.whiz.bang'));
228 * Install an extension and then harshly remove the underlying source.
229 * Subseuently disable and uninstall.
231 public function testInstall_DirtyRemove_Disable_Uninstall() {
232 $mockFunction = $this->mockMethod
;
233 $testingTypeManager = $this->$mockFunction('CRM_Extension_Manager_Interface');
234 $manager = $this->_createManager([
235 self
::TESTING_TYPE
=> $testingTypeManager,
237 $this->assertEquals('uninstalled', $manager->getStatus('test.foo.bar'));
239 $manager->install(['test.foo.bar']);
240 $this->assertEquals('installed', $manager->getStatus('test.foo.bar'));
242 $this->assertTrue(file_exists("{$this->basedir}/weird/foobar/info.xml"));
243 CRM_Utils_File
::cleanDir("{$this->basedir}/weird/foobar", TRUE, FALSE);
244 $this->assertFalse(file_exists("{$this->basedir}/weird/foobar/info.xml"));
246 $this->assertEquals('installed-missing', $manager->getStatus('test.foo.bar'));
249 ->expects($this->once())
250 ->method('onPreDisable');
252 ->expects($this->once())
253 ->method('onPostDisable');
254 $manager->disable(['test.foo.bar']);
255 $this->assertEquals('disabled-missing', $manager->getStatus('test.foo.bar'));
258 ->expects($this->once())
259 ->method('onPreUninstall');
261 ->expects($this->once())
262 ->method('onPostUninstall');
263 $manager->uninstall(['test.foo.bar']);
264 $this->assertEquals('unknown', $manager->getStatus('test.foo.bar'));
268 * Install an extension with a valid type name.
270 public function testInstall_Disable_Enable() {
271 $mockFunction = $this->mockMethod
;
272 $testingTypeManager = $this->$mockFunction('CRM_Extension_Manager_Interface');
273 $manager = $this->_createManager([
274 self
::TESTING_TYPE
=> $testingTypeManager,
276 $this->assertEquals('uninstalled', $manager->getStatus('test.foo.bar'));
277 $this->assertEquals('uninstalled', $manager->getStatus('test.whiz.bang'));
280 ->expects($this->exactly(2))
281 ->method('onPreInstall');
283 ->expects($this->exactly(2))
284 ->method('onPostInstall');
285 $manager->install(['test.whiz.bang', 'test.foo.bar']);
286 $this->assertEquals('installed', $manager->getStatus('test.foo.bar'));
287 $this->assertEquals('installed', $manager->getStatus('test.whiz.bang'));
290 ->expects($this->once())
291 ->method('onPreDisable');
293 ->expects($this->once())
294 ->method('onPostDisable');
295 $manager->disable(['test.foo.bar']);
296 $this->assertEquals('disabled', $manager->getStatus('test.foo.bar'));
297 $this->assertEquals('installed', $manager->getStatus('test.whiz.bang'));
300 ->expects($this->once())
301 ->method('onPreEnable');
303 ->expects($this->once())
304 ->method('onPostEnable');
305 $manager->enable(['test.foo.bar']);
306 $this->assertEquals('installed', $manager->getStatus('test.foo.bar'));
307 $this->assertEquals('installed', $manager->getStatus('test.whiz.bang'));
311 * Performing 'install' on a 'disabled' extension performs an 'enable'
313 public function testInstall_Disable_Install() {
314 $mockFunction = $this->mockMethod
;
315 $testingTypeManager = $this->$mockFunction('CRM_Extension_Manager_Interface');
316 $manager = $this->_createManager([
317 self
::TESTING_TYPE
=> $testingTypeManager,
319 $this->assertEquals('uninstalled', $manager->getStatus('test.foo.bar'));
322 ->expects($this->once())
323 ->method('onPreInstall');
325 ->expects($this->once())
326 ->method('onPostInstall');
327 $manager->install(['test.foo.bar']);
328 $this->assertEquals('installed', $manager->getStatus('test.foo.bar'));
331 ->expects($this->once())
332 ->method('onPreDisable');
334 ->expects($this->once())
335 ->method('onPostDisable');
336 $manager->disable(['test.foo.bar']);
337 $this->assertEquals('disabled', $manager->getStatus('test.foo.bar'));
340 ->expects($this->once())
341 ->method('onPreEnable');
343 ->expects($this->once())
344 ->method('onPostEnable');
345 // install() instead of enable()
346 $manager->install(['test.foo.bar']);
347 $this->assertEquals('installed', $manager->getStatus('test.foo.bar'));
351 * Install an extension with a valid type name.
353 public function testEnableBare() {
354 $mockFunction = $this->mockMethod
;
355 $testingTypeManager = $this->$mockFunction('CRM_Extension_Manager_Interface');
356 $manager = $this->_createManager([
357 self
::TESTING_TYPE
=> $testingTypeManager,
359 $this->assertEquals('uninstalled', $manager->getStatus('test.foo.bar'));
362 ->expects($this->once())
363 ->method('onPreInstall');
365 ->expects($this->once())
366 ->method('onPostInstall');
368 ->expects($this->never())
369 ->method('onPreEnable');
371 ->expects($this->never())
372 ->method('onPostEnable');
373 // enable not install
374 $manager->enable(['test.foo.bar']);
375 $this->assertEquals('installed', $manager->getStatus('test.foo.bar'));
379 * Get the status of an unknown extension.
381 public function testStatusUnknownKey() {
382 $mockFunction = $this->mockMethod
;
383 $testingTypeManager = $this->$mockFunction('CRM_Extension_Manager_Interface');
384 $testingTypeManager->expects($this->never())
385 ->method('onPreInstall');
386 $manager = $this->_createManager([
387 self
::TESTING_TYPE
=> $testingTypeManager,
389 $this->assertEquals('unknown', $manager->getStatus('test.foo.bar.whiz.bang'));
393 * Replace code for an extension that doesn't exist in the container
395 public function testReplace_Unknown() {
396 $mockFunction = $this->mockMethod
;
397 $testingTypeManager = $this->$mockFunction('CRM_Extension_Manager_Interface');
398 $manager = $this->_createManager([
399 self
::TESTING_TYPE
=> $testingTypeManager,
401 $this->assertEquals('unknown', $manager->getStatus('test.newextension'));
403 $this->download
= $this->_createDownload('test.newextension', 'newextension');
406 // no data to replace
407 ->expects($this->never())
408 ->method('onPreReplace');
410 // no data to replace
411 ->expects($this->never())
412 ->method('onPostReplace');
413 $manager->replace($this->download
);
414 $this->assertEquals('uninstalled', $manager->getStatus('test.newextension'));
415 $this->assertTrue(file_exists("{$this->basedir}/test.newextension/info.xml"));
416 $this->assertTrue(file_exists("{$this->basedir}/test.newextension/newextension.php"));
417 $this->assertEquals(self
::TESTING_TYPE
, $this->mapper
->keyToInfo('test.newextension')->type
);
418 $this->assertEquals('newextension', $this->mapper
->keyToInfo('test.newextension')->file
);
422 * Replace code for an extension that doesn't exist in the container
424 public function testReplace_Uninstalled() {
425 $mockFunction = $this->mockMethod
;
426 $testingTypeManager = $this->$mockFunction('CRM_Extension_Manager_Interface');
427 $manager = $this->_createManager([
428 self
::TESTING_TYPE
=> $testingTypeManager,
430 $this->assertEquals('uninstalled', $manager->getStatus('test.whiz.bang'));
431 $this->assertEquals('oddball', $this->mapper
->keyToInfo('test.whiz.bang')->file
);
433 $this->download
= $this->_createDownload('test.whiz.bang', 'newextension');
436 // no data to replace
437 ->expects($this->never())
438 ->method('onPreReplace');
440 // no data to replace
441 ->expects($this->never())
442 ->method('onPostReplace');
443 $manager->replace($this->download
);
444 $this->assertEquals('uninstalled', $manager->getStatus('test.whiz.bang'));
445 $this->assertTrue(file_exists("{$this->basedir}/weird/whizbang/info.xml"));
446 $this->assertTrue(file_exists("{$this->basedir}/weird/whizbang/newextension.php"));
447 $this->assertFalse(file_exists("{$this->basedir}/weird/whizbang/oddball.php"));
448 $this->assertEquals(self
::TESTING_TYPE
, $this->mapper
->keyToInfo('test.whiz.bang')->type
);
449 $this->assertEquals('newextension', $this->mapper
->keyToInfo('test.whiz.bang')->file
);
453 * Install a module and then replace it with new code.
455 * Note that some metadata changes between versions -- the original has
456 * file="oddball", and the upgrade has file="newextension".
458 public function testReplace_Installed() {
459 $mockFunction = $this->mockMethod
;
460 $testingTypeManager = $this->$mockFunction('CRM_Extension_Manager_Interface');
461 $manager = $this->_createManager([
462 self
::TESTING_TYPE
=> $testingTypeManager,
464 $this->assertEquals('uninstalled', $manager->getStatus('test.whiz.bang'));
465 $this->assertEquals('oddball', $this->mapper
->keyToInfo('test.whiz.bang')->file
);
467 $manager->install(['test.whiz.bang']);
468 $this->assertEquals('installed', $manager->getStatus('test.whiz.bang'));
469 $this->assertEquals('oddball', $this->mapper
->keyToInfo('test.whiz.bang')->file
);
470 $this->assertDBQuery('oddball', 'SELECT file FROM civicrm_extension WHERE full_name ="test.whiz.bang"');
472 $this->download
= $this->_createDownload('test.whiz.bang', 'newextension');
475 ->expects($this->once())
476 ->method('onPreReplace');
478 ->expects($this->once())
479 ->method('onPostReplace');
480 $manager->replace($this->download
);
481 $this->assertEquals('installed', $manager->getStatus('test.whiz.bang'));
482 $this->assertTrue(file_exists("{$this->basedir}/weird/whizbang/info.xml"));
483 $this->assertTrue(file_exists("{$this->basedir}/weird/whizbang/newextension.php"));
484 $this->assertFalse(file_exists("{$this->basedir}/weird/whizbang/oddball.php"));
485 $this->assertEquals('newextension', $this->mapper
->keyToInfo('test.whiz.bang')->file
);
486 $this->assertDBQuery('newextension', 'SELECT file FROM civicrm_extension WHERE full_name ="test.whiz.bang"');
490 * Install a module and then delete (leaving stale DB info); restore
491 * the module by downloading new code.
493 * Note that some metadata changes between versions -- the original has
494 * file="oddball", and the upgrade has file="newextension".
496 public function testReplace_InstalledMissing() {
497 $mockFunction = $this->mockMethod
;
498 $testingTypeManager = $this->$mockFunction('CRM_Extension_Manager_Interface');
499 $manager = $this->_createManager([
500 self
::TESTING_TYPE
=> $testingTypeManager,
503 // initial installation
504 $this->assertEquals('uninstalled', $manager->getStatus('test.whiz.bang'));
505 $manager->install(['test.whiz.bang']);
506 $this->assertEquals('installed', $manager->getStatus('test.whiz.bang'));
509 $this->assertTrue(file_exists("{$this->basedir}/weird/whizbang/info.xml"));
510 CRM_Utils_File
::cleanDir("{$this->basedir}/weird/whizbang", TRUE, FALSE);
511 $this->assertFalse(file_exists("{$this->basedir}/weird/whizbang/info.xml"));
513 $this->assertEquals('installed-missing', $manager->getStatus('test.whiz.bang'));
515 // download and reinstall
516 $this->download
= $this->_createDownload('test.whiz.bang', 'newextension');
519 ->expects($this->once())
520 ->method('onPreReplace');
522 ->expects($this->once())
523 ->method('onPostReplace');
524 $manager->replace($this->download
);
525 $this->assertEquals('installed', $manager->getStatus('test.whiz.bang'));
526 $this->assertTrue(file_exists("{$this->basedir}/test.whiz.bang/info.xml"));
527 $this->assertTrue(file_exists("{$this->basedir}/test.whiz.bang/newextension.php"));
528 $this->assertEquals('newextension', $this->mapper
->keyToInfo('test.whiz.bang')->file
);
529 $this->assertDBQuery('newextension', 'SELECT file FROM civicrm_extension WHERE full_name ="test.whiz.bang"');
533 * @param $typeManagers
535 * @return CRM_Extension_Manager
537 public function _createManager($typeManagers) {
538 //list ($basedir, $c) = $this->_createContainer();
539 $mapper = new CRM_Extension_Mapper($this->container
);
540 return new CRM_Extension_Manager($this->container
, $this->container
, $this->mapper
, $typeManagers);
544 * @param CRM_Utils_Cache_Interface $cache
545 * @param null $cacheKey
549 public function _createContainer(CRM_Utils_Cache_Interface
$cache = NULL, $cacheKey = NULL) {
550 $basedir = $this->createTempDir('ext-');
551 mkdir("$basedir/weird");
552 mkdir("$basedir/weird/foobar");
553 file_put_contents("$basedir/weird/foobar/info.xml", "<extension key='test.foo.bar' type='" . self
::TESTING_TYPE
. "'><file>oddball</file></extension>");
554 // not needed for now // file_put_contents("$basedir/weird/bar/oddball.php", "<?php\n");
555 mkdir("$basedir/weird/whizbang");
556 file_put_contents("$basedir/weird/whizbang/info.xml", "<extension key='test.whiz.bang' type='" . self
::TESTING_TYPE
. "'><file>oddball</file></extension>");
557 // not needed for now // file_put_contents("$basedir/weird/whizbang/oddball.php", "<?php\n");
558 mkdir("$basedir/weird/downstream");
559 file_put_contents("$basedir/weird/downstream/info.xml", "<extension key='test.foo.downstream' type='" . self
::TESTING_TYPE
. "'><file>oddball</file><requires><ext>test.foo.bar</ext></requires></extension>");
560 // not needed for now // file_put_contents("$basedir/weird/downstream/oddball.php", "<?php\n");
561 $c = new CRM_Extension_Container_Basic($basedir, 'http://example/basedir', $cache, $cacheKey);
562 return [$basedir, $c];
571 public function _createDownload($key, $file) {
572 $basedir = $this->createTempDir('ext-dl-');
573 file_put_contents("$basedir/info.xml", "<extension key='$key' type='" . self
::TESTING_TYPE
. "'><file>$file</file></extension>");
574 file_put_contents("$basedir/$file.php", "<?php\n");