[REF] Move Auto DSN Switching into a core function
[civicrm-core.git] / tests / phpunit / api / v3 / SyntaxConformanceTest.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 /**
13 * Test that the core actions for APIv3 entities comply with standard syntax+behavior.
14 *
15 * By default, this tests all API entities. To only test specific entities, call phpunit with
16 * environment variable SYNTAX_CONFORMANCE_ENTITIES, e.g.
17 *
18 * env SYNTAX_CONFORMANCE_ENTITIES="Contact Event" ./scripts/phpunit api_v3_SyntaxConformanceTest
19 *
20 * @package CiviCRM_APIv3
21 * @subpackage API_Core
22 * @group headless
23 */
24 class api_v3_SyntaxConformanceTest extends CiviUnitTestCase {
25 protected $_apiversion = 3;
26
27 /**
28 * @var array
29 * e.g. $this->deletes['CRM_Contact_DAO_Contact'][] = $contactID;
30 */
31 protected $deletableTestObjects;
32
33 /**
34 * This test case doesn't require DB reset.
35 * @var bool
36 */
37 public $DBResetRequired = FALSE;
38
39 protected $_entity;
40
41 /**
42 * Map custom group entities to civicrm components.
43 * @var array
44 */
45 protected static $componentMap = [
46 'Contribution' => 'CiviContribute',
47 'Membership' => 'CiviMember',
48 'Participant' => 'CiviEvent',
49 'Event' => 'CiviEvent',
50 'Case' => 'CiviCase',
51 'Pledge' => 'CiviPledge',
52 'Grant' => 'CiviGrant',
53 'Campaign' => 'CiviCampaign',
54 'Survey' => 'CiviCampaign',
55 ];
56
57 /**
58 * Set up function.
59 *
60 * There are two types of missing APIs:
61 * Those that are to be implemented
62 * (in some future version when someone steps in -hint hint-). List the entities in toBeImplemented[ {$action} ]
63 * Those that don't exist
64 * and that will never exist (eg an obsoleted Entity
65 * they need to be returned by the function toBeSkipped_{$action} (because it has to be a static method and therefore couldn't access a this->toBeSkipped)
66 */
67 public function setUp() {
68 parent::setUp();
69 $this->enableCiviCampaign();
70 $this->toBeImplemented['get'] = [
71 // CxnApp.get exists but relies on remote data outside our control; QA w/UtilsTest::testBasicArrayGet
72 'CxnApp',
73 'Profile',
74 'CustomValue',
75 'Constant',
76 'CustomSearch',
77 'Extension',
78 'ReportTemplate',
79 'System',
80 'Setting',
81 'Payment',
82 'Logging',
83 ];
84 $this->toBeImplemented['create'] = [
85 'Cxn',
86 'CxnApp',
87 'SurveyRespondant',
88 'OptionGroup',
89 'MailingRecipients',
90 'UFMatch',
91 'CustomSearch',
92 'Extension',
93 'ReportTemplate',
94 'System',
95 'User',
96 'Payment',
97 'Order',
98 //work fine in local
99 'Logging',
100 ];
101 $this->toBeImplemented['delete'] = [
102 'Cxn',
103 'CxnApp',
104 'MembershipPayment',
105 'OptionGroup',
106 'SurveyRespondant',
107 'UFJoin',
108 'UFMatch',
109 'Extension',
110 'System',
111 'Payment',
112 'Order',
113 ];
114 $this->onlyIDNonZeroCount['get'] = [
115 'ActivityType',
116 'Entity',
117 'Domain',
118 'Setting',
119 'User',
120 ];
121 $this->deprecatedAPI = ['Location', 'ActivityType', 'SurveyRespondant'];
122 $this->deletableTestObjects = [];
123 }
124
125 public function tearDown() {
126 foreach ($this->deletableTestObjects as $entityName => $entities) {
127 foreach ($entities as $entityID) {
128 CRM_Core_DAO::deleteTestObjects($entityName, ['id' => $entityID]);
129 }
130 }
131 }
132
133 /**
134 * Generate list of all entities.
135 *
136 * @param array $skip
137 * Entities to skip.
138 *
139 * @return array
140 */
141 public static function entities($skip = []) {
142 // The order of operations in here is screwy. In the case where SYNTAX_CONFORMANCE_ENTITIES is
143 // defined, we should be able to parse+return it immediately. However, some weird dependency
144 // crept into the system where civicrm_api('Entity','get') must be called as part of entities()
145 // (even if its return value is ignored).
146
147 $tmp = civicrm_api('Entity', 'Get', ['version' => 3]);
148 if (getenv('SYNTAX_CONFORMANCE_ENTITIES')) {
149 $tmp = [
150 'values' => explode(' ', getenv('SYNTAX_CONFORMANCE_ENTITIES')),
151 ];
152 }
153
154 if (!is_array($skip)) {
155 $skip = [];
156 }
157 $tmp = array_diff($tmp['values'], $skip);
158 $entities = [];
159 foreach ($tmp as $e) {
160 $entities[] = [$e];
161 }
162 return $entities;
163 }
164
165 /**
166 * Get list of entities for get test.
167 *
168 * @return array
169 */
170 public static function entities_get() {
171 // all the entities, beside the ones flagged
172 return static::entities(static::toBeSkipped_get(TRUE));
173 }
174
175 /**
176 * Get entities for create tests.
177 *
178 * @return array
179 */
180 public static function entities_create() {
181 return static::entities(static::toBeSkipped_create(TRUE));
182 }
183
184 /**
185 * @return array
186 */
187 public static function entities_updatesingle() {
188 return static::entities(static::toBeSkipped_updatesingle(TRUE));
189 }
190
191 /**
192 * @return array
193 */
194 public static function entities_getlimit() {
195 return static::entities(static::toBeSkipped_getlimit());
196 }
197
198 /**
199 * Generate list of entities that can be retrieved using SQL operator syntax.
200 *
201 * @return array
202 */
203 public static function entities_getSqlOperators() {
204 return static::entities(static::toBeSkipped_getSqlOperators());
205 }
206
207 /**
208 * @return array
209 */
210 public static function entities_delete() {
211 return static::entities(static::toBeSkipped_delete(TRUE));
212 }
213
214 /**
215 * @return array
216 */
217 public static function entities_getfields() {
218 return static::entities(static::toBeSkipped_getfields(TRUE));
219 }
220
221 /**
222 * @return array
223 */
224 public static function custom_data_entities_get() {
225 return static::custom_data_entities();
226 }
227
228 /**
229 * @return array
230 */
231 public static function custom_data_entities() {
232 $entities = CRM_Core_BAO_CustomQuery::$extendsMap;
233 $enabledComponents = Civi::settings()->get('enable_components');
234 $customDataEntities = [];
235 $invalidEntities = ['Individual', 'Organization', 'Household'];
236 $entitiesToFix = ['Case', 'Relationship'];
237 foreach ($entities as $entityName => $entity) {
238 if (!in_array($entityName, $invalidEntities)
239 && !in_array($entityName, $entitiesToFix)
240 ) {
241 if (!empty(self::$componentMap[$entityName]) && empty($enabledComponents[self::$componentMap[$entityName]])) {
242 CRM_Core_BAO_ConfigSetting::enableComponent(self::$componentMap[$entityName]);
243 }
244 $customDataEntities[] = [$entityName];
245 }
246 }
247 return $customDataEntities;
248 }
249
250 /**
251 * Add a smattering of entities that don't normally have custom data.
252 *
253 * @return array
254 */
255 public static function custom_data_incl_non_std_entities_get() {
256 return static::entities(static::toBeSkipped_custom_data_creatable(TRUE));
257 }
258
259 /**
260 * Get entities to be skipped on get tests.
261 *
262 * @param bool $sequential
263 *
264 * @return array
265 */
266 public static function toBeSkipped_get($sequential = FALSE) {
267 $entitiesWithoutGet = [
268 'MailingEventSubscribe',
269 'MailingEventConfirm',
270 'MailingEventResubscribe',
271 'MailingEventUnsubscribe',
272 'Location',
273 ];
274 if ($sequential === TRUE) {
275 return $entitiesWithoutGet;
276 }
277 $entities = [];
278 foreach ($entitiesWithoutGet as $e) {
279 $entities[] = [$e];
280 }
281 return $entities;
282 }
283
284 /**
285 * Get entities to be skipped for get call.
286 *
287 * Mailing Contact Just doesn't support id. We have always insisted on finding a way to
288 * support id in API but in this case the underlying tables are crying out for a restructure
289 * & it just doesn't make sense.
290 *
291 * User doesn't support get By ID because the user id is actually the CMS user ID & is not part of
292 * CiviCRM - so can only be tested through UserTest - not SyntaxConformanceTest.
293 *
294 * Entity doesn't support get By ID because it simply gives the result of string Entites in CiviCRM
295 *
296 * @param bool $sequential
297 *
298 * @return array
299 * Entities that cannot be retrieved by ID
300 */
301 public static function toBeSkipped_getByID($sequential = FALSE) {
302 return ['MailingContact', 'User', 'Attachment', 'Entity'];
303 }
304
305 /**
306 * @param bool $sequential
307 *
308 * @return array
309 */
310 public static function toBeSkipped_create($sequential = FALSE) {
311 $entitiesWithoutCreate = ['Constant', 'Entity', 'Location', 'Profile', 'MailingRecipients'];
312 if ($sequential === TRUE) {
313 return $entitiesWithoutCreate;
314 }
315 $entities = [];
316 foreach ($entitiesWithoutCreate as $e) {
317 $entities[] = [$e];
318 }
319 return $entities;
320 }
321
322 /**
323 * @param bool $sequential
324 *
325 * @return array
326 */
327 public static function toBeSkipped_delete($sequential = FALSE) {
328 $entitiesWithout = [
329 'MailingContact',
330 'MailingEventConfirm',
331 'MailingEventResubscribe',
332 'MailingEventSubscribe',
333 'MailingEventUnsubscribe',
334 'MailingRecipients',
335 'Constant',
336 'Entity',
337 'Location',
338 'Domain',
339 'Profile',
340 'CustomValue',
341 'Setting',
342 'User',
343 'Logging',
344 ];
345 if ($sequential === TRUE) {
346 return $entitiesWithout;
347 }
348 $entities = [];
349 foreach ($entitiesWithout as $e) {
350 $entities[] = [$e];
351 }
352 return $entities;
353 }
354
355 /**
356 * @param bool $sequential
357 *
358 * @return array
359 */
360 public static function toBeSkipped_custom_data_creatable($sequential = FALSE) {
361 $entitiesWithout = [
362 // Ones to fix.
363 'CaseContact',
364 'CustomField',
365 'CustomGroup',
366 'DashboardContact',
367 'Domain',
368 'File',
369 'FinancialType',
370 'LocBlock',
371 'MailingEventConfirm',
372 'MailingEventResubscribe',
373 'MailingEventSubscribe',
374 'MailingEventUnsubscribe',
375 'MembershipPayment',
376 'SavedSearch',
377 'UFJoin',
378 'UFField',
379 'PriceFieldValue',
380 'GroupContact',
381 'EntityTag',
382 'PledgePayment',
383 'Relationship',
384
385 // ones that are not real entities hence not extendable.
386 'ActivityType',
387 'Entity',
388 'Cxn',
389 'Constant',
390 'Attachment',
391 'CustomSearch',
392 'CustomValue',
393 'CxnApp',
394 'Extension',
395 'MailingContact',
396 'User',
397 'System',
398 'Setting',
399 'SystemLog',
400 'ReportTemplate',
401 'MailingRecipients',
402 'SurveyRespondant',
403 'Profile',
404 'Payment',
405 'Order',
406 'MailingGroup',
407 'Logging',
408 ];
409 if ($sequential === TRUE) {
410 return $entitiesWithout;
411 }
412 $entities = [];
413 foreach ($entitiesWithout as $e) {
414 $entities[] = [$e];
415 }
416 return $entities;
417 }
418
419 /**
420 * @param bool $sequential
421 *
422 * @return array
423 * @todo add metadata for ALL these entities
424 */
425 public static function toBeSkipped_getfields($sequential = FALSE) {
426 $entitiesWithMetadataNotYetFixed = ['ReportTemplate', 'CustomSearch'];
427 if ($sequential === TRUE) {
428 return $entitiesWithMetadataNotYetFixed;
429 }
430 $entities = [];
431 foreach ($entitiesWithMetadataNotYetFixed as $e) {
432 $entities[] = [$e];
433 }
434 return $entities;
435 }
436
437 /**
438 * Generate list of entities to test for get by id functions.
439 * @param bool $sequential
440 * @return array
441 * Entities to be skipped
442 */
443 public static function toBeSkipped_automock($sequential = FALSE) {
444 $entitiesWithoutGet = [
445 'MailingContact',
446 'EntityTag',
447 'Participant',
448 'Setting',
449 'SurveyRespondant',
450 'MailingRecipients',
451 'CustomSearch',
452 'Extension',
453 'ReportTemplate',
454 'System',
455 'Logging',
456 'Payment',
457 ];
458 if ($sequential === TRUE) {
459 return $entitiesWithoutGet;
460 }
461 $entities = [];
462 foreach ($entitiesWithoutGet as $e) {
463 $entities[] = [$e];
464 }
465 return $entities;
466 }
467
468 /**
469 * At this stage exclude the ones that don't pass & add them as we can troubleshoot them
470 * @param bool $sequential
471 * @return array
472 */
473 public static function toBeSkipped_updatesingle($sequential = FALSE) {
474 $entitiesWithout = [
475 'Attachment',
476 // pseudo-entity; testUpdateSingleValueAlter doesn't introspect properly on it. Multiple magic fields
477 'Mailing',
478 'MailingEventUnsubscribe',
479 'MailingEventSubscribe',
480 'Constant',
481 'Entity',
482 'Location',
483 'Profile',
484 'CustomValue',
485 'UFJoin',
486 'Relationship',
487 'RelationshipType',
488 'Note',
489 'Membership',
490 'Group',
491 'File',
492 'EntityTag',
493 'CustomField',
494 'CustomGroup',
495 'Contribution',
496 'ActivityType',
497 'MailingEventConfirm',
498 'Case',
499 'CaseContact',
500 'Contact',
501 'ContactType',
502 'MailingEventResubscribe',
503 'UFGroup',
504 'Activity',
505 'Event',
506 'GroupContact',
507 'MembershipPayment',
508 'Participant',
509 'LineItem',
510 'ContributionPage',
511 'Phone',
512 'PaymentProcessor',
513 'Setting',
514 'MailingContact',
515 'SystemLog',
516 //skip this because it doesn't make sense to update logs,
517 'Logging',
518 // Skip message template because workflow_id/workflow_name are sync'd.
519 'MessageTemplate',
520 ];
521 if ($sequential === TRUE) {
522 return $entitiesWithout;
523 }
524 $entities = [];
525 foreach ($entitiesWithout as $e) {
526 $entities[] = [
527 $e,
528 ];
529 }
530 // WTF
531 return ['pledge', 'MessageTemplate'];
532 return $entities;
533 }
534
535 /**
536 * At this stage exclude the ones that don't pass & add them as we can troubleshoot them
537 */
538 public static function toBeSkipped_getlimit() {
539 $entitiesWithout = [
540 'Case',
541 //case api has non-std mandatory fields one of (case_id, contact_id, activity_id, contact_id)
542 'EntityTag',
543 // non-standard api - has inappropriate mandatory fields & doesn't implement limit
544 'Event',
545 // failed 'check that a 5 limit returns 5' - probably is_template field is wrong or something, or could be limit doesn't work right
546 'Extension',
547 // can't handle creating 25
548 'Note',
549 // fails on 5 limit - probably a set up problem
550 'Setting',
551 //a bit of a pseudoapi - keys by domain
552 'Payment',
553 // pseudoapi - problems with creating required sub entities.
554 ];
555 return $entitiesWithout;
556 }
557
558 /**
559 * At this stage exclude the ones that don't pass & add them as we can troubleshoot them
560 */
561 public static function toBeSkipped_getSqlOperators() {
562 $entitiesWithout = [
563 //case api has non-std mandatory fields one of (case_id, contact_id, activity_id, contact_id)
564 'Case',
565 // on the todo list!
566 'Contact',
567 // non-standard api - has inappropriate mandatory fields & doesn't implement limit
568 'EntityTag',
569 // can't handle creating 25
570 'Extension',
571 // note has a default get that isn't implemented in createTestObject -meaning you don't 'get' them
572 'Note',
573 //a bit of a pseudoapi - keys by domain
574 'Setting',
575 ];
576
577 // The testSqlOperators fails sporadically on MySQL 5.5, which is deprecated anyway.
578 // Re:^^^ => the failure was probably correct behavior, and test is now fixed, but yeah 5.5 is deprecated, and don't care enough to verify.
579 // Test data providers should be able to run in pre-boot environment, so we connect directly to SQL server.
580 require_once 'DB.php';
581 $dsn = CRM_Utils_SQL::autoSwitchDSN(CIVICRM_DSN);
582 $db = DB::connect($dsn);
583 if ($db->connection instanceof mysqli && $db->connection->server_version < 50600) {
584 $entitiesWithout[] = 'Dedupe';
585 }
586
587 return $entitiesWithout;
588 }
589
590 /**
591 * @param $entity
592 * @param $key
593 *
594 * @return array
595 */
596 public function getKnownUnworkablesUpdateSingle($entity, $key) {
597 // can't update values are values for which updates don't result in the value being changed
598 $knownFailures = [
599 'ActionSchedule' => [
600 'cant_update' => [
601 'group_id',
602 ],
603 ],
604 'ActivityContact' => [
605 'cant_update' => [
606 'activity_id',
607 //we have an FK on activity_id + contact_id + record id so if we don't leave this one distinct we get an FK constraint error
608 ],
609 ],
610 'Address' => [
611 'cant_update' => [
612 //issues with country id - need to ensure same country
613 'state_province_id',
614 'world_region',
615 //creates relationship
616 'master_id',
617 ],
618 'cant_return' => ['street_parsing', 'skip_geocode', 'fix_address'],
619 ],
620 'Batch' => [
621 'cant_update' => [
622 // believe this field is defined in error
623 'entity_table',
624 ],
625 'cant_return' => [
626 'entity_table',
627 ],
628 ],
629 'CaseType' => [
630 'cant_update' => [
631 'definition',
632 ],
633 ],
634 'Domain' => ['cant_update' => ['domain_version']],
635 'MembershipBlock' => [
636 'cant_update' => [
637 // The fake/auto-generated values leave us unable to properly cleanup fake data
638 'entity_type',
639 'entity_id',
640 ],
641 ],
642 'MailingJob' => ['cant_update' => ['parent_id']],
643 'ContributionSoft' => [
644 'cant_update' => [
645 // can't be changed through api
646 'pcp_id',
647 ],
648 ],
649 'Email' => [
650 'cant_update' => [
651 // This is being legitimately manipulated to always have a valid primary - skip.
652 'is_primary',
653 ],
654 ],
655 'FinancialTrxn' => [
656 'cant_update' => [
657 // Altering fee amount will also cause net_amount to be recalculated.
658 'fee_amount',
659 ],
660 ],
661 'Navigation' => [
662 'cant_update' => [
663 // Weight is deliberately altered when this is changed - skip.
664 'parent_id',
665 ],
666 ],
667 'LocationType' => [
668 'cant_update' => [
669 // I'm on the fence about whether the test should skip or the behaviour is wrong.
670 // display_name is set to match name if display_name is not provided. It would be more 'normal'
671 // to only calculate a default IF id is not set - but perhaps the current behaviour is kind
672 // of what someone updating the name expects..
673 'name',
674 ],
675 ],
676 'Pledge' => [
677 'cant_update' => [
678 'pledge_original_installment_amount',
679 'installments',
680 'original_installment_amount',
681 'next_pay_date',
682 // can't be changed through API,
683 'amount',
684 ],
685 // if these are passed in they are retrieved from the wrong table
686 'break_return' => [
687 'honor_contact_id',
688 'cancel_date',
689 'contribution_page_id',
690 'financial_account_id',
691 'financial_type_id',
692 'currency',
693 ],
694 // can't be retrieved from api
695 'cant_return' => [
696 //due to uniquename missing
697 'honor_type_id',
698 'end_date',
699 'modified_date',
700 'acknowledge_date',
701 'start_date',
702 'frequency_day',
703 'currency',
704 'max_reminders',
705 'initial_reminder_day',
706 'additional_reminder_day',
707 'frequency_unit',
708 'pledge_contribution_page_id',
709 'pledge_status_id',
710 'pledge_campaign_id',
711 'pledge_financial_type_id',
712 ],
713 ],
714 'PaymentProcessorType' => [
715 'cant_update' => [
716 'billing_mode',
717 ],
718 'break_return' => [],
719 'cant_return' => [],
720 ],
721 'PriceFieldValue' => [
722 'cant_update' => [
723 //won't update as there is no 1 in the same price set
724 'weight',
725 ],
726 ],
727 'ReportInstance' => [
728 // View mode is part of the navigation which is not retrieved by the api.
729 'cant_return' => ['view_mode'],
730 ],
731 'StatusPreference' => [
732 'break_return' => [
733 'ignore_severity',
734 ],
735 ],
736 'UFField' => [
737 'cant_update' => [
738 // These fields get auto-adjusted by the BAO prior to saving
739 'weight',
740 'location_type_id',
741 'phone_type_id',
742 'website_type_id',
743 // Not a real field
744 'option.autoweight',
745 ],
746 'break_return' => [
747 // These fields get auto-adjusted by the BAO prior to saving
748 'weight',
749 'field_type',
750 'location_type_id',
751 'phone_type_id',
752 'website_type_id',
753 // Not a real field
754 'option.autoweight',
755 ],
756 ],
757 'JobLog' => [
758 // For better or worse triggers override.
759 'break_return' => ['run_time'],
760 'cant_update' => ['run_time'],
761 ],
762 ];
763 if (empty($knownFailures[$entity]) || empty($knownFailures[$entity][$key])) {
764 return [];
765 }
766 return $knownFailures[$entity][$key];
767 }
768
769 /* ----- testing the _get ----- */
770
771 /**
772 * @dataProvider toBeSkipped_get
773 * Entities that don't need a get action
774 * @param $Entity
775 */
776 public function testNotImplemented_get($Entity) {
777 $result = civicrm_api($Entity, 'Get', ['version' => 3]);
778 $this->assertEquals(1, $result['is_error']);
779 // $this->assertContains("API ($Entity, Get) does not exist", $result['error_message']);
780 $this->assertRegExp('/API (.*) does not exist/', $result['error_message']);
781 }
782
783 /**
784 * @dataProvider entities
785 * @param $Entity
786 */
787 public function testGetFields($Entity) {
788 if (in_array($Entity, $this->deprecatedAPI) || $Entity == 'Entity' || $Entity == 'CustomValue') {
789 return;
790 }
791
792 $result = civicrm_api($Entity, 'getfields', ['version' => 3]);
793 $this->assertTrue(is_array($result['values']), "$Entity ::get fields doesn't return values array in line " . __LINE__);
794 foreach ($result['values'] as $key => $value) {
795 $this->assertTrue(is_array($value), $Entity . "::" . $key . " is not an array in line " . __LINE__);
796 }
797 }
798
799 /**
800 * @dataProvider entities_get
801 * @param $Entity
802 */
803 public function testEmptyParam_get($Entity) {
804
805 if (in_array($Entity, $this->toBeImplemented['get'])) {
806 // $this->markTestIncomplete("civicrm_api3_{$Entity}_get to be implemented");
807 return;
808 }
809 $result = civicrm_api($Entity, 'Get', []);
810 $this->assertEquals(1, $result['is_error']);
811 $this->assertContains("Unknown api version", $result['error_message']);
812 }
813
814 /**
815 * @dataProvider entities_get
816 * @Xdepends testEmptyParam_get // no need to test the simple if the empty doesn't work/is skipped. doesn't seem to work
817 * @param $Entity
818 */
819 public function testSimple_get($Entity) {
820 // $this->markTestSkipped("test gives core error on test server (but not on our locals). Skip until we can get server to pass");
821 if (in_array($Entity, $this->toBeImplemented['get'])) {
822 return;
823 }
824 $result = civicrm_api($Entity, 'Get', ['version' => 3]);
825 // @TODO: list the get that have mandatory params
826 if ($result['is_error']) {
827 $this->assertContains("Mandatory key(s) missing from params array", $result['error_message']);
828 // either id or contact_id or entity_id is one of the field missing
829 $this->assertContains("id", $result['error_message']);
830 }
831 else {
832 $this->assertEquals(3, $result['version']);
833 $this->assertArrayHasKey('count', $result);
834 $this->assertArrayHasKey('values', $result);
835 }
836 }
837
838 /**
839 * @dataProvider custom_data_incl_non_std_entities_get
840 * @param $entityName
841 */
842 public function testCustomDataGet($entityName) {
843 if ($entityName === 'Note') {
844 $this->markTestIncomplete('Note can not be processed here because of a vagary in the note api, it adds entity_table=contact to the get params when id is not present - which makes sense almost always but kills this test');
845 }
846 $this->quickCleanup(['civicrm_uf_match']);
847 // so subsidiary activities are created
848 $this->createLoggedInUser();
849
850 $entitiesWithNamingIssues = [
851 'SmsProvider' => 'Provider',
852 'AclRole' => 'EntityRole',
853 'MailingEventQueue' => 'Queue',
854 'Dedupe' => 'PrevNextCache',
855 ];
856
857 $usableName = !empty($entitiesWithNamingIssues[$entityName]) ? $entitiesWithNamingIssues[$entityName] : $entityName;
858 $optionName = CRM_Core_DAO_AllCoreTables::getTableForClass(CRM_Core_DAO_AllCoreTables::getFullName($usableName));
859
860 if (!isset(CRM_Core_BAO_CustomQuery::$extendsMap[$entityName])) {
861 $createdValue = $this->callAPISuccess('OptionValue', 'create', [
862 'option_group_id' => 'cg_extend_objects',
863 'label' => $usableName,
864 'value' => $usableName,
865 'name' => $optionName,
866 ]);
867 }
868 // We are not passing 'check_permissions' so the the more limited permissions *should* be
869 // ignored but per CRM-17700 there is a history of custom data applying permissions when it shouldn't.
870 CRM_Core_Config::singleton()->userPermissionClass->permissions = ['access CiviCRM', 'view my contact'];
871 $objects = $this->getMockableBAOObjects($entityName, 1);
872
873 // simple custom field
874 $ids = $this->entityCustomGroupWithSingleFieldCreate(__FUNCTION__, $usableName . 'Test.php');
875 $customFieldName = 'custom_' . $ids['custom_field_id'];
876 $params = ['id' => $objects[0]->id, 'custom_' . $ids['custom_field_id'] => "custom string"];
877 $result = $this->callAPISuccess($entityName, 'create', $params);
878 $this->assertTrue(isset($result['id']), 'no id on ' . $entityName);
879 $getParams = ['id' => $result['id'], 'return' => [$customFieldName]];
880 $check = $this->callAPISuccess($entityName, 'get', $getParams);
881 $this->assertTrue(!empty($check['values'][$check['id']][$customFieldName]), 'Custom data not present for ' . $entityName);
882 $this->assertEquals("custom string", $check['values'][$check['id']][$customFieldName], 'Custom data not present for ' . $entityName);
883 $this->customFieldDelete($ids['custom_field_id']);
884 $this->customGroupDelete($ids['custom_group_id']);
885
886 $ids2 = $this->entityCustomGroupWithSingleStringMultiSelectFieldCreate(__FUNCTION__, $usableName . 'Test.php');
887 $customFieldNameMultiSelect = 'custom_' . $ids2['custom_field_id'];
888 // String custom field, Multi-Select html type
889 foreach ($ids2['custom_field_group_options'] as $option_value => $option_label) {
890 $params = ['id' => $objects[0]->id, 'custom_' . $ids2['custom_field_id'] => $option_value];
891 $result = $this->callAPISuccess($entityName, 'create', $params);
892 $getParams = [$customFieldNameMultiSelect => $option_value, 'return' => [$customFieldNameMultiSelect]];
893 $this->callAPISuccessGetCount($entityName, $getParams, 1);
894 }
895
896 // cleanup
897 $this->customFieldDelete($ids2['custom_field_id']);
898 $this->customGroupDelete($ids2['custom_group_id']);
899
900 $this->callAPISuccess($entityName, 'delete', ['id' => $result['id']]);
901 $this->quickCleanup(['civicrm_uf_match']);
902 if (!empty($createdValue)) {
903 $this->callAPISuccess('OptionValue', 'delete', ['id' => $createdValue['id']]);
904 }
905 }
906
907 /**
908 * @dataProvider entities_get
909 * @param $Entity
910 */
911 public function testAcceptsOnlyID_get($Entity) {
912 // big random number. fun fact: if you multiply it by pi^e, the result is another random number, but bigger ;)
913 $nonExistantID = 30867307034;
914 if (in_array($Entity, $this->toBeImplemented['get'])
915 || in_array($Entity, $this->toBeSkipped_getByID())
916 ) {
917 return;
918 }
919
920 // FIXME
921 // the below function returns different values and hence an early return
922 // we'll fix this once beta1 is released
923 // return;
924
925 $result = civicrm_api($Entity, 'Get', ['version' => 3, 'id' => $nonExistantID]);
926
927 if ($result['is_error']) {
928 // just to get a clearer message in the log
929 $this->assertEquals("only id should be enough", $result['error_message']);
930 }
931 if (!in_array($Entity, $this->onlyIDNonZeroCount['get'])) {
932 $this->assertEquals(0, $result['count']);
933 }
934 }
935
936 /**
937 * Test getlist works
938 * @dataProvider entities_get
939 * @param $Entity
940 */
941 public function testGetList($Entity) {
942 if (in_array($Entity, $this->toBeImplemented['get'])
943 || in_array($Entity, $this->toBeSkipped_getByID())
944 ) {
945 return;
946 }
947 if (in_array($Entity, ['ActivityType', 'SurveyRespondant'])) {
948 $this->markTestSkipped();
949 }
950 $this->callAPISuccess($Entity, 'getlist', ['label_field' => 'id']);
951 }
952
953 /**
954 * Test getlist works when entity is lowercase
955 * @dataProvider entities_get
956 * @param $Entity
957 */
958 public function testGetListLowerCaseEntity($Entity) {
959 if (in_array($Entity, $this->toBeImplemented['get'])
960 || in_array($Entity, $this->toBeSkipped_getByID())
961 ) {
962 return;
963 }
964 if (in_array($Entity, ['ActivityType', 'SurveyRespondant'])) {
965 $this->markTestSkipped();
966 }
967 if ($Entity == 'UFGroup') {
968 $Entity = 'ufgroup';
969 }
970 $this->callAPISuccess($Entity, 'getlist', ['label_field' => 'id']);
971 }
972
973 /**
974 * Create two entities and make sure we can fetch them individually by ID.
975 *
976 * @dataProvider entities_get
977 *
978 * limitations include the problem with avoiding loops when creating test objects -
979 * hence FKs only set by createTestObject when required. e.g parent_id on campaign is not being followed through
980 * Currency - only seems to support US
981 * @param $entityName
982 */
983 public function testByID_get($entityName) {
984 if (in_array($entityName, self::toBeSkipped_automock(TRUE))) {
985 // $this->markTestIncomplete("civicrm_api3_{$Entity}_create to be implemented");
986 return;
987 }
988
989 $baos = $this->getMockableBAOObjects($entityName);
990 list($baoObj1, $baoObj2) = $baos;
991
992 // fetch first by ID
993 $result = $this->callAPISuccess($entityName, 'get', [
994 'id' => $baoObj1->id,
995 ]);
996
997 $this->assertTrue(!empty($result['values'][$baoObj1->id]), 'Should find first object by id');
998 $this->assertEquals($baoObj1->id, $result['values'][$baoObj1->id]['id'], 'Should find id on first object');
999 $this->assertEquals(1, count($result['values']));
1000
1001 // fetch second by ID
1002 $result = $this->callAPISuccess($entityName, 'get', [
1003 'id' => $baoObj2->id,
1004 ]);
1005 $this->assertTrue(!empty($result['values'][$baoObj2->id]), 'Should find second object by id');
1006 $this->assertEquals($baoObj2->id, $result['values'][$baoObj2->id]['id'], 'Should find id on second object');
1007 $this->assertEquals(1, count($result['values']));
1008 }
1009
1010 /**
1011 * Ensure that the "get" operation accepts limiting the #result records.
1012 *
1013 * TODO Consider making a separate entity list ("entities_getlimit")
1014 * For the moment, the "entities_updatesingle" list should give a good
1015 * sense for which entities support createTestObject
1016 *
1017 * @dataProvider entities_getlimit
1018 *
1019 * @param string $entityName
1020 */
1021 public function testLimit($entityName) {
1022 // each case is array(0 => $inputtedApiOptions, 1 => $expectedResultCount)
1023 $cases = [];
1024 $cases[] = [
1025 ['options' => ['limit' => NULL]],
1026 30,
1027 'check that a NULL limit returns unlimited',
1028 ];
1029 $cases[] = [
1030 ['options' => ['limit' => FALSE]],
1031 30,
1032 'check that a FALSE limit returns unlimited',
1033 ];
1034 $cases[] = [
1035 ['options' => ['limit' => 0]],
1036 30,
1037 'check that a 0 limit returns unlimited',
1038 ];
1039 $cases[] = [
1040 ['options' => ['limit' => 5]],
1041 5,
1042 'check that a 5 limit returns 5',
1043 ];
1044 $cases[] = [
1045 [],
1046 25,
1047 'check that no limit returns 25',
1048 ];
1049
1050 $baoString = _civicrm_api3_get_BAO($entityName);
1051 if (empty($baoString)) {
1052 $this->markTestIncomplete("Entity [$entityName] cannot be mocked - no known DAO");
1053 return;
1054 }
1055
1056 // make 30 test items -- 30 > 25 (the default limit)
1057 $ids = [];
1058 for ($i = 0; $i < 30; $i++) {
1059 $baoObj = CRM_Core_DAO::createTestObject($baoString, ['currency' => 'USD']);
1060 $ids[] = $baoObj->id;
1061 }
1062
1063 // each case is array(0 => $inputtedApiOptions, 1 => $expectedResultCount)
1064 foreach ($cases as $case) {
1065 $this->checkLimitAgainstExpected($entityName, $case[0], $case[1], $case[2]);
1066
1067 //non preferred / legacy syntax
1068 if (isset($case[0]['options']['limit'])) {
1069 $this->checkLimitAgainstExpected($entityName, ['rowCount' => $case[0]['options']['limit']], $case[1], $case[2]);
1070 $this->checkLimitAgainstExpected($entityName, ['option_limit' => $case[0]['options']['limit']], $case[1], $case[2]);
1071 $this->checkLimitAgainstExpected($entityName, ['option.limit' => $case[0]['options']['limit']], $case[1], $case[2]);
1072 }
1073 }
1074 foreach ($ids as $id) {
1075 CRM_Core_DAO::deleteTestObjects($baoString, ['id' => $id]);
1076 }
1077 }
1078
1079 /**
1080 * Ensure that the "get" operation accepts limiting the #result records.
1081 *
1082 * @dataProvider entities_getSqlOperators
1083 *
1084 * @param string $entityName
1085 */
1086 public function testSqlOperators($entityName) {
1087 $toBeIgnored = array_merge($this->toBeImplemented['get'],
1088 $this->deprecatedAPI,
1089 $this->toBeSkipped_get(TRUE),
1090 $this->toBeSkipped_getByID()
1091 );
1092 if (in_array($entityName, $toBeIgnored)) {
1093 return;
1094 }
1095
1096 $baoString = _civicrm_api3_get_BAO($entityName);
1097
1098 $entities = $this->callAPISuccess($entityName, 'get', ['options' => ['limit' => 0], 'return' => 'id']);
1099 $entities = array_keys($entities['values']);
1100 $totalEntities = count($entities);
1101 if ($totalEntities < 3) {
1102 $ids = [];
1103 for ($i = 0; $i < 3 - $totalEntities; $i++) {
1104 $baoObj = CRM_Core_DAO::createTestObject($baoString, ['currency' => 'USD']);
1105 $ids[] = $baoObj->id;
1106 }
1107 $totalEntities = 3;
1108 }
1109 $entities = $this->callAPISuccess($entityName, 'get', ['options' => ['limit' => 0, 'sort' => 'id']]);
1110 $entities = array_keys($entities['values']);
1111 $this->assertGreaterThan(2, $totalEntities);
1112 $this->callAPISuccess($entityName, 'getsingle', ['id' => ['IN' => [$entities[0]]]]);
1113 $this->callAPISuccessGetCount($entityName, ['id' => ['NOT IN' => [$entities[0]]]], $totalEntities - 1);
1114 $this->callAPISuccessGetCount($entityName, ['id' => ['>' => $entities[0]]], $totalEntities - 1);
1115 }
1116
1117 /**
1118 * Check that get fetches an appropriate number of results.
1119 *
1120 * @param string $entityName
1121 * Name of entity to test.
1122 * @param array $params
1123 * @param int $limit
1124 * @param string $message
1125 */
1126 public function checkLimitAgainstExpected($entityName, $params, $limit, $message) {
1127 $result = $this->callAPISuccess($entityName, 'get', $params);
1128 if ($limit == 30) {
1129 $this->assertGreaterThanOrEqual($limit, $result['count'], $message);
1130 $this->assertGreaterThanOrEqual($limit, $result['count'], $message);
1131 }
1132 else {
1133 $this->assertEquals($limit, $result['count'], $message);
1134 $this->assertEquals($limit, count($result['values']), $message);
1135 }
1136 }
1137
1138 /**
1139 * Create two entities and make sure we can fetch them individually by ID (e.g. using "contact_id=>2"
1140 * or "group_id=>4")
1141 *
1142 * @dataProvider entities_get
1143 *
1144 * limitations include the problem with avoiding loops when creating test objects -
1145 * hence FKs only set by createTestObject when required. e.g parent_id on campaign is not being followed through
1146 * Currency - only seems to support US
1147 * @param $entityName
1148 * @throws \PHPUnit\Framework\IncompleteTestError
1149 */
1150 public function testByIDAlias_get($entityName) {
1151 if (in_array($entityName, self::toBeSkipped_automock(TRUE))) {
1152 // $this->markTestIncomplete("civicrm_api3_{$Entity}_create to be implemented");
1153 return;
1154 }
1155
1156 $baoString = _civicrm_api3_get_BAO($entityName);
1157 if (empty($baoString)) {
1158 $this->markTestIncomplete("Entity [$entityName] cannot be mocked - no known DAO");
1159 return;
1160 }
1161
1162 $idFieldName = _civicrm_api_get_entity_name_from_camel($entityName) . '_id';
1163
1164 // create entities
1165 $baoObj1 = CRM_Core_DAO::createTestObject($baoString, ['currency' => 'USD']);
1166 $this->assertTrue(is_int($baoObj1->id), 'check first id');
1167 $this->deletableTestObjects[$baoString][] = $baoObj1->id;
1168 $baoObj2 = CRM_Core_DAO::createTestObject($baoString, ['currency' => 'USD']);
1169 $this->assertTrue(is_int($baoObj2->id), 'check second id');
1170 $this->deletableTestObjects[$baoString][] = $baoObj2->id;
1171
1172 // fetch first by ID
1173 $result = civicrm_api($entityName, 'get', [
1174 'version' => 3,
1175 $idFieldName => $baoObj1->id,
1176 ]);
1177 $this->assertAPISuccess($result);
1178 $this->assertTrue(!empty($result['values'][$baoObj1->id]), 'Should find first object by id');
1179 $this->assertEquals($baoObj1->id, $result['values'][$baoObj1->id]['id'], 'Should find id on first object');
1180 $this->assertEquals(1, count($result['values']));
1181
1182 // fetch second by ID
1183 $result = civicrm_api($entityName, 'get', [
1184 'version' => 3,
1185 $idFieldName => $baoObj2->id,
1186 ]);
1187 $this->assertAPISuccess($result);
1188 $this->assertTrue(!empty($result['values'][$baoObj2->id]), 'Should find second object by id');
1189 $this->assertEquals($baoObj2->id, $result['values'][$baoObj2->id]['id'], 'Should find id on second object');
1190 $this->assertEquals(1, count($result['values']));
1191 }
1192
1193 /**
1194 * @dataProvider entities_get
1195 * @param $Entity
1196 */
1197 public function testNonExistantID_get($Entity) {
1198 // cf testAcceptsOnlyID_get
1199 $nonExistantID = 30867307034;
1200 if (in_array($Entity, $this->toBeImplemented['get'])) {
1201 return;
1202 }
1203
1204 $result = civicrm_api($Entity, 'Get', ['version' => 3, 'id' => $nonExistantID]);
1205
1206 // redundant with testAcceptsOnlyID_get
1207 if ($result['is_error']) {
1208 return;
1209 }
1210
1211 $this->assertArrayHasKey('version', $result);
1212 $this->assertEquals(3, $result['version']);
1213 if (!in_array($Entity, $this->onlyIDNonZeroCount['get'])) {
1214 $this->assertEquals(0, $result['count']);
1215 }
1216 }
1217
1218 /* ---- testing the _create ---- */
1219
1220 /**
1221 * @dataProvider toBeSkipped_create
1222 * entities that don't need a create action
1223 * @param $Entity
1224 */
1225 public function testNotImplemented_create($Entity) {
1226 $result = civicrm_api($Entity, 'Create', ['version' => 3]);
1227 $this->assertEquals(1, $result['is_error']);
1228 $this->assertContains(strtolower("API ($Entity, Create) does not exist"), strtolower($result['error_message']));
1229 }
1230
1231 /**
1232 * @dataProvider entities
1233 * @expectedException CiviCRM_API3_Exception
1234 * @param $Entity
1235 */
1236 public function testWithoutParam_create($Entity) {
1237 if ($Entity === 'Setting') {
1238 $this->markTestSkipped('It seems OK for setting to skip here as it silently sips invalid params');
1239 }
1240 elseif ($Entity === 'Mailing') {
1241 $this->markTestSkipped('It seems OK for "Mailing" to skip here because you can create empty drafts');
1242 }
1243 // should create php complaining that a param is missing
1244 civicrm_api3($Entity, 'Create');
1245 }
1246
1247 /**
1248 * @dataProvider entities_create
1249 *
1250 * Check that create doesn't work with an invalid
1251 * @param $Entity
1252 * @throws \PHPUnit\Framework\IncompleteTestError
1253 */
1254 public function testInvalidSort_get($Entity) {
1255 $invalidEntitys = ['ActivityType', 'Setting', 'System'];
1256 if (in_array($Entity, $invalidEntitys)) {
1257 $this->markTestSkipped('It seems OK for ' . $Entity . ' to skip here as it silently sips invalid params');
1258 }
1259 $result = $this->callAPIFailure($Entity, 'get', ['options' => ['sort' => 'sleep(1)']]);
1260 }
1261
1262 /**
1263 * @dataProvider entities_create
1264 *
1265 * Check that create doesn't work with an invalid
1266 * @param $Entity
1267 * @throws \PHPUnit\Framework\IncompleteTestError
1268 */
1269 public function testValidSortSingleArrayById_get($Entity) {
1270 $invalidEntitys = ['ActivityType', 'Setting', 'System'];
1271 $tests = [
1272 'id' => '_id',
1273 'id desc' => '_id desc',
1274 'id DESC' => '_id DESC',
1275 'id ASC' => '_id ASC',
1276 'id asc' => '_id asc',
1277 ];
1278 foreach ($tests as $test => $expected) {
1279 if (in_array($Entity, $invalidEntitys)) {
1280 $this->markTestSkipped('It seems OK for ' . $Entity . ' to skip here as it silently ignores passed in params');
1281 }
1282 $params = ['sort' => [$test]];
1283 $result = _civicrm_api3_get_options_from_params($params, FALSE, $Entity, 'get');
1284 $lowercase_entity = _civicrm_api_get_entity_name_from_camel($Entity);
1285 $this->assertEquals($lowercase_entity . $expected, $result['sort']);
1286 }
1287 }
1288
1289 /**
1290 * @dataProvider entities_create
1291 *
1292 * Check that create doesn't work with an invalid
1293 * @param $Entity
1294 * @throws \PHPUnit\Framework\IncompleteTestError
1295 */
1296 public function testInvalidID_create($Entity) {
1297 // turn test off for noew
1298 $this->markTestIncomplete("Entity [ $Entity ] cannot be mocked - no known DAO");
1299 return;
1300 if (in_array($Entity, $this->toBeImplemented['create'])) {
1301 // $this->markTestIncomplete("civicrm_api3_{$Entity}_create to be implemented");
1302 return;
1303 }
1304 $result = $this->callAPIFailure($Entity, 'Create', ['id' => 999]);
1305 }
1306
1307 /**
1308 * @dataProvider entities_updatesingle
1309 *
1310 * limitations include the problem with avoiding loops when creating test objects -
1311 * hence FKs only set by createTestObject when required. e.g parent_id on campaign is not being followed through
1312 * Currency - only seems to support US
1313 * @param $entityName
1314 */
1315 public function testCreateSingleValueAlter($entityName) {
1316 if (in_array($entityName, $this->toBeImplemented['create'])) {
1317 // $this->markTestIncomplete("civicrm_api3_{$Entity}_create to be implemented");
1318 return;
1319 }
1320
1321 $floatFields = [];
1322 $baoString = _civicrm_api3_get_BAO($entityName);
1323 $this->assertNotEmpty($baoString, $entityName);
1324 $this->assertNotEmpty($entityName, $entityName);
1325 $fieldsGet = $fields = $this->callAPISuccess($entityName, 'getfields', ['action' => 'get', 'options' => ['get_options' => 'all']]);
1326 if ($entityName != 'Pledge') {
1327 $fields = $this->callAPISuccess($entityName, 'getfields', ['action' => 'create', 'options' => ['get_options' => 'all']]);
1328 }
1329 $fields = $fields['values'];
1330 $return = array_keys($fieldsGet['values']);
1331 $valuesNotToReturn = $this->getKnownUnworkablesUpdateSingle($entityName, 'break_return');
1332 // these can't be requested as return values
1333 $entityValuesThatDoNotWork = array_merge(
1334 $this->getKnownUnworkablesUpdateSingle($entityName, 'cant_update'),
1335 $this->getKnownUnworkablesUpdateSingle($entityName, 'cant_return'),
1336 $valuesNotToReturn
1337 );
1338
1339 $return = array_diff($return, $valuesNotToReturn);
1340 $baoObj = new CRM_Core_DAO();
1341 $baoObj->createTestObject($baoString, ['currency' => 'USD'], 2, 0);
1342
1343 $getEntities = $this->callAPISuccess($entityName, 'get', [
1344 'sequential' => 1,
1345 'return' => $return,
1346 'options' => [
1347 'sort' => 'id DESC',
1348 'limit' => 2,
1349 ],
1350 ]);
1351
1352 // lets use first rather than assume only one exists
1353 $entity = $getEntities['values'][0];
1354 $entity2 = $getEntities['values'][1];
1355 $this->deletableTestObjects[$baoString][] = $entity['id'];
1356 $this->deletableTestObjects[$baoString][] = $entity2['id'];
1357 // Skip these fields that we never really expect to update well.
1358 $genericFieldsToSkip = ['currency', 'id', strtolower($entityName) . '_id', 'is_primary'];
1359 foreach ($fields as $field => $specs) {
1360 $resetFKTo = NULL;
1361 $fieldName = $field;
1362
1363 if (in_array($field, $genericFieldsToSkip)
1364 || in_array($field, $entityValuesThatDoNotWork)
1365 ) {
1366 //@todo id & entity_id are correct but we should fix currency & frequency_day
1367 continue;
1368 }
1369 $this->assertArrayHasKey('type', $specs, "the _spec function for $entityName field $field does not specify the type");
1370 switch ($specs['type']) {
1371 case CRM_Utils_Type::T_DATE:
1372 $entity[$fieldName] = '2012-05-20';
1373 break;
1374
1375 case CRM_Utils_Type::T_TIMESTAMP:
1376 case 12:
1377 $entity[$fieldName] = '2012-05-20 03:05:20';
1378 break;
1379
1380 case CRM_Utils_Type::T_STRING:
1381 case CRM_Utils_Type::T_BLOB:
1382 case CRM_Utils_Type::T_MEDIUMBLOB:
1383 case CRM_Utils_Type::T_TEXT:
1384 case CRM_Utils_Type::T_LONGTEXT:
1385 case CRM_Utils_Type::T_EMAIL:
1386 if ($fieldName == 'form_values' && $entityName == 'SavedSearch') {
1387 // This is a hack for the SavedSearch API.
1388 // It expects form_values to be an array.
1389 // If you want to fix this, you should definitely read this forum
1390 // post.
1391 // http://forum.civicrm.org/index.php/topic,33990.0.html
1392 // See also my question on the CiviCRM Stack Exchange:
1393 // https://civicrm.stackexchange.com/questions/3437
1394 $entity[$fieldName] = ['sort_name' => "SortName2"];
1395 }
1396 else {
1397 $entity[$fieldName] = substr('New String', 0, CRM_Utils_Array::Value('maxlength', $specs, 100));
1398 if ($fieldName == 'email') {
1399 $entity[$fieldName] = strtolower($entity[$fieldName]);
1400 }
1401 // typecast with array to satisfy changes made in CRM-13160
1402 if ($entityName == 'MembershipType' && in_array($fieldName, [
1403 'relationship_type_id',
1404 'relationship_direction',
1405 ])) {
1406 $entity[$fieldName] = (array) $entity[$fieldName];
1407 }
1408 }
1409 break;
1410
1411 case CRM_Utils_Type::T_INT:
1412 // probably created with a 1
1413 if ($fieldName == 'weight') {
1414 $entity[$fieldName] = 2;
1415 }
1416 elseif (!empty($specs['FKClassName'])) {
1417 if ($specs['FKClassName'] == $baoString) {
1418 $entity[$fieldName] = (string) $entity2['id'];
1419 }
1420 else {
1421 if (!empty($entity[$fieldName])) {
1422 $resetFKTo = [$fieldName => $entity[$fieldName]];
1423 }
1424 $entity[$fieldName] = (string) empty($entity2[$field]) ? '' : $entity2[$field];
1425 //todo - there isn't always something set here - & our checking on unset values is limited
1426 if (empty($entity[$field])) {
1427 unset($entity[$field]);
1428 }
1429 }
1430 }
1431 else {
1432 $entity[$fieldName] = '6';
1433 }
1434 break;
1435
1436 case CRM_Utils_Type::T_BOOLEAN:
1437 // probably created with a 1
1438 $entity[$fieldName] = '0';
1439 break;
1440
1441 case CRM_Utils_Type::T_FLOAT:
1442 case CRM_Utils_Type::T_MONEY:
1443 $floatFields[] = $field;
1444 $entity[$field] = '22.75';
1445 break;
1446
1447 case CRM_Utils_Type::T_URL:
1448 $entity[$field] = 'warm.beer.com';
1449 }
1450 if (empty($specs['FKClassName']) && (!empty($specs['pseudoconstant']) || !empty($specs['options']))) {
1451 $options = CRM_Utils_Array::value('options', $specs, []);
1452 if (!$options) {
1453 //eg. pdf_format id doesn't ship with any
1454 if (isset($specs['pseudoconstant']['optionGroupName'])) {
1455 $optionValue = $this->callAPISuccess('option_value', 'create', [
1456 'option_group_id' => $specs['pseudoconstant']['optionGroupName'],
1457 'label' => 'new option value',
1458 'sequential' => 1,
1459 ]);
1460 $optionValue = $optionValue['values'];
1461 $keyColumn = CRM_Utils_Array::value('keyColumn', $specs['pseudoconstant'], 'value');
1462 $options[$optionValue[0][$keyColumn]] = 'new option value';
1463 }
1464 }
1465 $entity[$field] = array_rand($options);
1466 }
1467 if (!empty($specs['FKClassName']) && !empty($specs['pseudoconstant'])) {
1468 // in the weird situation where a field has both an fk and pseudoconstant defined,
1469 // e.g. campaign_id field, need to flush caches.
1470 // FIXME: Why doesn't creating a campaign clear caches?
1471 civicrm_api3($entityName, 'getfields', ['cache_clear' => 1]);
1472 }
1473 $updateParams = [
1474 'id' => $entity['id'],
1475 $field => $entity[$field] ?? NULL,
1476 ];
1477 if (isset($updateParams['financial_type_id']) && in_array($entityName, ['Grant'])) {
1478 //api has special handling on these 2 fields for backward compatibility reasons
1479 $entity['contribution_type_id'] = $updateParams['financial_type_id'];
1480 }
1481 if (isset($updateParams['next_sched_contribution_date']) && in_array($entityName, ['ContributionRecur'])) {
1482 //api has special handling on these 2 fields for backward compatibility reasons
1483 $entity['next_sched_contribution'] = $updateParams['next_sched_contribution_date'];
1484 }
1485 if (isset($updateParams['image'])) {
1486 // Image field is passed through simplifyURL function so may be different, do the same here for comparison
1487 $entity['image'] = CRM_Utils_String::simplifyURL($updateParams['image'], TRUE);
1488 }
1489 if (isset($updateParams['thumbnail'])) {
1490 // Thumbnail field is passed through simplifyURL function so may be different, do the same here for comparison
1491 $entity['thumbnail'] = CRM_Utils_String::simplifyURL($updateParams['thumbnail'], TRUE);
1492 }
1493
1494 $update = $this->callAPISuccess($entityName, 'create', $updateParams);
1495 $checkParams = [
1496 'id' => $entity['id'],
1497 'sequential' => 1,
1498 'return' => $return,
1499 'options' => [
1500 'sort' => 'id DESC',
1501 'limit' => 2,
1502 ],
1503 ];
1504
1505 $checkEntity = $this->callAPISuccess($entityName, 'getsingle', $checkParams);
1506 if (!empty($specs['serialize']) && !is_array($checkEntity[$field])) {
1507 // Put into serialized format for comparison if 'get' has not returned serialized.
1508 $entity[$field] = CRM_Core_DAO::serializeField($checkEntity[$field], $specs['serialize']);
1509 }
1510
1511 foreach ($floatFields as $floatField) {
1512 $checkEntity[$floatField] = rtrim($checkEntity[$floatField], "0");
1513 }
1514 $this->assertAPIArrayComparison($entity, $checkEntity, [], "checking if $fieldName was correctly updated\n" . print_r([
1515 'update-params' => $updateParams,
1516 'update-result' => $update,
1517 'getsingle-params' => $checkParams,
1518 'getsingle-result' => $checkEntity,
1519 'expected entity' => $entity,
1520 ], TRUE));
1521 if ($resetFKTo) {
1522 //reset the foreign key fields because otherwise our cleanup routine fails & some other unexpected stuff can kick in
1523 $entity = array_merge($entity, $resetFKTo);
1524 $updateParams = array_merge($updateParams, $resetFKTo);
1525 $this->callAPISuccess($entityName, 'create', $updateParams);
1526 if (isset($updateParams['financial_type_id']) && in_array($entityName, ['Grant'])) {
1527 //api has special handling on these 2 fields for backward compatibility reasons
1528 $entity['contribution_type_id'] = $updateParams['financial_type_id'];
1529 }
1530 if (isset($updateParams['next_sched_contribution_date']) && in_array($entityName, ['ContributionRecur'])) {
1531 //api has special handling on these 2 fields for backward compatibility reasons
1532 $entity['next_sched_contribution'] = $updateParams['next_sched_contribution_date'];
1533 }
1534 }
1535 }
1536 }
1537
1538 /* ---- testing the _getFields ---- */
1539
1540 /* ---- testing the _delete ---- */
1541
1542 /**
1543 * @dataProvider toBeSkipped_delete
1544 * entities that don't need a delete action
1545 * @param $Entity
1546 */
1547 public function testNotImplemented_delete($Entity) {
1548 $nonExistantID = 151416349;
1549 $result = civicrm_api($Entity, 'Delete', ['version' => 3, 'id' => $nonExistantID]);
1550 $this->assertEquals(1, $result['is_error']);
1551 $this->assertContains(strtolower("API ($Entity, Delete) does not exist"), strtolower($result['error_message']));
1552 }
1553
1554 /**
1555 * @dataProvider entities_delete
1556 * @param $Entity
1557 */
1558 public function testEmptyParam_delete($Entity) {
1559 if (in_array($Entity, $this->toBeImplemented['delete'])) {
1560 // $this->markTestIncomplete("civicrm_api3_{$Entity}_delete to be implemented");
1561 return;
1562 }
1563 $result = civicrm_api($Entity, 'Delete', []);
1564 $this->assertEquals(1, $result['is_error']);
1565 $this->assertContains("Unknown api version", $result['error_message']);
1566 }
1567
1568 /**
1569 * @dataProvider entities_delete
1570 * @param $Entity
1571 * @throws \PHPUnit\Framework\IncompleteTestError
1572 */
1573 public function testInvalidID_delete($Entity) {
1574 $result = $this->callAPIFailure($Entity, 'Delete', ['id' => 999999]);
1575 }
1576
1577 /**
1578 * Create two entities and make sure delete action only deletes one!
1579 *
1580 * @dataProvider entities_delete
1581 *
1582 * limitations include the problem with avoiding loops when creating test objects -
1583 * hence FKs only set by createTestObject when required. e.g parent_id on campaign is not being followed through
1584 * Currency - only seems to support US
1585 * @param $entityName
1586 * @throws \PHPUnit\Framework\IncompleteTestError
1587 */
1588 public function testByID_delete($entityName) {
1589 // turn test off for noew
1590 $this->markTestIncomplete("Entity [$entityName] cannot be mocked - no known DAO");
1591 return;
1592
1593 if (in_array($entityName, self::toBeSkipped_automock(TRUE))) {
1594 // $this->markTestIncomplete("civicrm_api3_{$Entity}_create to be implemented");
1595 return;
1596 }
1597 $startCount = $this->callAPISuccess($entityName, 'getcount', []);
1598 $createcount = 2;
1599 $baos = $this->getMockableBAOObjects($entityName, $createcount);
1600 list($baoObj1, $baoObj2) = $baos;
1601
1602 // make sure exactly 2 exist
1603 $result = $this->callAPISuccess($entityName, 'getcount', [],
1604 $createcount + $startCount
1605 );
1606
1607 $this->callAPISuccess($entityName, 'delete', ['id' => $baoObj2->id]);
1608 //make sure 1 less exists now
1609 $result = $this->callAPISuccess($entityName, 'getcount', [],
1610 ($createcount + $startCount) - 1
1611 );
1612
1613 //make sure id #1 exists
1614 $result = $this->callAPISuccess($entityName, 'getcount', ['id' => $baoObj1->id],
1615 1
1616 );
1617 //make sure id #2 desn't exist
1618 $result = $this->callAPISuccess($entityName, 'getcount', ['id' => $baoObj2->id],
1619 0
1620 );
1621 }
1622
1623 /**
1624 * Create two entities and make sure delete action only deletes one!
1625 *
1626 * @dataProvider entities_getfields
1627 * @param $entity
1628 */
1629 public function testGetfieldsHasTitle($entity) {
1630 $entities = $this->getEntitiesSupportingCustomFields();
1631 if (in_array($entity, $entities)) {
1632 $ids = $this->entityCustomGroupWithSingleFieldCreate(__FUNCTION__, $entity . 'Test.php');
1633 }
1634 $actions = $this->callAPISuccess($entity, 'getactions', []);
1635 foreach ($actions['values'] as $action) {
1636 if (substr($action, -7) == '_create' || substr($action, -4) == '_get' || substr($action, -7) == '_delete') {
1637 //getactions can't distinguish between contribution_page.create & contribution_page.create
1638 continue;
1639 }
1640 $fields = $this->callAPISuccess($entity, 'getfields', ['action' => $action]);
1641 if (!empty($ids) && in_array($action, ['create', 'get'])) {
1642 $this->assertArrayHasKey('custom_' . $ids['custom_field_id'], $fields['values']);
1643 }
1644
1645 foreach ($fields['values'] as $fieldName => $fieldSpec) {
1646 $this->assertArrayHasKey('title', $fieldSpec, "no title for $entity - $fieldName on action $action");
1647 $this->assertNotEmpty($fieldSpec['title'], "empty title for $entity - $fieldName");
1648 }
1649 }
1650 if (!empty($ids)) {
1651 $this->customFieldDelete($ids['custom_field_id']);
1652 $this->customGroupDelete($ids['custom_group_id']);
1653 }
1654 }
1655
1656 /**
1657 * @return array
1658 */
1659 public function getEntitiesSupportingCustomFields() {
1660 $entities = self::custom_data_entities_get();
1661 $returnEntities = [];
1662 foreach ($entities as $entityArray) {
1663 $returnEntities[] = $entityArray[0];
1664 }
1665 return $returnEntities;
1666 }
1667
1668 /**
1669 * @param string $entityName
1670 * @param int $count
1671 *
1672 * @return array
1673 */
1674 private function getMockableBAOObjects($entityName, $count = 2) {
1675 $baoString = _civicrm_api3_get_BAO($entityName);
1676 if (empty($baoString)) {
1677 $this->markTestIncomplete("Entity [$entityName] cannot be mocked - no known DAO");
1678 return [];
1679 }
1680 $baos = [];
1681 $i = 0;
1682 while ($i < $count) {
1683 // create entities
1684 $baoObj = CRM_Core_DAO::createTestObject($baoString, ['currency' => 'USD']);
1685 $this->assertTrue(is_int($baoObj->id), 'check first id');
1686 $this->deletableTestObjects[$baoString][] = $baoObj->id;
1687 $baos[] = $baoObj;
1688 $i++;
1689 }
1690 return $baos;
1691 }
1692
1693 /**
1694 * Verify that HTML metacharacters provided as inputs appear consistently.
1695 * as outputs.
1696 *
1697 * At time of writing, the encoding scheme requires (for example) that an
1698 * event title be partially-HTML-escaped before writing to DB. To provide
1699 * consistency, the API must perform extra encoding and decoding on some
1700 * fields.
1701 *
1702 * In this example, the event 'title' is subject to encoding, but the
1703 * event 'description' is not.
1704 */
1705 public function testEncodeDecodeConsistency() {
1706 // Create example
1707 $createResult = civicrm_api('Event', 'Create', [
1708 'version' => 3,
1709 'title' => 'CiviCRM <> TheRest',
1710 'description' => 'TheRest <> CiviCRM',
1711 'event_type_id' => 1,
1712 'is_public' => 1,
1713 'start_date' => 20081021,
1714 ]);
1715 $this->assertAPISuccess($createResult);
1716 $eventId = $createResult['id'];
1717 $this->assertEquals('CiviCRM <> TheRest', $createResult['values'][$eventId]['title']);
1718 $this->assertEquals('TheRest <> CiviCRM', $createResult['values'][$eventId]['description']);
1719
1720 // Verify "get" handles decoding in result value
1721 $getByIdResult = civicrm_api('Event', 'Get', [
1722 'version' => 3,
1723 'id' => $eventId,
1724 ]);
1725 $this->assertAPISuccess($getByIdResult);
1726 $this->assertEquals('CiviCRM <> TheRest', $getByIdResult['values'][$eventId]['title']);
1727 $this->assertEquals('TheRest <> CiviCRM', $getByIdResult['values'][$eventId]['description']);
1728
1729 // Verify "get" handles encoding in search value
1730 $getByTitleResult = civicrm_api('Event', 'Get', [
1731 'version' => 3,
1732 'title' => 'CiviCRM <> TheRest',
1733 ]);
1734 $this->assertAPISuccess($getByTitleResult);
1735 $this->assertEquals('CiviCRM <> TheRest', $getByTitleResult['values'][$eventId]['title']);
1736 $this->assertEquals('TheRest <> CiviCRM', $getByTitleResult['values'][$eventId]['description']);
1737
1738 // Verify that "getSingle" handles decoding
1739 $getSingleResult = $this->callAPISuccess('Event', 'GetSingle', [
1740 'id' => $eventId,
1741 ]);
1742
1743 $this->assertEquals('CiviCRM <> TheRest', $getSingleResult['title']);
1744 $this->assertEquals('TheRest <> CiviCRM', $getSingleResult['description']);
1745
1746 // Verify that chaining handles decoding
1747 $chainResult = $this->callAPISuccess('Event', 'Get', [
1748 'id' => $eventId,
1749 'api.event.get' => [],
1750 ]);
1751 $this->assertEquals('CiviCRM <> TheRest', $chainResult['values'][$eventId]['title']);
1752 $this->assertEquals('TheRest <> CiviCRM', $chainResult['values'][$eventId]['description']);
1753 $this->assertEquals('CiviCRM <> TheRest', $chainResult['values'][$eventId]['api.event.get']['values'][0]['title']);
1754 $this->assertEquals('TheRest <> CiviCRM', $chainResult['values'][$eventId]['api.event.get']['values'][0]['description']);
1755
1756 // Verify that "setvalue" handles encoding for updates
1757 $setValueTitleResult = civicrm_api('Event', 'setvalue', [
1758 'version' => 3,
1759 'id' => $eventId,
1760 'field' => 'title',
1761 'value' => 'setValueTitle: CiviCRM <> TheRest',
1762 ]);
1763 $this->assertAPISuccess($setValueTitleResult);
1764 $this->assertEquals('setValueTitle: CiviCRM <> TheRest', $setValueTitleResult['values']['title']);
1765 $setValueDescriptionResult = civicrm_api('Event', 'setvalue', [
1766 'version' => 3,
1767 'id' => $eventId,
1768 'field' => 'description',
1769 'value' => 'setValueDescription: TheRest <> CiviCRM',
1770 ]);
1771 //$this->assertTrue((bool)$setValueDescriptionResult['is_error']); // not supported by setValue
1772 $this->assertEquals('setValueDescription: TheRest <> CiviCRM', $setValueDescriptionResult['values']['description']);
1773 }
1774
1775 /**
1776 * Verify that write operations (create/update) use partial HTML-encoding
1777 *
1778 * In this example, the event 'title' is subject to encoding, but the
1779 * event 'description' is not.
1780 */
1781 public function testEncodeWrite() {
1782 // Create example
1783 $createResult = civicrm_api('Event', 'Create', [
1784 'version' => 3,
1785 'title' => 'createNew: CiviCRM <> TheRest',
1786 'description' => 'createNew: TheRest <> CiviCRM',
1787 'event_type_id' => 1,
1788 'is_public' => 1,
1789 'start_date' => 20081021,
1790 ]);
1791 $this->assertAPISuccess($createResult);
1792 $eventId = $createResult['id'];
1793 $this->assertDBQuery('createNew: CiviCRM &lt;&gt; TheRest', 'SELECT title FROM civicrm_event WHERE id = %1', [
1794 1 => [$eventId, 'Integer'],
1795 ]);
1796 $this->assertDBQuery('createNew: TheRest <> CiviCRM', 'SELECT description FROM civicrm_event WHERE id = %1', [
1797 1 => [$eventId, 'Integer'],
1798 ]);
1799
1800 // Verify that "create" handles encoding for updates
1801 $createWithIdResult = civicrm_api('Event', 'Create', [
1802 'version' => 3,
1803 'id' => $eventId,
1804 'title' => 'createWithId: CiviCRM <> TheRest',
1805 'description' => 'createWithId: TheRest <> CiviCRM',
1806 ]);
1807 $this->assertAPISuccess($createWithIdResult);
1808 $this->assertDBQuery('createWithId: CiviCRM &lt;&gt; TheRest', 'SELECT title FROM civicrm_event WHERE id = %1', [
1809 1 => [$eventId, 'Integer'],
1810 ]);
1811 $this->assertDBQuery('createWithId: TheRest <> CiviCRM', 'SELECT description FROM civicrm_event WHERE id = %1', [
1812 1 => [$eventId, 'Integer'],
1813 ]);
1814
1815 // Verify that "setvalue" handles encoding for updates
1816 $setValueTitleResult = civicrm_api('Event', 'setvalue', [
1817 'version' => 3,
1818 'id' => $eventId,
1819 'field' => 'title',
1820 'value' => 'setValueTitle: CiviCRM <> TheRest',
1821 ]);
1822 $this->assertAPISuccess($setValueTitleResult);
1823 $this->assertDBQuery('setValueTitle: CiviCRM &lt;&gt; TheRest', 'SELECT title FROM civicrm_event WHERE id = %1', [
1824 1 => [$eventId, 'Integer'],
1825 ]);
1826 $setValueDescriptionResult = civicrm_api('Event', 'setvalue', [
1827 'version' => 3,
1828 'id' => $eventId,
1829 'field' => 'description',
1830 'value' => 'setValueDescription: TheRest <> CiviCRM',
1831 ]);
1832 //$this->assertTrue((bool)$setValueDescriptionResult['is_error']); // not supported by setValue
1833 $this->assertAPISuccess($setValueDescriptionResult);
1834 $this->assertDBQuery('setValueDescription: TheRest <> CiviCRM', 'SELECT description FROM civicrm_event WHERE id = %1', [
1835 1 => [$eventId, 'Integer'],
1836 ]);
1837 }
1838
1839 }