Merge pull request #15903 from eileenmcnaughton/except
[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 e.g. $this->deletes['CRM_Contact_DAO_Contact'][] = $contactID;
29 */
30 protected $deletableTestObjects;
31
32 /**
33 * This test case doesn't require DB reset.
34 * @var bool
35 */
36 public $DBResetRequired = FALSE;
37
38 protected $_entity;
39
40 /**
41 * Map custom group entities to civicrm components.
42 * @var array
43 */
44 protected static $componentMap = [
45 'Contribution' => 'CiviContribute',
46 'Membership' => 'CiviMember',
47 'Participant' => 'CiviEvent',
48 'Event' => 'CiviEvent',
49 'Case' => 'CiviCase',
50 'Pledge' => 'CiviPledge',
51 'Grant' => 'CiviGrant',
52 'Campaign' => 'CiviCampaign',
53 'Survey' => 'CiviCampaign',
54 ];
55
56 /**
57 * Set up function.
58 *
59 * There are two types of missing APIs:
60 * Those that are to be implemented
61 * (in some future version when someone steps in -hint hint-). List the entities in toBeImplemented[ {$action} ]
62 * Those that don't exist
63 * and that will never exist (eg an obsoleted Entity
64 * 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)
65 */
66 public function setUp() {
67 parent::setUp();
68 $this->enableCiviCampaign();
69 $this->toBeImplemented['get'] = [
70 // CxnApp.get exists but relies on remote data outside our control; QA w/UtilsTest::testBasicArrayGet
71 'CxnApp',
72 'Profile',
73 'CustomValue',
74 'Constant',
75 'CustomSearch',
76 'Extension',
77 'ReportTemplate',
78 'System',
79 'Setting',
80 'Payment',
81 'Logging',
82 ];
83 $this->toBeImplemented['create'] = [
84 'Cxn',
85 'CxnApp',
86 'SurveyRespondant',
87 'OptionGroup',
88 'MailingRecipients',
89 'UFMatch',
90 'CustomSearch',
91 'Extension',
92 'ReportTemplate',
93 'System',
94 'User',
95 'Payment',
96 'Order',
97 //work fine in local
98 'SavedSearch',
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 ];
519 if ($sequential === TRUE) {
520 return $entitiesWithout;
521 }
522 $entities = [];
523 foreach ($entitiesWithout as $e) {
524 $entities[] = [
525 $e,
526 ];
527 }
528 return ['pledge'];
529 return $entities;
530 }
531
532 /**
533 * At this stage exclude the ones that don't pass & add them as we can troubleshoot them
534 */
535 public static function toBeSkipped_getlimit() {
536 $entitiesWithout = [
537 'Case',
538 //case api has non-std mandatory fields one of (case_id, contact_id, activity_id, contact_id)
539 'EntityTag',
540 // non-standard api - has inappropriate mandatory fields & doesn't implement limit
541 'Event',
542 // failed 'check that a 5 limit returns 5' - probably is_template field is wrong or something, or could be limit doesn't work right
543 'Extension',
544 // can't handle creating 25
545 'Note',
546 // fails on 5 limit - probably a set up problem
547 'Setting',
548 //a bit of a pseudoapi - keys by domain
549 'Payment',
550 // pseudoapi - problems with creating required sub entities.
551 ];
552 return $entitiesWithout;
553 }
554
555 /**
556 * At this stage exclude the ones that don't pass & add them as we can troubleshoot them
557 */
558 public static function toBeSkipped_getSqlOperators() {
559 $entitiesWithout = [
560 //case api has non-std mandatory fields one of (case_id, contact_id, activity_id, contact_id)
561 'Case',
562 // on the todo list!
563 'Contact',
564 // non-standard api - has inappropriate mandatory fields & doesn't implement limit
565 'EntityTag',
566 // can't handle creating 25
567 'Extension',
568 // note has a default get that isn't implemented in createTestObject -meaning you don't 'get' them
569 'Note',
570 //a bit of a pseudoapi - keys by domain
571 'Setting',
572 ];
573
574 // The testSqlOperators fails sporadically on MySQL 5.5, which is deprecated anyway.
575 // Test data providers should be able to run in pre-boot environment, so we connect directly to SQL server.
576 require_once 'DB.php';
577 $db = DB::connect(CIVICRM_DSN);
578 if ($db->connection instanceof mysqli && $db->connection->server_version < 50600) {
579 $entitiesWithout[] = 'Dedupe';
580 }
581
582 return $entitiesWithout;
583 }
584
585 /**
586 * @param $entity
587 * @param $key
588 *
589 * @return array
590 */
591 public function getKnownUnworkablesUpdateSingle($entity, $key) {
592 // can't update values are values for which updates don't result in the value being changed
593 $knownFailures = [
594 'ActionSchedule' => [
595 'cant_update' => [
596 'group_id',
597 ],
598 ],
599 'ActivityContact' => [
600 'cant_update' => [
601 'activity_id',
602 //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
603 ],
604 ],
605 'Address' => [
606 'cant_update' => [
607 //issues with country id - need to ensure same country
608 'state_province_id',
609 'world_region',
610 //creates relationship
611 'master_id',
612 ],
613 'cant_return' => ['street_parsing', 'skip_geocode', 'fix_address'],
614 ],
615 'Batch' => [
616 'cant_update' => [
617 // believe this field is defined in error
618 'entity_table',
619 ],
620 'cant_return' => [
621 'entity_table',
622 ],
623 ],
624 'CaseType' => [
625 'cant_update' => [
626 'definition',
627 ],
628 ],
629 'Domain' => ['cant_update' => ['domain_version']],
630 'MembershipBlock' => [
631 'cant_update' => [
632 // The fake/auto-generated values leave us unable to properly cleanup fake data
633 'entity_type',
634 'entity_id',
635 ],
636 ],
637 'MailingJob' => ['cant_update' => ['parent_id']],
638 'ContributionSoft' => [
639 'cant_update' => [
640 // can't be changed through api
641 'pcp_id',
642 ],
643 ],
644 'Email' => [
645 'cant_update' => [
646 // This is being legitimately manipulated to always have a valid primary - skip.
647 'is_primary',
648 ],
649 ],
650 'FinancialTrxn' => [
651 'cant_update' => [
652 // Altering fee amount will also cause net_amount to be recalculated.
653 'fee_amount',
654 ],
655 ],
656 'Navigation' => [
657 'cant_update' => [
658 // Weight is deliberately altered when this is changed - skip.
659 'parent_id',
660 ],
661 ],
662 'LocationType' => [
663 'cant_update' => [
664 // I'm on the fence about whether the test should skip or the behaviour is wrong.
665 // display_name is set to match name if display_name is not provided. It would be more 'normal'
666 // to only calculate a default IF id is not set - but perhaps the current behaviour is kind
667 // of what someone updating the name expects..
668 'name',
669 ],
670 ],
671 'Pledge' => [
672 'cant_update' => [
673 'pledge_original_installment_amount',
674 'installments',
675 'original_installment_amount',
676 'next_pay_date',
677 // can't be changed through API,
678 'amount',
679 ],
680 // if these are passed in they are retrieved from the wrong table
681 'break_return' => [
682 'honor_contact_id',
683 'cancel_date',
684 'contribution_page_id',
685 'financial_account_id',
686 'financial_type_id',
687 'currency',
688 ],
689 // can't be retrieved from api
690 'cant_return' => [
691 //due to uniquename missing
692 'honor_type_id',
693 'end_date',
694 'modified_date',
695 'acknowledge_date',
696 'start_date',
697 'frequency_day',
698 'currency',
699 'max_reminders',
700 'initial_reminder_day',
701 'additional_reminder_day',
702 'frequency_unit',
703 'pledge_contribution_page_id',
704 'pledge_status_id',
705 'pledge_campaign_id',
706 'pledge_financial_type_id',
707 ],
708 ],
709 'PaymentProcessorType' => [
710 'cant_update' => [
711 'billing_mode',
712 ],
713 'break_return' => [],
714 'cant_return' => [],
715 ],
716 'PriceFieldValue' => [
717 'cant_update' => [
718 //won't update as there is no 1 in the same price set
719 'weight',
720 ],
721 ],
722 'ReportInstance' => [
723 // View mode is part of the navigation which is not retrieved by the api.
724 'cant_return' => ['view_mode'],
725 ],
726 'SavedSearch' => [
727 // I think the fields below are generated based on form_values.
728 'cant_update' => [
729 'search_custom_id',
730 'where_clause',
731 'select_tables',
732 'where_tables',
733 ],
734 ],
735 'StatusPreference' => [
736 'break_return' => [
737 'ignore_severity',
738 ],
739 ],
740 'UFField' => [
741 'cant_update' => [
742 // These fields get auto-adjusted by the BAO prior to saving
743 'weight',
744 'location_type_id',
745 'phone_type_id',
746 'website_type_id',
747 // Not a real field
748 'option.autoweight',
749 ],
750 'break_return' => [
751 // These fields get auto-adjusted by the BAO prior to saving
752 'weight',
753 'field_type',
754 'location_type_id',
755 'phone_type_id',
756 'website_type_id',
757 // Not a real field
758 'option.autoweight',
759 ],
760 ],
761 'JobLog' => [
762 // For better or worse triggers override.
763 'break_return' => ['run_time'],
764 'cant_update' => ['run_time'],
765 ],
766 ];
767 if (empty($knownFailures[$entity]) || empty($knownFailures[$entity][$key])) {
768 return [];
769 }
770 return $knownFailures[$entity][$key];
771 }
772
773 /* ----- testing the _get ----- */
774
775 /**
776 * @dataProvider toBeSkipped_get
777 * Entities that don't need a get action
778 * @param $Entity
779 */
780 public function testNotImplemented_get($Entity) {
781 $result = civicrm_api($Entity, 'Get', ['version' => 3]);
782 $this->assertEquals(1, $result['is_error']);
783 // $this->assertContains("API ($Entity, Get) does not exist", $result['error_message']);
784 $this->assertRegExp('/API (.*) does not exist/', $result['error_message']);
785 }
786
787 /**
788 * @dataProvider entities
789 * @param $Entity
790 */
791 public function testGetFields($Entity) {
792 if (in_array($Entity, $this->deprecatedAPI) || $Entity == 'Entity' || $Entity == 'CustomValue') {
793 return;
794 }
795
796 $result = civicrm_api($Entity, 'getfields', ['version' => 3]);
797 $this->assertTrue(is_array($result['values']), "$Entity ::get fields doesn't return values array in line " . __LINE__);
798 foreach ($result['values'] as $key => $value) {
799 $this->assertTrue(is_array($value), $Entity . "::" . $key . " is not an array in line " . __LINE__);
800 }
801 }
802
803 /**
804 * @dataProvider entities_get
805 * @param $Entity
806 */
807 public function testEmptyParam_get($Entity) {
808
809 if (in_array($Entity, $this->toBeImplemented['get'])) {
810 // $this->markTestIncomplete("civicrm_api3_{$Entity}_get to be implemented");
811 return;
812 }
813 $result = civicrm_api($Entity, 'Get', []);
814 $this->assertEquals(1, $result['is_error']);
815 $this->assertContains("Unknown api version", $result['error_message']);
816 }
817
818 /**
819 * @dataProvider entities_get
820 * @Xdepends testEmptyParam_get // no need to test the simple if the empty doesn't work/is skipped. doesn't seem to work
821 * @param $Entity
822 */
823 public function testSimple_get($Entity) {
824 // $this->markTestSkipped("test gives core error on test server (but not on our locals). Skip until we can get server to pass");
825 if (in_array($Entity, $this->toBeImplemented['get'])) {
826 return;
827 }
828 $result = civicrm_api($Entity, 'Get', ['version' => 3]);
829 // @TODO: list the get that have mandatory params
830 if ($result['is_error']) {
831 $this->assertContains("Mandatory key(s) missing from params array", $result['error_message']);
832 // either id or contact_id or entity_id is one of the field missing
833 $this->assertContains("id", $result['error_message']);
834 }
835 else {
836 $this->assertEquals(3, $result['version']);
837 $this->assertArrayHasKey('count', $result);
838 $this->assertArrayHasKey('values', $result);
839 }
840 }
841
842 /**
843 * @dataProvider custom_data_incl_non_std_entities_get
844 * @param $entityName
845 */
846 public function testCustomDataGet($entityName) {
847 if ($entityName === 'Note') {
848 $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');
849 }
850 $this->quickCleanup(['civicrm_uf_match']);
851 // so subsidiary activities are created
852 $this->createLoggedInUser();
853
854 $entitiesWithNamingIssues = [
855 'SmsProvider' => 'Provider',
856 'AclRole' => 'EntityRole',
857 'MailingEventQueue' => 'Queue',
858 'Dedupe' => 'PrevNextCache',
859 ];
860
861 $usableName = !empty($entitiesWithNamingIssues[$entityName]) ? $entitiesWithNamingIssues[$entityName] : $entityName;
862 $optionName = CRM_Core_DAO_AllCoreTables::getTableForClass(CRM_Core_DAO_AllCoreTables::getFullName($usableName));
863
864 if (!isset(CRM_Core_BAO_CustomQuery::$extendsMap[$entityName])) {
865 $createdValue = $this->callAPISuccess('OptionValue', 'create', [
866 'option_group_id' => 'cg_extend_objects',
867 'label' => $usableName,
868 'value' => $usableName,
869 'name' => $optionName,
870 ]);
871 }
872 // We are not passing 'check_permissions' so the the more limited permissions *should* be
873 // ignored but per CRM-17700 there is a history of custom data applying permissions when it shouldn't.
874 CRM_Core_Config::singleton()->userPermissionClass->permissions = ['access CiviCRM', 'view my contact'];
875 $objects = $this->getMockableBAOObjects($entityName, 1);
876
877 // simple custom field
878 $ids = $this->entityCustomGroupWithSingleFieldCreate(__FUNCTION__, $usableName . 'Test.php');
879 $customFieldName = 'custom_' . $ids['custom_field_id'];
880 $params = ['id' => $objects[0]->id, 'custom_' . $ids['custom_field_id'] => "custom string"];
881 $result = $this->callAPISuccess($entityName, 'create', $params);
882 $this->assertTrue(isset($result['id']), 'no id on ' . $entityName);
883 $getParams = ['id' => $result['id'], 'return' => [$customFieldName]];
884 $check = $this->callAPISuccess($entityName, 'get', $getParams);
885 $this->assertTrue(!empty($check['values'][$check['id']][$customFieldName]), 'Custom data not present for ' . $entityName);
886 $this->assertEquals("custom string", $check['values'][$check['id']][$customFieldName], 'Custom data not present for ' . $entityName);
887 $this->customFieldDelete($ids['custom_field_id']);
888 $this->customGroupDelete($ids['custom_group_id']);
889
890 $ids2 = $this->entityCustomGroupWithSingleStringMultiSelectFieldCreate(__FUNCTION__, $usableName . 'Test.php');
891 $customFieldNameMultiSelect = 'custom_' . $ids2['custom_field_id'];
892 // String custom field, Multi-Select html type
893 foreach ($ids2['custom_field_group_options'] as $option_value => $option_label) {
894 $params = ['id' => $objects[0]->id, 'custom_' . $ids2['custom_field_id'] => $option_value];
895 $result = $this->callAPISuccess($entityName, 'create', $params);
896 $getParams = [$customFieldNameMultiSelect => $option_value, 'return' => [$customFieldNameMultiSelect]];
897 $this->callAPISuccessGetCount($entityName, $getParams, 1);
898 }
899
900 // cleanup
901 $this->customFieldDelete($ids2['custom_field_id']);
902 $this->customGroupDelete($ids2['custom_group_id']);
903
904 $this->callAPISuccess($entityName, 'delete', ['id' => $result['id']]);
905 $this->quickCleanup(['civicrm_uf_match']);
906 if (!empty($createdValue)) {
907 $this->callAPISuccess('OptionValue', 'delete', ['id' => $createdValue['id']]);
908 }
909 }
910
911 /**
912 * @dataProvider entities_get
913 * @param $Entity
914 */
915 public function testAcceptsOnlyID_get($Entity) {
916 // big random number. fun fact: if you multiply it by pi^e, the result is another random number, but bigger ;)
917 $nonExistantID = 30867307034;
918 if (in_array($Entity, $this->toBeImplemented['get'])
919 || in_array($Entity, $this->toBeSkipped_getByID())
920 ) {
921 return;
922 }
923
924 // FIXME
925 // the below function returns different values and hence an early return
926 // we'll fix this once beta1 is released
927 // return;
928
929 $result = civicrm_api($Entity, 'Get', ['version' => 3, 'id' => $nonExistantID]);
930
931 if ($result['is_error']) {
932 // just to get a clearer message in the log
933 $this->assertEquals("only id should be enough", $result['error_message']);
934 }
935 if (!in_array($Entity, $this->onlyIDNonZeroCount['get'])) {
936 $this->assertEquals(0, $result['count']);
937 }
938 }
939
940 /**
941 * Test getlist works
942 * @dataProvider entities_get
943 * @param $Entity
944 */
945 public function testGetList($Entity) {
946 if (in_array($Entity, $this->toBeImplemented['get'])
947 || in_array($Entity, $this->toBeSkipped_getByID())
948 ) {
949 return;
950 }
951 if (in_array($Entity, ['ActivityType', 'SurveyRespondant'])) {
952 $this->markTestSkipped();
953 }
954 $this->callAPISuccess($Entity, 'getlist', ['label_field' => 'id']);
955 }
956
957 /**
958 * Test getlist works when entity is lowercase
959 * @dataProvider entities_get
960 * @param $Entity
961 */
962 public function testGetListLowerCaseEntity($Entity) {
963 if (in_array($Entity, $this->toBeImplemented['get'])
964 || in_array($Entity, $this->toBeSkipped_getByID())
965 ) {
966 return;
967 }
968 if (in_array($Entity, ['ActivityType', 'SurveyRespondant'])) {
969 $this->markTestSkipped();
970 }
971 if ($Entity == 'UFGroup') {
972 $Entity = 'ufgroup';
973 }
974 $this->callAPISuccess($Entity, 'getlist', ['label_field' => 'id']);
975 }
976
977 /**
978 * Create two entities and make sure we can fetch them individually by ID.
979 *
980 * @dataProvider entities_get
981 *
982 * limitations include the problem with avoiding loops when creating test objects -
983 * hence FKs only set by createTestObject when required. e.g parent_id on campaign is not being followed through
984 * Currency - only seems to support US
985 * @param $entityName
986 */
987 public function testByID_get($entityName) {
988 if (in_array($entityName, self::toBeSkipped_automock(TRUE))) {
989 // $this->markTestIncomplete("civicrm_api3_{$Entity}_create to be implemented");
990 return;
991 }
992
993 $baos = $this->getMockableBAOObjects($entityName);
994 list($baoObj1, $baoObj2) = $baos;
995
996 // fetch first by ID
997 $result = $this->callAPISuccess($entityName, 'get', [
998 'id' => $baoObj1->id,
999 ]);
1000
1001 $this->assertTrue(!empty($result['values'][$baoObj1->id]), 'Should find first object by id');
1002 $this->assertEquals($baoObj1->id, $result['values'][$baoObj1->id]['id'], 'Should find id on first object');
1003 $this->assertEquals(1, count($result['values']));
1004
1005 // fetch second by ID
1006 $result = $this->callAPISuccess($entityName, 'get', [
1007 'id' => $baoObj2->id,
1008 ]);
1009 $this->assertTrue(!empty($result['values'][$baoObj2->id]), 'Should find second object by id');
1010 $this->assertEquals($baoObj2->id, $result['values'][$baoObj2->id]['id'], 'Should find id on second object');
1011 $this->assertEquals(1, count($result['values']));
1012 }
1013
1014 /**
1015 * Ensure that the "get" operation accepts limiting the #result records.
1016 *
1017 * TODO Consider making a separate entity list ("entities_getlimit")
1018 * For the moment, the "entities_updatesingle" list should give a good
1019 * sense for which entities support createTestObject
1020 *
1021 * @dataProvider entities_getlimit
1022 *
1023 * @param string $entityName
1024 */
1025 public function testLimit($entityName) {
1026 // each case is array(0 => $inputtedApiOptions, 1 => $expectedResultCount)
1027 $cases = [];
1028 $cases[] = [
1029 ['options' => ['limit' => NULL]],
1030 30,
1031 'check that a NULL limit returns unlimited',
1032 ];
1033 $cases[] = [
1034 ['options' => ['limit' => FALSE]],
1035 30,
1036 'check that a FALSE limit returns unlimited',
1037 ];
1038 $cases[] = [
1039 ['options' => ['limit' => 0]],
1040 30,
1041 'check that a 0 limit returns unlimited',
1042 ];
1043 $cases[] = [
1044 ['options' => ['limit' => 5]],
1045 5,
1046 'check that a 5 limit returns 5',
1047 ];
1048 $cases[] = [
1049 [],
1050 25,
1051 'check that no limit returns 25',
1052 ];
1053
1054 $baoString = _civicrm_api3_get_BAO($entityName);
1055 if (empty($baoString)) {
1056 $this->markTestIncomplete("Entity [$entityName] cannot be mocked - no known DAO");
1057 return;
1058 }
1059
1060 // make 30 test items -- 30 > 25 (the default limit)
1061 $ids = [];
1062 for ($i = 0; $i < 30; $i++) {
1063 $baoObj = CRM_Core_DAO::createTestObject($baoString, ['currency' => 'USD']);
1064 $ids[] = $baoObj->id;
1065 }
1066
1067 // each case is array(0 => $inputtedApiOptions, 1 => $expectedResultCount)
1068 foreach ($cases as $case) {
1069 $this->checkLimitAgainstExpected($entityName, $case[0], $case[1], $case[2]);
1070
1071 //non preferred / legacy syntax
1072 if (isset($case[0]['options']['limit'])) {
1073 $this->checkLimitAgainstExpected($entityName, ['rowCount' => $case[0]['options']['limit']], $case[1], $case[2]);
1074 $this->checkLimitAgainstExpected($entityName, ['option_limit' => $case[0]['options']['limit']], $case[1], $case[2]);
1075 $this->checkLimitAgainstExpected($entityName, ['option.limit' => $case[0]['options']['limit']], $case[1], $case[2]);
1076 }
1077 }
1078 foreach ($ids as $id) {
1079 CRM_Core_DAO::deleteTestObjects($baoString, ['id' => $id]);
1080 }
1081 }
1082
1083 /**
1084 * Ensure that the "get" operation accepts limiting the #result records.
1085 *
1086 * @dataProvider entities_getSqlOperators
1087 *
1088 * @param string $entityName
1089 */
1090 public function testSqlOperators($entityName) {
1091 $toBeIgnored = array_merge($this->toBeImplemented['get'],
1092 $this->deprecatedAPI,
1093 $this->toBeSkipped_get(TRUE),
1094 $this->toBeSkipped_getByID()
1095 );
1096 if (in_array($entityName, $toBeIgnored)) {
1097 return;
1098 }
1099
1100 $baoString = _civicrm_api3_get_BAO($entityName);
1101
1102 $entities = $this->callAPISuccess($entityName, 'get', ['options' => ['limit' => 0], 'return' => 'id']);
1103 $entities = array_keys($entities['values']);
1104 $totalEntities = count($entities);
1105 if ($totalEntities < 3) {
1106 $ids = [];
1107 for ($i = 0; $i < 3 - $totalEntities; $i++) {
1108 $baoObj = CRM_Core_DAO::createTestObject($baoString, ['currency' => 'USD']);
1109 $ids[] = $baoObj->id;
1110 }
1111 $totalEntities = 3;
1112 }
1113 $entities = $this->callAPISuccess($entityName, 'get', ['options' => ['limit' => 0]]);
1114 $entities = array_keys($entities['values']);
1115 $this->assertGreaterThan(2, $totalEntities);
1116 $this->callAPISuccess($entityName, 'getsingle', ['id' => ['IN' => [$entities[0]]]]);
1117 $this->callAPISuccessGetCount($entityName, ['id' => ['NOT IN' => [$entities[0]]]], $totalEntities - 1);
1118 $this->callAPISuccessGetCount($entityName, ['id' => ['>' => $entities[0]]], $totalEntities - 1);
1119 }
1120
1121 /**
1122 * Check that get fetches an appropriate number of results.
1123 *
1124 * @param string $entityName
1125 * Name of entity to test.
1126 * @param array $params
1127 * @param int $limit
1128 * @param string $message
1129 */
1130 public function checkLimitAgainstExpected($entityName, $params, $limit, $message) {
1131 $result = $this->callAPISuccess($entityName, 'get', $params);
1132 if ($limit == 30) {
1133 $this->assertGreaterThanOrEqual($limit, $result['count'], $message);
1134 $this->assertGreaterThanOrEqual($limit, $result['count'], $message);
1135 }
1136 else {
1137 $this->assertEquals($limit, $result['count'], $message);
1138 $this->assertEquals($limit, count($result['values']), $message);
1139 }
1140 }
1141
1142 /**
1143 * Create two entities and make sure we can fetch them individually by ID (e.g. using "contact_id=>2"
1144 * or "group_id=>4")
1145 *
1146 * @dataProvider entities_get
1147 *
1148 * limitations include the problem with avoiding loops when creating test objects -
1149 * hence FKs only set by createTestObject when required. e.g parent_id on campaign is not being followed through
1150 * Currency - only seems to support US
1151 * @param $entityName
1152 * @throws \PHPUnit\Framework\IncompleteTestError
1153 */
1154 public function testByIDAlias_get($entityName) {
1155 if (in_array($entityName, self::toBeSkipped_automock(TRUE))) {
1156 // $this->markTestIncomplete("civicrm_api3_{$Entity}_create to be implemented");
1157 return;
1158 }
1159
1160 $baoString = _civicrm_api3_get_BAO($entityName);
1161 if (empty($baoString)) {
1162 $this->markTestIncomplete("Entity [$entityName] cannot be mocked - no known DAO");
1163 return;
1164 }
1165
1166 $idFieldName = _civicrm_api_get_entity_name_from_camel($entityName) . '_id';
1167
1168 // create entities
1169 $baoObj1 = CRM_Core_DAO::createTestObject($baoString, ['currency' => 'USD']);
1170 $this->assertTrue(is_int($baoObj1->id), 'check first id');
1171 $this->deletableTestObjects[$baoString][] = $baoObj1->id;
1172 $baoObj2 = CRM_Core_DAO::createTestObject($baoString, ['currency' => 'USD']);
1173 $this->assertTrue(is_int($baoObj2->id), 'check second id');
1174 $this->deletableTestObjects[$baoString][] = $baoObj2->id;
1175
1176 // fetch first by ID
1177 $result = civicrm_api($entityName, 'get', [
1178 'version' => 3,
1179 $idFieldName => $baoObj1->id,
1180 ]);
1181 $this->assertAPISuccess($result);
1182 $this->assertTrue(!empty($result['values'][$baoObj1->id]), 'Should find first object by id');
1183 $this->assertEquals($baoObj1->id, $result['values'][$baoObj1->id]['id'], 'Should find id on first object');
1184 $this->assertEquals(1, count($result['values']));
1185
1186 // fetch second by ID
1187 $result = civicrm_api($entityName, 'get', [
1188 'version' => 3,
1189 $idFieldName => $baoObj2->id,
1190 ]);
1191 $this->assertAPISuccess($result);
1192 $this->assertTrue(!empty($result['values'][$baoObj2->id]), 'Should find second object by id');
1193 $this->assertEquals($baoObj2->id, $result['values'][$baoObj2->id]['id'], 'Should find id on second object');
1194 $this->assertEquals(1, count($result['values']));
1195 }
1196
1197 /**
1198 * @dataProvider entities_get
1199 * @param $Entity
1200 */
1201 public function testNonExistantID_get($Entity) {
1202 // cf testAcceptsOnlyID_get
1203 $nonExistantID = 30867307034;
1204 if (in_array($Entity, $this->toBeImplemented['get'])) {
1205 return;
1206 }
1207
1208 $result = civicrm_api($Entity, 'Get', ['version' => 3, 'id' => $nonExistantID]);
1209
1210 // redundant with testAcceptsOnlyID_get
1211 if ($result['is_error']) {
1212 return;
1213 }
1214
1215 $this->assertArrayHasKey('version', $result);
1216 $this->assertEquals(3, $result['version']);
1217 if (!in_array($Entity, $this->onlyIDNonZeroCount['get'])) {
1218 $this->assertEquals(0, $result['count']);
1219 }
1220 }
1221
1222 /* ---- testing the _create ---- */
1223
1224 /**
1225 * @dataProvider toBeSkipped_create
1226 * entities that don't need a create action
1227 * @param $Entity
1228 */
1229 public function testNotImplemented_create($Entity) {
1230 $result = civicrm_api($Entity, 'Create', ['version' => 3]);
1231 $this->assertEquals(1, $result['is_error']);
1232 $this->assertContains(strtolower("API ($Entity, Create) does not exist"), strtolower($result['error_message']));
1233 }
1234
1235 /**
1236 * @dataProvider entities
1237 * @expectedException CiviCRM_API3_Exception
1238 * @param $Entity
1239 */
1240 public function testWithoutParam_create($Entity) {
1241 if ($Entity === 'Setting') {
1242 $this->markTestSkipped('It seems OK for setting to skip here as it silently sips invalid params');
1243 }
1244 elseif ($Entity === 'Mailing') {
1245 $this->markTestSkipped('It seems OK for "Mailing" to skip here because you can create empty drafts');
1246 }
1247 // should create php complaining that a param is missing
1248 civicrm_api3($Entity, 'Create');
1249 }
1250
1251 /**
1252 * @dataProvider entities_create
1253 *
1254 * Check that create doesn't work with an invalid
1255 * @param $Entity
1256 * @throws \PHPUnit\Framework\IncompleteTestError
1257 */
1258 public function testInvalidSort_get($Entity) {
1259 $invalidEntitys = ['ActivityType', 'Setting', 'System'];
1260 if (in_array($Entity, $invalidEntitys)) {
1261 $this->markTestSkipped('It seems OK for ' . $Entity . ' to skip here as it silently sips invalid params');
1262 }
1263 $result = $this->callAPIFailure($Entity, 'get', ['options' => ['sort' => 'sleep(1)']]);
1264 }
1265
1266 /**
1267 * @dataProvider entities_create
1268 *
1269 * Check that create doesn't work with an invalid
1270 * @param $Entity
1271 * @throws \PHPUnit\Framework\IncompleteTestError
1272 */
1273 public function testValidSortSingleArrayById_get($Entity) {
1274 $invalidEntitys = ['ActivityType', 'Setting', 'System'];
1275 $tests = [
1276 'id' => '_id',
1277 'id desc' => '_id desc',
1278 'id DESC' => '_id DESC',
1279 'id ASC' => '_id ASC',
1280 'id asc' => '_id asc',
1281 ];
1282 foreach ($tests as $test => $expected) {
1283 if (in_array($Entity, $invalidEntitys)) {
1284 $this->markTestSkipped('It seems OK for ' . $Entity . ' to skip here as it silently ignores passed in params');
1285 }
1286 $params = ['sort' => [$test]];
1287 $result = _civicrm_api3_get_options_from_params($params, FALSE, $Entity, 'get');
1288 $lowercase_entity = _civicrm_api_get_entity_name_from_camel($Entity);
1289 $this->assertEquals($lowercase_entity . $expected, $result['sort']);
1290 }
1291 }
1292
1293 /**
1294 * @dataProvider entities_create
1295 *
1296 * Check that create doesn't work with an invalid
1297 * @param $Entity
1298 * @throws \PHPUnit\Framework\IncompleteTestError
1299 */
1300 public function testInvalidID_create($Entity) {
1301 // turn test off for noew
1302 $this->markTestIncomplete("Entity [ $Entity ] cannot be mocked - no known DAO");
1303 return;
1304 if (in_array($Entity, $this->toBeImplemented['create'])) {
1305 // $this->markTestIncomplete("civicrm_api3_{$Entity}_create to be implemented");
1306 return;
1307 }
1308 $result = $this->callAPIFailure($Entity, 'Create', ['id' => 999]);
1309 }
1310
1311 /**
1312 * @dataProvider entities_updatesingle
1313 *
1314 * limitations include the problem with avoiding loops when creating test objects -
1315 * hence FKs only set by createTestObject when required. e.g parent_id on campaign is not being followed through
1316 * Currency - only seems to support US
1317 * @param $entityName
1318 */
1319 public function testCreateSingleValueAlter($entityName) {
1320 if (in_array($entityName, $this->toBeImplemented['create'])) {
1321 // $this->markTestIncomplete("civicrm_api3_{$Entity}_create to be implemented");
1322 return;
1323 }
1324
1325 $baoString = _civicrm_api3_get_BAO($entityName);
1326 $this->assertNotEmpty($baoString, $entityName);
1327 $this->assertNotEmpty($entityName, $entityName);
1328 $fieldsGet = $fields = $this->callAPISuccess($entityName, 'getfields', ['action' => 'get', 'options' => ['get_options' => 'all']]);
1329 if ($entityName != 'Pledge') {
1330 $fields = $this->callAPISuccess($entityName, 'getfields', ['action' => 'create', 'options' => ['get_options' => 'all']]);
1331 }
1332 $fields = $fields['values'];
1333 $return = array_keys($fieldsGet['values']);
1334 $valuesNotToReturn = $this->getKnownUnworkablesUpdateSingle($entityName, 'break_return');
1335 // these can't be requested as return values
1336 $entityValuesThatDoNotWork = array_merge(
1337 $this->getKnownUnworkablesUpdateSingle($entityName, 'cant_update'),
1338 $this->getKnownUnworkablesUpdateSingle($entityName, 'cant_return'),
1339 $valuesNotToReturn
1340 );
1341
1342 $return = array_diff($return, $valuesNotToReturn);
1343 $baoObj = new CRM_Core_DAO();
1344 $baoObj->createTestObject($baoString, ['currency' => 'USD'], 2, 0);
1345
1346 $getEntities = $this->callAPISuccess($entityName, 'get', [
1347 'sequential' => 1,
1348 'return' => $return,
1349 'options' => [
1350 'sort' => 'id DESC',
1351 'limit' => 2,
1352 ],
1353 ]);
1354
1355 // lets use first rather than assume only one exists
1356 $entity = $getEntities['values'][0];
1357 $entity2 = $getEntities['values'][1];
1358 $this->deletableTestObjects[$baoString][] = $entity['id'];
1359 $this->deletableTestObjects[$baoString][] = $entity2['id'];
1360 // Skip these fields that we never really expect to update well.
1361 $genericFieldsToSkip = ['currency', 'id', strtolower($entityName) . '_id', 'is_primary'];
1362 foreach ($fields as $field => $specs) {
1363 $resetFKTo = NULL;
1364 $fieldName = $field;
1365
1366 if (in_array($field, $genericFieldsToSkip)
1367 || in_array($field, $entityValuesThatDoNotWork)
1368 ) {
1369 //@todo id & entity_id are correct but we should fix currency & frequency_day
1370 continue;
1371 }
1372 $this->assertArrayHasKey('type', $specs, "the _spec function for $entityName field $field does not specify the type");
1373 switch ($specs['type']) {
1374 case CRM_Utils_Type::T_DATE:
1375 $entity[$fieldName] = '2012-05-20';
1376 break;
1377
1378 case CRM_Utils_Type::T_TIMESTAMP:
1379 case 12:
1380 $entity[$fieldName] = '2012-05-20 03:05:20';
1381 break;
1382
1383 case CRM_Utils_Type::T_STRING:
1384 case CRM_Utils_Type::T_BLOB:
1385 case CRM_Utils_Type::T_MEDIUMBLOB:
1386 case CRM_Utils_Type::T_TEXT:
1387 case CRM_Utils_Type::T_LONGTEXT:
1388 case CRM_Utils_Type::T_EMAIL:
1389 if ($fieldName == 'form_values' && $entityName == 'SavedSearch') {
1390 // This is a hack for the SavedSearch API.
1391 // It expects form_values to be an array.
1392 // If you want to fix this, you should definitely read this forum
1393 // post.
1394 // http://forum.civicrm.org/index.php/topic,33990.0.html
1395 // See also my question on the CiviCRM Stack Exchange:
1396 // https://civicrm.stackexchange.com/questions/3437
1397 $entity[$fieldName] = ['sort_name' => "SortName2"];
1398 }
1399 else {
1400 $entity[$fieldName] = substr('New String', 0, CRM_Utils_Array::Value('maxlength', $specs, 100));
1401 if ($fieldName == 'email') {
1402 $entity[$fieldName] = strtolower($entity[$fieldName]);
1403 }
1404 // typecast with array to satisfy changes made in CRM-13160
1405 if ($entityName == 'MembershipType' && in_array($fieldName, [
1406 'relationship_type_id',
1407 'relationship_direction',
1408 ])) {
1409 $entity[$fieldName] = (array) $entity[$fieldName];
1410 }
1411 }
1412 break;
1413
1414 case CRM_Utils_Type::T_INT:
1415 // probably created with a 1
1416 if ($fieldName == 'weight') {
1417 $entity[$fieldName] = 2;
1418 }
1419 elseif (!empty($specs['FKClassName'])) {
1420 if ($specs['FKClassName'] == $baoString) {
1421 $entity[$fieldName] = (string) $entity2['id'];
1422 }
1423 else {
1424 if (!empty($entity[$fieldName])) {
1425 $resetFKTo = [$fieldName => $entity[$fieldName]];
1426 }
1427 $entity[$fieldName] = (string) empty($entity2[$field]) ? '' : $entity2[$field];
1428 //todo - there isn't always something set here - & our checking on unset values is limited
1429 if (empty($entity[$field])) {
1430 unset($entity[$field]);
1431 }
1432 }
1433 }
1434 else {
1435 $entity[$fieldName] = '6';
1436 }
1437 break;
1438
1439 case CRM_Utils_Type::T_BOOLEAN:
1440 // probably created with a 1
1441 $entity[$fieldName] = '0';
1442 break;
1443
1444 case CRM_Utils_Type::T_FLOAT:
1445 case CRM_Utils_Type::T_MONEY:
1446 $entity[$field] = '22.75';
1447 break;
1448
1449 case CRM_Utils_Type::T_URL:
1450 $entity[$field] = 'warm.beer.com';
1451 }
1452 if (empty($specs['FKClassName']) && (!empty($specs['pseudoconstant']) || !empty($specs['options']))) {
1453 $options = CRM_Utils_Array::value('options', $specs, []);
1454 if (!$options) {
1455 //eg. pdf_format id doesn't ship with any
1456 if (isset($specs['pseudoconstant']['optionGroupName'])) {
1457 $optionValue = $this->callAPISuccess('option_value', 'create', [
1458 'option_group_id' => $specs['pseudoconstant']['optionGroupName'],
1459 'label' => 'new option value',
1460 'sequential' => 1,
1461 ]);
1462 $optionValue = $optionValue['values'];
1463 $keyColumn = CRM_Utils_Array::value('keyColumn', $specs['pseudoconstant'], 'value');
1464 $options[$optionValue[0][$keyColumn]] = 'new option value';
1465 }
1466 }
1467 $entity[$field] = array_rand($options);
1468 }
1469 if (!empty($specs['FKClassName']) && !empty($specs['pseudoconstant'])) {
1470 // in the weird situation where a field has both an fk and pseudoconstant defined,
1471 // e.g. campaign_id field, need to flush caches.
1472 // FIXME: Why doesn't creating a campaign clear caches?
1473 civicrm_api3($entityName, 'getfields', ['cache_clear' => 1]);
1474 }
1475 $updateParams = [
1476 'id' => $entity['id'],
1477 $field => isset($entity[$field]) ? $entity[$field] : NULL,
1478 ];
1479 if (isset($updateParams['financial_type_id']) && in_array($entityName, ['Grant'])) {
1480 //api has special handling on these 2 fields for backward compatibility reasons
1481 $entity['contribution_type_id'] = $updateParams['financial_type_id'];
1482 }
1483 if (isset($updateParams['next_sched_contribution_date']) && in_array($entityName, ['ContributionRecur'])) {
1484 //api has special handling on these 2 fields for backward compatibility reasons
1485 $entity['next_sched_contribution'] = $updateParams['next_sched_contribution_date'];
1486 }
1487 if (isset($updateParams['image'])) {
1488 // Image field is passed through simplifyURL function so may be different, do the same here for comparison
1489 $entity['image'] = CRM_Utils_String::simplifyURL($updateParams['image'], TRUE);
1490 }
1491 if (isset($updateParams['thumbnail'])) {
1492 // Thumbnail field is passed through simplifyURL function so may be different, do the same here for comparison
1493 $entity['thumbnail'] = CRM_Utils_String::simplifyURL($updateParams['thumbnail'], TRUE);
1494 }
1495
1496 $update = $this->callAPISuccess($entityName, 'create', $updateParams);
1497 $checkParams = [
1498 'id' => $entity['id'],
1499 'sequential' => 1,
1500 'return' => $return,
1501 'options' => [
1502 'sort' => 'id DESC',
1503 'limit' => 2,
1504 ],
1505 ];
1506
1507 $checkEntity = $this->callAPISuccess($entityName, 'getsingle', $checkParams);
1508 if (!empty($specs['serialize']) && !is_array($checkEntity[$field])) {
1509 // Put into serialized format for comparison if 'get' has not returned serialized.
1510 $entity[$field] = CRM_Core_DAO::serializeField($checkEntity[$field], $specs['serialize']);
1511 }
1512
1513 $this->assertAPIArrayComparison($entity, $checkEntity, [], "checking if $fieldName was correctly updated\n" . print_r([
1514 'update-params' => $updateParams,
1515 'update-result' => $update,
1516 'getsingle-params' => $checkParams,
1517 'getsingle-result' => $checkEntity,
1518 'expected entity' => $entity,
1519 ], TRUE));
1520 if ($resetFKTo) {
1521 //reset the foreign key fields because otherwise our cleanup routine fails & some other unexpected stuff can kick in
1522 $entity = array_merge($entity, $resetFKTo);
1523 $updateParams = array_merge($updateParams, $resetFKTo);
1524 $this->callAPISuccess($entityName, 'create', $updateParams);
1525 if (isset($updateParams['financial_type_id']) && in_array($entityName, ['Grant'])) {
1526 //api has special handling on these 2 fields for backward compatibility reasons
1527 $entity['contribution_type_id'] = $updateParams['financial_type_id'];
1528 }
1529 if (isset($updateParams['next_sched_contribution_date']) && in_array($entityName, ['ContributionRecur'])) {
1530 //api has special handling on these 2 fields for backward compatibility reasons
1531 $entity['next_sched_contribution'] = $updateParams['next_sched_contribution_date'];
1532 }
1533 }
1534 }
1535 }
1536
1537 /* ---- testing the _getFields ---- */
1538
1539 /* ---- testing the _delete ---- */
1540
1541 /**
1542 * @dataProvider toBeSkipped_delete
1543 * entities that don't need a delete action
1544 * @param $Entity
1545 */
1546 public function testNotImplemented_delete($Entity) {
1547 $nonExistantID = 151416349;
1548 $result = civicrm_api($Entity, 'Delete', ['version' => 3, 'id' => $nonExistantID]);
1549 $this->assertEquals(1, $result['is_error']);
1550 $this->assertContains(strtolower("API ($Entity, Delete) does not exist"), strtolower($result['error_message']));
1551 }
1552
1553 /**
1554 * @dataProvider entities_delete
1555 * @param $Entity
1556 */
1557 public function testEmptyParam_delete($Entity) {
1558 if (in_array($Entity, $this->toBeImplemented['delete'])) {
1559 // $this->markTestIncomplete("civicrm_api3_{$Entity}_delete to be implemented");
1560 return;
1561 }
1562 $result = civicrm_api($Entity, 'Delete', []);
1563 $this->assertEquals(1, $result['is_error']);
1564 $this->assertContains("Unknown api version", $result['error_message']);
1565 }
1566
1567 /**
1568 * @dataProvider entities_delete
1569 * @param $Entity
1570 * @throws \PHPUnit\Framework\IncompleteTestError
1571 */
1572 public function testInvalidID_delete($Entity) {
1573 $result = $this->callAPIFailure($Entity, 'Delete', ['id' => 999999]);
1574 }
1575
1576 /**
1577 * Create two entities and make sure delete action only deletes one!
1578 *
1579 * @dataProvider entities_delete
1580 *
1581 * limitations include the problem with avoiding loops when creating test objects -
1582 * hence FKs only set by createTestObject when required. e.g parent_id on campaign is not being followed through
1583 * Currency - only seems to support US
1584 * @param $entityName
1585 * @throws \PHPUnit\Framework\IncompleteTestError
1586 */
1587 public function testByID_delete($entityName) {
1588 // turn test off for noew
1589 $this->markTestIncomplete("Entity [$entityName] cannot be mocked - no known DAO");
1590 return;
1591
1592 if (in_array($entityName, self::toBeSkipped_automock(TRUE))) {
1593 // $this->markTestIncomplete("civicrm_api3_{$Entity}_create to be implemented");
1594 return;
1595 }
1596 $startCount = $this->callAPISuccess($entityName, 'getcount', []);
1597 $createcount = 2;
1598 $baos = $this->getMockableBAOObjects($entityName, $createcount);
1599 list($baoObj1, $baoObj2) = $baos;
1600
1601 // make sure exactly 2 exist
1602 $result = $this->callAPISuccess($entityName, 'getcount', [],
1603 $createcount + $startCount
1604 );
1605
1606 $this->callAPISuccess($entityName, 'delete', ['id' => $baoObj2->id]);
1607 //make sure 1 less exists now
1608 $result = $this->callAPISuccess($entityName, 'getcount', [],
1609 ($createcount + $startCount) - 1
1610 );
1611
1612 //make sure id #1 exists
1613 $result = $this->callAPISuccess($entityName, 'getcount', ['id' => $baoObj1->id],
1614 1
1615 );
1616 //make sure id #2 desn't exist
1617 $result = $this->callAPISuccess($entityName, 'getcount', ['id' => $baoObj2->id],
1618 0
1619 );
1620 }
1621
1622 /**
1623 * Create two entities and make sure delete action only deletes one!
1624 *
1625 * @dataProvider entities_getfields
1626 * @param $entity
1627 */
1628 public function testGetfieldsHasTitle($entity) {
1629 $entities = $this->getEntitiesSupportingCustomFields();
1630 if (in_array($entity, $entities)) {
1631 $ids = $this->entityCustomGroupWithSingleFieldCreate(__FUNCTION__, $entity . 'Test.php');
1632 }
1633 $actions = $this->callAPISuccess($entity, 'getactions', []);
1634 foreach ($actions['values'] as $action) {
1635 if (substr($action, -7) == '_create' || substr($action, -4) == '_get' || substr($action, -7) == '_delete') {
1636 //getactions can't distinguish between contribution_page.create & contribution_page.create
1637 continue;
1638 }
1639 $fields = $this->callAPISuccess($entity, 'getfields', ['action' => $action]);
1640 if (!empty($ids) && in_array($action, ['create', 'get'])) {
1641 $this->assertArrayHasKey('custom_' . $ids['custom_field_id'], $fields['values']);
1642 }
1643
1644 foreach ($fields['values'] as $fieldName => $fieldSpec) {
1645 $this->assertArrayHasKey('title', $fieldSpec, "no title for $entity - $fieldName on action $action");
1646 $this->assertNotEmpty($fieldSpec['title'], "empty title for $entity - $fieldName");
1647 }
1648 }
1649 if (!empty($ids)) {
1650 $this->customFieldDelete($ids['custom_field_id']);
1651 $this->customGroupDelete($ids['custom_group_id']);
1652 }
1653 }
1654
1655 /**
1656 * @return array
1657 */
1658 public function getEntitiesSupportingCustomFields() {
1659 $entities = self::custom_data_entities_get();
1660 $returnEntities = [];
1661 foreach ($entities as $entityArray) {
1662 $returnEntities[] = $entityArray[0];
1663 }
1664 return $returnEntities;
1665 }
1666
1667 /**
1668 * @param string $entityName
1669 * @param int $count
1670 *
1671 * @return array
1672 */
1673 private function getMockableBAOObjects($entityName, $count = 2) {
1674 $baoString = _civicrm_api3_get_BAO($entityName);
1675 if (empty($baoString)) {
1676 $this->markTestIncomplete("Entity [$entityName] cannot be mocked - no known DAO");
1677 return [];
1678 }
1679 $baos = [];
1680 $i = 0;
1681 while ($i < $count) {
1682 // create entities
1683 $baoObj = CRM_Core_DAO::createTestObject($baoString, ['currency' => 'USD']);
1684 $this->assertTrue(is_int($baoObj->id), 'check first id');
1685 $this->deletableTestObjects[$baoString][] = $baoObj->id;
1686 $baos[] = $baoObj;
1687 $i++;
1688 }
1689 return $baos;
1690 }
1691
1692 /**
1693 * Verify that HTML metacharacters provided as inputs appear consistently.
1694 * as outputs.
1695 *
1696 * At time of writing, the encoding scheme requires (for example) that an
1697 * event title be partially-HTML-escaped before writing to DB. To provide
1698 * consistency, the API must perform extra encoding and decoding on some
1699 * fields.
1700 *
1701 * In this example, the event 'title' is subject to encoding, but the
1702 * event 'description' is not.
1703 */
1704 public function testEncodeDecodeConsistency() {
1705 // Create example
1706 $createResult = civicrm_api('Event', 'Create', [
1707 'version' => 3,
1708 'title' => 'CiviCRM <> TheRest',
1709 'description' => 'TheRest <> CiviCRM',
1710 'event_type_id' => 1,
1711 'is_public' => 1,
1712 'start_date' => 20081021,
1713 ]);
1714 $this->assertAPISuccess($createResult);
1715 $eventId = $createResult['id'];
1716 $this->assertEquals('CiviCRM <> TheRest', $createResult['values'][$eventId]['title']);
1717 $this->assertEquals('TheRest <> CiviCRM', $createResult['values'][$eventId]['description']);
1718
1719 // Verify "get" handles decoding in result value
1720 $getByIdResult = civicrm_api('Event', 'Get', [
1721 'version' => 3,
1722 'id' => $eventId,
1723 ]);
1724 $this->assertAPISuccess($getByIdResult);
1725 $this->assertEquals('CiviCRM <> TheRest', $getByIdResult['values'][$eventId]['title']);
1726 $this->assertEquals('TheRest <> CiviCRM', $getByIdResult['values'][$eventId]['description']);
1727
1728 // Verify "get" handles encoding in search value
1729 $getByTitleResult = civicrm_api('Event', 'Get', [
1730 'version' => 3,
1731 'title' => 'CiviCRM <> TheRest',
1732 ]);
1733 $this->assertAPISuccess($getByTitleResult);
1734 $this->assertEquals('CiviCRM <> TheRest', $getByTitleResult['values'][$eventId]['title']);
1735 $this->assertEquals('TheRest <> CiviCRM', $getByTitleResult['values'][$eventId]['description']);
1736
1737 // Verify that "getSingle" handles decoding
1738 $getSingleResult = $this->callAPISuccess('Event', 'GetSingle', [
1739 'id' => $eventId,
1740 ]);
1741
1742 $this->assertEquals('CiviCRM <> TheRest', $getSingleResult['title']);
1743 $this->assertEquals('TheRest <> CiviCRM', $getSingleResult['description']);
1744
1745 // Verify that chaining handles decoding
1746 $chainResult = $this->callAPISuccess('Event', 'Get', [
1747 'id' => $eventId,
1748 'api.event.get' => [],
1749 ]);
1750 $this->assertEquals('CiviCRM <> TheRest', $chainResult['values'][$eventId]['title']);
1751 $this->assertEquals('TheRest <> CiviCRM', $chainResult['values'][$eventId]['description']);
1752 $this->assertEquals('CiviCRM <> TheRest', $chainResult['values'][$eventId]['api.event.get']['values'][0]['title']);
1753 $this->assertEquals('TheRest <> CiviCRM', $chainResult['values'][$eventId]['api.event.get']['values'][0]['description']);
1754
1755 // Verify that "setvalue" handles encoding for updates
1756 $setValueTitleResult = civicrm_api('Event', 'setvalue', [
1757 'version' => 3,
1758 'id' => $eventId,
1759 'field' => 'title',
1760 'value' => 'setValueTitle: CiviCRM <> TheRest',
1761 ]);
1762 $this->assertAPISuccess($setValueTitleResult);
1763 $this->assertEquals('setValueTitle: CiviCRM <> TheRest', $setValueTitleResult['values']['title']);
1764 $setValueDescriptionResult = civicrm_api('Event', 'setvalue', [
1765 'version' => 3,
1766 'id' => $eventId,
1767 'field' => 'description',
1768 'value' => 'setValueDescription: TheRest <> CiviCRM',
1769 ]);
1770 //$this->assertTrue((bool)$setValueDescriptionResult['is_error']); // not supported by setValue
1771 $this->assertEquals('setValueDescription: TheRest <> CiviCRM', $setValueDescriptionResult['values']['description']);
1772 }
1773
1774 /**
1775 * Verify that write operations (create/update) use partial HTML-encoding
1776 *
1777 * In this example, the event 'title' is subject to encoding, but the
1778 * event 'description' is not.
1779 */
1780 public function testEncodeWrite() {
1781 // Create example
1782 $createResult = civicrm_api('Event', 'Create', [
1783 'version' => 3,
1784 'title' => 'createNew: CiviCRM <> TheRest',
1785 'description' => 'createNew: TheRest <> CiviCRM',
1786 'event_type_id' => 1,
1787 'is_public' => 1,
1788 'start_date' => 20081021,
1789 ]);
1790 $this->assertAPISuccess($createResult);
1791 $eventId = $createResult['id'];
1792 $this->assertDBQuery('createNew: CiviCRM &lt;&gt; TheRest', 'SELECT title FROM civicrm_event WHERE id = %1', [
1793 1 => [$eventId, 'Integer'],
1794 ]);
1795 $this->assertDBQuery('createNew: TheRest <> CiviCRM', 'SELECT description FROM civicrm_event WHERE id = %1', [
1796 1 => [$eventId, 'Integer'],
1797 ]);
1798
1799 // Verify that "create" handles encoding for updates
1800 $createWithIdResult = civicrm_api('Event', 'Create', [
1801 'version' => 3,
1802 'id' => $eventId,
1803 'title' => 'createWithId: CiviCRM <> TheRest',
1804 'description' => 'createWithId: TheRest <> CiviCRM',
1805 ]);
1806 $this->assertAPISuccess($createWithIdResult);
1807 $this->assertDBQuery('createWithId: CiviCRM &lt;&gt; TheRest', 'SELECT title FROM civicrm_event WHERE id = %1', [
1808 1 => [$eventId, 'Integer'],
1809 ]);
1810 $this->assertDBQuery('createWithId: TheRest <> CiviCRM', 'SELECT description FROM civicrm_event WHERE id = %1', [
1811 1 => [$eventId, 'Integer'],
1812 ]);
1813
1814 // Verify that "setvalue" handles encoding for updates
1815 $setValueTitleResult = civicrm_api('Event', 'setvalue', [
1816 'version' => 3,
1817 'id' => $eventId,
1818 'field' => 'title',
1819 'value' => 'setValueTitle: CiviCRM <> TheRest',
1820 ]);
1821 $this->assertAPISuccess($setValueTitleResult);
1822 $this->assertDBQuery('setValueTitle: CiviCRM &lt;&gt; TheRest', 'SELECT title FROM civicrm_event WHERE id = %1', [
1823 1 => [$eventId, 'Integer'],
1824 ]);
1825 $setValueDescriptionResult = civicrm_api('Event', 'setvalue', [
1826 'version' => 3,
1827 'id' => $eventId,
1828 'field' => 'description',
1829 'value' => 'setValueDescription: TheRest <> CiviCRM',
1830 ]);
1831 //$this->assertTrue((bool)$setValueDescriptionResult['is_error']); // not supported by setValue
1832 $this->assertAPISuccess($setValueDescriptionResult);
1833 $this->assertDBQuery('setValueDescription: TheRest <> CiviCRM', 'SELECT description FROM civicrm_event WHERE id = %1', [
1834 1 => [$eventId, 'Integer'],
1835 ]);
1836 }
1837
1838 }