Merge pull request #16469 from civicrm/5.22
[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 'SavedSearch',
100 'Logging',
101 ];
102 $this->toBeImplemented['delete'] = [
103 'Cxn',
104 'CxnApp',
105 'MembershipPayment',
106 'OptionGroup',
107 'SurveyRespondant',
108 'UFJoin',
109 'UFMatch',
110 'Extension',
111 'System',
112 'Payment',
113 'Order',
114 ];
115 $this->onlyIDNonZeroCount['get'] = [
116 'ActivityType',
117 'Entity',
118 'Domain',
119 'Setting',
120 'User',
121 ];
122 $this->deprecatedAPI = ['Location', 'ActivityType', 'SurveyRespondant'];
123 $this->deletableTestObjects = [];
124 }
125
126 public function tearDown() {
127 foreach ($this->deletableTestObjects as $entityName => $entities) {
128 foreach ($entities as $entityID) {
129 CRM_Core_DAO::deleteTestObjects($entityName, ['id' => $entityID]);
130 }
131 }
132 }
133
134 /**
135 * Generate list of all entities.
136 *
137 * @param array $skip
138 * Entities to skip.
139 *
140 * @return array
141 */
142 public static function entities($skip = []) {
143 // The order of operations in here is screwy. In the case where SYNTAX_CONFORMANCE_ENTITIES is
144 // defined, we should be able to parse+return it immediately. However, some weird dependency
145 // crept into the system where civicrm_api('Entity','get') must be called as part of entities()
146 // (even if its return value is ignored).
147
148 $tmp = civicrm_api('Entity', 'Get', ['version' => 3]);
149 if (getenv('SYNTAX_CONFORMANCE_ENTITIES')) {
150 $tmp = [
151 'values' => explode(' ', getenv('SYNTAX_CONFORMANCE_ENTITIES')),
152 ];
153 }
154
155 if (!is_array($skip)) {
156 $skip = [];
157 }
158 $tmp = array_diff($tmp['values'], $skip);
159 $entities = [];
160 foreach ($tmp as $e) {
161 $entities[] = [$e];
162 }
163 return $entities;
164 }
165
166 /**
167 * Get list of entities for get test.
168 *
169 * @return array
170 */
171 public static function entities_get() {
172 // all the entities, beside the ones flagged
173 return static::entities(static::toBeSkipped_get(TRUE));
174 }
175
176 /**
177 * Get entities for create tests.
178 *
179 * @return array
180 */
181 public static function entities_create() {
182 return static::entities(static::toBeSkipped_create(TRUE));
183 }
184
185 /**
186 * @return array
187 */
188 public static function entities_updatesingle() {
189 return static::entities(static::toBeSkipped_updatesingle(TRUE));
190 }
191
192 /**
193 * @return array
194 */
195 public static function entities_getlimit() {
196 return static::entities(static::toBeSkipped_getlimit());
197 }
198
199 /**
200 * Generate list of entities that can be retrieved using SQL operator syntax.
201 *
202 * @return array
203 */
204 public static function entities_getSqlOperators() {
205 return static::entities(static::toBeSkipped_getSqlOperators());
206 }
207
208 /**
209 * @return array
210 */
211 public static function entities_delete() {
212 return static::entities(static::toBeSkipped_delete(TRUE));
213 }
214
215 /**
216 * @return array
217 */
218 public static function entities_getfields() {
219 return static::entities(static::toBeSkipped_getfields(TRUE));
220 }
221
222 /**
223 * @return array
224 */
225 public static function custom_data_entities_get() {
226 return static::custom_data_entities();
227 }
228
229 /**
230 * @return array
231 */
232 public static function custom_data_entities() {
233 $entities = CRM_Core_BAO_CustomQuery::$extendsMap;
234 $enabledComponents = Civi::settings()->get('enable_components');
235 $customDataEntities = [];
236 $invalidEntities = ['Individual', 'Organization', 'Household'];
237 $entitiesToFix = ['Case', 'Relationship'];
238 foreach ($entities as $entityName => $entity) {
239 if (!in_array($entityName, $invalidEntities)
240 && !in_array($entityName, $entitiesToFix)
241 ) {
242 if (!empty(self::$componentMap[$entityName]) && empty($enabledComponents[self::$componentMap[$entityName]])) {
243 CRM_Core_BAO_ConfigSetting::enableComponent(self::$componentMap[$entityName]);
244 }
245 $customDataEntities[] = [$entityName];
246 }
247 }
248 return $customDataEntities;
249 }
250
251 /**
252 * Add a smattering of entities that don't normally have custom data.
253 *
254 * @return array
255 */
256 public static function custom_data_incl_non_std_entities_get() {
257 return static::entities(static::toBeSkipped_custom_data_creatable(TRUE));
258 }
259
260 /**
261 * Get entities to be skipped on get tests.
262 *
263 * @param bool $sequential
264 *
265 * @return array
266 */
267 public static function toBeSkipped_get($sequential = FALSE) {
268 $entitiesWithoutGet = [
269 'MailingEventSubscribe',
270 'MailingEventConfirm',
271 'MailingEventResubscribe',
272 'MailingEventUnsubscribe',
273 'Location',
274 ];
275 if ($sequential === TRUE) {
276 return $entitiesWithoutGet;
277 }
278 $entities = [];
279 foreach ($entitiesWithoutGet as $e) {
280 $entities[] = [$e];
281 }
282 return $entities;
283 }
284
285 /**
286 * Get entities to be skipped for get call.
287 *
288 * Mailing Contact Just doesn't support id. We have always insisted on finding a way to
289 * support id in API but in this case the underlying tables are crying out for a restructure
290 * & it just doesn't make sense.
291 *
292 * User doesn't support get By ID because the user id is actually the CMS user ID & is not part of
293 * CiviCRM - so can only be tested through UserTest - not SyntaxConformanceTest.
294 *
295 * Entity doesn't support get By ID because it simply gives the result of string Entites in CiviCRM
296 *
297 * @param bool $sequential
298 *
299 * @return array
300 * Entities that cannot be retrieved by ID
301 */
302 public static function toBeSkipped_getByID($sequential = FALSE) {
303 return ['MailingContact', 'User', 'Attachment', 'Entity'];
304 }
305
306 /**
307 * @param bool $sequential
308 *
309 * @return array
310 */
311 public static function toBeSkipped_create($sequential = FALSE) {
312 $entitiesWithoutCreate = ['Constant', 'Entity', 'Location', 'Profile', 'MailingRecipients'];
313 if ($sequential === TRUE) {
314 return $entitiesWithoutCreate;
315 }
316 $entities = [];
317 foreach ($entitiesWithoutCreate as $e) {
318 $entities[] = [$e];
319 }
320 return $entities;
321 }
322
323 /**
324 * @param bool $sequential
325 *
326 * @return array
327 */
328 public static function toBeSkipped_delete($sequential = FALSE) {
329 $entitiesWithout = [
330 'MailingContact',
331 'MailingEventConfirm',
332 'MailingEventResubscribe',
333 'MailingEventSubscribe',
334 'MailingEventUnsubscribe',
335 'MailingRecipients',
336 'Constant',
337 'Entity',
338 'Location',
339 'Domain',
340 'Profile',
341 'CustomValue',
342 'Setting',
343 'User',
344 'Logging',
345 ];
346 if ($sequential === TRUE) {
347 return $entitiesWithout;
348 }
349 $entities = [];
350 foreach ($entitiesWithout as $e) {
351 $entities[] = [$e];
352 }
353 return $entities;
354 }
355
356 /**
357 * @param bool $sequential
358 *
359 * @return array
360 */
361 public static function toBeSkipped_custom_data_creatable($sequential = FALSE) {
362 $entitiesWithout = [
363 // Ones to fix.
364 'CaseContact',
365 'CustomField',
366 'CustomGroup',
367 'DashboardContact',
368 'Domain',
369 'File',
370 'FinancialType',
371 'LocBlock',
372 'MailingEventConfirm',
373 'MailingEventResubscribe',
374 'MailingEventSubscribe',
375 'MailingEventUnsubscribe',
376 'MembershipPayment',
377 'SavedSearch',
378 'UFJoin',
379 'UFField',
380 'PriceFieldValue',
381 'GroupContact',
382 'EntityTag',
383 'PledgePayment',
384 'Relationship',
385
386 // ones that are not real entities hence not extendable.
387 'ActivityType',
388 'Entity',
389 'Cxn',
390 'Constant',
391 'Attachment',
392 'CustomSearch',
393 'CustomValue',
394 'CxnApp',
395 'Extension',
396 'MailingContact',
397 'User',
398 'System',
399 'Setting',
400 'SystemLog',
401 'ReportTemplate',
402 'MailingRecipients',
403 'SurveyRespondant',
404 'Profile',
405 'Payment',
406 'Order',
407 'MailingGroup',
408 'Logging',
409 ];
410 if ($sequential === TRUE) {
411 return $entitiesWithout;
412 }
413 $entities = [];
414 foreach ($entitiesWithout as $e) {
415 $entities[] = [$e];
416 }
417 return $entities;
418 }
419
420 /**
421 * @param bool $sequential
422 *
423 * @return array
424 * @todo add metadata for ALL these entities
425 */
426 public static function toBeSkipped_getfields($sequential = FALSE) {
427 $entitiesWithMetadataNotYetFixed = ['ReportTemplate', 'CustomSearch'];
428 if ($sequential === TRUE) {
429 return $entitiesWithMetadataNotYetFixed;
430 }
431 $entities = [];
432 foreach ($entitiesWithMetadataNotYetFixed as $e) {
433 $entities[] = [$e];
434 }
435 return $entities;
436 }
437
438 /**
439 * Generate list of entities to test for get by id functions.
440 * @param bool $sequential
441 * @return array
442 * Entities to be skipped
443 */
444 public static function toBeSkipped_automock($sequential = FALSE) {
445 $entitiesWithoutGet = [
446 'MailingContact',
447 'EntityTag',
448 'Participant',
449 'Setting',
450 'SurveyRespondant',
451 'MailingRecipients',
452 'CustomSearch',
453 'Extension',
454 'ReportTemplate',
455 'System',
456 'Logging',
457 'Payment',
458 ];
459 if ($sequential === TRUE) {
460 return $entitiesWithoutGet;
461 }
462 $entities = [];
463 foreach ($entitiesWithoutGet as $e) {
464 $entities[] = [$e];
465 }
466 return $entities;
467 }
468
469 /**
470 * At this stage exclude the ones that don't pass & add them as we can troubleshoot them
471 * @param bool $sequential
472 * @return array
473 */
474 public static function toBeSkipped_updatesingle($sequential = FALSE) {
475 $entitiesWithout = [
476 'Attachment',
477 // pseudo-entity; testUpdateSingleValueAlter doesn't introspect properly on it. Multiple magic fields
478 'Mailing',
479 'MailingEventUnsubscribe',
480 'MailingEventSubscribe',
481 'Constant',
482 'Entity',
483 'Location',
484 'Profile',
485 'CustomValue',
486 'UFJoin',
487 'Relationship',
488 'RelationshipType',
489 'Note',
490 'Membership',
491 'Group',
492 'File',
493 'EntityTag',
494 'CustomField',
495 'CustomGroup',
496 'Contribution',
497 'ActivityType',
498 'MailingEventConfirm',
499 'Case',
500 'CaseContact',
501 'Contact',
502 'ContactType',
503 'MailingEventResubscribe',
504 'UFGroup',
505 'Activity',
506 'Event',
507 'GroupContact',
508 'MembershipPayment',
509 'Participant',
510 'LineItem',
511 'ContributionPage',
512 'Phone',
513 'PaymentProcessor',
514 'Setting',
515 'MailingContact',
516 'SystemLog',
517 //skip this because it doesn't make sense to update logs,
518 'Logging',
519 ];
520 if ($sequential === TRUE) {
521 return $entitiesWithout;
522 }
523 $entities = [];
524 foreach ($entitiesWithout as $e) {
525 $entities[] = [
526 $e,
527 ];
528 }
529 return ['pledge'];
530 return $entities;
531 }
532
533 /**
534 * At this stage exclude the ones that don't pass & add them as we can troubleshoot them
535 */
536 public static function toBeSkipped_getlimit() {
537 $entitiesWithout = [
538 'Case',
539 //case api has non-std mandatory fields one of (case_id, contact_id, activity_id, contact_id)
540 'EntityTag',
541 // non-standard api - has inappropriate mandatory fields & doesn't implement limit
542 'Event',
543 // failed 'check that a 5 limit returns 5' - probably is_template field is wrong or something, or could be limit doesn't work right
544 'Extension',
545 // can't handle creating 25
546 'Note',
547 // fails on 5 limit - probably a set up problem
548 'Setting',
549 //a bit of a pseudoapi - keys by domain
550 'Payment',
551 // pseudoapi - problems with creating required sub entities.
552 ];
553 return $entitiesWithout;
554 }
555
556 /**
557 * At this stage exclude the ones that don't pass & add them as we can troubleshoot them
558 */
559 public static function toBeSkipped_getSqlOperators() {
560 $entitiesWithout = [
561 //case api has non-std mandatory fields one of (case_id, contact_id, activity_id, contact_id)
562 'Case',
563 // on the todo list!
564 'Contact',
565 // non-standard api - has inappropriate mandatory fields & doesn't implement limit
566 'EntityTag',
567 // can't handle creating 25
568 'Extension',
569 // note has a default get that isn't implemented in createTestObject -meaning you don't 'get' them
570 'Note',
571 //a bit of a pseudoapi - keys by domain
572 'Setting',
573 ];
574
575 // The testSqlOperators fails sporadically on MySQL 5.5, which is deprecated anyway.
576 // Test data providers should be able to run in pre-boot environment, so we connect directly to SQL server.
577 require_once 'DB.php';
578 $db = DB::connect(CIVICRM_DSN);
579 if ($db->connection instanceof mysqli && $db->connection->server_version < 50600) {
580 $entitiesWithout[] = 'Dedupe';
581 }
582
583 return $entitiesWithout;
584 }
585
586 /**
587 * @param $entity
588 * @param $key
589 *
590 * @return array
591 */
592 public function getKnownUnworkablesUpdateSingle($entity, $key) {
593 // can't update values are values for which updates don't result in the value being changed
594 $knownFailures = [
595 'ActionSchedule' => [
596 'cant_update' => [
597 'group_id',
598 ],
599 ],
600 'ActivityContact' => [
601 'cant_update' => [
602 'activity_id',
603 //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
604 ],
605 ],
606 'Address' => [
607 'cant_update' => [
608 //issues with country id - need to ensure same country
609 'state_province_id',
610 'world_region',
611 //creates relationship
612 'master_id',
613 ],
614 'cant_return' => ['street_parsing', 'skip_geocode', 'fix_address'],
615 ],
616 'Batch' => [
617 'cant_update' => [
618 // believe this field is defined in error
619 'entity_table',
620 ],
621 'cant_return' => [
622 'entity_table',
623 ],
624 ],
625 'CaseType' => [
626 'cant_update' => [
627 'definition',
628 ],
629 ],
630 'Domain' => ['cant_update' => ['domain_version']],
631 'MembershipBlock' => [
632 'cant_update' => [
633 // The fake/auto-generated values leave us unable to properly cleanup fake data
634 'entity_type',
635 'entity_id',
636 ],
637 ],
638 'MailingJob' => ['cant_update' => ['parent_id']],
639 'ContributionSoft' => [
640 'cant_update' => [
641 // can't be changed through api
642 'pcp_id',
643 ],
644 ],
645 'Email' => [
646 'cant_update' => [
647 // This is being legitimately manipulated to always have a valid primary - skip.
648 'is_primary',
649 ],
650 ],
651 'FinancialTrxn' => [
652 'cant_update' => [
653 // Altering fee amount will also cause net_amount to be recalculated.
654 'fee_amount',
655 ],
656 ],
657 'Navigation' => [
658 'cant_update' => [
659 // Weight is deliberately altered when this is changed - skip.
660 'parent_id',
661 ],
662 ],
663 'LocationType' => [
664 'cant_update' => [
665 // I'm on the fence about whether the test should skip or the behaviour is wrong.
666 // display_name is set to match name if display_name is not provided. It would be more 'normal'
667 // to only calculate a default IF id is not set - but perhaps the current behaviour is kind
668 // of what someone updating the name expects..
669 'name',
670 ],
671 ],
672 'Pledge' => [
673 'cant_update' => [
674 'pledge_original_installment_amount',
675 'installments',
676 'original_installment_amount',
677 'next_pay_date',
678 // can't be changed through API,
679 'amount',
680 ],
681 // if these are passed in they are retrieved from the wrong table
682 'break_return' => [
683 'honor_contact_id',
684 'cancel_date',
685 'contribution_page_id',
686 'financial_account_id',
687 'financial_type_id',
688 'currency',
689 ],
690 // can't be retrieved from api
691 'cant_return' => [
692 //due to uniquename missing
693 'honor_type_id',
694 'end_date',
695 'modified_date',
696 'acknowledge_date',
697 'start_date',
698 'frequency_day',
699 'currency',
700 'max_reminders',
701 'initial_reminder_day',
702 'additional_reminder_day',
703 'frequency_unit',
704 'pledge_contribution_page_id',
705 'pledge_status_id',
706 'pledge_campaign_id',
707 'pledge_financial_type_id',
708 ],
709 ],
710 'PaymentProcessorType' => [
711 'cant_update' => [
712 'billing_mode',
713 ],
714 'break_return' => [],
715 'cant_return' => [],
716 ],
717 'PriceFieldValue' => [
718 'cant_update' => [
719 //won't update as there is no 1 in the same price set
720 'weight',
721 ],
722 ],
723 'ReportInstance' => [
724 // View mode is part of the navigation which is not retrieved by the api.
725 'cant_return' => ['view_mode'],
726 ],
727 'SavedSearch' => [
728 // I think the fields below are generated based on form_values.
729 'cant_update' => [
730 'search_custom_id',
731 'where_clause',
732 'select_tables',
733 'where_tables',
734 ],
735 ],
736 'StatusPreference' => [
737 'break_return' => [
738 'ignore_severity',
739 ],
740 ],
741 'UFField' => [
742 'cant_update' => [
743 // These fields get auto-adjusted by the BAO prior to saving
744 'weight',
745 'location_type_id',
746 'phone_type_id',
747 'website_type_id',
748 // Not a real field
749 'option.autoweight',
750 ],
751 'break_return' => [
752 // These fields get auto-adjusted by the BAO prior to saving
753 'weight',
754 'field_type',
755 'location_type_id',
756 'phone_type_id',
757 'website_type_id',
758 // Not a real field
759 'option.autoweight',
760 ],
761 ],
762 'JobLog' => [
763 // For better or worse triggers override.
764 'break_return' => ['run_time'],
765 'cant_update' => ['run_time'],
766 ],
767 ];
768 if (empty($knownFailures[$entity]) || empty($knownFailures[$entity][$key])) {
769 return [];
770 }
771 return $knownFailures[$entity][$key];
772 }
773
774 /* ----- testing the _get ----- */
775
776 /**
777 * @dataProvider toBeSkipped_get
778 * Entities that don't need a get action
779 * @param $Entity
780 */
781 public function testNotImplemented_get($Entity) {
782 $result = civicrm_api($Entity, 'Get', ['version' => 3]);
783 $this->assertEquals(1, $result['is_error']);
784 // $this->assertContains("API ($Entity, Get) does not exist", $result['error_message']);
785 $this->assertRegExp('/API (.*) does not exist/', $result['error_message']);
786 }
787
788 /**
789 * @dataProvider entities
790 * @param $Entity
791 */
792 public function testGetFields($Entity) {
793 if (in_array($Entity, $this->deprecatedAPI) || $Entity == 'Entity' || $Entity == 'CustomValue') {
794 return;
795 }
796
797 $result = civicrm_api($Entity, 'getfields', ['version' => 3]);
798 $this->assertTrue(is_array($result['values']), "$Entity ::get fields doesn't return values array in line " . __LINE__);
799 foreach ($result['values'] as $key => $value) {
800 $this->assertTrue(is_array($value), $Entity . "::" . $key . " is not an array in line " . __LINE__);
801 }
802 }
803
804 /**
805 * @dataProvider entities_get
806 * @param $Entity
807 */
808 public function testEmptyParam_get($Entity) {
809
810 if (in_array($Entity, $this->toBeImplemented['get'])) {
811 // $this->markTestIncomplete("civicrm_api3_{$Entity}_get to be implemented");
812 return;
813 }
814 $result = civicrm_api($Entity, 'Get', []);
815 $this->assertEquals(1, $result['is_error']);
816 $this->assertContains("Unknown api version", $result['error_message']);
817 }
818
819 /**
820 * @dataProvider entities_get
821 * @Xdepends testEmptyParam_get // no need to test the simple if the empty doesn't work/is skipped. doesn't seem to work
822 * @param $Entity
823 */
824 public function testSimple_get($Entity) {
825 // $this->markTestSkipped("test gives core error on test server (but not on our locals). Skip until we can get server to pass");
826 if (in_array($Entity, $this->toBeImplemented['get'])) {
827 return;
828 }
829 $result = civicrm_api($Entity, 'Get', ['version' => 3]);
830 // @TODO: list the get that have mandatory params
831 if ($result['is_error']) {
832 $this->assertContains("Mandatory key(s) missing from params array", $result['error_message']);
833 // either id or contact_id or entity_id is one of the field missing
834 $this->assertContains("id", $result['error_message']);
835 }
836 else {
837 $this->assertEquals(3, $result['version']);
838 $this->assertArrayHasKey('count', $result);
839 $this->assertArrayHasKey('values', $result);
840 }
841 }
842
843 /**
844 * @dataProvider custom_data_incl_non_std_entities_get
845 * @param $entityName
846 */
847 public function testCustomDataGet($entityName) {
848 if ($entityName === 'Note') {
849 $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');
850 }
851 $this->quickCleanup(['civicrm_uf_match']);
852 // so subsidiary activities are created
853 $this->createLoggedInUser();
854
855 $entitiesWithNamingIssues = [
856 'SmsProvider' => 'Provider',
857 'AclRole' => 'EntityRole',
858 'MailingEventQueue' => 'Queue',
859 'Dedupe' => 'PrevNextCache',
860 ];
861
862 $usableName = !empty($entitiesWithNamingIssues[$entityName]) ? $entitiesWithNamingIssues[$entityName] : $entityName;
863 $optionName = CRM_Core_DAO_AllCoreTables::getTableForClass(CRM_Core_DAO_AllCoreTables::getFullName($usableName));
864
865 if (!isset(CRM_Core_BAO_CustomQuery::$extendsMap[$entityName])) {
866 $createdValue = $this->callAPISuccess('OptionValue', 'create', [
867 'option_group_id' => 'cg_extend_objects',
868 'label' => $usableName,
869 'value' => $usableName,
870 'name' => $optionName,
871 ]);
872 }
873 // We are not passing 'check_permissions' so the the more limited permissions *should* be
874 // ignored but per CRM-17700 there is a history of custom data applying permissions when it shouldn't.
875 CRM_Core_Config::singleton()->userPermissionClass->permissions = ['access CiviCRM', 'view my contact'];
876 $objects = $this->getMockableBAOObjects($entityName, 1);
877
878 // simple custom field
879 $ids = $this->entityCustomGroupWithSingleFieldCreate(__FUNCTION__, $usableName . 'Test.php');
880 $customFieldName = 'custom_' . $ids['custom_field_id'];
881 $params = ['id' => $objects[0]->id, 'custom_' . $ids['custom_field_id'] => "custom string"];
882 $result = $this->callAPISuccess($entityName, 'create', $params);
883 $this->assertTrue(isset($result['id']), 'no id on ' . $entityName);
884 $getParams = ['id' => $result['id'], 'return' => [$customFieldName]];
885 $check = $this->callAPISuccess($entityName, 'get', $getParams);
886 $this->assertTrue(!empty($check['values'][$check['id']][$customFieldName]), 'Custom data not present for ' . $entityName);
887 $this->assertEquals("custom string", $check['values'][$check['id']][$customFieldName], 'Custom data not present for ' . $entityName);
888 $this->customFieldDelete($ids['custom_field_id']);
889 $this->customGroupDelete($ids['custom_group_id']);
890
891 $ids2 = $this->entityCustomGroupWithSingleStringMultiSelectFieldCreate(__FUNCTION__, $usableName . 'Test.php');
892 $customFieldNameMultiSelect = 'custom_' . $ids2['custom_field_id'];
893 // String custom field, Multi-Select html type
894 foreach ($ids2['custom_field_group_options'] as $option_value => $option_label) {
895 $params = ['id' => $objects[0]->id, 'custom_' . $ids2['custom_field_id'] => $option_value];
896 $result = $this->callAPISuccess($entityName, 'create', $params);
897 $getParams = [$customFieldNameMultiSelect => $option_value, 'return' => [$customFieldNameMultiSelect]];
898 $this->callAPISuccessGetCount($entityName, $getParams, 1);
899 }
900
901 // cleanup
902 $this->customFieldDelete($ids2['custom_field_id']);
903 $this->customGroupDelete($ids2['custom_group_id']);
904
905 $this->callAPISuccess($entityName, 'delete', ['id' => $result['id']]);
906 $this->quickCleanup(['civicrm_uf_match']);
907 if (!empty($createdValue)) {
908 $this->callAPISuccess('OptionValue', 'delete', ['id' => $createdValue['id']]);
909 }
910 }
911
912 /**
913 * @dataProvider entities_get
914 * @param $Entity
915 */
916 public function testAcceptsOnlyID_get($Entity) {
917 // big random number. fun fact: if you multiply it by pi^e, the result is another random number, but bigger ;)
918 $nonExistantID = 30867307034;
919 if (in_array($Entity, $this->toBeImplemented['get'])
920 || in_array($Entity, $this->toBeSkipped_getByID())
921 ) {
922 return;
923 }
924
925 // FIXME
926 // the below function returns different values and hence an early return
927 // we'll fix this once beta1 is released
928 // return;
929
930 $result = civicrm_api($Entity, 'Get', ['version' => 3, 'id' => $nonExistantID]);
931
932 if ($result['is_error']) {
933 // just to get a clearer message in the log
934 $this->assertEquals("only id should be enough", $result['error_message']);
935 }
936 if (!in_array($Entity, $this->onlyIDNonZeroCount['get'])) {
937 $this->assertEquals(0, $result['count']);
938 }
939 }
940
941 /**
942 * Test getlist works
943 * @dataProvider entities_get
944 * @param $Entity
945 */
946 public function testGetList($Entity) {
947 if (in_array($Entity, $this->toBeImplemented['get'])
948 || in_array($Entity, $this->toBeSkipped_getByID())
949 ) {
950 return;
951 }
952 if (in_array($Entity, ['ActivityType', 'SurveyRespondant'])) {
953 $this->markTestSkipped();
954 }
955 $this->callAPISuccess($Entity, 'getlist', ['label_field' => 'id']);
956 }
957
958 /**
959 * Test getlist works when entity is lowercase
960 * @dataProvider entities_get
961 * @param $Entity
962 */
963 public function testGetListLowerCaseEntity($Entity) {
964 if (in_array($Entity, $this->toBeImplemented['get'])
965 || in_array($Entity, $this->toBeSkipped_getByID())
966 ) {
967 return;
968 }
969 if (in_array($Entity, ['ActivityType', 'SurveyRespondant'])) {
970 $this->markTestSkipped();
971 }
972 if ($Entity == 'UFGroup') {
973 $Entity = 'ufgroup';
974 }
975 $this->callAPISuccess($Entity, 'getlist', ['label_field' => 'id']);
976 }
977
978 /**
979 * Create two entities and make sure we can fetch them individually by ID.
980 *
981 * @dataProvider entities_get
982 *
983 * limitations include the problem with avoiding loops when creating test objects -
984 * hence FKs only set by createTestObject when required. e.g parent_id on campaign is not being followed through
985 * Currency - only seems to support US
986 * @param $entityName
987 */
988 public function testByID_get($entityName) {
989 if (in_array($entityName, self::toBeSkipped_automock(TRUE))) {
990 // $this->markTestIncomplete("civicrm_api3_{$Entity}_create to be implemented");
991 return;
992 }
993
994 $baos = $this->getMockableBAOObjects($entityName);
995 list($baoObj1, $baoObj2) = $baos;
996
997 // fetch first by ID
998 $result = $this->callAPISuccess($entityName, 'get', [
999 'id' => $baoObj1->id,
1000 ]);
1001
1002 $this->assertTrue(!empty($result['values'][$baoObj1->id]), 'Should find first object by id');
1003 $this->assertEquals($baoObj1->id, $result['values'][$baoObj1->id]['id'], 'Should find id on first object');
1004 $this->assertEquals(1, count($result['values']));
1005
1006 // fetch second by ID
1007 $result = $this->callAPISuccess($entityName, 'get', [
1008 'id' => $baoObj2->id,
1009 ]);
1010 $this->assertTrue(!empty($result['values'][$baoObj2->id]), 'Should find second object by id');
1011 $this->assertEquals($baoObj2->id, $result['values'][$baoObj2->id]['id'], 'Should find id on second object');
1012 $this->assertEquals(1, count($result['values']));
1013 }
1014
1015 /**
1016 * Ensure that the "get" operation accepts limiting the #result records.
1017 *
1018 * TODO Consider making a separate entity list ("entities_getlimit")
1019 * For the moment, the "entities_updatesingle" list should give a good
1020 * sense for which entities support createTestObject
1021 *
1022 * @dataProvider entities_getlimit
1023 *
1024 * @param string $entityName
1025 */
1026 public function testLimit($entityName) {
1027 // each case is array(0 => $inputtedApiOptions, 1 => $expectedResultCount)
1028 $cases = [];
1029 $cases[] = [
1030 ['options' => ['limit' => NULL]],
1031 30,
1032 'check that a NULL limit returns unlimited',
1033 ];
1034 $cases[] = [
1035 ['options' => ['limit' => FALSE]],
1036 30,
1037 'check that a FALSE limit returns unlimited',
1038 ];
1039 $cases[] = [
1040 ['options' => ['limit' => 0]],
1041 30,
1042 'check that a 0 limit returns unlimited',
1043 ];
1044 $cases[] = [
1045 ['options' => ['limit' => 5]],
1046 5,
1047 'check that a 5 limit returns 5',
1048 ];
1049 $cases[] = [
1050 [],
1051 25,
1052 'check that no limit returns 25',
1053 ];
1054
1055 $baoString = _civicrm_api3_get_BAO($entityName);
1056 if (empty($baoString)) {
1057 $this->markTestIncomplete("Entity [$entityName] cannot be mocked - no known DAO");
1058 return;
1059 }
1060
1061 // make 30 test items -- 30 > 25 (the default limit)
1062 $ids = [];
1063 for ($i = 0; $i < 30; $i++) {
1064 $baoObj = CRM_Core_DAO::createTestObject($baoString, ['currency' => 'USD']);
1065 $ids[] = $baoObj->id;
1066 }
1067
1068 // each case is array(0 => $inputtedApiOptions, 1 => $expectedResultCount)
1069 foreach ($cases as $case) {
1070 $this->checkLimitAgainstExpected($entityName, $case[0], $case[1], $case[2]);
1071
1072 //non preferred / legacy syntax
1073 if (isset($case[0]['options']['limit'])) {
1074 $this->checkLimitAgainstExpected($entityName, ['rowCount' => $case[0]['options']['limit']], $case[1], $case[2]);
1075 $this->checkLimitAgainstExpected($entityName, ['option_limit' => $case[0]['options']['limit']], $case[1], $case[2]);
1076 $this->checkLimitAgainstExpected($entityName, ['option.limit' => $case[0]['options']['limit']], $case[1], $case[2]);
1077 }
1078 }
1079 foreach ($ids as $id) {
1080 CRM_Core_DAO::deleteTestObjects($baoString, ['id' => $id]);
1081 }
1082 }
1083
1084 /**
1085 * Ensure that the "get" operation accepts limiting the #result records.
1086 *
1087 * @dataProvider entities_getSqlOperators
1088 *
1089 * @param string $entityName
1090 */
1091 public function testSqlOperators($entityName) {
1092 $toBeIgnored = array_merge($this->toBeImplemented['get'],
1093 $this->deprecatedAPI,
1094 $this->toBeSkipped_get(TRUE),
1095 $this->toBeSkipped_getByID()
1096 );
1097 if (in_array($entityName, $toBeIgnored)) {
1098 return;
1099 }
1100
1101 $baoString = _civicrm_api3_get_BAO($entityName);
1102
1103 $entities = $this->callAPISuccess($entityName, 'get', ['options' => ['limit' => 0], 'return' => 'id']);
1104 $entities = array_keys($entities['values']);
1105 $totalEntities = count($entities);
1106 if ($totalEntities < 3) {
1107 $ids = [];
1108 for ($i = 0; $i < 3 - $totalEntities; $i++) {
1109 $baoObj = CRM_Core_DAO::createTestObject($baoString, ['currency' => 'USD']);
1110 $ids[] = $baoObj->id;
1111 }
1112 $totalEntities = 3;
1113 }
1114 $entities = $this->callAPISuccess($entityName, 'get', ['options' => ['limit' => 0]]);
1115 $entities = array_keys($entities['values']);
1116 $this->assertGreaterThan(2, $totalEntities);
1117 $this->callAPISuccess($entityName, 'getsingle', ['id' => ['IN' => [$entities[0]]]]);
1118 $this->callAPISuccessGetCount($entityName, ['id' => ['NOT IN' => [$entities[0]]]], $totalEntities - 1);
1119 $this->callAPISuccessGetCount($entityName, ['id' => ['>' => $entities[0]]], $totalEntities - 1);
1120 }
1121
1122 /**
1123 * Check that get fetches an appropriate number of results.
1124 *
1125 * @param string $entityName
1126 * Name of entity to test.
1127 * @param array $params
1128 * @param int $limit
1129 * @param string $message
1130 */
1131 public function checkLimitAgainstExpected($entityName, $params, $limit, $message) {
1132 $result = $this->callAPISuccess($entityName, 'get', $params);
1133 if ($limit == 30) {
1134 $this->assertGreaterThanOrEqual($limit, $result['count'], $message);
1135 $this->assertGreaterThanOrEqual($limit, $result['count'], $message);
1136 }
1137 else {
1138 $this->assertEquals($limit, $result['count'], $message);
1139 $this->assertEquals($limit, count($result['values']), $message);
1140 }
1141 }
1142
1143 /**
1144 * Create two entities and make sure we can fetch them individually by ID (e.g. using "contact_id=>2"
1145 * or "group_id=>4")
1146 *
1147 * @dataProvider entities_get
1148 *
1149 * limitations include the problem with avoiding loops when creating test objects -
1150 * hence FKs only set by createTestObject when required. e.g parent_id on campaign is not being followed through
1151 * Currency - only seems to support US
1152 * @param $entityName
1153 * @throws \PHPUnit\Framework\IncompleteTestError
1154 */
1155 public function testByIDAlias_get($entityName) {
1156 if (in_array($entityName, self::toBeSkipped_automock(TRUE))) {
1157 // $this->markTestIncomplete("civicrm_api3_{$Entity}_create to be implemented");
1158 return;
1159 }
1160
1161 $baoString = _civicrm_api3_get_BAO($entityName);
1162 if (empty($baoString)) {
1163 $this->markTestIncomplete("Entity [$entityName] cannot be mocked - no known DAO");
1164 return;
1165 }
1166
1167 $idFieldName = _civicrm_api_get_entity_name_from_camel($entityName) . '_id';
1168
1169 // create entities
1170 $baoObj1 = CRM_Core_DAO::createTestObject($baoString, ['currency' => 'USD']);
1171 $this->assertTrue(is_int($baoObj1->id), 'check first id');
1172 $this->deletableTestObjects[$baoString][] = $baoObj1->id;
1173 $baoObj2 = CRM_Core_DAO::createTestObject($baoString, ['currency' => 'USD']);
1174 $this->assertTrue(is_int($baoObj2->id), 'check second id');
1175 $this->deletableTestObjects[$baoString][] = $baoObj2->id;
1176
1177 // fetch first by ID
1178 $result = civicrm_api($entityName, 'get', [
1179 'version' => 3,
1180 $idFieldName => $baoObj1->id,
1181 ]);
1182 $this->assertAPISuccess($result);
1183 $this->assertTrue(!empty($result['values'][$baoObj1->id]), 'Should find first object by id');
1184 $this->assertEquals($baoObj1->id, $result['values'][$baoObj1->id]['id'], 'Should find id on first object');
1185 $this->assertEquals(1, count($result['values']));
1186
1187 // fetch second by ID
1188 $result = civicrm_api($entityName, 'get', [
1189 'version' => 3,
1190 $idFieldName => $baoObj2->id,
1191 ]);
1192 $this->assertAPISuccess($result);
1193 $this->assertTrue(!empty($result['values'][$baoObj2->id]), 'Should find second object by id');
1194 $this->assertEquals($baoObj2->id, $result['values'][$baoObj2->id]['id'], 'Should find id on second object');
1195 $this->assertEquals(1, count($result['values']));
1196 }
1197
1198 /**
1199 * @dataProvider entities_get
1200 * @param $Entity
1201 */
1202 public function testNonExistantID_get($Entity) {
1203 // cf testAcceptsOnlyID_get
1204 $nonExistantID = 30867307034;
1205 if (in_array($Entity, $this->toBeImplemented['get'])) {
1206 return;
1207 }
1208
1209 $result = civicrm_api($Entity, 'Get', ['version' => 3, 'id' => $nonExistantID]);
1210
1211 // redundant with testAcceptsOnlyID_get
1212 if ($result['is_error']) {
1213 return;
1214 }
1215
1216 $this->assertArrayHasKey('version', $result);
1217 $this->assertEquals(3, $result['version']);
1218 if (!in_array($Entity, $this->onlyIDNonZeroCount['get'])) {
1219 $this->assertEquals(0, $result['count']);
1220 }
1221 }
1222
1223 /* ---- testing the _create ---- */
1224
1225 /**
1226 * @dataProvider toBeSkipped_create
1227 * entities that don't need a create action
1228 * @param $Entity
1229 */
1230 public function testNotImplemented_create($Entity) {
1231 $result = civicrm_api($Entity, 'Create', ['version' => 3]);
1232 $this->assertEquals(1, $result['is_error']);
1233 $this->assertContains(strtolower("API ($Entity, Create) does not exist"), strtolower($result['error_message']));
1234 }
1235
1236 /**
1237 * @dataProvider entities
1238 * @expectedException CiviCRM_API3_Exception
1239 * @param $Entity
1240 */
1241 public function testWithoutParam_create($Entity) {
1242 if ($Entity === 'Setting') {
1243 $this->markTestSkipped('It seems OK for setting to skip here as it silently sips invalid params');
1244 }
1245 elseif ($Entity === 'Mailing') {
1246 $this->markTestSkipped('It seems OK for "Mailing" to skip here because you can create empty drafts');
1247 }
1248 // should create php complaining that a param is missing
1249 civicrm_api3($Entity, 'Create');
1250 }
1251
1252 /**
1253 * @dataProvider entities_create
1254 *
1255 * Check that create doesn't work with an invalid
1256 * @param $Entity
1257 * @throws \PHPUnit\Framework\IncompleteTestError
1258 */
1259 public function testInvalidSort_get($Entity) {
1260 $invalidEntitys = ['ActivityType', 'Setting', 'System'];
1261 if (in_array($Entity, $invalidEntitys)) {
1262 $this->markTestSkipped('It seems OK for ' . $Entity . ' to skip here as it silently sips invalid params');
1263 }
1264 $result = $this->callAPIFailure($Entity, 'get', ['options' => ['sort' => 'sleep(1)']]);
1265 }
1266
1267 /**
1268 * @dataProvider entities_create
1269 *
1270 * Check that create doesn't work with an invalid
1271 * @param $Entity
1272 * @throws \PHPUnit\Framework\IncompleteTestError
1273 */
1274 public function testValidSortSingleArrayById_get($Entity) {
1275 $invalidEntitys = ['ActivityType', 'Setting', 'System'];
1276 $tests = [
1277 'id' => '_id',
1278 'id desc' => '_id desc',
1279 'id DESC' => '_id DESC',
1280 'id ASC' => '_id ASC',
1281 'id asc' => '_id asc',
1282 ];
1283 foreach ($tests as $test => $expected) {
1284 if (in_array($Entity, $invalidEntitys)) {
1285 $this->markTestSkipped('It seems OK for ' . $Entity . ' to skip here as it silently ignores passed in params');
1286 }
1287 $params = ['sort' => [$test]];
1288 $result = _civicrm_api3_get_options_from_params($params, FALSE, $Entity, 'get');
1289 $lowercase_entity = _civicrm_api_get_entity_name_from_camel($Entity);
1290 $this->assertEquals($lowercase_entity . $expected, $result['sort']);
1291 }
1292 }
1293
1294 /**
1295 * @dataProvider entities_create
1296 *
1297 * Check that create doesn't work with an invalid
1298 * @param $Entity
1299 * @throws \PHPUnit\Framework\IncompleteTestError
1300 */
1301 public function testInvalidID_create($Entity) {
1302 // turn test off for noew
1303 $this->markTestIncomplete("Entity [ $Entity ] cannot be mocked - no known DAO");
1304 return;
1305 if (in_array($Entity, $this->toBeImplemented['create'])) {
1306 // $this->markTestIncomplete("civicrm_api3_{$Entity}_create to be implemented");
1307 return;
1308 }
1309 $result = $this->callAPIFailure($Entity, 'Create', ['id' => 999]);
1310 }
1311
1312 /**
1313 * @dataProvider entities_updatesingle
1314 *
1315 * limitations include the problem with avoiding loops when creating test objects -
1316 * hence FKs only set by createTestObject when required. e.g parent_id on campaign is not being followed through
1317 * Currency - only seems to support US
1318 * @param $entityName
1319 */
1320 public function testCreateSingleValueAlter($entityName) {
1321 if (in_array($entityName, $this->toBeImplemented['create'])) {
1322 // $this->markTestIncomplete("civicrm_api3_{$Entity}_create to be implemented");
1323 return;
1324 }
1325
1326 $baoString = _civicrm_api3_get_BAO($entityName);
1327 $this->assertNotEmpty($baoString, $entityName);
1328 $this->assertNotEmpty($entityName, $entityName);
1329 $fieldsGet = $fields = $this->callAPISuccess($entityName, 'getfields', ['action' => 'get', 'options' => ['get_options' => 'all']]);
1330 if ($entityName != 'Pledge') {
1331 $fields = $this->callAPISuccess($entityName, 'getfields', ['action' => 'create', 'options' => ['get_options' => 'all']]);
1332 }
1333 $fields = $fields['values'];
1334 $return = array_keys($fieldsGet['values']);
1335 $valuesNotToReturn = $this->getKnownUnworkablesUpdateSingle($entityName, 'break_return');
1336 // these can't be requested as return values
1337 $entityValuesThatDoNotWork = array_merge(
1338 $this->getKnownUnworkablesUpdateSingle($entityName, 'cant_update'),
1339 $this->getKnownUnworkablesUpdateSingle($entityName, 'cant_return'),
1340 $valuesNotToReturn
1341 );
1342
1343 $return = array_diff($return, $valuesNotToReturn);
1344 $baoObj = new CRM_Core_DAO();
1345 $baoObj->createTestObject($baoString, ['currency' => 'USD'], 2, 0);
1346
1347 $getEntities = $this->callAPISuccess($entityName, 'get', [
1348 'sequential' => 1,
1349 'return' => $return,
1350 'options' => [
1351 'sort' => 'id DESC',
1352 'limit' => 2,
1353 ],
1354 ]);
1355
1356 // lets use first rather than assume only one exists
1357 $entity = $getEntities['values'][0];
1358 $entity2 = $getEntities['values'][1];
1359 $this->deletableTestObjects[$baoString][] = $entity['id'];
1360 $this->deletableTestObjects[$baoString][] = $entity2['id'];
1361 // Skip these fields that we never really expect to update well.
1362 $genericFieldsToSkip = ['currency', 'id', strtolower($entityName) . '_id', 'is_primary'];
1363 foreach ($fields as $field => $specs) {
1364 $resetFKTo = NULL;
1365 $fieldName = $field;
1366
1367 if (in_array($field, $genericFieldsToSkip)
1368 || in_array($field, $entityValuesThatDoNotWork)
1369 ) {
1370 //@todo id & entity_id are correct but we should fix currency & frequency_day
1371 continue;
1372 }
1373 $this->assertArrayHasKey('type', $specs, "the _spec function for $entityName field $field does not specify the type");
1374 switch ($specs['type']) {
1375 case CRM_Utils_Type::T_DATE:
1376 $entity[$fieldName] = '2012-05-20';
1377 break;
1378
1379 case CRM_Utils_Type::T_TIMESTAMP:
1380 case 12:
1381 $entity[$fieldName] = '2012-05-20 03:05:20';
1382 break;
1383
1384 case CRM_Utils_Type::T_STRING:
1385 case CRM_Utils_Type::T_BLOB:
1386 case CRM_Utils_Type::T_MEDIUMBLOB:
1387 case CRM_Utils_Type::T_TEXT:
1388 case CRM_Utils_Type::T_LONGTEXT:
1389 case CRM_Utils_Type::T_EMAIL:
1390 if ($fieldName == 'form_values' && $entityName == 'SavedSearch') {
1391 // This is a hack for the SavedSearch API.
1392 // It expects form_values to be an array.
1393 // If you want to fix this, you should definitely read this forum
1394 // post.
1395 // http://forum.civicrm.org/index.php/topic,33990.0.html
1396 // See also my question on the CiviCRM Stack Exchange:
1397 // https://civicrm.stackexchange.com/questions/3437
1398 $entity[$fieldName] = ['sort_name' => "SortName2"];
1399 }
1400 else {
1401 $entity[$fieldName] = substr('New String', 0, CRM_Utils_Array::Value('maxlength', $specs, 100));
1402 if ($fieldName == 'email') {
1403 $entity[$fieldName] = strtolower($entity[$fieldName]);
1404 }
1405 // typecast with array to satisfy changes made in CRM-13160
1406 if ($entityName == 'MembershipType' && in_array($fieldName, [
1407 'relationship_type_id',
1408 'relationship_direction',
1409 ])) {
1410 $entity[$fieldName] = (array) $entity[$fieldName];
1411 }
1412 }
1413 break;
1414
1415 case CRM_Utils_Type::T_INT:
1416 // probably created with a 1
1417 if ($fieldName == 'weight') {
1418 $entity[$fieldName] = 2;
1419 }
1420 elseif (!empty($specs['FKClassName'])) {
1421 if ($specs['FKClassName'] == $baoString) {
1422 $entity[$fieldName] = (string) $entity2['id'];
1423 }
1424 else {
1425 if (!empty($entity[$fieldName])) {
1426 $resetFKTo = [$fieldName => $entity[$fieldName]];
1427 }
1428 $entity[$fieldName] = (string) empty($entity2[$field]) ? '' : $entity2[$field];
1429 //todo - there isn't always something set here - & our checking on unset values is limited
1430 if (empty($entity[$field])) {
1431 unset($entity[$field]);
1432 }
1433 }
1434 }
1435 else {
1436 $entity[$fieldName] = '6';
1437 }
1438 break;
1439
1440 case CRM_Utils_Type::T_BOOLEAN:
1441 // probably created with a 1
1442 $entity[$fieldName] = '0';
1443 break;
1444
1445 case CRM_Utils_Type::T_FLOAT:
1446 case CRM_Utils_Type::T_MONEY:
1447 $entity[$field] = '22.75';
1448 break;
1449
1450 case CRM_Utils_Type::T_URL:
1451 $entity[$field] = 'warm.beer.com';
1452 }
1453 if (empty($specs['FKClassName']) && (!empty($specs['pseudoconstant']) || !empty($specs['options']))) {
1454 $options = CRM_Utils_Array::value('options', $specs, []);
1455 if (!$options) {
1456 //eg. pdf_format id doesn't ship with any
1457 if (isset($specs['pseudoconstant']['optionGroupName'])) {
1458 $optionValue = $this->callAPISuccess('option_value', 'create', [
1459 'option_group_id' => $specs['pseudoconstant']['optionGroupName'],
1460 'label' => 'new option value',
1461 'sequential' => 1,
1462 ]);
1463 $optionValue = $optionValue['values'];
1464 $keyColumn = CRM_Utils_Array::value('keyColumn', $specs['pseudoconstant'], 'value');
1465 $options[$optionValue[0][$keyColumn]] = 'new option value';
1466 }
1467 }
1468 $entity[$field] = array_rand($options);
1469 }
1470 if (!empty($specs['FKClassName']) && !empty($specs['pseudoconstant'])) {
1471 // in the weird situation where a field has both an fk and pseudoconstant defined,
1472 // e.g. campaign_id field, need to flush caches.
1473 // FIXME: Why doesn't creating a campaign clear caches?
1474 civicrm_api3($entityName, 'getfields', ['cache_clear' => 1]);
1475 }
1476 $updateParams = [
1477 'id' => $entity['id'],
1478 $field => isset($entity[$field]) ? $entity[$field] : NULL,
1479 ];
1480 if (isset($updateParams['financial_type_id']) && in_array($entityName, ['Grant'])) {
1481 //api has special handling on these 2 fields for backward compatibility reasons
1482 $entity['contribution_type_id'] = $updateParams['financial_type_id'];
1483 }
1484 if (isset($updateParams['next_sched_contribution_date']) && in_array($entityName, ['ContributionRecur'])) {
1485 //api has special handling on these 2 fields for backward compatibility reasons
1486 $entity['next_sched_contribution'] = $updateParams['next_sched_contribution_date'];
1487 }
1488 if (isset($updateParams['image'])) {
1489 // Image field is passed through simplifyURL function so may be different, do the same here for comparison
1490 $entity['image'] = CRM_Utils_String::simplifyURL($updateParams['image'], TRUE);
1491 }
1492 if (isset($updateParams['thumbnail'])) {
1493 // Thumbnail field is passed through simplifyURL function so may be different, do the same here for comparison
1494 $entity['thumbnail'] = CRM_Utils_String::simplifyURL($updateParams['thumbnail'], TRUE);
1495 }
1496
1497 $update = $this->callAPISuccess($entityName, 'create', $updateParams);
1498 $checkParams = [
1499 'id' => $entity['id'],
1500 'sequential' => 1,
1501 'return' => $return,
1502 'options' => [
1503 'sort' => 'id DESC',
1504 'limit' => 2,
1505 ],
1506 ];
1507
1508 $checkEntity = $this->callAPISuccess($entityName, 'getsingle', $checkParams);
1509 if (!empty($specs['serialize']) && !is_array($checkEntity[$field])) {
1510 // Put into serialized format for comparison if 'get' has not returned serialized.
1511 $entity[$field] = CRM_Core_DAO::serializeField($checkEntity[$field], $specs['serialize']);
1512 }
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 }