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