3 * File for the CiviUnitTestCase class
7 * @copyright Copyright CiviCRM LLC (C) 2009
8 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html
9 * GNU Affero General Public License version 3
12 * This file is part of CiviCRM
14 * CiviCRM is free software; you can redistribute it and/or
15 * modify it under the terms of the GNU Affero General Public License
16 * as published by the Free Software Foundation; either version 3 of
17 * the License, or (at your option) any later version.
19 * CiviCRM is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU Affero General Public License for more details.
24 * You should have received a copy of the GNU Affero General Public
25 * License along with this program. If not, see
26 * <http://www.gnu.org/licenses/>.
29 use Civi\Api4\Contribution
;
30 use Civi\Api4\CustomField
;
31 use Civi\Api4\CustomGroup
;
32 use Civi\Api4\LineItem
;
33 use Civi\Api4\OptionGroup
;
34 use Civi\Api4\RelationshipType
;
35 use Civi\Payment\System
;
36 use Civi\Api4\OptionValue
;
37 use Civi\Test\Api3DocTrait
;
38 use League\Csv\Reader
;
41 * Include class definitions
43 require_once 'api/api.php';
44 define('API_LATEST_VERSION', 3);
47 * Base class for CiviCRM unit tests
49 * This class supports two (mutually-exclusive) techniques for cleaning up test data. Subclasses
50 * may opt for one or neither:
52 * 1. quickCleanup() is a helper which truncates a series of tables. Call quickCleanup()
53 * as part of setUp() and/or tearDown(). quickCleanup() is thorough - but it can
54 * be cumbersome to use (b/c you must identify the tables to cleanup) and slow to execute.
55 * 2. useTransaction() executes the test inside a transaction. It's easier to use
56 * (because you don't need to identify specific tables), but it doesn't work for tests
57 * which manipulate schema or truncate data -- and could behave inconsistently
58 * for tests which specifically examine DB transactions.
60 * Common functions for unit tests
64 class CiviUnitTestCase
extends PHPUnit\Framework\TestCase
{
67 use \Civi\Test\GenericAssertionsTrait
;
68 use \Civi\Test\DbTestTrait
;
69 use \Civi\Test\ContactTestTrait
;
70 use \Civi\Test\MailingTestTrait
;
73 * Database has been initialized.
77 private static $dbInit = FALSE;
80 * Database connection.
82 * @var PHPUnit_Extensions_Database_DB_IDatabaseConnection
91 static protected $_dbName;
98 protected $_apiversion = 3;
101 * Track tables we have modified during a test.
105 protected $_tablesToTruncate = [];
109 * Array of temporary directory names
115 * populateOnce allows to skip db resets in setUp
117 * WARNING! USE WITH CAUTION - IT'LL RENDER DATA DEPENDENCIES
118 * BETWEEN TESTS WHEN RUN IN SUITE. SUITABLE FOR LOCAL, LIMITED
121 * IF POSSIBLE, USE $this->DBResetRequired = FALSE IN YOUR TEST CASE!
123 * @see http://forum.civicrm.org/index.php/topic,18065.0.html
125 public static $populateOnce = FALSE;
128 * DBResetRequired allows skipping DB reset
129 * in specific test case. If you still need
130 * to reset single test (method) of such case, call
131 * $this->cleanDB() in the first line of this
135 public $DBResetRequired = TRUE;
138 * @var CRM_Core_Transaction|null
143 * Array of IDs created to support the test.
146 * $this->ids = ['Contact' => ['descriptive_key' => $contactID], 'Group' => [$groupID]];
153 * Should financials be checked after the test but before tear down.
155 * Ideally all tests (or at least all that call any financial api calls ) should do this but there
156 * are some test data issues and some real bugs currently blocking.
160 protected $isValidateFinancialsOnPostAssert = FALSE;
163 * Should location types be checked to ensure primary addresses are correctly assigned after each test.
167 protected $isLocationTypesOnPostAssert = TRUE;
170 * Has the test class been verified as 'getsafe'.
172 * If a class is getsafe it means that where
173 * callApiSuccess is called 'return' is specified or 'return' =>'id'
174 * can be added by that function. This is part of getting away
175 * from open-ended get calls.
177 * Eventually we want to not be doing these in our test classes & start
178 * to work to not do them in our main code base. Note they mainly
179 * cause issues for activity.get and contact.get as these are where the
180 * too many joins limit is most likely to be hit.
184 protected $isGetSafe = FALSE;
187 * Class used for hooks during tests.
189 * This can be used to test hooks within tests. For example in the ACL_PermissionTrait:
191 * $this->hookClass->setHook('civicrm_aclWhereClause', [$this, 'aclWhereHookAllResults']);
193 * @var \CRM_Utils_Hook_UnitTests
199 * Common values to be re-used multiple times within a class - usually to create the relevant entity
201 protected $_params = [];
204 * @var CRM_Extension_System
206 protected $origExtensionSystem;
209 * Array of IDs created during test setup routine.
211 * The cleanUpSetUpIds method can be used to clear these at the end of the test.
215 public $setupIDs = [];
220 * Because we are overriding the parent class constructor, we
221 * need to show the same arguments as exist in the constructor of
222 * PHPUnit_Framework_TestCase, since
223 * PHPUnit_Framework_TestSuite::createTest() creates a
224 * ReflectionClass of the Test class and checks the constructor
225 * of that class to decide how to set up the test.
227 * @param string $name
229 * @param string $dataName
231 public function __construct($name = NULL, array $data = [], $dataName = '') {
232 parent
::__construct($name, $data, $dataName);
234 // we need full error reporting
235 error_reporting(E_ALL
& ~E_NOTICE
);
237 self
::$_dbName = self
::getDBName();
239 // also load the class loader
240 require_once 'CRM/Core/ClassLoader.php';
241 CRM_Core_ClassLoader
::singleton()->register();
242 if (function_exists('_civix_phpunit_setUp')) {
243 // FIXME: loosen coupling
244 _civix_phpunit_setUp();
249 * Override to run the test and assert its state.
253 * @throws \PHPUnit_Framework_IncompleteTest
254 * @throws \PHPUnit_Framework_SkippedTest
256 protected function runTest() {
258 return parent
::runTest();
260 catch (PEAR_Exception
$e) {
261 // PEAR_Exception has metadata in funny places, and PHPUnit won't log it nicely
262 throw new Exception(\CRM_Core_Error
::formatTextException($e), $e->getCode());
269 public function requireDBReset() {
270 return $this->DBResetRequired
;
276 public static function getDBName() {
277 static $dbName = NULL;
278 if ($dbName === NULL) {
279 require_once "DB.php";
280 $dsn = CRM_Utils_SQL
::autoSwitchDSN(CIVICRM_DSN
);
281 $dsninfo = DB
::parseDSN($dsn);
282 $dbName = $dsninfo['database'];
288 * Create database connection for this instance.
290 * Initialize the test database if it hasn't been initialized
293 protected function getConnection() {
294 if (!self
::$dbInit) {
295 $dbName = self
::getDBName();
297 // install test database
298 echo PHP_EOL
. "Installing {$dbName} database" . PHP_EOL
;
300 static::_populateDB(FALSE, $this);
302 self
::$dbInit = TRUE;
308 * Required implementation of abstract method.
310 protected function getDataSet() {
314 * @param bool $perClass
315 * @param null $object
318 * TRUE if the populate logic runs; FALSE if it is skipped
320 protected static function _populateDB($perClass = FALSE, &$object = NULL) {
321 if (CIVICRM_UF
!== 'UnitTests') {
322 throw new \
RuntimeException("_populateDB requires CIVICRM_UF=UnitTests");
325 if ($perClass ||
$object == NULL) {
329 $dbreset = $object->requireDBReset();
332 if (self
::$populateOnce ||
!$dbreset) {
335 self
::$populateOnce = NULL;
337 Civi\Test
::data()->populate();
342 public static function setUpBeforeClass(): void
{
343 static::_populateDB(TRUE);
345 // also set this global hack
346 $GLOBALS['_PEAR_ERRORSTACK_OVERRIDE_CALLBACK'] = [];
350 * Common setup functions for all unit tests.
352 protected function setUp(): void
{
353 $session = CRM_Core_Session
::singleton();
354 $session->set('userID', NULL);
356 $this->_apiversion
= 3;
358 // Use a temporary file for STDIN
359 $GLOBALS['stdin'] = tmpfile();
360 if ($GLOBALS['stdin'] === FALSE) {
361 echo "Couldn't open temporary file\n";
365 // Get and save a connection to the database
366 $this->_dbconn
= $this->getConnection();
368 // reload database before each test
369 // $this->_populateDB();
371 // "initialize" CiviCRM to avoid problems when running single tests
372 // FIXME: look at it closer in second stage
374 $GLOBALS['civicrm_setting']['domain']['fatalErrorHandler'] = 'CiviUnitTestCase_fatalErrorHandler';
375 $GLOBALS['civicrm_setting']['domain']['backtrace'] = 1;
377 // disable any left-over test extensions
378 CRM_Core_DAO
::executeQuery('DELETE FROM civicrm_extension WHERE full_name LIKE "test.%"');
380 // reset all the caches
381 CRM_Utils_System
::flushCache();
383 // initialize the object once db is loaded
384 \Civi
::$statics = [];
386 $config = CRM_Core_Config
::singleton(TRUE, TRUE);
388 // when running unit tests, use mockup user framework
389 $this->hookClass
= CRM_Utils_Hook
::singleton();
391 // Make sure the DB connection is setup properly
392 $config->userSystem
->setMySQLTimeZone();
393 $env = new CRM_Utils_Check_Component_Env();
394 CRM_Utils_Check
::singleton()->assertValid($env->checkMysqlTime());
396 // clear permissions stub to not check permissions
397 $config->userPermissionClass
->permissions
= NULL;
399 //flush component settings
400 CRM_Core_Component
::getEnabledComponents(TRUE);
402 $_REQUEST = $_GET = $_POST = [];
403 error_reporting(E_ALL
);
405 $this->renameLabels();
406 $this->_sethtmlGlobals();
407 $this->ensureMySQLMode(['IGNORE_SPACE', 'ERROR_FOR_DIVISION_BY_ZERO', 'STRICT_TRANS_TABLES']);
411 * Read everything from the datasets directory and insert into the db.
413 public function loadAllFixtures(): void
{
414 $fixturesDir = __DIR__
. '/../../fixtures';
416 CRM_Core_DAO
::executeQuery("SET FOREIGN_KEY_CHECKS = 0;");
418 $jsonFiles = glob($fixturesDir . '/*.json');
419 foreach ($jsonFiles as $jsonFixture) {
420 $json = json_decode(file_get_contents($jsonFixture));
421 foreach ($json as $tableName => $vars) {
422 if ($tableName === 'civicrm_contact') {
423 CRM_Core_DAO
::executeQuery('DELETE c FROM civicrm_contact c LEFT JOIN civicrm_domain d ON d.contact_id = c.id WHERE d.id IS NULL');
426 CRM_Core_DAO
::executeQuery("TRUNCATE $tableName");
428 foreach ($vars as $entity) {
429 $keys = $values = [];
430 foreach ($entity as $key => $value) {
432 $values[] = is_numeric($value) ?
$value : "'{$value}'";
434 CRM_Core_DAO
::executeQuery("
435 INSERT INTO $tableName (" . implode(',', $keys) . ') VALUES(' . implode(',', $values) . ')'
442 CRM_Core_DAO
::executeQuery("SET FOREIGN_KEY_CHECKS = 1;");
446 * Load the data that used to be handled by the discontinued dbunit class.
448 * This could do with further tidy up - the initial priority is simply to get rid of
449 * the dbunity package which is no longer supported.
451 * @param string $fileName
453 protected function loadXMLDataSet($fileName) {
454 CRM_Core_DAO
::executeQuery('SET FOREIGN_KEY_CHECKS = 0');
455 $xml = json_decode(json_encode(simplexml_load_file($fileName)), TRUE);
456 foreach ($xml as $tableName => $element) {
457 if (!empty($element)) {
458 foreach ($element as $row) {
459 $keys = $values = [];
460 if (isset($row['@attributes'])) {
461 foreach ($row['@attributes'] as $key => $value) {
463 $values[] = is_numeric($value) ?
$value : "'{$value}'";
466 elseif (!empty($row)) {
467 // cos we copied it & it is inconsistent....
468 foreach ($row as $key => $value) {
470 $values[] = is_numeric($value) ?
$value : "'{$value}'";
474 if (!empty($values)) {
475 CRM_Core_DAO
::executeQuery("
476 INSERT INTO $tableName (" . implode(',', $keys) . ') VALUES(' . implode(',', $values) . ')'
482 CRM_Core_DAO
::executeQuery('SET FOREIGN_KEY_CHECKS = 1');
486 * Create default domain contacts for the two domains added during test class.
487 * database population.
489 * @throws \CiviCRM_API3_Exception
490 * @throws \API_Exception
492 public function createDomainContacts(): void
{
493 $this->organizationCreate(['api.Email.create' => ['email' => 'fixme.domainemail@example.org']]);
494 $this->organizationCreate([
495 'organization_name' => 'Second Domain',
496 'api.Email.create' => ['email' => 'domainemail2@example.org'],
497 'api.Address.create' => [
498 'street_address' => '15 Main St',
499 'location_type_id' => 1,
500 'city' => 'Collinsville',
501 'country_id' => 1228,
502 'state_province_id' => 1003,
503 'postal_code' => 6022,
506 OptionValue
::replace(FALSE)->addWhere(
507 'option_group_id:name', '=', 'from_email_address'
510 'name' => '"FIXME" <info@EXAMPLE.ORG>',
511 'label' => '"FIXME" <info@EXAMPLE.ORG>',
512 ])->setRecords([['domain_id' => 1], ['domain_id' => 2]])->execute();
516 * Common teardown functions for all unit tests.
518 * @throws \CiviCRM_API3_Exception
519 * @throws \CRM_Core_Exception
520 * @throws \API_Exception
522 protected function tearDown(): void
{
523 $this->_apiversion
= 3;
524 $this->resetLabels();
526 error_reporting(E_ALL
& ~E_NOTICE
);
527 CRM_Utils_Hook
::singleton()->reset();
528 if ($this->hookClass
) {
529 $this->hookClass
->reset();
531 CRM_Core_Session
::singleton()->reset(1);
534 $this->tx
->rollback()->commit();
537 CRM_Core_Transaction
::forceRollbackIfEnabled();
538 \Civi\Core\Transaction\Manager
::singleton(TRUE);
541 CRM_Core_Transaction
::forceRollbackIfEnabled();
542 \Civi\Core\Transaction\Manager
::singleton(TRUE);
544 $tablesToTruncate = ['civicrm_contact', 'civicrm_uf_match', 'civicrm_email', 'civicrm_address'];
545 $this->quickCleanup($tablesToTruncate);
546 $this->createDomainContacts();
549 $this->cleanTempDirs();
550 $this->unsetExtensionSystem();
551 $this->assertEquals([], CRM_Core_DAO
::$_nullArray);
552 $this->assertEquals(NULL, CRM_Core_DAO
::$_nullObject);
553 // Ensure the destruct runs by unsetting it. Also, unsetting
554 // classes frees memory as they are not otherwise unset until the
560 * CHeck that all tests that have created payments have created them with the right financial entities.
562 * @throws \API_Exception
563 * @throws \CRM_Core_Exception
565 protected function assertPostConditions(): void
{
566 // Reset to version 3 as not all (e.g payments) work on v4
567 $this->_apiversion
= 3;
568 if ($this->isLocationTypesOnPostAssert
) {
569 $this->assertLocationValidity();
571 $this->assertCount(1, OptionGroup
::get(FALSE)
572 ->addWhere('name', '=', 'from_email_address')
574 if (!$this->isValidateFinancialsOnPostAssert
) {
577 $this->validateAllPayments();
578 $this->validateAllContributions();
582 * Create a batch of external API calls which can
583 * be executed concurrently.
586 * $calls = $this->createExternalAPI()
587 * ->addCall('Contact', 'get', ...)
588 * ->addCall('Contact', 'get', ...)
594 * @return \Civi\API\ExternalBatch
595 * @throws PHPUnit_Framework_SkippedTestError
597 public function createExternalAPI() {
598 global $civicrm_root;
600 'version' => $this->_apiversion
,
604 $calls = new \Civi\API\
ExternalBatch($defaultParams);
606 if (!$calls->isSupported()) {
607 $this->markTestSkipped('The test relies on Civi\API\ExternalBatch. This is unsupported in the local environment.');
614 * Create required data based on $this->entity & $this->params
615 * This is just a way to set up the test data for delete & get functions
616 * so the distinction between set
617 * up & tested functions is clearer
622 public function createTestEntity() {
623 return $entity = $this->callAPISuccess($this->entity
, 'create', $this->params
);
627 * @param int $contactTypeId
631 public function contactTypeDelete($contactTypeId) {
632 $result = CRM_Contact_BAO_ContactType
::del($contactTypeId);
634 throw new Exception('Could not delete contact type');
639 * @param array $params
643 public function membershipTypeCreate($params = []) {
644 CRM_Member_PseudoConstant
::flush('membershipType');
645 CRM_Core_Config
::clearDBCache();
646 $this->setupIDs
['contact'] = $memberOfOrganization = $this->organizationCreate();
647 $params = array_merge([
649 'duration_unit' => 'year',
650 'duration_interval' => 1,
651 'period_type' => 'rolling',
652 'member_of_contact_id' => $memberOfOrganization,
654 'financial_type_id' => 2,
657 'visibility' => 'Public',
660 $result = $this->callAPISuccess('MembershipType', 'Create', $params);
662 CRM_Member_PseudoConstant
::flush('membershipType');
663 CRM_Utils_Cache
::singleton()->flush();
665 return (int) $result['id'];
671 * @param array $params
674 * @throws \CRM_Core_Exception
676 public function contactMembershipCreate($params) {
677 $params = array_merge([
678 'join_date' => '2007-01-21',
679 'start_date' => '2007-01-21',
680 'end_date' => '2007-12-21',
681 'source' => 'Payment',
682 'membership_type_id' => 'General',
684 if (!is_numeric($params['membership_type_id'])) {
685 $membershipTypes = $this->callAPISuccess('Membership', 'getoptions', ['action' => 'create', 'field' => 'membership_type_id']);
686 if (!in_array($params['membership_type_id'], $membershipTypes['values'], TRUE)) {
687 $this->membershipTypeCreate(['name' => $params['membership_type_id']]);
691 $result = $this->callAPISuccess('Membership', 'create', $params);
692 return $result['id'];
696 * Delete Membership Type.
698 * @param array $params
700 public function membershipTypeDelete($params) {
701 $this->callAPISuccess('MembershipType', 'Delete', $params);
705 * @param int $membershipID
707 public function membershipDelete($membershipID) {
708 $deleteParams = ['id' => $membershipID];
709 $result = $this->callAPISuccess('Membership', 'Delete', $deleteParams);
713 * @param string $name
717 * @throws \CRM_Core_Exception
719 public function membershipStatusCreate($name = 'test member status') {
720 $params['name'] = $name;
721 $params['start_event'] = 'start_date';
722 $params['end_event'] = 'end_date';
723 $params['is_current_member'] = 1;
724 $params['is_active'] = 1;
726 $result = $this->callAPISuccess('MembershipStatus', 'Create', $params);
727 CRM_Member_PseudoConstant
::flush('membershipStatus');
728 return (int) $result['id'];
732 * Delete the given membership status, deleting any memberships of the status first.
734 * @param int $membershipStatusID
736 * @throws \CRM_Core_Exception
738 public function membershipStatusDelete(int $membershipStatusID) {
739 $this->callAPISuccess('Membership', 'get', ['status_id' => $membershipStatusID, 'api.Membership.delete' => 1]);
740 $this->callAPISuccess('MembershipStatus', 'Delete', ['id' => $membershipStatusID]);
743 public function membershipRenewalDate($durationUnit, $membershipEndDate) {
744 // We only have an end_date if frequency units match, otherwise membership won't be autorenewed and dates won't be calculated.
745 $renewedMembershipEndDate = new DateTime($membershipEndDate);
746 switch ($durationUnit) {
748 $renewedMembershipEndDate->add(new DateInterval('P1Y'));
752 // We have to add 1 day first in case it's the end of the month, then subtract afterwards
753 // eg. 2018-02-28 should renew to 2018-03-31, if we just added 1 month we'd get 2018-03-28
754 $renewedMembershipEndDate->add(new DateInterval('P1D'));
755 $renewedMembershipEndDate->add(new DateInterval('P1M'));
756 $renewedMembershipEndDate->sub(new DateInterval('P1D'));
759 return $renewedMembershipEndDate->format('Y-m-d');
763 * Create a relationship type.
765 * @param array $params
769 * @throws \CRM_Core_Exception
771 public function relationshipTypeCreate($params = []) {
772 $params = array_merge([
773 'name_a_b' => 'Relation 1 for relationship type create',
774 'name_b_a' => 'Relation 2 for relationship type create',
775 'contact_type_a' => 'Individual',
776 'contact_type_b' => 'Organization',
781 $result = $this->callAPISuccess('relationship_type', 'create', $params);
782 CRM_Core_PseudoConstant
::flush('relationshipType');
784 return $result['id'];
788 * Delete Relatinship Type.
790 * @param int $relationshipTypeID
792 public function relationshipTypeDelete($relationshipTypeID) {
793 $params['id'] = $relationshipTypeID;
794 $check = $this->callAPISuccess('relationship_type', 'get', $params);
795 if (!empty($check['count'])) {
796 $this->callAPISuccess('relationship_type', 'delete', $params);
801 * @param array $params
804 * @throws \CRM_Core_Exception
806 public function paymentProcessorTypeCreate($params = []) {
807 $params = array_merge([
808 'name' => 'API_Test_PP',
809 'title' => 'API Test Payment Processor',
810 'class_name' => 'CRM_Core_Payment_APITest',
811 'billing_mode' => 'form',
816 $result = $this->callAPISuccess('PaymentProcessorType', 'create', $params);
818 CRM_Core_PseudoConstant
::flush('paymentProcessorType');
820 return $result['id'];
824 * Create test Authorize.net instance.
826 * @param array $params
829 * @throws \CRM_Core_Exception
831 public function paymentProcessorAuthorizeNetCreate($params = []) {
832 $params = array_merge([
833 'name' => 'Authorize',
834 'domain_id' => CRM_Core_Config
::domainID(),
835 'payment_processor_type_id' => 'AuthNet',
836 'title' => 'AuthNet',
841 'user_name' => '4y5BfuW7jm',
842 'password' => '4cAmW927n8uLf5J8',
843 'url_site' => 'https://test.authorize.net/gateway/transact.dll',
844 'url_recur' => 'https://apitest.authorize.net/xml/v1/request.api',
845 'class_name' => 'Payment_AuthorizeNet',
849 $result = $this->callAPISuccess('PaymentProcessor', 'create', $params);
850 return (int) $result['id'];
854 * Create Participant.
856 * @param array $params
857 * Array of contact id and event id values.
860 * $id of participant created
862 public function participantCreate($params = []) {
863 if (empty($params['contact_id'])) {
864 $params['contact_id'] = $this->individualCreate();
866 if (empty($params['event_id'])) {
867 $event = $this->eventCreate();
868 $params['event_id'] = $event['id'];
873 'register_date' => 20070219,
874 'source' => 'Wimbeldon',
875 'event_level' => 'Payment',
879 $params = array_merge($defaults, $params);
880 $result = $this->callAPISuccess('Participant', 'create', $params);
881 return $result['id'];
885 * Create Payment Processor.
888 * Id Payment Processor
890 public function processorCreate($params = []) {
894 'payment_processor_type_id' => 'Dummy',
895 'financial_account_id' => 12,
899 'url_site' => 'http://dummy.com',
900 'url_recur' => 'http://dummy.com',
903 'payment_instrument_id' => 'Debit Card',
905 $processorParams = array_merge($processorParams, $params);
906 $processor = $this->callAPISuccess('PaymentProcessor', 'create', $processorParams);
907 return $processor['id'];
911 * Create Payment Processor.
913 * @param array $processorParams
915 * @return \CRM_Core_Payment_Dummy
916 * Instance of Dummy Payment Processor
918 * @throws \CiviCRM_API3_Exception
920 public function dummyProcessorCreate($processorParams = []) {
921 $paymentProcessorID = $this->processorCreate($processorParams);
922 // For the tests we don't need a live processor, but as core ALWAYS creates a processor in live mode and one in test mode we do need to create both
923 // Otherwise we are testing a scenario that only exists in tests (and some tests fail because the live processor has not been defined).
924 $processorParams['is_test'] = FALSE;
925 $this->processorCreate($processorParams);
926 return System
::singleton()->getById($paymentProcessorID);
930 * Create contribution page.
932 * @param array $params
935 * Array of contribution page
937 public function contributionPageCreate($params = []) {
938 $this->_pageParams
= array_merge([
939 'title' => 'Test Contribution Page',
940 'financial_type_id' => 1,
942 'financial_account_id' => 1,
944 'is_allow_other_amount' => 1,
946 'max_amount' => 1000,
948 return $this->callAPISuccess('contribution_page', 'create', $this->_pageParams
);
952 * Create a sample batch.
954 public function batchCreate() {
955 $params = $this->_params
;
956 $params['name'] = $params['title'] = 'Batch_433397';
957 $params['status_id'] = 1;
958 $result = $this->callAPISuccess('batch', 'create', $params);
959 return $result['id'];
965 * @param array $params
968 * result of created tag
970 public function tagCreate($params = []) {
972 'name' => 'New Tag3',
973 'description' => 'This is description for Our New Tag ',
976 $params = array_merge($defaults, $params);
977 $result = $this->callAPISuccess('Tag', 'create', $params);
978 return $result['values'][$result['id']];
985 * Id of the tag to be deleted.
989 public function tagDelete($tagId) {
990 require_once 'api/api.php';
994 $result = $this->callAPISuccess('Tag', 'delete', $params);
995 return $result['id'];
999 * Add entity(s) to the tag
1001 * @param array $params
1005 public function entityTagAdd($params) {
1006 $result = $this->callAPISuccess('entity_tag', 'create', $params);
1013 * @param array $params
1017 * id of created pledge
1019 * @throws \CRM_Core_Exception
1021 public function pledgeCreate($params) {
1022 $params = array_merge([
1023 'pledge_create_date' => date('Ymd'),
1024 'start_date' => date('Ymd'),
1025 'scheduled_date' => date('Ymd'),
1027 'pledge_status_id' => '2',
1028 'financial_type_id' => '1',
1029 'pledge_original_installment_amount' => 20,
1030 'frequency_interval' => 5,
1031 'frequency_unit' => 'year',
1032 'frequency_day' => 15,
1033 'installments' => 5,
1037 $result = $this->callAPISuccess('Pledge', 'create', $params);
1038 return $result['id'];
1042 * Delete contribution.
1044 * @param int $pledgeId
1046 * @throws \CRM_Core_Exception
1048 public function pledgeDelete($pledgeId) {
1050 'pledge_id' => $pledgeId,
1052 $this->callAPISuccess('Pledge', 'delete', $params);
1056 * Create contribution.
1058 * @param array $params
1059 * Array of parameters.
1062 * id of created contribution
1064 public function contributionCreate(array $params): int {
1065 $params = array_merge([
1067 'receive_date' => date('Ymd'),
1068 'total_amount' => 100.00,
1069 'fee_amount' => 5.00,
1070 'financial_type_id' => 1,
1071 'payment_instrument_id' => 1,
1072 'non_deductible_amount' => 10.00,
1074 'contribution_status_id' => 1,
1077 $result = $this->callAPISuccess('contribution', 'create', $params);
1078 return $result['id'];
1082 * Delete contribution.
1084 * @param int $contributionId
1087 * @throws \CRM_Core_Exception
1089 public function contributionDelete($contributionId) {
1091 'contribution_id' => $contributionId,
1093 $result = $this->callAPISuccess('contribution', 'delete', $params);
1100 * @param array $params
1101 * Name-value pair for an event.
1104 * @throws \CRM_Core_Exception
1106 public function eventCreate($params = []) {
1107 // if no contact was passed, make up a dummy event creator
1108 if (!isset($params['contact_id'])) {
1109 $params['contact_id'] = $this->_contactCreate([
1110 'contact_type' => 'Individual',
1111 'first_name' => 'Event',
1112 'last_name' => 'Creator',
1116 // set defaults for missing params
1117 $params = array_merge([
1118 'title' => 'Annual CiviCRM meet',
1119 'summary' => 'If you have any CiviCRM related issues or want to track where CiviCRM is heading, Sign up now',
1120 'description' => 'This event is intended to give brief idea about progess of CiviCRM and giving solutions to common user issues',
1121 'event_type_id' => 1,
1123 'start_date' => 20081021,
1124 'end_date' => 20081023,
1125 'is_online_registration' => 1,
1126 'registration_start_date' => 20080601,
1127 'registration_end_date' => 20081015,
1128 'max_participants' => 100,
1129 'event_full_text' => 'Sorry! We are already full',
1132 'is_show_location' => 0,
1133 'is_email_confirm' => 1,
1136 return $this->callAPISuccess('Event', 'create', $params);
1140 * Create a paid event.
1142 * @param array $params
1144 * @param array $options
1146 * @param string $key
1147 * Index for storing event ID in ids array.
1151 * @throws \CRM_Core_Exception
1153 protected function eventCreatePaid($params, $options = [['name' => 'hundy', 'amount' => 100]], $key = 'event') {
1154 $params['is_monetary'] = TRUE;
1155 $event = $this->eventCreate($params);
1156 $this->ids
['Event'][$key] = (int) $event['id'];
1157 $this->ids
['PriceSet'][$key] = $this->eventPriceSetCreate(55, 0, 'Radio', $options);
1158 CRM_Price_BAO_PriceSet
::addTo('civicrm_event', $event['id'], $this->ids
['PriceSet'][$key]);
1159 $priceSet = CRM_Price_BAO_PriceSet
::getSetDetail($this->ids
['PriceSet'][$key], TRUE, FALSE);
1160 $priceSet = $priceSet[$this->ids
['PriceSet'][$key]] ??
NULL;
1161 $this->eventFeeBlock
= $priceSet['fields'] ??
NULL;
1173 public function eventDelete($id) {
1177 return $this->callAPISuccess('event', 'delete', $params);
1181 * Delete participant.
1183 * @param int $participantID
1187 public function participantDelete($participantID) {
1189 'id' => $participantID,
1191 $check = $this->callAPISuccess('Participant', 'get', $params);
1192 if ($check['count'] > 0) {
1193 return $this->callAPISuccess('Participant', 'delete', $params);
1198 * Create participant payment.
1200 * @param int $participantID
1201 * @param int $contributionID
1204 * $id of created payment
1206 public function participantPaymentCreate($participantID, $contributionID = NULL) {
1207 //Create Participant Payment record With Values
1209 'participant_id' => $participantID,
1210 'contribution_id' => $contributionID,
1213 $result = $this->callAPISuccess('participant_payment', 'create', $params);
1214 return $result['id'];
1218 * Delete participant payment.
1220 * @param int $paymentID
1222 public function participantPaymentDelete($paymentID) {
1226 $result = $this->callAPISuccess('participant_payment', 'delete', $params);
1232 * @param int $contactID
1235 * location id of created location
1237 public function locationAdd($contactID) {
1240 'location_type' => 'New Location Type',
1242 'name' => 'Saint Helier St',
1243 'county' => 'Marin',
1244 'country' => 'UNITED STATES',
1245 'state_province' => 'Michigan',
1246 'supplemental_address_1' => 'Hallmark Ct',
1247 'supplemental_address_2' => 'Jersey Village',
1248 'supplemental_address_3' => 'My Town',
1253 'contact_id' => $contactID,
1254 'address' => $address,
1255 'location_format' => '2.0',
1256 'location_type' => 'New Location Type',
1259 $result = $this->callAPISuccess('Location', 'create', $params);
1264 * Delete Locations of contact.
1266 * @param array $params
1269 public function locationDelete($params) {
1270 $this->callAPISuccess('Location', 'delete', $params);
1274 * Add a Location Type.
1276 * @param array $params
1278 * @return CRM_Core_DAO_LocationType
1279 * location id of created location
1281 public function locationTypeCreate($params = NULL) {
1282 if ($params === NULL) {
1284 'name' => 'New Location Type',
1285 'vcard_name' => 'New Location Type',
1286 'description' => 'Location Type for Delete',
1291 $locationType = new CRM_Core_DAO_LocationType();
1292 $locationType->copyValues($params);
1293 $locationType->save();
1294 // clear getfields cache
1295 CRM_Core_PseudoConstant
::flush();
1296 $this->callAPISuccess('phone', 'getfields', ['version' => 3, 'cache_clear' => 1]);
1297 return $locationType->id
;
1301 * Delete a Location Type.
1303 * @param int $locationTypeId
1305 public function locationTypeDelete($locationTypeId) {
1306 $locationType = new CRM_Core_DAO_LocationType();
1307 $locationType->id
= $locationTypeId;
1308 $locationType->delete();
1314 * @param array $params
1316 * @return CRM_Core_DAO_Mapping
1317 * Mapping id of created mapping
1319 public function mappingCreate($params = NULL) {
1320 if ($params === NULL) {
1322 'name' => 'Mapping name',
1323 'description' => 'Mapping description',
1324 // 'Export Contact' mapping.
1325 'mapping_type_id' => 7,
1329 $mapping = new CRM_Core_DAO_Mapping();
1330 $mapping->copyValues($params);
1332 // clear getfields cache
1333 CRM_Core_PseudoConstant
::flush();
1334 $this->callAPISuccess('mapping', 'getfields', ['version' => 3, 'cache_clear' => 1]);
1341 * @param int $mappingId
1343 public function mappingDelete($mappingId) {
1344 $mapping = new CRM_Core_DAO_Mapping();
1345 $mapping->id
= $mappingId;
1350 * Prepare class for ACLs.
1352 protected function prepareForACLs() {
1353 $config = CRM_Core_Config
::singleton();
1354 $config->userPermissionClass
->permissions
= [];
1360 protected function cleanUpAfterACLs() {
1361 CRM_Utils_Hook
::singleton()->reset();
1362 $tablesToTruncate = [
1364 'civicrm_acl_cache',
1365 'civicrm_acl_entity_role',
1366 'civicrm_acl_contact_cache',
1368 $this->quickCleanup($tablesToTruncate);
1369 $config = CRM_Core_Config
::singleton();
1370 unset($config->userPermissionClass
->permissions
);
1374 * Create a smart group.
1376 * By default it will be a group of households.
1378 * @param array $smartGroupParams
1379 * @param array $groupParams
1380 * @param string $contactType
1384 public function smartGroupCreate($smartGroupParams = [], $groupParams = [], $contactType = 'Household') {
1385 $smartGroupParams = array_merge(['form_values' => ['contact_type' => ['IN' => [$contactType]]]], $smartGroupParams);
1386 $savedSearch = CRM_Contact_BAO_SavedSearch
::create($smartGroupParams);
1388 $groupParams['saved_search_id'] = $savedSearch->id
;
1389 return $this->groupCreate($groupParams);
1395 * @param array $params
1397 public function uFFieldCreate($params = []) {
1398 $params = array_merge([
1400 'field_name' => 'first_name',
1403 'visibility' => 'Public Pages and Listings',
1404 'is_searchable' => '1',
1405 'label' => 'first_name',
1406 'field_type' => 'Individual',
1409 $this->callAPISuccess('uf_field', 'create', $params);
1413 * Add a UF Join Entry.
1415 * @param array $params
1418 * $id of created UF Join
1420 public function ufjoinCreate($params = NULL) {
1421 if ($params === NULL) {
1424 'module' => 'CiviEvent',
1425 'entity_table' => 'civicrm_event',
1431 $result = $this->callAPISuccess('uf_join', 'create', $params);
1436 * @param array $params
1437 * Optional parameters.
1438 * @param bool $reloadConfig
1439 * While enabling CiviCampaign component, we shouldn't always forcibly
1440 * reload config as this hinder hook call in test environment
1445 public function campaignCreate($params = [], $reloadConfig = TRUE) {
1446 $this->enableCiviCampaign($reloadConfig);
1447 $campaign = $this->callAPISuccess('campaign', 'create', array_merge([
1448 'name' => 'big_campaign',
1449 'title' => 'Campaign',
1451 return $campaign['id'];
1455 * Create Group for a contact.
1457 * @param int $contactId
1459 public function contactGroupCreate($contactId) {
1461 'contact_id.1' => $contactId,
1465 $this->callAPISuccess('GroupContact', 'Create', $params);
1469 * Delete Group for a contact.
1471 * @param int $contactId
1473 public function contactGroupDelete($contactId) {
1475 'contact_id.1' => $contactId,
1478 $this->civicrm_api('GroupContact', 'Delete', $params);
1484 * @param array $params
1488 * @throws \CRM_Core_Exception
1489 * @throws \CiviCRM_API3_Exception
1491 public function activityCreate($params = []) {
1492 $params = array_merge([
1493 'subject' => 'Discussion on warm beer',
1494 'activity_date_time' => date('Ymd'),
1496 'location' => 'Baker Street',
1497 'details' => 'Lets schedule a meeting',
1499 'activity_type_id' => 'Meeting',
1501 if (!isset($params['source_contact_id'])) {
1502 $params['source_contact_id'] = $this->individualCreate();
1504 if (!isset($params['target_contact_id'])) {
1505 $params['target_contact_id'] = $this->individualCreate([
1506 'first_name' => 'Julia',
1507 'last_name' => 'Anderson',
1509 'email' => 'julia_anderson@civicrm.org',
1510 'contact_type' => 'Individual',
1513 if (!isset($params['assignee_contact_id'])) {
1514 $params['assignee_contact_id'] = $params['target_contact_id'];
1517 $result = civicrm_api3('Activity', 'create', $params);
1519 $result['target_contact_id'] = $params['target_contact_id'];
1520 $result['assignee_contact_id'] = $params['assignee_contact_id'];
1525 * Create an activity type.
1527 * @param array $params
1532 public function activityTypeCreate($params) {
1533 return $this->callAPISuccess('ActivityType', 'create', $params);
1537 * Delete activity type.
1539 * @param int $activityTypeId
1540 * Id of the activity type.
1544 public function activityTypeDelete($activityTypeId) {
1545 $params['activity_type_id'] = $activityTypeId;
1546 return $this->callAPISuccess('ActivityType', 'delete', $params);
1550 * Create custom group.
1552 * @param array $params
1556 public function customGroupCreate($params = []) {
1558 'title' => 'new custom group',
1559 'extends' => 'Contact',
1561 'style' => 'Inline',
1565 $params = array_merge($defaults, $params);
1567 return $this->callAPISuccess('custom_group', 'create', $params);
1571 * Existing function doesn't allow params to be over-ridden so need a new one
1572 * this one allows you to only pass in the params you want to change
1574 * @param array $params
1578 public function CustomGroupCreateByParams($params = []) {
1580 'title' => "API Custom Group",
1581 'extends' => 'Contact',
1583 'style' => 'Inline',
1586 $params = array_merge($defaults, $params);
1587 return $this->callAPISuccess('custom_group', 'create', $params);
1591 * Create custom group with multi fields.
1593 * @param array $params
1597 public function CustomGroupMultipleCreateByParams($params = []) {
1602 $params = array_merge($defaults, $params);
1603 return $this->CustomGroupCreateByParams($params);
1607 * Create custom group with multi fields.
1609 * @param array $params
1613 public function CustomGroupMultipleCreateWithFields($params = []) {
1614 // also need to pass on $params['custom_field'] if not set but not in place yet
1616 $customGroup = $this->CustomGroupMultipleCreateByParams($params);
1617 $ids['custom_group_id'] = $customGroup['id'];
1619 $customField = $this->customFieldCreate([
1620 'custom_group_id' => $ids['custom_group_id'],
1621 'label' => 'field_1' . $ids['custom_group_id'],
1625 $ids['custom_field_id'][] = $customField['id'];
1627 $customField = $this->customFieldCreate([
1628 'custom_group_id' => $ids['custom_group_id'],
1629 'default_value' => '',
1630 'label' => 'field_2' . $ids['custom_group_id'],
1633 $ids['custom_field_id'][] = $customField['id'];
1635 $customField = $this->customFieldCreate([
1636 'custom_group_id' => $ids['custom_group_id'],
1637 'default_value' => '',
1638 'label' => 'field_3' . $ids['custom_group_id'],
1641 $ids['custom_field_id'][] = $customField['id'];
1647 * Create a custom group with a single text custom field. See
1648 * participant:testCreateWithCustom for how to use this
1650 * @param string $function
1652 * @param string $filename
1656 * ids of created objects
1658 public function entityCustomGroupWithSingleFieldCreate($function, $filename) {
1659 $params = ['title' => $function];
1660 $entity = substr(basename($filename), 0, strlen(basename($filename)) - 8);
1661 $params['extends'] = $entity ?
$entity : 'Contact';
1662 $customGroup = $this->customGroupCreate($params);
1663 $customField = $this->customFieldCreate(['custom_group_id' => $customGroup['id'], 'label' => $function]);
1664 CRM_Core_PseudoConstant
::flush();
1666 return ['custom_group_id' => $customGroup['id'], 'custom_field_id' => $customField['id']];
1670 * Create a custom group with a single text custom field, multi-select widget, with a variety of option values including upper and lower case.
1671 * See api_v3_SyntaxConformanceTest:testCustomDataGet for how to use this
1673 * @param string $function
1675 * @param string $filename
1679 * ids of created objects
1681 public function entityCustomGroupWithSingleStringMultiSelectFieldCreate($function, $filename) {
1682 $params = ['title' => $function];
1683 $entity = substr(basename($filename), 0, strlen(basename($filename)) - 8);
1684 $params['extends'] = $entity ?
$entity : 'Contact';
1685 $customGroup = $this->customGroupCreate($params);
1686 $customField = $this->customFieldCreate(['custom_group_id' => $customGroup['id'], 'label' => $function, 'html_type' => 'Multi-Select', 'default_value' => 1]);
1687 CRM_Core_PseudoConstant
::flush();
1689 'defaultValue' => 'Default Value',
1690 'lowercasevalue' => 'Lowercase Value',
1691 1 => 'Integer Value',
1694 $custom_field_params = ['sequential' => 1, 'id' => $customField['id']];
1695 $custom_field_api_result = $this->callAPISuccess('custom_field', 'get', $custom_field_params);
1696 $this->assertNotEmpty($custom_field_api_result['values'][0]['option_group_id']);
1697 $option_group_params = ['sequential' => 1, 'id' => $custom_field_api_result['values'][0]['option_group_id']];
1698 $option_group_result = $this->callAPISuccess('OptionGroup', 'get', $option_group_params);
1699 $this->assertNotEmpty($option_group_result['values'][0]['name']);
1700 foreach ($options as $option_value => $option_label) {
1701 $option_group_params = ['option_group_id' => $option_group_result['values'][0]['name'], 'value' => $option_value, 'label' => $option_label];
1702 $option_value_result = $this->callAPISuccess('OptionValue', 'create', $option_group_params);
1706 'custom_group_id' => $customGroup['id'],
1707 'custom_field_id' => $customField['id'],
1708 'custom_field_option_group_id' => $custom_field_api_result['values'][0]['option_group_id'],
1709 'custom_field_group_options' => $options,
1714 * Delete custom group.
1716 * @param int $customGroupID
1720 public function customGroupDelete($customGroupID) {
1721 $params['id'] = $customGroupID;
1722 return $this->callAPISuccess('custom_group', 'delete', $params);
1726 * Create custom field.
1728 * @param array $params
1729 * (custom_group_id) is required.
1733 public function customFieldCreate($params) {
1734 $params = array_merge([
1735 'label' => 'Custom Field',
1736 'data_type' => 'String',
1737 'html_type' => 'Text',
1738 'is_searchable' => 1,
1740 'default_value' => 'defaultValue',
1743 $result = $this->callAPISuccess('custom_field', 'create', $params);
1744 // these 2 functions are called with force to flush static caches
1745 CRM_Core_BAO_CustomField
::getTableColumnGroup($result['id'], 1);
1746 CRM_Core_Component
::getEnabledComponents(1);
1751 * Delete custom field.
1753 * @param int $customFieldID
1757 public function customFieldDelete($customFieldID) {
1759 $params['id'] = $customFieldID;
1760 return $this->callAPISuccess('custom_field', 'delete', $params);
1770 public function noteCreate($cId) {
1772 'entity_table' => 'civicrm_contact',
1773 'entity_id' => $cId,
1774 'note' => 'hello I am testing Note',
1775 'contact_id' => $cId,
1776 'modified_date' => date('Ymd'),
1777 'subject' => 'Test Note',
1780 return $this->callAPISuccess('Note', 'create', $params);
1784 * Enable CiviCampaign Component.
1786 * @param bool $reloadConfig
1787 * Force relaod config or not
1789 public function enableCiviCampaign($reloadConfig = TRUE) {
1790 CRM_Core_BAO_ConfigSetting
::enableComponent('CiviCampaign');
1791 if ($reloadConfig) {
1792 // force reload of config object
1793 $config = CRM_Core_Config
::singleton(TRUE, TRUE);
1795 //flush cache by calling with reset
1796 $activityTypes = CRM_Core_PseudoConstant
::activityType(TRUE, TRUE, TRUE, 'name', TRUE);
1800 * Create custom field with Option Values.
1802 * @param array $customGroup
1803 * @param string $name
1804 * Name of custom field.
1805 * @param array $extraParams
1806 * Additional parameters to pass through.
1810 public function customFieldOptionValueCreate($customGroup, $name, $extraParams = []) {
1812 'custom_group_id' => $customGroup['id'],
1813 'name' => 'test_custom_group',
1814 'label' => 'Country',
1815 'html_type' => 'Select',
1816 'data_type' => 'String',
1819 'is_searchable' => 0,
1825 'name' => 'option_group1',
1826 'label' => 'option_group_label1',
1830 'option_label' => ['Label1', 'Label2'],
1831 'option_value' => ['value1', 'value2'],
1832 'option_name' => [$name . '_1', $name . '_2'],
1833 'option_weight' => [1, 2],
1834 'option_status' => [1, 1],
1837 $params = array_merge($fieldParams, $optionGroup, $optionValue, $extraParams);
1839 return $this->callAPISuccess('custom_field', 'create', $params);
1847 public function confirmEntitiesDeleted($entities) {
1848 foreach ($entities as $entity) {
1850 $result = $this->callAPISuccess($entity, 'Get', []);
1851 if ($result['error'] == 1 ||
$result['count'] > 0) {
1852 // > than $entity[0] to allow a value to be passed in? e.g. domain?
1860 * Quick clean by emptying tables created for the test.
1862 * @param array $tablesToTruncate
1863 * @param bool $dropCustomValueTables
1865 * @throws \CRM_Core_Exception
1866 * @throws \API_Exception
1868 public function quickCleanup($tablesToTruncate, $dropCustomValueTables = FALSE) {
1870 throw new \
CRM_Core_Exception("CiviUnitTestCase: quickCleanup() is not compatible with useTransaction()");
1872 if ($dropCustomValueTables) {
1874 CustomField
::get(FALSE)->setSelect(['option_group_id', 'custom_group_id'])
1875 ->addChain('delete_options', OptionGroup
::delete()
1876 ->addWhere('id', '=', '$option_group_id')
1878 ->addChain('delete_fields', CustomField
::delete()
1879 ->addWhere('id', '=', '$id')
1882 CustomGroup
::delete(FALSE)->addWhere('id', '>', 0)->execute();
1883 // Reset autoincrement too.
1884 $tablesToTruncate[] = 'civicrm_custom_group';
1885 $tablesToTruncate[] = 'civicrm_custom_field';
1888 $tablesToTruncate = array_unique(array_merge($this->_tablesToTruncate
, $tablesToTruncate));
1890 CRM_Core_DAO
::executeQuery("SET FOREIGN_KEY_CHECKS = 0;");
1891 foreach ($tablesToTruncate as $table) {
1892 $sql = "TRUNCATE TABLE $table";
1893 CRM_Core_DAO
::executeQuery($sql);
1895 CRM_Core_DAO
::executeQuery("SET FOREIGN_KEY_CHECKS = 1;");
1899 * Clean up financial entities after financial tests (so we remember to get all the tables :-))
1901 * @throws \CRM_Core_Exception
1903 public function quickCleanUpFinancialEntities() {
1904 $tablesToTruncate = [
1906 'civicrm_activity_contact',
1907 'civicrm_contribution',
1908 'civicrm_contribution_soft',
1909 'civicrm_contribution_product',
1910 'civicrm_financial_trxn',
1911 'civicrm_financial_item',
1912 'civicrm_contribution_recur',
1913 'civicrm_line_item',
1914 'civicrm_contribution_page',
1915 'civicrm_payment_processor',
1916 'civicrm_entity_financial_trxn',
1917 'civicrm_membership',
1918 'civicrm_membership_type',
1919 'civicrm_membership_payment',
1920 'civicrm_membership_log',
1921 'civicrm_membership_block',
1923 'civicrm_participant',
1924 'civicrm_participant_payment',
1926 'civicrm_pcp_block',
1928 'civicrm_pledge_block',
1929 'civicrm_pledge_payment',
1930 'civicrm_price_set_entity',
1931 'civicrm_price_field_value',
1932 'civicrm_price_field',
1934 $this->quickCleanup($tablesToTruncate);
1935 CRM_Core_DAO
::executeQuery("DELETE FROM civicrm_membership_status WHERE name NOT IN('New', 'Current', 'Grace', 'Expired', 'Pending', 'Cancelled', 'Deceased')");
1936 $this->restoreDefaultPriceSetConfig();
1937 $this->disableTaxAndInvoicing();
1938 $this->setCurrencySeparators(',');
1939 CRM_Core_PseudoConstant
::flush('taxRates');
1940 System
::singleton()->flushProcessors();
1941 // @fixme this parameter is leaking - it should not be defined as a class static
1942 // but for now we just handle in tear down.
1943 CRM_Contribute_BAO_Query
::$_contribOrSoftCredit = 'only contribs';
1947 * Reset the price set config so results exist.
1949 public function restoreDefaultPriceSetConfig() {
1950 CRM_Core_DAO
::executeQuery("DELETE FROM civicrm_price_set WHERE name NOT IN('default_contribution_amount', 'default_membership_type_amount')");
1951 CRM_Core_DAO
::executeQuery("UPDATE civicrm_price_set SET id = 1 WHERE name ='default_contribution_amount'");
1952 CRM_Core_DAO
::executeQuery("INSERT INTO `civicrm_price_field` (`id`, `price_set_id`, `name`, `label`, `html_type`, `is_enter_qty`, `help_pre`, `help_post`, `weight`, `is_display_amounts`, `options_per_line`, `is_active`, `is_required`, `active_on`, `expire_on`, `javascript`, `visibility_id`) VALUES (1, 1, 'contribution_amount', 'Contribution Amount', 'Text', 0, NULL, NULL, 1, 1, 1, 1, 1, NULL, NULL, NULL, 1)");
1953 CRM_Core_DAO
::executeQuery("INSERT INTO `civicrm_price_field_value` (`id`, `price_field_id`, `name`, `label`, `description`, `amount`, `count`, `max_value`, `weight`, `membership_type_id`, `membership_num_terms`, `is_default`, `is_active`, `financial_type_id`, `non_deductible_amount`) VALUES (1, 1, 'contribution_amount', 'Contribution Amount', NULL, '1', NULL, NULL, 1, NULL, NULL, 0, 1, 1, 0.00)");
1957 * Recreate default membership types.
1959 public function restoreMembershipTypes() {
1960 CRM_Core_DAO
::executeQuery(
1961 "REPLACE INTO civicrm_membership_type
1962 (id, domain_id, name, description, member_of_contact_id, financial_type_id, minimum_fee, duration_unit, duration_interval, period_type, fixed_period_start_day, fixed_period_rollover_day, relationship_type_id, relationship_direction, visibility, weight, is_active)
1964 (1, 1, 'General', 'Regular annual membership.', 1, 2, 100.00, 'year', 2, 'rolling', NULL, NULL, 7, 'b_a', 'Public', 1, 1),
1965 (2, 1, 'Student', 'Discount membership for full-time students.', 1, 2, 50.00, 'year', 1, 'rolling', NULL, NULL, NULL, NULL, 'Public', 2, 1),
1966 (3, 1, 'Lifetime', 'Lifetime membership.', 1, 2, 1200.00, 'lifetime', 1, 'rolling', NULL, NULL, 7, 'b_a', 'Admin', 3, 1);
1971 * Function does a 'Get' on the entity & compares the fields in the Params with those returned
1972 * Default behaviour is to also delete the entity
1973 * @param array $params
1974 * Params array to check against.
1976 * Id of the entity concerned.
1977 * @param string $entity
1978 * Name of entity concerned (e.g. membership).
1979 * @param bool $delete
1980 * Should the entity be deleted as part of this check.
1981 * @param string $errorText
1982 * Text to print on error.
1986 * @param array $params
1989 * @param int $delete
1990 * @param string $errorText
1992 * @throws CRM_Core_Exception
1994 public function getAndCheck(array $params, int $id, $entity, int $delete = 1, string $errorText = ''): void
{
1996 $result = $this->callAPISuccessGetSingle($entity, [
1998 'return' => array_keys($params),
2002 $this->callAPISuccess($entity, 'Delete', [
2006 $dateFields = $keys = $dateTimeFields = [];
2007 $fields = $this->callAPISuccess($entity, 'getfields', ['version' => 3, 'action' => 'get']);
2008 foreach ($fields['values'] as $field => $settings) {
2009 if (array_key_exists($field, $result)) {
2010 $keys[CRM_Utils_Array
::value('name', $settings, $field)] = $field;
2013 $keys[CRM_Utils_Array
::value('name', $settings, $field)] = CRM_Utils_Array
::value('name', $settings, $field);
2015 $type = $settings['type'] ??
NULL;
2016 if ($type === CRM_Utils_Type
::T_DATE
) {
2017 $dateFields[] = $settings['name'];
2018 // we should identify both real names & unique names as dates
2019 if ($field !== $settings['name']) {
2020 $dateFields[] = $field;
2023 if ($type === CRM_Utils_Type
::T_DATE + CRM_Utils_Type
::T_TIME
) {
2024 $dateTimeFields[] = $settings['name'];
2025 // we should identify both real names & unique names as dates
2026 if ($field !== $settings['name']) {
2027 $dateTimeFields[] = $field;
2032 if (strtolower($entity) === 'contribution') {
2033 $params['receive_date'] = date('Y-m-d', strtotime($params['receive_date']));
2034 // this is not returned in id format
2035 unset($params['payment_instrument_id']);
2036 $params['contribution_source'] = $params['source'];
2037 unset($params['source']);
2040 foreach ($params as $key => $value) {
2041 if ($key === 'version' ||
strpos($key, 'api') === 0 ||
(!array_key_exists($key, $keys) ||
!array_key_exists($keys[$key], $result))) {
2044 if (in_array($key, $dateFields, TRUE)) {
2045 $value = date('Y-m-d', strtotime($value));
2046 $result[$key] = date('Y-m-d', strtotime($result[$key]));
2048 if (in_array($key, $dateTimeFields, TRUE)) {
2049 $value = date('Y-m-d H:i:s', strtotime($value));
2050 $result[$keys[$key]] = date('Y-m-d H:i:s', strtotime(CRM_Utils_Array
::value($keys[$key], $result, CRM_Utils_Array
::value($key, $result))));
2052 $this->assertEquals($value, $result[$keys[$key]], $key . " GetandCheck function determines that for key {$key} value: $value doesn't match " . print_r($result[$keys[$key]], TRUE) . $errorText);
2057 * Get formatted values in the actual and expected result.
2059 * @param array $actual
2060 * Actual calculated values.
2061 * @param array $expected
2064 public function checkArrayEquals(&$actual, &$expected) {
2065 self
::unsetId($actual);
2066 self
::unsetId($expected);
2067 $this->assertEquals($expected, $actual);
2071 * Unset the key 'id' from the array
2073 * @param array $unformattedArray
2074 * The array from which the 'id' has to be unset.
2076 public static function unsetId(&$unformattedArray) {
2077 $formattedArray = [];
2078 if (array_key_exists('id', $unformattedArray)) {
2079 unset($unformattedArray['id']);
2081 if (!empty($unformattedArray['values']) && is_array($unformattedArray['values'])) {
2082 foreach ($unformattedArray['values'] as $key => $value) {
2083 if (is_array($value)) {
2084 foreach ($value as $k => $v) {
2090 elseif ($key == 'id') {
2091 $unformattedArray[$key];
2093 $formattedArray = [$value];
2095 $unformattedArray['values'] = $formattedArray;
2100 * Helper to enable/disable custom directory support
2102 * @param array $customDirs
2104 * 'php_path' Set to TRUE to use the default, FALSE or "" to disable support, or a string path to use another path
2105 * 'template_path' Set to TRUE to use the default, FALSE or "" to disable support, or a string path to use another path
2107 public function customDirectories($customDirs) {
2108 $config = CRM_Core_Config
::singleton();
2110 if (empty($customDirs['php_path']) ||
$customDirs['php_path'] === FALSE) {
2111 unset($config->customPHPPathDir
);
2113 elseif ($customDirs['php_path'] === TRUE) {
2114 $config->customPHPPathDir
= dirname(dirname(__FILE__
)) . '/custom_directories/php/';
2117 $config->customPHPPathDir
= $php_path;
2120 if (empty($customDirs['template_path']) ||
$customDirs['template_path'] === FALSE) {
2121 unset($config->customTemplateDir
);
2123 elseif ($customDirs['template_path'] === TRUE) {
2124 $config->customTemplateDir
= dirname(dirname(__FILE__
)) . '/custom_directories/templates/';
2127 $config->customTemplateDir
= $template_path;
2132 * Generate a temporary folder.
2134 * @param string $prefix
2138 public function createTempDir($prefix = 'test-') {
2139 $tempDir = CRM_Utils_File
::tempdir($prefix);
2140 $this->tempDirs
[] = $tempDir;
2144 public function cleanTempDirs() {
2145 if (!is_array($this->tempDirs
)) {
2146 // fix test errors where this is not set
2149 foreach ($this->tempDirs
as $tempDir) {
2150 if (is_dir($tempDir)) {
2151 CRM_Utils_File
::cleanDir($tempDir, TRUE, FALSE);
2157 * Temporarily replace the singleton extension with a different one.
2159 * @param \CRM_Extension_System $system
2161 public function setExtensionSystem(CRM_Extension_System
$system) {
2162 if ($this->origExtensionSystem
== NULL) {
2163 $this->origExtensionSystem
= CRM_Extension_System
::singleton();
2165 CRM_Extension_System
::setSingleton($this->origExtensionSystem
);
2168 public function unsetExtensionSystem() {
2169 if ($this->origExtensionSystem
!== NULL) {
2170 CRM_Extension_System
::setSingleton($this->origExtensionSystem
);
2171 $this->origExtensionSystem
= NULL;
2176 * Temporarily alter the settings-metadata to add a mock setting.
2178 * WARNING: The setting metadata will disappear on the next cache-clear.
2184 public function setMockSettingsMetaData($extras) {
2185 CRM_Utils_Hook
::singleton()
2186 ->setHook('civicrm_alterSettingsMetaData', function (&$metadata, $domainId, $profile) use ($extras) {
2187 $metadata = array_merge($metadata, $extras);
2190 Civi
::service('settings_manager')->flush();
2192 $fields = $this->callAPISuccess('setting', 'getfields', []);
2193 foreach ($extras as $key => $spec) {
2194 $this->assertNotEmpty($spec['title']);
2195 $this->assertEquals($spec['title'], $fields['values'][$key]['title']);
2200 * @param string $name
2202 public function financialAccountDelete($name) {
2203 $financialAccount = new CRM_Financial_DAO_FinancialAccount();
2204 $financialAccount->name
= $name;
2205 if ($financialAccount->find(TRUE)) {
2206 $entityFinancialType = new CRM_Financial_DAO_EntityFinancialAccount();
2207 $entityFinancialType->financial_account_id
= $financialAccount->id
;
2208 $entityFinancialType->delete();
2209 $financialAccount->delete();
2214 * FIXME: something NULLs $GLOBALS['_HTML_QuickForm_registered_rules'] when the tests are ran all together
2215 * (NB unclear if this is still required)
2217 public function _sethtmlGlobals() {
2218 $GLOBALS['_HTML_QuickForm_registered_rules'] = [
2220 'html_quickform_rule_required',
2221 'HTML/QuickForm/Rule/Required.php',
2224 'html_quickform_rule_range',
2225 'HTML/QuickForm/Rule/Range.php',
2228 'html_quickform_rule_range',
2229 'HTML/QuickForm/Rule/Range.php',
2232 'html_quickform_rule_range',
2233 'HTML/QuickForm/Rule/Range.php',
2236 'html_quickform_rule_email',
2237 'HTML/QuickForm/Rule/Email.php',
2240 'html_quickform_rule_regex',
2241 'HTML/QuickForm/Rule/Regex.php',
2244 'html_quickform_rule_regex',
2245 'HTML/QuickForm/Rule/Regex.php',
2248 'html_quickform_rule_regex',
2249 'HTML/QuickForm/Rule/Regex.php',
2252 'html_quickform_rule_regex',
2253 'HTML/QuickForm/Rule/Regex.php',
2255 'nopunctuation' => [
2256 'html_quickform_rule_regex',
2257 'HTML/QuickForm/Rule/Regex.php',
2260 'html_quickform_rule_regex',
2261 'HTML/QuickForm/Rule/Regex.php',
2264 'html_quickform_rule_callback',
2265 'HTML/QuickForm/Rule/Callback.php',
2268 'html_quickform_rule_compare',
2269 'HTML/QuickForm/Rule/Compare.php',
2272 // FIXME: …ditto for $GLOBALS['HTML_QUICKFORM_ELEMENT_TYPES']
2273 $GLOBALS['HTML_QUICKFORM_ELEMENT_TYPES'] = [
2275 'HTML/QuickForm/group.php',
2276 'HTML_QuickForm_group',
2279 'HTML/QuickForm/hidden.php',
2280 'HTML_QuickForm_hidden',
2283 'HTML/QuickForm/reset.php',
2284 'HTML_QuickForm_reset',
2287 'HTML/QuickForm/checkbox.php',
2288 'HTML_QuickForm_checkbox',
2291 'HTML/QuickForm/file.php',
2292 'HTML_QuickForm_file',
2295 'HTML/QuickForm/image.php',
2296 'HTML_QuickForm_image',
2299 'HTML/QuickForm/password.php',
2300 'HTML_QuickForm_password',
2303 'HTML/QuickForm/radio.php',
2304 'HTML_QuickForm_radio',
2307 'HTML/QuickForm/button.php',
2308 'HTML_QuickForm_button',
2311 'HTML/QuickForm/submit.php',
2312 'HTML_QuickForm_submit',
2315 'HTML/QuickForm/select.php',
2316 'HTML_QuickForm_select',
2319 'HTML/QuickForm/hiddenselect.php',
2320 'HTML_QuickForm_hiddenselect',
2323 'HTML/QuickForm/text.php',
2324 'HTML_QuickForm_text',
2327 'HTML/QuickForm/textarea.php',
2328 'HTML_QuickForm_textarea',
2331 'HTML/QuickForm/fckeditor.php',
2332 'HTML_QuickForm_FCKEditor',
2335 'HTML/QuickForm/tinymce.php',
2336 'HTML_QuickForm_TinyMCE',
2339 'HTML/QuickForm/dojoeditor.php',
2340 'HTML_QuickForm_dojoeditor',
2343 'HTML/QuickForm/link.php',
2344 'HTML_QuickForm_link',
2347 'HTML/QuickForm/advcheckbox.php',
2348 'HTML_QuickForm_advcheckbox',
2351 'HTML/QuickForm/date.php',
2352 'HTML_QuickForm_date',
2355 'HTML/QuickForm/static.php',
2356 'HTML_QuickForm_static',
2359 'HTML/QuickForm/header.php',
2360 'HTML_QuickForm_header',
2363 'HTML/QuickForm/html.php',
2364 'HTML_QuickForm_html',
2367 'HTML/QuickForm/hierselect.php',
2368 'HTML_QuickForm_hierselect',
2371 'HTML/QuickForm/autocomplete.php',
2372 'HTML_QuickForm_autocomplete',
2375 'HTML/QuickForm/xbutton.php',
2376 'HTML_QuickForm_xbutton',
2378 'advmultiselect' => [
2379 'HTML/QuickForm/advmultiselect.php',
2380 'HTML_QuickForm_advmultiselect',
2386 * Set up an acl allowing contact to see 2 specified groups
2387 * - $this->_permissionedGroup & $this->_permissionedDisabledGroup
2389 * You need to have pre-created these groups & created the user e.g
2390 * $this->createLoggedInUser();
2391 * $this->_permissionedDisabledGroup = $this->groupCreate(array('title' => 'pick-me-disabled', 'is_active' => 0, 'name' => 'pick-me-disabled'));
2392 * $this->_permissionedGroup = $this->groupCreate(array('title' => 'pick-me-active', 'is_active' => 1, 'name' => 'pick-me-active'));
2394 * @param bool $isProfile
2396 public function setupACL($isProfile = FALSE) {
2398 $_REQUEST = $this->_params
;
2400 CRM_Core_Config
::singleton()->userPermissionClass
->permissions
= ['access CiviCRM'];
2401 $optionGroupID = $this->callAPISuccessGetValue('option_group', ['return' => 'id', 'name' => 'acl_role']);
2402 $ov = new CRM_Core_DAO_OptionValue();
2403 $ov->option_group_id
= $optionGroupID;
2405 if ($ov->find(TRUE)) {
2406 CRM_Core_DAO
::executeQuery("DELETE FROM civicrm_option_value WHERE id = {$ov->id}");
2408 $optionValue = $this->callAPISuccess('option_value', 'create', [
2409 'option_group_id' => $optionGroupID,
2410 'label' => 'pick me',
2414 CRM_Core_DAO
::executeQuery("
2415 TRUNCATE civicrm_acl_cache
2418 CRM_Core_DAO
::executeQuery("
2419 TRUNCATE civicrm_acl_contact_cache
2422 CRM_Core_DAO
::executeQuery("
2423 INSERT INTO civicrm_acl_entity_role (
2424 `acl_role_id`, `entity_table`, `entity_id`, `is_active`
2425 ) VALUES (55, 'civicrm_group', {$this->_permissionedGroup}, 1);
2429 CRM_Core_DAO
::executeQuery("
2430 INSERT INTO civicrm_acl (
2431 `name`, `entity_table`, `entity_id`, `operation`, `object_table`, `object_id`, `is_active`
2434 'view picked', 'civicrm_acl_role', 55, 'Edit', 'civicrm_uf_group', 0, 1
2439 CRM_Core_DAO
::executeQuery("
2440 INSERT INTO civicrm_acl (
2441 `name`, `entity_table`, `entity_id`, `operation`, `object_table`, `object_id`, `is_active`
2444 'view picked', 'civicrm_group', $this->_permissionedGroup , 'Edit', 'civicrm_saved_search', {$this->_permissionedGroup}, 1
2448 CRM_Core_DAO
::executeQuery("
2449 INSERT INTO civicrm_acl (
2450 `name`, `entity_table`, `entity_id`, `operation`, `object_table`, `object_id`, `is_active`
2453 'view picked', 'civicrm_group', $this->_permissionedGroup, 'Edit', 'civicrm_saved_search', {$this->_permissionedDisabledGroup}, 1
2458 $this->_loggedInUser
= CRM_Core_Session
::singleton()->get('userID');
2459 $this->callAPISuccess('group_contact', 'create', [
2460 'group_id' => $this->_permissionedGroup
,
2461 'contact_id' => $this->_loggedInUser
,
2466 CRM_ACL_BAO_Cache
::resetCache();
2467 CRM_ACL_API
::groupPermission('whatever', 9999, NULL, 'civicrm_saved_search', NULL, NULL);
2472 * Alter default price set so that the field numbers are not all 1 (hiding errors)
2474 public function offsetDefaultPriceSet() {
2475 $contributionPriceSet = $this->callAPISuccess('price_set', 'getsingle', ['name' => 'default_contribution_amount']);
2476 $firstID = $contributionPriceSet['id'];
2477 $this->callAPISuccess('price_set', 'create', [
2478 'id' => $contributionPriceSet['id'],
2482 unset($contributionPriceSet['id']);
2483 $newPriceSet = $this->callAPISuccess('price_set', 'create', $contributionPriceSet);
2484 $priceField = $this->callAPISuccess('price_field', 'getsingle', [
2485 'price_set_id' => $firstID,
2486 'options' => ['limit' => 1],
2488 unset($priceField['id']);
2489 $priceField['price_set_id'] = $newPriceSet['id'];
2490 $newPriceField = $this->callAPISuccess('price_field', 'create', $priceField);
2491 $priceFieldValue = $this->callAPISuccess('price_field_value', 'getsingle', [
2492 'price_set_id' => $firstID,
2494 'options' => ['limit' => 1],
2497 unset($priceFieldValue['id']);
2498 //create some padding to use up ids
2499 $this->callAPISuccess('price_field_value', 'create', $priceFieldValue);
2500 $this->callAPISuccess('price_field_value', 'create', $priceFieldValue);
2501 $this->callAPISuccess('price_field_value', 'create', array_merge($priceFieldValue, ['price_field_id' => $newPriceField['id']]));
2505 * Create an instance of the paypal processor.
2507 * @todo this isn't a great place to put it - but really it belongs on a class that extends
2508 * this parent class & we don't have a structure for that yet
2509 * There is another function to this effect on the PaypalPro test but it appears to be silently failing
2510 * & the best protection against that is the functions this class affords
2512 * @param array $params
2514 * @return int $result['id'] payment processor id
2516 public function paymentProcessorCreate($params = []) {
2517 $params = array_merge([
2519 'domain_id' => CRM_Core_Config
::domainID(),
2520 'payment_processor_type_id' => 'PayPal',
2524 'user_name' => 'sunil._1183377782_biz_api1.webaccess.co.in',
2525 'password' => '1183377788',
2526 'signature' => 'APixCoQ-Zsaj-u3IH7mD5Do-7HUqA9loGnLSzsZga9Zr-aNmaJa3WGPH',
2527 'url_site' => 'https://www.sandbox.paypal.com/',
2528 'url_api' => 'https://api-3t.sandbox.paypal.com/',
2529 'url_button' => 'https://www.paypal.com/en_US/i/btn/btn_xpressCheckout.gif',
2530 'class_name' => 'Payment_PayPalImpl',
2531 'billing_mode' => 3,
2532 'financial_type_id' => 1,
2533 'financial_account_id' => 12,
2534 // Credit card = 1 so can pass 'by accident'.
2535 'payment_instrument_id' => 'Debit Card',
2537 if (!is_numeric($params['payment_processor_type_id'])) {
2538 // really the api should handle this through getoptions but it's not exactly api call so lets just sort it
2540 $params['payment_processor_type_id'] = $this->callAPISuccess('payment_processor_type', 'getvalue', [
2541 'name' => $params['payment_processor_type_id'],
2545 $result = $this->callAPISuccess('payment_processor', 'create', $params);
2546 return $result['id'];
2550 * Set up initial recurring payment allowing subsequent IPN payments.
2552 * @param array $recurParams (Optional)
2553 * @param array $contributionParams (Optional)
2555 public function setupRecurringPaymentProcessorTransaction($recurParams = [], $contributionParams = []): void
{
2556 $this->ids
['campaign'][0] = $this->callAPISuccess('Campaign', 'create', ['title' => 'get the money'])['id'];
2557 $contributionParams = array_merge([
2558 'total_amount' => '200',
2559 'invoice_id' => $this->_invoiceID
,
2560 'financial_type_id' => 'Donation',
2561 'contact_id' => $this->_contactID
,
2562 'contribution_page_id' => $this->_contributionPageID
,
2563 'payment_processor_id' => $this->_paymentProcessorID
,
2564 'receive_date' => '2019-07-25 07:34:23',
2565 'skipCleanMoney' => TRUE,
2566 'amount_level' => 'expensive',
2567 'campaign_id' => $this->ids
['campaign'][0],
2568 'source' => 'Online Contribution: Page name',
2569 ], $contributionParams);
2570 $contributionRecur = $this->callAPISuccess('contribution_recur', 'create', array_merge([
2571 'contact_id' => $this->_contactID
,
2574 'installments' => 5,
2575 'frequency_unit' => 'Month',
2576 'frequency_interval' => 1,
2577 'invoice_id' => $this->_invoiceID
,
2578 'contribution_status_id' => 2,
2579 'payment_processor_id' => $this->_paymentProcessorID
,
2580 // processor provided ID - use contact ID as proxy.
2581 'processor_id' => $this->_contactID
,
2582 'api.Order.create' => $contributionParams,
2583 ], $recurParams))['values'][0];
2584 $this->_contributionRecurID
= $contributionRecur['id'];
2585 $this->_contributionID
= $contributionRecur['api.Order.create']['id'];
2586 $this->ids
['Contribution'][0] = $this->_contributionID
;
2590 * We don't have a good way to set up a recurring contribution with a membership so let's just do one then alter it
2592 * @param array $params Optionally modify params for membership/recur (duration_unit/frequency_unit)
2594 * @throws \API_Exception
2596 public function setupMembershipRecurringPaymentProcessorTransaction($params = []): void
{
2597 $membershipParams = $recurParams = [];
2598 if (!empty($params['duration_unit'])) {
2599 $membershipParams['duration_unit'] = $params['duration_unit'];
2601 if (!empty($params['frequency_unit'])) {
2602 $recurParams['frequency_unit'] = $params['frequency_unit'];
2605 $this->ids
['membership_type'] = $this->membershipTypeCreate($membershipParams);
2606 //create a contribution so our membership & contribution don't both have id = 1
2607 if ($this->callAPISuccess('Contribution', 'getcount', []) === 0) {
2608 $this->contributionCreate([
2609 'contact_id' => $this->_contactID
,
2611 'financial_type_id' => 1,
2612 'invoice_id' => 'abcd',
2614 'receive_date' => '2019-07-25 07:34:23',
2618 $this->setupRecurringPaymentProcessorTransaction($recurParams, [
2623 'label' => 'General',
2625 'unit_price' => 200,
2626 'line_total' => 200,
2627 'financial_type_id' => 1,
2628 'membership_type_id' => $this->ids
['membership_type'],
2632 'contact_id' => $this->_contactID
,
2633 'membership_type_id' => $this->ids
['membership_type'],
2634 'source' => 'Payment',
2639 $this->ids
['membership'] = LineItem
::get()
2640 ->addWhere('contribution_id', '=', $this->ids
['Contribution'][0])
2641 ->addWhere('entity_table', '=', 'civicrm_membership')
2642 ->addSelect('entity_id')
2643 ->execute()->first()['entity_id'];
2651 public function CiviUnitTestCase_fatalErrorHandler($message) {
2652 throw new Exception("{$message['message']}: {$message['code']}");
2656 * Wrap the entire test case in a transaction.
2658 * Only subsequent DB statements will be wrapped in TX -- this cannot
2659 * retroactively wrap old DB statements. Therefore, it makes sense to
2660 * call this at the beginning of setUp().
2662 * Note: Recall that TRUNCATE and ALTER will force-commit transactions, so
2663 * this option does not work with, e.g., custom-data.
2665 * WISHLIST: Monitor SQL queries in unit-tests and generate an exception
2666 * if TRUNCATE or ALTER is called while using a transaction.
2669 * Whether to use nesting or reference-counting.
2671 public function useTransaction($nest = TRUE) {
2673 $this->tx
= new CRM_Core_Transaction($nest);
2674 $this->tx
->rollback();
2679 * Assert the attachment exists.
2681 * @param bool $exists
2682 * @param array $apiResult
2684 protected function assertAttachmentExistence($exists, $apiResult) {
2685 $fileId = $apiResult['id'];
2686 $this->assertTrue(is_numeric($fileId));
2687 $this->assertEquals($exists, file_exists($apiResult['values'][$fileId]['path']));
2688 $this->assertDBQuery($exists ?
1 : 0, 'SELECT count(*) FROM civicrm_file WHERE id = %1', [
2689 1 => [$fileId, 'Int'],
2691 $this->assertDBQuery($exists ?
1 : 0, 'SELECT count(*) FROM civicrm_entity_file WHERE id = %1', [
2692 1 => [$fileId, 'Int'],
2697 * Assert 2 sql strings are the same, ignoring double spaces.
2699 * @param string $expectedSQL
2700 * @param string $actualSQL
2701 * @param string $message
2703 protected function assertLike($expectedSQL, $actualSQL, $message = 'different sql') {
2704 $expected = trim((preg_replace('/[ \r\n\t]+/', ' ', $expectedSQL)));
2705 $actual = trim((preg_replace('/[ \r\n\t]+/', ' ', $actualSQL)));
2706 $this->assertEquals($expected, $actual, $message);
2710 * Create a price set for an event.
2712 * @param int $feeTotal
2713 * @param int $minAmt
2714 * @param string $type
2716 * @param array $options
2720 * @throws \CRM_Core_Exception
2722 protected function eventPriceSetCreate($feeTotal, $minAmt = 0, $type = 'Text', $options = [['name' => 'hundy', 'amount' => 100]]) {
2723 // creating price set, price field
2724 $paramsSet['title'] = 'Price Set';
2725 $paramsSet['name'] = CRM_Utils_String
::titleToVar('Price Set');
2726 $paramsSet['is_active'] = FALSE;
2727 $paramsSet['extends'] = 1;
2728 $paramsSet['min_amount'] = $minAmt;
2730 $priceSet = CRM_Price_BAO_PriceSet
::create($paramsSet);
2731 $this->_ids
['price_set'] = $priceSet->id
;
2734 'label' => 'Price Field',
2735 'name' => CRM_Utils_String
::titleToVar('Price Field'),
2736 'html_type' => $type,
2737 'price' => $feeTotal,
2738 'option_label' => ['1' => 'Price Field'],
2739 'option_value' => ['1' => $feeTotal],
2740 'option_name' => ['1' => $feeTotal],
2741 'option_weight' => ['1' => 1],
2742 'option_amount' => ['1' => 1],
2743 'is_display_amounts' => 1,
2745 'options_per_line' => 1,
2746 'is_active' => ['1' => 1],
2747 'price_set_id' => $this->_ids
['price_set'],
2748 'is_enter_qty' => 1,
2749 'financial_type_id' => $this->getFinancialTypeId('Event Fee'),
2751 if ($type === 'Radio') {
2752 foreach ($options as $index => $option) {
2753 $paramsField['is_enter_qty'] = 0;
2754 $optionID = $index +
2;
2755 $paramsField['option_value'][$optionID] = $paramsField['option_weight'][$optionID] = $paramsField['option_amount'][$optionID] = $option['amount'];
2756 $paramsField['option_label'][$optionID] = $paramsField['option_name'][$optionID] = $option['name'];
2760 $this->callAPISuccess('PriceField', 'create', $paramsField);
2761 $fields = $this->callAPISuccess('PriceField', 'get', ['price_set_id' => $this->_ids
['price_set']]);
2762 $this->_ids
['price_field'] = array_keys($fields['values']);
2763 $fieldValues = $this->callAPISuccess('PriceFieldValue', 'get', ['price_field_id' => $this->_ids
['price_field'][0]]);
2764 $this->_ids
['price_field_value'] = array_keys($fieldValues['values']);
2766 return $this->_ids
['price_set'];
2770 * Add a profile to a contribution page.
2772 * @param string $name
2773 * @param int $contributionPageID
2774 * @param string $module
2776 protected function addProfile($name, $contributionPageID, $module = 'CiviContribute') {
2778 'uf_group_id' => $name,
2779 'module' => $module,
2780 'entity_table' => 'civicrm_contribution_page',
2781 'entity_id' => $contributionPageID,
2784 if ($module !== 'CiviContribute') {
2785 $params['module_data'] = [$module => []];
2787 $this->callAPISuccess('UFJoin', 'create', $params);
2791 * Add participant with contribution
2795 * @throws \CRM_Core_Exception
2797 protected function createPartiallyPaidParticipantOrder() {
2798 $orderParams = $this->getParticipantOrderParams();
2799 $orderParams['api.Payment.create'] = ['total_amount' => 150];
2800 return $this->callAPISuccess('Order', 'create', $orderParams);
2806 * @param string $component
2807 * @param int $componentId
2808 * @param array $priceFieldOptions
2812 protected function createPriceSet($component = 'contribution_page', $componentId = NULL, $priceFieldOptions = []) {
2813 $paramsSet['title'] = 'Price Set' . substr(sha1(rand()), 0, 7);
2814 $paramsSet['name'] = CRM_Utils_String
::titleToVar($paramsSet['title']);
2815 $paramsSet['is_active'] = TRUE;
2816 $paramsSet['financial_type_id'] = 'Event Fee';
2817 $paramsSet['extends'] = 1;
2818 $priceSet = $this->callAPISuccess('price_set', 'create', $paramsSet);
2820 CRM_Price_BAO_PriceSet
::addTo('civicrm_' . $component, $componentId, $priceSet['id']);
2822 $paramsField = array_merge([
2823 'label' => 'Price Field',
2824 'name' => CRM_Utils_String
::titleToVar('Price Field'),
2825 'html_type' => 'CheckBox',
2826 'option_label' => ['1' => 'Price Field 1', '2' => 'Price Field 2'],
2827 'option_value' => ['1' => 100, '2' => 200],
2828 'option_name' => ['1' => 'Price Field 1', '2' => 'Price Field 2'],
2829 'option_weight' => ['1' => 1, '2' => 2],
2830 'option_amount' => ['1' => 100, '2' => 200],
2831 'is_display_amounts' => 1,
2833 'options_per_line' => 1,
2834 'is_active' => ['1' => 1, '2' => 1],
2835 'price_set_id' => $priceSet['id'],
2836 'is_enter_qty' => 1,
2837 'financial_type_id' => $this->getFinancialTypeId('Event Fee'),
2838 ], $priceFieldOptions);
2840 $priceField = CRM_Price_BAO_PriceField
::create($paramsField);
2841 return $this->callAPISuccess('PriceFieldValue', 'get', ['price_field_id' => $priceField->id
]);
2845 * Replace the template with a test-oriented template designed to show all the variables.
2847 * @param string $templateName
2848 * @param string $type
2850 protected function swapMessageTemplateForTestTemplate($templateName = 'contribution_online_receipt', $type = 'html') {
2851 $testTemplate = file_get_contents(__DIR__
. '/../../templates/message_templates/' . $templateName . '_' . $type . '.tpl');
2852 CRM_Core_DAO
::executeQuery(
2853 "UPDATE civicrm_msg_template
2854 SET msg_{$type} = %1
2855 WHERE workflow_name = '{$templateName}'
2856 AND is_default = 1", [1 => [$testTemplate, 'String']]
2861 * Reinstate the default template.
2863 * @param string $templateName
2864 * @param string $type
2866 protected function revertTemplateToReservedTemplate($templateName = 'contribution_online_receipt', $type = 'html') {
2867 CRM_Core_DAO
::executeQuery(
2868 "UPDATE civicrm_option_group og
2869 LEFT JOIN civicrm_option_value ov ON ov.option_group_id = og.id
2870 LEFT JOIN civicrm_msg_template m ON m.workflow_id = ov.id
2871 LEFT JOIN civicrm_msg_template m2 ON m2.workflow_id = ov.id AND m2.is_reserved = 1
2872 SET m.msg_{$type} = m2.msg_{$type}
2873 WHERE og.name = 'msg_tpl_workflow_contribution'
2874 AND ov.name = '{$templateName}'
2875 AND m.is_default = 1"
2880 * Flush statics relating to financial type.
2882 protected function flushFinancialTypeStatics() {
2883 if (isset(\Civi
::$statics['CRM_Financial_BAO_FinancialType'])) {
2884 unset(\Civi
::$statics['CRM_Financial_BAO_FinancialType']);
2886 if (isset(\Civi
::$statics['CRM_Contribute_PseudoConstant'])) {
2887 unset(\Civi
::$statics['CRM_Contribute_PseudoConstant']);
2889 CRM_Contribute_PseudoConstant
::flush('financialType');
2890 CRM_Contribute_PseudoConstant
::flush('membershipType');
2891 // Pseudoconstants may be saved to the cache table.
2892 CRM_Core_DAO
::executeQuery("TRUNCATE civicrm_cache");
2893 CRM_Financial_BAO_FinancialType
::$_statusACLFt = [];
2894 CRM_Financial_BAO_FinancialType
::$_availableFinancialTypes = NULL;
2898 * Set the permissions to the supplied array.
2900 * @param array $permissions
2902 protected function setPermissions($permissions) {
2903 CRM_Core_Config
::singleton()->userPermissionClass
->permissions
= $permissions;
2904 $this->flushFinancialTypeStatics();
2908 * @param array $params
2911 public function _checkFinancialRecords($params, $context) {
2913 'entity_id' => $params['id'],
2914 'entity_table' => 'civicrm_contribution',
2916 $contribution = $this->callAPISuccess('Contribution', 'getsingle', [
2917 'id' => $params['id'],
2918 'return' => ['total_amount', 'fee_amount', 'net_amount'],
2920 $this->assertEquals($contribution['total_amount'] - $contribution['fee_amount'], $contribution['net_amount']);
2921 if ($context == 'pending') {
2922 $trxn = CRM_Financial_BAO_FinancialItem
::retrieveEntityFinancialTrxn($entityParams);
2923 $this->assertNull($trxn, 'No Trxn to be created until IPN callback');
2926 $trxn = current(CRM_Financial_BAO_FinancialItem
::retrieveEntityFinancialTrxn($entityParams));
2928 'id' => $trxn['financial_trxn_id'],
2930 if ($context != 'online' && $context != 'payLater') {
2932 'to_financial_account_id' => 6,
2933 'total_amount' => (float) CRM_Utils_Array
::value('total_amount', $params, 100.00),
2937 if ($context == 'feeAmount') {
2938 $compareParams['fee_amount'] = 50;
2940 elseif ($context === 'online') {
2942 'to_financial_account_id' => 12,
2943 'total_amount' => (float) CRM_Utils_Array
::value('total_amount', $params, 100.00),
2945 'payment_instrument_id' => CRM_Utils_Array
::value('payment_instrument_id', $params, 1),
2948 elseif ($context == 'payLater') {
2950 'to_financial_account_id' => 7,
2951 'total_amount' => (float) CRM_Utils_Array
::value('total_amount', $params, 100.00),
2955 $this->assertDBCompareValues('CRM_Financial_DAO_FinancialTrxn', $trxnParams, $compareParams);
2957 'financial_trxn_id' => $trxn['financial_trxn_id'],
2958 'entity_table' => 'civicrm_financial_item',
2960 $entityTrxn = current(CRM_Financial_BAO_FinancialItem
::retrieveEntityFinancialTrxn($entityParams));
2962 'id' => $entityTrxn['entity_id'],
2965 'amount' => (float) CRM_Utils_Array
::value('total_amount', $params, 100.00),
2967 'financial_account_id' => CRM_Utils_Array
::value('financial_account_id', $params, 1),
2969 if ($context == 'payLater') {
2971 'amount' => (float) CRM_Utils_Array
::value('total_amount', $params, 100.00),
2973 'financial_account_id' => CRM_Utils_Array
::value('financial_account_id', $params, 1),
2976 $this->assertDBCompareValues('CRM_Financial_DAO_FinancialItem', $fitemParams, $compareParams);
2977 if ($context == 'feeAmount') {
2979 'entity_id' => $params['id'],
2980 'entity_table' => 'civicrm_contribution',
2982 $maxTrxn = current(CRM_Financial_BAO_FinancialItem
::retrieveEntityFinancialTrxn($maxParams, TRUE));
2984 'id' => $maxTrxn['financial_trxn_id'],
2987 'to_financial_account_id' => 5,
2988 'from_financial_account_id' => 6,
2989 'total_amount' => 50,
2992 $trxnId = CRM_Core_BAO_FinancialTrxn
::getFinancialTrxnId($params['id'], 'DESC');
2993 $this->assertDBCompareValues('CRM_Financial_DAO_FinancialTrxn', $trxnParams, $compareParams);
2995 'entity_id' => $trxnId['financialTrxnId'],
2996 'entity_table' => 'civicrm_financial_trxn',
3001 'financial_account_id' => 5,
3003 $this->assertDBCompareValues('CRM_Financial_DAO_FinancialItem', $fitemParams, $compareParams);
3005 // This checks that empty Sales tax rows are not being created. If for any reason it needs to be removed the
3006 // line should be copied into all the functions that call this function & evaluated there
3007 // Be really careful not to remove or bypass this without ensuring stray rows do not re-appear
3008 // when calling completeTransaction or repeatTransaction.
3009 $this->callAPISuccessGetCount('FinancialItem', ['description' => 'Sales Tax', 'amount' => 0], 0);
3013 * Return financial type id on basis of name
3015 * @param string $name Financial type m/c name
3019 public function getFinancialTypeId($name) {
3020 return CRM_Core_DAO
::getFieldValue('CRM_Financial_DAO_FinancialType', $name, 'id', 'name');
3024 * Cleanup function for contents of $this->ids.
3026 * This is a best effort cleanup to use in tear downs etc.
3028 * It will not fail if the data has already been removed (some tests may do
3029 * their own cleanup).
3031 protected function cleanUpSetUpIDs() {
3032 foreach ($this->setupIDs
as $entity => $id) {
3034 civicrm_api3($entity, 'delete', ['id' => $id, 'skip_undelete' => 1]);
3036 catch (CiviCRM_API3_Exception
$e) {
3037 // This is a best-effort cleanup function, ignore.
3043 * Create Financial Type.
3045 * @param array $params
3049 protected function createFinancialType($params = []) {
3050 $params = array_merge($params,
3052 'name' => 'Financial-Type -' . substr(sha1(rand()), 0, 7),
3056 return $this->callAPISuccess('FinancialType', 'create', $params);
3060 * Create Payment Instrument.
3062 * @param array $params
3063 * @param string $financialAccountName
3067 protected function createPaymentInstrument($params = [], $financialAccountName = 'Donation') {
3068 $params = array_merge([
3069 'label' => 'Payment Instrument -' . substr(sha1(rand()), 0, 7),
3070 'option_group_id' => 'payment_instrument',
3073 $newPaymentInstrument = $this->callAPISuccess('OptionValue', 'create', $params)['id'];
3075 $relationTypeID = key(CRM_Core_PseudoConstant
::accountOptionValues('account_relationship', NULL, " AND v.name LIKE 'Asset Account is' "));
3077 $financialAccountParams = [
3078 'entity_table' => 'civicrm_option_value',
3079 'entity_id' => $newPaymentInstrument,
3080 'account_relationship' => $relationTypeID,
3081 'financial_account_id' => $this->callAPISuccess('FinancialAccount', 'getValue', ['name' => $financialAccountName, 'return' => 'id']),
3083 CRM_Financial_BAO_FinancialTypeAccount
::add($financialAccountParams);
3085 return CRM_Core_PseudoConstant
::getKey('CRM_Contribute_BAO_Contribution', 'payment_instrument_id', $params['label']);
3089 * Enable Tax and Invoicing
3091 * @param array $params
3093 * @return \Civi\Core\SettingsBag
3095 protected function enableTaxAndInvoicing($params = []) {
3096 // Enable component contribute setting
3097 $contributeSetting = array_merge($params,
3100 'invoice_prefix' => 'INV_',
3102 'due_date_period' => 'days',
3104 'is_email_pdf' => 1,
3105 'tax_term' => 'Sales Tax',
3106 'tax_display_settings' => 'Inclusive',
3109 return Civi
::settings()->set('contribution_invoice_settings', $contributeSetting);
3113 * Enable Tax and Invoicing
3115 * @throws \CRM_Core_Exception
3117 protected function disableTaxAndInvoicing(): \Civi\Core\SettingsBag
{
3118 $accounts = $this->callAPISuccess('EntityFinancialAccount', 'get', ['account_relationship' => 'Sales Tax Account is'])['values'];
3119 foreach ($accounts as $account) {
3120 $this->callAPISuccess('EntityFinancialAccount', 'delete', ['id' => $account['id']]);
3121 $this->callAPISuccess('FinancialAccount', 'delete', ['id' => $account['financial_account_id']]);
3124 if (!empty(\Civi
::$statics['CRM_Core_PseudoConstant']) && isset(\Civi
::$statics['CRM_Core_PseudoConstant']['taxRates'])) {
3125 unset(\Civi
::$statics['CRM_Core_PseudoConstant']['taxRates']);
3127 return Civi
::settings()->set('invoicing', FALSE);
3131 * Add Sales Tax Account for the financial type.
3133 * @param int $financialTypeId
3135 * @param array $accountParams
3137 * @return CRM_Financial_DAO_EntityFinancialAccount
3138 * @throws \CRM_Core_Exception
3140 protected function addTaxAccountToFinancialType(int $financialTypeId, $accountParams = []) {
3141 $params = array_merge([
3142 'name' => 'Sales tax account ' . substr(sha1(rand()), 0, 4),
3143 'financial_account_type_id' => key(CRM_Core_PseudoConstant
::accountOptionValues('financial_account_type', NULL, " AND v.name LIKE 'Liability' ")),
3144 'is_deductible' => 1,
3149 $account = CRM_Financial_BAO_FinancialAccount
::add($params);
3151 'entity_table' => 'civicrm_financial_type',
3152 'entity_id' => $financialTypeId,
3153 'account_relationship' => key(CRM_Core_PseudoConstant
::accountOptionValues('account_relationship', NULL, " AND v.name LIKE 'Sales Tax Account is' ")),
3156 // set tax rate (as 10) for provided financial type ID to static variable, later used to fetch tax rates of all financial types
3157 \Civi
::$statics['CRM_Core_PseudoConstant']['taxRates'][$financialTypeId] = $params['tax_rate'];
3159 //CRM-20313: As per unique index added in civicrm_entity_financial_account table,
3160 // first check if there's any record on basis of unique key (entity_table, account_relationship, entity_id)
3161 $dao = new CRM_Financial_DAO_EntityFinancialAccount();
3162 $dao->copyValues($entityParams);
3164 if ($dao->fetch()) {
3165 $entityParams['id'] = $dao->id
;
3167 $entityParams['financial_account_id'] = $account->id
;
3169 return CRM_Financial_BAO_FinancialTypeAccount
::add($entityParams);
3173 * Create price set with contribution test for test setup.
3175 * This could be merged with 4.5 function setup in api_v3_ContributionPageTest::setUpContributionPage
3176 * on parent class at some point (fn is not in 4.4).
3179 * @param array $params
3181 public function createPriceSetWithPage($entity = NULL, $params = []) {
3182 $membershipTypeID = $this->membershipTypeCreate(['name' => 'Special']);
3183 $contributionPageResult = $this->callAPISuccess('contribution_page', 'create', [
3184 'title' => "Test Contribution Page",
3185 'financial_type_id' => 1,
3186 'currency' => 'NZD',
3187 'goal_amount' => 50,
3188 'is_pay_later' => 1,
3189 'is_monetary' => TRUE,
3190 'is_email_receipt' => FALSE,
3192 $priceSet = $this->callAPISuccess('price_set', 'create', [
3193 'is_quick_config' => 0,
3194 'extends' => 'CiviMember',
3195 'financial_type_id' => 1,
3196 'title' => 'my Page',
3198 $priceSetID = $priceSet['id'];
3200 CRM_Price_BAO_PriceSet
::addTo('civicrm_contribution_page', $contributionPageResult['id'], $priceSetID);
3201 $priceField = $this->callAPISuccess('price_field', 'create', [
3202 'price_set_id' => $priceSetID,
3203 'label' => 'Goat Breed',
3204 'html_type' => 'Radio',
3206 $priceFieldValue = $this->callAPISuccess('price_field_value', 'create', [
3207 'price_set_id' => $priceSetID,
3208 'price_field_id' => $priceField['id'],
3209 'label' => 'Long Haired Goat',
3211 'financial_type_id' => 'Donation',
3212 'membership_type_id' => $membershipTypeID,
3213 'membership_num_terms' => 1,
3215 $this->_ids
['price_field_value'] = [$priceFieldValue['id']];
3216 $priceFieldValue = $this->callAPISuccess('price_field_value', 'create', [
3217 'price_set_id' => $priceSetID,
3218 'price_field_id' => $priceField['id'],
3219 'label' => 'Shoe-eating Goat',
3221 'financial_type_id' => 'Donation',
3222 'membership_type_id' => $membershipTypeID,
3223 'membership_num_terms' => 2,
3225 $this->_ids
['price_field_value'][] = $priceFieldValue['id'];
3227 $priceFieldValue = $this->callAPISuccess('price_field_value', 'create', [
3228 'price_set_id' => $priceSetID,
3229 'price_field_id' => $priceField['id'],
3230 'label' => 'Shoe-eating Goat',
3232 'financial_type_id' => 'Donation',
3234 $this->_ids
['price_field_value']['cont'] = $priceFieldValue['id'];
3236 $this->_ids
['price_set'] = $priceSetID;
3237 $this->_ids
['contribution_page'] = $contributionPageResult['id'];
3238 $this->_ids
['price_field'] = [$priceField['id']];
3240 $this->_ids
['membership_type'] = $membershipTypeID;
3244 * Only specified contact returned.
3246 * @implements CRM_Utils_Hook::aclWhereClause
3250 * @param $whereTables
3254 public function aclWhereMultipleContacts($type, &$tables, &$whereTables, &$contactID, &$where) {
3255 $where = " contact_a.id IN (" . implode(', ', $this->allowedContacts
) . ")";
3259 * @implements CRM_Utils_Hook::selectWhereClause
3261 * @param string $entity
3262 * @param array $clauses
3264 public function selectWhereClauseHook($entity, &$clauses) {
3265 if ($entity == 'Event') {
3266 $clauses['event_type_id'][] = "IN (2, 3, 4)";
3271 * An implementation of hook_civicrm_post used with all our test cases.
3274 * @param string $objectName
3275 * @param int $objectId
3278 public function onPost($op, $objectName, $objectId, &$objectRef) {
3279 if ($op == 'create' && $objectName == 'Individual') {
3280 CRM_Core_DAO
::executeQuery(
3281 "UPDATE civicrm_contact SET nick_name = 'munged' WHERE id = %1",
3283 1 => [$objectId, 'Integer'],
3288 if ($op == 'edit' && $objectName == 'Participant') {
3290 1 => [$objectId, 'Integer'],
3292 $query = "UPDATE civicrm_participant SET source = 'Post Hook Update' WHERE id = %1";
3293 CRM_Core_DAO
::executeQuery($query, $params);
3298 * Instantiate form object.
3300 * We need to instantiate the form to run preprocess, which means we have to trick it about the request method.
3302 * @param string $class
3303 * Name of form class.
3305 * @param array $formValues
3307 * @param string $pageName
3309 * @return \CRM_Core_Form
3310 * @throws \CRM_Core_Exception
3312 public function getFormObject($class, $formValues = [], $pageName = '') {
3313 $_POST = $formValues;
3314 /* @var CRM_Core_Form $form */
3315 $form = new $class();
3316 $_SERVER['REQUEST_METHOD'] = 'GET';
3318 case 'CRM_Event_Cart_Form_Checkout_Payment':
3319 case 'CRM_Event_Cart_Form_Checkout_ParticipantsAndPrices':
3320 $form->controller
= new CRM_Event_Cart_Controller_Checkout();
3324 $form->controller
= new CRM_Core_Controller();
3327 $pageName = $form->getName();
3329 $form->controller
->setStateMachine(new CRM_Core_StateMachine($form->controller
));
3330 $_SESSION['_' . $form->controller
->_name
. '_container']['values'][$pageName] = $formValues;
3335 * Get possible thousand separators.
3339 public function getThousandSeparators() {
3340 return [['.'], [',']];
3344 * Get the boolean options as a provider.
3348 public function getBooleanDataProvider() {
3349 return [[TRUE], [FALSE]];
3353 * Set the separators for thousands and decimal points.
3355 * Note that this only covers some common scenarios.
3357 * It does not cater for a situation where the thousand separator is a [space]
3358 * Latter is the Norwegian localization. At least some tests need to
3359 * use setMonetaryDecimalPoint and setMonetaryThousandSeparator directly
3360 * to provide broader coverage.
3362 * @param string $thousandSeparator
3364 protected function setCurrencySeparators($thousandSeparator) {
3365 Civi
::settings()->set('monetaryThousandSeparator', $thousandSeparator);
3366 Civi
::settings()->set('monetaryDecimalPoint', ($thousandSeparator === ',' ?
'.' : ','));
3370 * Sets the thousand separator.
3372 * If you use this function also set the decimal separator: setMonetaryDecimalSeparator
3374 * @param $thousandSeparator
3376 protected function setMonetaryThousandSeparator($thousandSeparator) {
3377 Civi
::settings()->set('monetaryThousandSeparator', $thousandSeparator);
3381 * Sets the decimal separator.
3383 * If you use this function also set the thousand separator setMonetaryDecimalPoint
3385 * @param $decimalPoint
3387 protected function setMonetaryDecimalPoint($decimalPoint) {
3388 Civi
::settings()->set('monetaryDecimalPoint', $decimalPoint);
3392 * Sets the default currency.
3396 protected function setDefaultCurrency($currency) {
3397 Civi
::settings()->set('defaultCurrency', $currency);
3401 * Format money as it would be input.
3403 * @param string $amount
3407 protected function formatMoneyInput($amount) {
3408 return CRM_Utils_Money
::format($amount, NULL, '%a');
3412 * Get the contribution object.
3414 * @param int $contributionID
3416 * @return \CRM_Contribute_BAO_Contribution
3418 protected function getContributionObject($contributionID) {
3419 $contributionObj = new CRM_Contribute_BAO_Contribution();
3420 $contributionObj->id
= $contributionID;
3421 $contributionObj->find(TRUE);
3422 return $contributionObj;
3426 * Enable multilingual.
3428 public function enableMultilingual() {
3429 $this->callAPISuccess('Setting', 'create', [
3430 'lcMessages' => 'en_US',
3431 'languageLimit' => [
3436 CRM_Core_I18n_Schema
::makeMultilingual('en_US');
3439 $dbLocale = '_en_US';
3443 * Setup or clean up SMS tests
3445 * @param bool $teardown
3447 * @throws \CiviCRM_API3_Exception
3449 public function setupForSmsTests($teardown = FALSE) {
3450 require_once 'CiviTest/CiviTestSMSProvider.php';
3452 // Option value params for CiviTestSMSProvider
3453 $groupID = CRM_Core_DAO
::getFieldValue('CRM_Core_DAO_OptionGroup', 'sms_provider_name', 'id', 'name');
3455 'option_group_id' => $groupID,
3456 'label' => 'unittestSMS',
3457 'value' => 'unit.test.sms',
3458 'name' => 'CiviTestSMSProvider',
3465 // Test completed, delete provider
3466 $providerOptionValueResult = civicrm_api3('option_value', 'get', $params);
3467 civicrm_api3('option_value', 'delete', ['id' => $providerOptionValueResult['id']]);
3471 // Create an SMS provider "CiviTestSMSProvider". Civi handles "CiviTestSMSProvider" as a special case and allows it to be instantiated
3472 // in CRM/Sms/Provider.php even though it is not an extension.
3473 return civicrm_api3('option_value', 'create', $params);
3477 * Start capturing browser output.
3479 * The starts the process of browser output being captured, setting any variables needed for e-notice prevention.
3481 protected function startCapturingOutput() {
3483 $_SERVER['HTTP_USER_AGENT'] = 'unittest';
3487 * Stop capturing browser output and return as a csv.
3489 * @param bool $isFirstRowHeaders
3491 * @return \League\Csv\Reader
3493 * @throws \League\Csv\Exception
3495 protected function captureOutputToCSV($isFirstRowHeaders = TRUE) {
3496 $output = ob_get_flush();
3497 $stream = fopen('php://memory', 'r+');
3498 fwrite($stream, $output);
3500 $this->assertEquals("\xEF\xBB\xBF", substr($output, 0, 3));
3501 $csv = Reader
::createFromString($output);
3502 if ($isFirstRowHeaders) {
3503 $csv->setHeaderOffset(0);
3510 * Rename various labels to not match the names.
3512 * Doing these mimics the fact the name != the label in international installs & triggers failures in
3513 * code that expects it to.
3515 protected function renameLabels() {
3516 $replacements = ['Pending', 'Refunded'];
3517 foreach ($replacements as $name) {
3518 CRM_Core_DAO
::executeQuery("UPDATE civicrm_option_value SET label = '{$name} Label**' where label = '{$name}' AND name = '{$name}'");
3523 * Undo any label renaming.
3525 protected function resetLabels() {
3526 CRM_Core_DAO
::executeQuery("UPDATE civicrm_option_value SET label = REPLACE(name, ' Label**', '') WHERE label LIKE '% Label**'");
3530 * Get parameters to set up a multi-line participant order.
3533 * @throws \CRM_Core_Exception
3535 protected function getParticipantOrderParams(): array {
3536 $event = $this->eventCreate();
3537 $this->_eventId
= $event['id'];
3539 'id' => $this->_eventId
,
3540 'financial_type_id' => 4,
3543 $this->callAPISuccess('event', 'create', $eventParams);
3544 $priceFields = $this->createPriceSet('event', $this->_eventId
);
3546 'total_amount' => 300,
3547 'currency' => 'USD',
3548 'contact_id' => $this->individualCreate(),
3549 'financial_type_id' => 4,
3550 'contribution_status_id' => 'Pending',
3552 foreach ($priceFields['values'] as $key => $priceField) {
3553 $orderParams['line_items'][] = [
3556 'price_field_id' => $priceField['price_field_id'],
3557 'price_field_value_id' => $priceField['id'],
3558 'label' => $priceField['label'],
3559 'field_title' => $priceField['label'],
3561 'unit_price' => $priceField['amount'],
3562 'line_total' => $priceField['amount'],
3563 'financial_type_id' => $priceField['financial_type_id'],
3564 'entity_table' => 'civicrm_participant',
3568 'financial_type_id' => 4,
3569 'event_id' => $this->_eventId
,
3572 'fee_currency' => 'USD',
3573 'contact_id' => $this->individualCreate(),
3577 return $orderParams;
3583 * @throws \CRM_Core_Exception
3585 protected function validatePayments($payments): void
{
3586 foreach ($payments as $payment) {
3587 $balance = CRM_Contribute_BAO_Contribution
::getContributionBalance($payment['contribution_id']);
3588 if ($balance < 0 && $balance +
$payment['total_amount'] === 0.0) {
3589 // This is an overpayment situation. there are no financial items to allocate the overpayment.
3590 // This is a pretty rough way at guessing which payment is the overpayment - but
3591 // for the test suite it should be enough.
3594 $items = $this->callAPISuccess('EntityFinancialTrxn', 'get', [
3595 'financial_trxn_id' => $payment['id'],
3596 'entity_table' => 'civicrm_financial_item',
3597 'return' => ['amount'],
3600 foreach ($items as $item) {
3601 $itemTotal +
= $item['amount'];
3603 $this->assertEquals($payment['total_amount'], $itemTotal);
3608 * Validate all created payments.
3610 * @throws \CRM_Core_Exception
3612 protected function validateAllPayments() {
3613 $payments = $this->callAPISuccess('Payment', 'get', [
3614 'return' => ['total_amount', 'tax_amount'],
3615 'options' => ['limit' => 0],
3617 $this->validatePayments($payments);
3621 * Validate all created contributions.
3623 * @throws \API_Exception
3625 protected function validateAllContributions(): void
{
3626 $contributions = Contribution
::get(FALSE)->setSelect(['total_amount', 'tax_amount'])->execute();
3627 foreach ($contributions as $contribution) {
3628 $lineItems = $this->callAPISuccess('LineItem', 'get', [
3629 'contribution_id' => $contribution['id'],
3630 'return' => ['tax_amount', 'line_total', 'entity_table', 'entity_id', 'qty'],
3636 foreach ($lineItems as $lineItem) {
3637 $total +
= $lineItem['line_total'];
3638 $taxTotal +
= (float) ($lineItem['tax_amount'] ??
0);
3639 if ($lineItem['entity_table'] === 'civicrm_membership') {
3640 $memberships[] = $lineItem['entity_id'];
3642 if ($lineItem['entity_table'] === 'civicrm_participant' && $lineItem['qty'] > 0) {
3643 $participants[$lineItem['entity_id']] = $lineItem['entity_id'];
3646 $membershipPayments = $this->callAPISuccess('MembershipPayment', 'get', ['contribution_id' => $contribution['id'], 'return' => 'membership_id'])['values'];
3647 $participantPayments = $this->callAPISuccess('ParticipantPayment', 'get', ['contribution_id' => $contribution['id'], 'return' => 'participant_id'])['values'];
3648 $this->assertCount(count($memberships), $membershipPayments);
3649 $this->assertCount(count($participants), $participantPayments);
3650 foreach ($membershipPayments as $payment) {
3651 $this->assertContains($payment['membership_id'], $memberships);
3653 foreach ($participantPayments as $payment) {
3654 $this->assertContains($payment['participant_id'], $participants);
3656 $this->assertEquals($taxTotal, (float) ($contribution['tax_amount'] ??
0));
3657 $this->assertEquals($total +
$taxTotal, $contribution['total_amount']);
3663 * @throws \CRM_Core_Exception
3665 protected function createRuleGroup() {
3666 $ruleGroup = $this->callAPISuccess('RuleGroup', 'create', [
3667 'contact_type' => 'Individual',
3669 'used' => 'General',
3670 'name' => 'TestRule',
3671 'title' => 'TestRule',
3678 * Generic create test.
3680 * @param int $version
3682 * @throws \CRM_Core_Exception
3684 protected function basicCreateTest(int $version) {
3685 $this->_apiversion
= $version;
3686 $result = $this->callAPIAndDocument($this->_entity
, 'create', $this->params
, __FUNCTION__
, __FILE__
);
3687 $this->assertEquals(1, $result['count']);
3688 $this->assertNotNull($result['values'][$result['id']]['id']);
3689 $this->getAndCheck($this->params
, $result['id'], $this->_entity
);
3693 * Generic delete test.
3695 * @param int $version
3697 * @throws \CRM_Core_Exception
3699 protected function basicDeleteTest($version) {
3700 $this->_apiversion
= $version;
3701 $result = $this->callAPISuccess($this->_entity
, 'create', $this->params
);
3702 $deleteParams = ['id' => $result['id']];
3703 $this->callAPIAndDocument($this->_entity
, 'delete', $deleteParams, __FUNCTION__
, __FILE__
);
3704 $checkDeleted = $this->callAPISuccess($this->_entity
, 'get', []);
3705 $this->assertEquals(0, $checkDeleted['count']);
3709 * Create and return a case object for the given Client ID.
3711 * @param int $clientId
3712 * @param int $loggedInUser
3713 * Omit or pass NULL to use the same as clientId
3714 * @param array $extra
3715 * Optional specific parameters such as start_date
3717 * @return CRM_Case_BAO_Case
3719 public function createCase($clientId, $loggedInUser = NULL, $extra = []) {
3720 if (empty($loggedInUser)) {
3721 // backwards compatibility - but it's more typical that the creator is a different person than the client
3722 $loggedInUser = $clientId;
3724 $caseParams = array_merge([
3725 'activity_subject' => 'Case Subject',
3726 'client_id' => $clientId,
3727 'case_type_id' => 1,
3729 'case_type' => 'housing_support',
3730 'subject' => 'Case Subject',
3731 'start_date' => date("Y-m-d"),
3732 'start_date_time' => date("YmdHis"),
3734 'activity_details' => '',
3736 $form = new CRM_Case_Form_Case();
3737 return $form->testSubmit($caseParams, 'OpenCase', $loggedInUser, 'standalone');
3741 * Validate that all location entities have exactly one primary.
3743 * This query takes about 2 minutes on a DB with 10s of millions of contacts.
3745 public function assertLocationValidity() {
3746 $this->assertEquals(0, CRM_Core_DAO
::singleValueQuery('SELECT COUNT(*) FROM
3748 (SELECT a1.contact_id
3749 FROM civicrm_address a1
3750 LEFT JOIN civicrm_address a2 ON a1.id <> a2.id AND a2.is_primary = 1
3751 AND a1.contact_id = a2.contact_id
3754 AND a2.id IS NOT NULL
3755 AND a1.contact_id IS NOT NULL
3757 SELECT a1.contact_id
3758 FROM civicrm_address a1
3759 LEFT JOIN civicrm_address a2 ON a1.id <> a2.id AND a2.is_primary = 1
3760 AND a1.contact_id = a2.contact_id
3761 WHERE a1.is_primary = 0
3763 AND a1.contact_id IS NOT NULL
3767 SELECT a1.contact_id
3768 FROM civicrm_email a1
3769 LEFT JOIN civicrm_email a2 ON a1.id <> a2.id AND a2.is_primary = 1
3770 AND a1.contact_id = a2.contact_id
3773 AND a2.id IS NOT NULL
3774 AND a1.contact_id IS NOT NULL
3776 SELECT a1.contact_id
3777 FROM civicrm_email a1
3778 LEFT JOIN civicrm_email a2 ON a1.id <> a2.id AND a2.is_primary = 1
3779 AND a1.contact_id = a2.contact_id
3780 WHERE a1.is_primary = 0
3782 AND a1.contact_id IS NOT NULL
3786 SELECT a1.contact_id
3787 FROM civicrm_phone a1
3788 LEFT JOIN civicrm_phone a2 ON a1.id <> a2.id AND a2.is_primary = 1
3789 AND a1.contact_id = a2.contact_id
3792 AND a2.id IS NOT NULL
3793 AND a1.contact_id IS NOT NULL
3795 SELECT a1.contact_id
3796 FROM civicrm_phone a1
3797 LEFT JOIN civicrm_phone a2 ON a1.id <> a2.id AND a2.is_primary = 1
3798 AND a1.contact_id = a2.contact_id
3799 WHERE a1.is_primary = 0
3801 AND a1.contact_id IS NOT NULL
3805 SELECT a1.contact_id
3807 LEFT JOIN civicrm_im a2 ON a1.id <> a2.id AND a2.is_primary = 1
3808 AND a1.contact_id = a2.contact_id
3811 AND a2.id IS NOT NULL
3812 AND a1.contact_id IS NOT NULL
3814 SELECT a1.contact_id
3816 LEFT JOIN civicrm_im a2 ON a1.id <> a2.id AND a2.is_primary = 1
3817 AND a1.contact_id = a2.contact_id
3818 WHERE a1.is_primary = 0
3820 AND a1.contact_id IS NOT NULL
3824 SELECT a1.contact_id
3825 FROM civicrm_openid a1
3826 LEFT JOIN civicrm_openid a2 ON a1.id <> a2.id AND a2.is_primary = 1
3827 AND a1.contact_id = a2.contact_id
3828 WHERE (a1.is_primary = 1 AND a2.id IS NOT NULL)
3831 SELECT a1.contact_id
3832 FROM civicrm_openid a1
3833 LEFT JOIN civicrm_openid a2 ON a1.id <> a2.id AND a2.is_primary = 1
3834 AND a1.contact_id = a2.contact_id
3837 AND a2.id IS NOT NULL
3838 AND a1.contact_id IS NOT NULL
3840 SELECT a1.contact_id
3841 FROM civicrm_openid a1
3842 LEFT JOIN civicrm_openid a2 ON a1.id <> a2.id AND a2.is_primary = 1
3843 AND a1.contact_id = a2.contact_id
3844 WHERE a1.is_primary = 0
3846 AND a1.contact_id IS NOT NULL) as primary_descrepancies
3851 * Ensure the specified mysql mode/s are activated.
3853 * @param array $modes
3855 protected function ensureMySQLMode(array $modes): void
{
3856 $currentModes = array_fill_keys(CRM_Utils_SQL
::getSqlModes(), 1);
3857 $currentModes = array_merge($currentModes, array_fill_keys($modes, 1));
3858 CRM_Core_DAO
::executeQuery("SET GLOBAL sql_mode = '" . implode(',', array_keys($currentModes)) . "'");
3859 CRM_Core_DAO
::executeQuery("SET sql_mode = '" . implode(',', array_keys($currentModes)) . "'");
3863 * Delete any extraneous relationship types.
3865 * @throws \API_Exception
3866 * @throws \Civi\API\Exception\UnauthorizedException
3868 protected function deleteNonDefaultRelationshipTypes(): void
{
3869 RelationshipType
::delete(FALSE)->addWhere('name_a_b', 'NOT IN', [
3876 'Head of Household for',
3877 'Household Member of',
3878 'Case Coordinator is',