3793e52431443f358480e4301659d2ecf6001678
[civicrm-core.git] / tests / phpunit / api / v3 / SyntaxConformanceTest.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2014 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
26 */
27
28 require_once 'CiviTest/CiviUnitTestCase.php';
29
30
31 /**
32 * Test APIv3 civicrm_sytanc conformance* functions
33 *
34 * @package CiviCRM_APIv3
35 * @subpackage API_Core
36 */
37
38 class api_v3_SyntaxConformanceTest extends CiviUnitTestCase {
39 protected $_apiversion = 3;
40
41 /**
42 * @var array e.g. $this->deletes['CRM_Contact_DAO_Contact'][] = $contactID;
43 */
44 protected $deletableTestObjects;
45
46 /** This test case doesn't require DB reset */
47 public $DBResetRequired = FALSE;
48
49 protected $_entity;
50
51 /** Map custom group entities to civicrm components */
52 static $componentMap = array(
53 'Contact' => NULL,
54 'Individual' => NULL,
55 'Household' => NULL,
56 'Organization' => NULL,
57 'Contribution' => 'CiviContribute',
58 'Membership' => 'CiviMember',
59 'Participant' => 'CiviEvent',
60 'Group' => NULL,
61 'Relationship' => NULL,
62 'Event' => 'CiviEvent',
63 'Case' => 'CiviCase',
64 'Activity' => NULL,
65 'Pledge' => 'CiviPledge',
66 'Grant' => 'CiviGrant',
67 'Address' => NULL,
68 );
69
70 /* they are two types of missing APIs:
71 - Those that are to be implemented
72 (in some future version when someone steps in -hint hint-). List the entities in toBeImplemented[ {$action} ]
73 Those that don't exist
74 and that will never exist (eg an obsoleted Entity
75 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)
76 */
77 function setUp() {
78 parent::setUp();
79 $this->enableCiviCampaign();
80 $this->toBeImplemented['get'] = array('Profile', 'CustomValue', 'Constant', 'CustomSearch', 'Extension', 'ReportTemplate', 'System', 'Setting');
81 $this->toBeImplemented['create'] = array('SurveyRespondant', 'OptionGroup', 'MailingRecipients', 'UFMatch', 'LocationType', 'CustomSearch', 'Extension', 'ReportTemplate', 'System');
82 $this->toBeImplemented['delete'] = array('MembershipPayment', 'OptionGroup', 'SurveyRespondant', 'UFJoin', 'UFMatch', 'Extension', 'LocationType', 'System');
83 $this->onlyIDNonZeroCount['get'] = array('ActivityType', 'Entity', 'Domain','Setting');
84 $this->deprecatedAPI = array('Location', 'ActivityType', 'SurveyRespondant');
85 $this->deletableTestObjects = array();
86 }
87
88 function tearDown() {
89 foreach ($this->deletableTestObjects as $entityName => $entities) {
90 foreach ($entities as $entityID) {
91 CRM_Core_DAO::deleteTestObjects($entityName, array('id' => $entityID));
92 }
93 }
94 }
95
96 /**
97 * @param null $skip
98 *
99 * @return array
100 */
101 public static function entities($skip = NULL) {
102 // To only test specific entities, call phpunit with SYNTAX_CONFORMANCE_ENTITIES="TheEntityName"
103 // or uncomment this line:
104 //return array(array ('Tag'), array ('Activity') );
105
106 if (getenv('SYNTAX_CONFORMANCE_ENTITIES')) {
107 $result = array();
108 foreach (explode(' ', getenv('SYNTAX_CONFORMANCE_ENTITIES')) as $entity) {
109 $result[] = array($entity);
110 }
111 return $result;
112 }
113
114 $tmp = civicrm_api('Entity', 'Get', array('version' => 3));
115 if (!is_array($skip)) {
116 $skip = array();
117 }
118 $tmp = array_diff($tmp['values'], $skip);
119 $entities = array();
120 foreach ($tmp as $e) {
121 $entities[] = array($e);
122 }
123 return $entities;
124 }
125
126 /**
127 * @return array
128 */
129 public static function entities_get() {
130 // all the entities, beside the ones flagged
131 return static::entities(static::toBeSkipped_get(TRUE));
132 }
133
134 /**
135 * @return array
136 */
137 public static function entities_create() {
138 return static::entities(static::toBeSkipped_create(TRUE));
139 }
140
141 /**
142 * @return array
143 */
144 public static function entities_updatesingle() {
145 return static::entities(static::toBeSkipped_updatesingle(TRUE));
146 }
147
148 /**
149 * @return array
150 */
151 public static function entities_getlimit() {
152 return static::entities(static::toBeSkipped_getlimit());
153 }
154
155 /**
156 * @return array
157 */
158 public static function entities_delete() {
159 return static::entities(static::toBeSkipped_delete(TRUE));
160 }
161
162 /**
163 * @return array
164 */
165 public static function custom_data_entities_get() {
166 return static::custom_data_entities();
167 }
168
169 /**
170 * @return array
171 */
172 public static function custom_data_entities() {
173 $enableComponents = CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'enable_components', NULL, array());
174 $entities = CRM_Core_BAO_CustomQuery::$extendsMap;
175 $components = self::$componentMap;
176 $customDataEntities = array();
177 $invalidEntities = array('Individual', 'Organization', 'Household');
178 $entitiesToFix = array('Case', 'Relationship');
179 foreach ($entities as $entityName => $entity ) {
180 if(!in_array($entityName, $invalidEntities)
181 && !in_array($entityName, $entitiesToFix)
182 && (!empty($components[$entityName]) && in_array($components[$entityName], $enableComponents) || $components[$entityName] == NULL)) {
183 $customDataEntities[] = array($entityName );
184 }
185 }
186 return $customDataEntities;
187 }
188
189 /**
190 * @param bool $sequential
191 *
192 * @return array
193 */
194 public static function toBeSkipped_get($sequential = FALSE) {
195 $entitiesWithoutGet = array('MailingEventSubscribe', 'MailingEventConfirm', 'MailingEventResubscribe', 'MailingEventUnsubscribe', 'MailingGroup', 'Location');
196 if ($sequential === TRUE) {
197 return $entitiesWithoutGet;
198 }
199 $entities = array();
200 foreach ($entitiesWithoutGet as $e) {
201 $entities[] = array($e);
202 }
203 return $entities;
204 }
205
206 /**
207 * Mailing Contact Just doesn't support id. We have always insisted on finding a way to
208 * support id in API but in this case the underlying tables are crying out for a restructue
209 * & it just doesn't make sense
210 *
211 * @param bool|\unknown_type $sequential
212 *
213 * @return multitype:string |multitype:multitype:string
214 */
215 public static function toBeSkipped_getByID($sequential = FALSE) {
216 return array('MailingContact');
217 }
218
219 /**
220 * @param bool $sequential
221 *
222 * @return array
223 */
224 public static function toBeSkipped_create($sequential = FALSE) {
225 $entitiesWithoutCreate = array('MailingGroup', 'Constant', 'Entity', 'Location', 'Profile', 'MailingRecipients');
226 if ($sequential === TRUE) {
227 return $entitiesWithoutCreate;
228 }
229 $entities = array();
230 foreach ($entitiesWithoutCreate as $e) {
231 $entities[] = array($e);
232 }
233 return $entities;
234 }
235
236 /**
237 * @param bool $sequential
238 *
239 * @return array
240 */
241 public static function toBeSkipped_delete($sequential = FALSE) {
242 $entitiesWithout = array('MailingContact', 'MailingEventConfirm', 'MailingEventResubscribe', 'MailingEventSubscribe', 'MailingEventUnsubscribe', 'MailingGroup', 'MailingRecipients', 'Constant', 'Entity', 'Location', 'Domain', 'Profile', 'CustomValue', 'Setting');
243 if ($sequential === TRUE) {
244 return $entitiesWithout;
245 }
246 $entities = array();
247 foreach ($entitiesWithout as $e) {
248 $entities[] = array($e);
249 }
250 return $entities;
251 }
252
253 /**
254 * Generate list of entities to test for get by id functions
255 * @param boolean $sequential
256 * @return multitype:string |multitype:multitype:string
257 */
258 public static function toBeSkipped_automock($sequential = FALSE) {
259 $entitiesWithoutGet = array('MailingContact', 'EntityTag', 'Participant', 'ParticipantPayment', 'Setting', 'SurveyRespondant', 'MailingRecipients', 'CustomSearch', 'Extension', 'ReportTemplate', 'System');
260 if ($sequential === TRUE) {
261 return $entitiesWithoutGet;
262 }
263 $entities = array();
264 foreach ($entitiesWithoutGet as $e) {
265 $entities[] = array($e);
266 }
267 return $entities;
268 }
269
270
271 /**
272 * At this stage exclude the ones that don't pass & add them as we can troubleshoot them
273 */
274 public static function toBeSkipped_updatesingle($sequential = FALSE) {
275 $entitiesWithout = array(
276 'Mailing',
277 'MailingGroup',
278 'MailingJob',
279 'Address',
280 'MailingEventUnsubscribe',
281 'MailingEventSubscribe',
282 'Constant',
283 'Entity',
284 'Location',
285 'Domain',
286 'Profile',
287 'CustomValue',
288 'SurveyRespondant',
289 'Tag',
290 'UFMatch',
291 'UFJoin',
292 'UFField',
293 'OptionValue',
294 'Relationship',
295 'RelationshipType',
296 'ParticipantStatusType',
297 'Note',
298 'OptionGroup',
299 'Membership',
300 'MembershipType',
301 'MembershipStatus',
302 'Group',
303 'GroupOrganization',
304 'GroupNesting',
305 'Job',
306 'File',
307 'EntityTag',
308 'CustomField',
309 'CustomGroup',
310 'Contribution',
311 'ContributionRecur',
312 'ActivityType',
313 'MailingEventConfirm',
314 'Case',
315 'Contact',
316 'ContactType',
317 'MailingEventResubscribe',
318 'UFGroup',
319 'Activity',
320 'Email',
321 'Event',
322 'GroupContact',
323 'MembershipPayment',
324 'Participant',
325 'ParticipantPayment',
326 'LineItem',
327 'PriceSet',
328 'PriceField',
329 'PriceFieldValue',
330 'PledgePayment',
331 'ContributionPage',
332 'Phone',
333 'PaymentProcessor',
334 'MailSettings',
335 'Setting',
336 'MailingContact',
337 'SystemLog' //skip this because it doesn't make sense to update logs
338 );
339 if ($sequential === TRUE) {
340 return $entitiesWithout;
341 }
342 $entities = array();
343 foreach ($entitiesWithout as $e) {
344 $entities[] = array(
345 $e,
346 );
347 }
348 return array('pledge');
349 return $entities;
350 }
351
352 /**
353 * At this stage exclude the ones that don't pass & add them as we can troubleshoot them
354 */
355 public static function toBeSkipped_getlimit() {
356 $entitiesWithout = array(
357 'Case',//case api has non-std mandatory fields one of (case_id, contact_id, activity_id, contact_id)
358 'EntityTag', // non-standard api - has inappropriate mandatory fields & doesn't implement limit
359 'Event', // failed 'check that a 5 limit returns 5' - probably is_template field is wrong or something, or could be limit doesn't work right
360 'Extension', // can't handle creating 25
361 'MailingGroup', // no get call on MailingGroup
362 'Note', // fails on 5 limit - probably a set up problem
363 'Setting', //a bit of a pseudoapi - keys by domain
364 );
365 return $entitiesWithout;
366 }
367
368 /**
369 * @param $entity
370 * @param $key
371 *
372 * @return array
373 */
374 public function getKnownUnworkablesUpdateSingle($entity, $key){
375 // can't update values are values for which updates don't result in the value being changed
376 $knownFailures = array(
377 'ActionSchedule' => array(
378 'cant_update' => array(
379 'group_id',
380 ),
381 ),
382 'ActivityContact' => array(
383 'cant_update' => array(
384 'activity_id', //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
385 ),
386 ),
387 'Address' => array(
388 'cant_update' => array(
389 'state_province_id', //issues with country id - need to ensure same country
390 'master_id',//creates relationship
391 ),
392 'cant_return' => array(
393 )
394 ),
395 'Batch' => array(
396 'cant_update' => array(
397 'entity_table', // believe this field is defined in error
398 ),
399 'cant_return' => array(
400 'entity_table',
401 )
402 ),
403 'Pledge' => array(
404 'cant_update' => array(
405 'pledge_original_installment_amount',
406 'installments',
407 'original_installment_amount',
408 'next_pay_date',
409 'amount' // can't be changed through API
410 ),
411 'break_return' => array(// if these are passed in they are retrieved from the wrong table
412 'honor_contact_id',
413 'cancel_date',
414 'contribution_page_id',
415 'financial_account_id',
416 'financial_type_id',
417 'currency'
418 ),
419 'cant_return' => array(// can't be retrieved from api
420 'honor_type_id', //due to uniquename missing
421 'end_date',
422 'modified_date',
423 'acknowledge_date',
424 'start_date',
425 'frequency_day',
426 'currency',
427 'max_reminders',
428 'initial_reminder_day',
429 'additional_reminder_day',
430 'frequency_unit',
431 'pledge_contribution_page_id',
432 'pledge_status_id',
433 'pledge_campaign_id',
434 )
435 ),
436 'PaymentProcessorType' => array(
437 'cant_update' => array(
438 'billing_mode',
439 ),
440 'break_return' => array(
441 ),
442 'cant_return' => array(
443 ),
444 ),
445 );
446 if(empty($knownFailures[$entity]) || empty($knownFailures[$entity][$key])){
447 return array();
448 }
449 return $knownFailures[$entity][$key];
450 }
451
452 /** testing the _get **/
453
454 /**
455 * @dataProvider toBeSkipped_get
456 entities that don't need a get action
457 */
458 public function testNotImplemented_get($Entity) {
459 $result = civicrm_api($Entity, 'Get', array('version' => 3));
460 $this->assertEquals(1, $result['is_error'], 'In line ' . __LINE__);
461 // $this->assertContains("API ($Entity, Get) does not exist", $result['error_message']);
462 $this->assertRegExp('/API (.*) does not exist/', $result['error_message']);
463 }
464
465 /**
466 * @dataProvider entities
467 * @expectedException PHPUnit_Framework_Error
468 */
469 public function testWithoutParam_get($Entity) {
470 // should get php complaining that a param is missing
471 $result = civicrm_api($Entity, 'Get');
472 }
473
474 /**
475 * @dataProvider entities
476 */
477 public function testGetFields($Entity) {
478 if (in_array($Entity, $this->deprecatedAPI) || $Entity == 'Entity' || $Entity == 'CustomValue' || $Entity == 'MailingGroup') {
479 return;
480 }
481
482 $result = civicrm_api($Entity, 'getfields', array('version' => 3));
483 $this->assertTrue(is_array($result['values']), "$Entity ::get fields doesn't return values array in line " . __LINE__);
484 foreach ($result['values'] as $key => $value) {
485 $this->assertTrue(is_array($value), $Entity . "::" . $key . " is not an array in line " . __LINE__);
486 }
487 }
488
489 /**
490 * @dataProvider entities_get
491 */
492 public function testEmptyParam_get($Entity) {
493
494 if (in_array($Entity, $this->toBeImplemented['get'])) {
495 // $this->markTestIncomplete("civicrm_api3_{$Entity}_get to be implemented");
496 return;
497 }
498 $result = civicrm_api($Entity, 'Get', array());
499 $this->assertEquals(1, $result['is_error'], 'In line ' . __LINE__);
500 $this->assertContains("Mandatory key(s) missing from params array", $result['error_message']);
501 }
502 /**
503 * @dataProvider entities_get
504 */
505 public function testEmptyParam_getString($Entity) {
506
507 if (in_array($Entity, $this->toBeImplemented['get'])) {
508 // $this->markTestIncomplete("civicrm_api3_{$Entity}_get to be implemented");
509 return;
510 }
511 $result = $this->callAPIFailure($Entity, 'Get', 'string');
512 $this->assertEquals(2000, $result['error_code']);
513 $this->assertEquals('Input variable `params` is not an array', $result['error_message']);
514 }
515 /**
516 * @dataProvider entities_get
517 * @Xdepends testEmptyParam_get // no need to test the simple if the empty doesn't work/is skipped. doesn't seem to work
518 */
519 public function testSimple_get($Entity) {
520 // $this->markTestSkipped("test gives core error on test server (but not on our locals). Skip until we can get server to pass");
521 if (in_array($Entity, $this->toBeImplemented['get'])) {
522 return;
523 }
524 $result = civicrm_api($Entity, 'Get', array('version' => 3));
525 // @TODO: list the get that have mandatory params
526 if ($result['is_error']) {
527 $this->assertContains("Mandatory key(s) missing from params array", $result['error_message']);
528 // either id or contact_id or entity_id is one of the field missing
529 $this->assertContains("id", $result['error_message']);
530 }
531 else {
532 $this->assertEquals(3, $result['version']);
533 $this->assertArrayHasKey('count', $result);
534 $this->assertArrayHasKey('values', $result);
535 }
536 }
537
538 /**
539 * @dataProvider custom_data_entities_get
540 */
541 public function testCustomDataGet($entityName) {
542 $this->createLoggedInUser();// so subsidiary activities are created
543 $ids = $this->entityCustomGroupWithSingleFieldCreate(__FUNCTION__, $entityName . 'Test.php');
544 $customFieldName = 'custom_' . $ids['custom_field_id'];
545 $objects = $this->getMockableBAOObjects($entityName, 1);
546 $params = array('id' => $objects[0]->id, 'custom_' . $ids['custom_field_id'] => "custom string");
547 $result = $this->callAPISuccess($entityName, 'create', $params);
548
549 $getParams = array('id' => $result['id'], 'return' => array($customFieldName));
550 $check = $this->callAPISuccess($entityName, 'get', $getParams);
551 $this->assertEquals("custom string", $check['values'][$check['id']][$customFieldName]);
552
553 $this->customFieldDelete($ids['custom_field_id']);
554 $this->customGroupDelete($ids['custom_group_id']);
555 $this->callAPISuccess($entityName, 'delete', array('id' => $result['id']));
556 }
557
558 /**
559 * @dataProvider entities_get
560 */
561 public function testAcceptsOnlyID_get($Entity) {
562 // big random number. fun fact: if you multiply it by pi^e, the result is another random number, but bigger ;)
563 $nonExistantID = 30867307034;
564 if (in_array($Entity, $this->toBeImplemented['get'])
565 || in_array($Entity, $this->toBeSkipped_getByID())
566 ) {
567 return;
568 }
569
570 // FIXME
571 // the below function returns different values and hence an early return
572 // we'll fix this once beta1 is released
573 // return;
574
575 $result = civicrm_api($Entity, 'Get', array('version' => 3, 'id' => $nonExistantID));
576
577 if ($result['is_error']) {
578 // just to get a clearer message in the log
579 $this->assertEquals("only id should be enough", $result['error_message']);
580 }
581 if (!in_array($Entity, $this->onlyIDNonZeroCount['get'])) {
582 $this->assertEquals(0, $result['count']);
583 }
584 }
585
586 /**
587 * Create two entities and make sure we can fetch them individually by ID
588 *
589 * @dataProvider entities_get
590 *
591 * limitations include the problem with avoiding loops when creating test objects -
592 * hence FKs only set by createTestObject when required. e.g parent_id on campaign is not being followed through
593 * Currency - only seems to support US
594 */
595 public function testByID_get($entityName) {
596 if (in_array($entityName, self::toBeSkipped_automock(TRUE))) {
597 // $this->markTestIncomplete("civicrm_api3_{$Entity}_create to be implemented");
598 return;
599 }
600
601 $baos = $this->getMockableBAOObjects($entityName);
602 list($baoObj1, $baoObj2) = $baos;
603
604 // fetch first by ID
605 $result = $this->callAPISuccess($entityName, 'get', array(
606 'id' => $baoObj1->id,
607 ));
608
609 $this->assertTrue(!empty($result['values'][$baoObj1->id]), 'Should find first object by id');
610 $this->assertEquals($baoObj1->id, $result['values'][$baoObj1->id]['id'], 'Should find id on first object');
611 $this->assertEquals(1, count($result['values']));
612
613 // fetch second by ID
614 $result = $this->callAPISuccess($entityName, 'get', array(
615 'id' => $baoObj2->id,
616 ));
617 $this->assertTrue(!empty($result['values'][$baoObj2->id]), 'Should find second object by id');
618 $this->assertEquals($baoObj2->id, $result['values'][$baoObj2->id]['id'], 'Should find id on second object');
619 $this->assertEquals(1, count($result['values']));
620 }
621
622 /**
623 * Ensure that the "get" operation accepts limiting the #result records.
624 *
625 * TODO Consider making a separate entity list ("entities_getlimit")
626 * For the moment, the "entities_updatesingle" list should give a good
627 * sense for which entities support createTestObject
628 *
629 * @dataProvider entities_getlimit
630 *
631 * @param $entityName
632 *
633 * @internal param string $entity
634 */
635 function testLimit($entityName) {
636 $cases = array(); // each case is array(0 => $inputtedApiOptions, 1 => $expectedResultCount)
637 $cases[] = array(
638 array('options' => array('limit' => NULL)),
639 30,
640 'check that a NULL limit returns unlimited',
641 );
642 $cases[] = array(
643 array('options' => array('limit' => FALSE)),
644 30,
645 'check that a FALSE limit returns unlimited',
646 );
647 $cases[] = array(
648 array('options' => array('limit' => 0)),
649 30,
650 'check that a 0 limit returns unlimited',
651 );
652 $cases[] = array(
653 array('options' => array('limit' => 5)),
654 5,
655 'check that a 5 limit returns 5',
656 );
657 $cases[] = array(
658 array(),
659 25,
660 'check that no limit returns 25',
661 );
662
663 $baoString = _civicrm_api3_get_DAO($entityName);
664 if (empty($baoString)) {
665 $this->markTestIncomplete("Entity [$entityName] cannot be mocked - no known DAO");
666 return;
667 }
668
669 // make 30 test items -- 30 > 25 (the default limit)
670 $ids = array();
671 for ($i = 0; $i < 30; $i++) {
672 $baoObj = CRM_Core_DAO::createTestObject($baoString, array('currency' => 'USD'));
673 $ids[] = $baoObj->id;
674 }
675
676 // each case is array(0 => $inputtedApiOptions, 1 => $expectedResultCount)
677 foreach ($cases as $case) {
678 $this->checkLimitAgainstExpected($entityName, $case[0], $case[1], $case[2]);
679
680 //non preferred / legacy syntax
681 if(isset($case[0]['options']['limit'])) {
682 $this->checkLimitAgainstExpected($entityName, array('rowCount' => $case[0]['options']['limit']), $case[1], $case[2]);
683 $this->checkLimitAgainstExpected($entityName, array('option_limit' => $case[0]['options']['limit']), $case[1], $case[2]);
684 $this->checkLimitAgainstExpected($entityName, array('option.limit' => $case[0]['options']['limit']), $case[1], $case[2]);
685 }
686 }
687 foreach ($ids as $id) {
688 CRM_Core_DAO::deleteTestObjects($baoString, array('id' => $id));
689 }
690 $baoObj->free();
691 }
692
693 /**
694 * Check that get fetches an appropriate number of results
695 *
696 * @param string $entityName Name of entity to test
697 * @param unknown $params
698 * @param unknown $limit
699 * @param unknown $message
700 */
701 function checkLimitAgainstExpected($entityName, $params, $limit, $message) {
702 $result = $this->callAPISuccess($entityName, 'get', $params);
703 if($limit == 30) {
704 $this->assertGreaterThanOrEqual($limit, $result['count'], $message);
705 $this->assertGreaterThanOrEqual($limit, $result['count'], $message);
706 }
707 else {
708 $this->assertEquals($limit, $result['count'], $message);
709 $this->assertEquals($limit, count($result['values']), $message);
710 }
711 }
712 /**
713 * Create two entities and make sure we can fetch them individually by ID (e.g. using "contact_id=>2"
714 * or "group_id=>4")
715 *
716 * @dataProvider entities_get
717 *
718 * limitations include the problem with avoiding loops when creating test objects -
719 * hence FKs only set by createTestObject when required. e.g parent_id on campaign is not being followed through
720 * Currency - only seems to support US
721 */
722 public function testByIDAlias_get($entityName) {
723 if (in_array($entityName, self::toBeSkipped_automock(TRUE))) {
724 // $this->markTestIncomplete("civicrm_api3_{$Entity}_create to be implemented");
725 return;
726 }
727
728 $baoString = _civicrm_api3_get_DAO($entityName);
729 if (empty($baoString)) {
730 $this->markTestIncomplete("Entity [$entityName] cannot be mocked - no known DAO");
731 return;
732 }
733
734 $idFieldName = _civicrm_api_get_entity_name_from_camel($entityName) . '_id';
735
736 // create entities
737 $baoObj1 = CRM_Core_DAO::createTestObject($baoString, array('currency' => 'USD'));
738 $this->assertTrue(is_integer($baoObj1->id), 'check first id');
739 $this->deletableTestObjects[$baoString][] = $baoObj1->id;
740 $baoObj2 = CRM_Core_DAO::createTestObject($baoString, array('currency' => 'USD'));
741 $this->assertTrue(is_integer($baoObj2->id), 'check second id');
742 $this->deletableTestObjects[$baoString][] = $baoObj2->id;
743
744 // fetch first by ID
745 $result = civicrm_api($entityName, 'get', array(
746 'version' => 3,
747 $idFieldName => $baoObj1->id,
748 ));
749 $this->assertAPISuccess($result);
750 $this->assertTrue(!empty($result['values'][$baoObj1->id]), 'Should find first object by id');
751 $this->assertEquals($baoObj1->id, $result['values'][$baoObj1->id]['id'], 'Should find id on first object');
752 $this->assertEquals(1, count($result['values']));
753
754 // fetch second by ID
755 $result = civicrm_api($entityName, 'get', array(
756 'version' => 3,
757 $idFieldName => $baoObj2->id,
758 ));
759 $this->assertAPISuccess($result);
760 $this->assertTrue(!empty($result['values'][$baoObj2->id]), 'Should find second object by id');
761 $this->assertEquals($baoObj2->id, $result['values'][$baoObj2->id]['id'], 'Should find id on second object');
762 $this->assertEquals(1, count($result['values']));
763 }
764
765 /**
766 * @dataProvider entities_get
767 */
768 public function testNonExistantID_get($Entity) {
769 // cf testAcceptsOnlyID_get
770 $nonExistantID = 30867307034;
771 if (in_array($Entity, $this->toBeImplemented['get'])) {
772 return;
773 }
774
775 $result = civicrm_api($Entity, 'Get', array('version' => 3, 'id' => $nonExistantID));
776
777 // redundant with testAcceptsOnlyID_get
778 if ($result['is_error']) {
779 return;
780 }
781
782
783 $this->assertArrayHasKey('version', $result);
784 $this->assertEquals(3, $result['version']);
785 if (!in_array($Entity, $this->onlyIDNonZeroCount['get'])) {
786 $this->assertEquals(0, $result['count']);
787 }
788 }
789
790 /** testing the _create **/
791
792 /**
793 * @dataProvider toBeSkipped_create
794 entities that don't need a create action
795 */
796 public function testNotImplemented_create($Entity) {
797 $result = civicrm_api($Entity, 'Create', array('version' => 3));
798 $this->assertEquals(1, $result['is_error'], 'In line ' . __LINE__);
799 $this->assertContains(strtolower("API ($Entity, Create) does not exist"), strtolower($result['error_message']));
800 }
801
802 /**
803 * @dataProvider entities
804 * @expectedException PHPUnit_Framework_Error
805 */
806 public function testWithoutParam_create($Entity) {
807 // should create php complaining that a param is missing
808 $result = civicrm_api($Entity, 'Create');
809 }
810
811 /**
812 * @dataProvider entities_create
813 */
814 public function testEmptyParam_create($Entity) {
815 $this->markTestIncomplete("fixing this test to test the api functions fails on numberous tests
816 which will either create a completely blank entity (batch, participant status) or
817 have a damn good crack at it (e.g mailing job). Marking this as incomplete beats false success");
818 //
819 return;
820 if (in_array($Entity, $this->toBeImplemented['create'])) {
821 // $this->markTestIncomplete("civicrm_api3_{$Entity}_create to be implemented");
822 return;
823 }
824 $result = $this->callAPIFailure($Entity, 'Create', array());
825 $this->assertContains("Mandatory key(s) missing from params array", $result['error_message']);
826 }
827
828 /**
829 * @dataProvider entities_create
830 *
831 * Check that create doesn't work with an invalid
832 */
833 public function testInvalidID_create($Entity) {
834 // turn test off for noew
835 $this->markTestIncomplete("Entity [ $Entity ] cannot be mocked - no known DAO");
836 return;
837 if (in_array($Entity, $this->toBeImplemented['create'])) {
838 // $this->markTestIncomplete("civicrm_api3_{$Entity}_create to be implemented");
839 return;
840 }
841 $result = $this->callAPIFailure($Entity, 'Create', array('id' => 999));
842 }
843
844 /**
845 * @dataProvider entities
846 */
847 public function testCreateWrongTypeParamTag_create() {
848 $result = civicrm_api("Tag", 'Create', 'this is not a string');
849 $this->assertEquals(1, $result['is_error'], 'In line ' . __LINE__);
850 $this->assertEquals("Input variable `params` is not an array", $result['error_message']);
851 }
852
853 /**
854 * @dataProvider entities_updatesingle
855 *
856 * limitations include the problem with avoiding loops when creating test objects -
857 * hence FKs only set by createTestObject when required. e.g parent_id on campaign is not being followed through
858 * Currency - only seems to support US
859 */
860 public function testCreateSingleValueAlter($entityName) {
861 if (in_array($entityName, $this->toBeImplemented['create'])) {
862 // $this->markTestIncomplete("civicrm_api3_{$Entity}_create to be implemented");
863 return;
864 }
865
866 $baoString = _civicrm_api3_get_DAO($entityName);
867 $this->assertNotEmpty($baoString, $entityName);
868 $this->assertNotEmpty($entityName, $entityName);
869 $fieldsGet = $fields = $this->callAPISuccess($entityName, 'getfields', array('action' => 'get'));
870 if($entityName != 'Pledge'){
871 $fields = $this->callAPISuccess($entityName, 'getfields', array('action' => 'create'));
872 }
873 $fields = $fields['values'];
874 $return = array_keys($fieldsGet['values']);
875 $valuesNotToReturn = $this->getKnownUnworkablesUpdateSingle($entityName, 'break_return');
876 // these can't be requested as return values
877 $entityValuesThatDoNotWork = array_merge(
878 $this->getKnownUnworkablesUpdateSingle($entityName, 'cant_update'),
879 $this->getKnownUnworkablesUpdateSingle($entityName, 'cant_return'),
880 $valuesNotToReturn
881 );
882
883 $return = array_diff($return,$valuesNotToReturn);
884 $baoObj = new CRM_Core_DAO();
885 $baoObj->createTestObject($baoString, array('currency' => 'USD'), 2, 0);
886
887 $getEntities = $this->callAPISuccess($entityName, 'get', array(
888 'sequential' => 1,
889 'return' => $return,
890 'options' => array(
891 'sort' => 'id DESC',
892 'limit' => 2,
893 ),
894 ));
895 // lets use first rather than assume only one exists
896 $entity = $getEntities['values'][0];
897 $entity2 = $getEntities['values'][1];
898 $this->deletableTestObjects[$baoString][] = $entity['id'];
899 $this->deletableTestObjects[$baoString][] = $entity2['id'];
900 foreach ($fields as $field => $specs) {
901 $fieldName = $field;
902 if (!empty($specs['uniquename'])) {
903 $fieldName = $specs['uniquename'];
904 }
905 if ($field == 'currency' || $field == 'id' || $field == strtolower($entityName) . '_id'
906 || in_array($field,$entityValuesThatDoNotWork)) {
907 //@todo id & entity_id are correct but we should fix currency & frequency_day
908 continue;
909 }
910 $this->assertArrayHasKey('type', $specs, "the _spec function for $entityName field $field does not specify the type");
911 switch ($specs['type']) {
912 case CRM_Utils_Type::T_DATE:
913 case CRM_Utils_Type::T_TIMESTAMP:
914 $entity[$fieldName] = '2012-05-20';
915 break;
916 //case CRM_Utils_Type::T_DATETIME:
917
918 case 12:
919 $entity[$fieldName] = '2012-05-20 03:05:20';
920 break;
921
922 case CRM_Utils_Type::T_STRING:
923 case CRM_Utils_Type::T_BLOB:
924 case CRM_Utils_Type::T_MEDIUMBLOB:
925 case CRM_Utils_Type::T_TEXT:
926 case CRM_Utils_Type::T_LONGTEXT:
927 case CRM_Utils_Type::T_EMAIL:
928 $entity[$fieldName] = substr('New String',0, CRM_Utils_Array::Value('maxlength',$specs,100));
929 break;
930
931 case CRM_Utils_Type::T_INT:
932 // probably created with a 1
933 $entity[$fieldName] = '6';
934 if (!empty($specs['FKClassName'])) {
935 if($specs['FKClassName'] == $baoString){
936 $entity[$fieldName] = (string) $entity2['id'];
937 }
938 else{
939 $uniqueName = CRM_Utils_Array::value('uniqueName', $specs);
940 $entity[$fieldName] = (string) empty($entity2[$field]) ? CRM_Utils_Array::value($uniqueName, $entity2) : $entity2[$field];
941 //todo - there isn't always something set here - & our checking on unset values is limited
942 if (empty($entity[$field])) {
943 unset($entity[$field]);
944 }
945 }
946 }
947 break;
948
949 case CRM_Utils_Type::T_BOOLEAN:
950 // probably created with a 1
951 $entity[$fieldName] = '0';
952 break;
953
954 case CRM_Utils_Type::T_FLOAT:
955 case CRM_Utils_Type::T_MONEY:
956 $entity[$field] = '22.75';
957 break;
958
959 case CRM_Utils_Type::T_URL:
960 $entity[$field] = 'warm.beer.com';
961 }
962 if (!empty($specs['pseudoconstant'])) {
963 $options = $this->callAPISuccess($entityName, 'getoptions', array('context' => 'create', 'field' => $field));
964 if (empty($options['values'])) {
965 //eg. pdf_format id doesn't ship with any
966 if(isset($specs['pseudoconstant']['optionGroupName'])) {
967 $optionGroupID = $this->callAPISuccess('option_group', 'getvalue', array('name' => 'pdf_format', 'return' => 'id'));
968 $optionValue = $this->callAPISuccess('option_value', 'create', array('option_group_id' => $optionGroupID, 'label' => 'new option value'));
969 $options['values'][] = $optionValue['id'];
970 }
971 }
972 $entity[$field] = array_rand($options['values']);
973 }
974 $updateParams = array(
975 'id' => $entity['id'],
976 $field => isset($entity[$field]) ? $entity[$field] : NULL,
977 );
978
979 $update = $this->callAPISuccess($entityName, 'create', $updateParams);
980 $checkParams = array(
981 'id' => $entity['id'],
982 'sequential' => 1,
983 'return' => $return,
984 'options' => array(
985 'sort' => 'id DESC',
986 'limit' => 2,
987 ),
988 );
989
990 $checkEntity = $this->callAPISuccess($entityName, 'getsingle', $checkParams);
991 $this->assertAPIArrayComparison($entity, $checkEntity, array(), "checking if $fieldName was correctly updated\n" . print_r(array('update-params' => $updateParams, 'update-result' => $update, 'getsingle-params' => $checkParams, 'getsingle-result' => $checkEntity, 'expected entity' => $entity), TRUE));
992 }
993 $baoObj->free();
994 }
995
996 /** testing the _getFields **/
997
998 /** testing the _delete **/
999
1000 /**
1001 * @dataProvider toBeSkipped_delete
1002 entities that don't need a delete action
1003 */
1004 public function testNotImplemented_delete($Entity) {
1005 $nonExistantID = 151416349;
1006 $result = civicrm_api($Entity, 'Delete', array('version' => 3, 'id' => $nonExistantID));
1007 $this->assertEquals(1, $result['is_error'], 'In line ' . __LINE__);
1008 $this->assertContains(strtolower("API ($Entity, Delete) does not exist"), strtolower($result['error_message']));
1009 }
1010
1011 /**
1012 * @dataProvider entities
1013 * @expectedException PHPUnit_Framework_Error
1014 */
1015 public function testWithoutParam_delete($Entity) {
1016 // should delete php complaining that a param is missing
1017 $result = civicrm_api($Entity, 'Delete');
1018 }
1019
1020 /**
1021 * @dataProvider entities_delete
1022 */
1023 public function testEmptyParam_delete($Entity) {
1024 if (in_array($Entity, $this->toBeImplemented['delete'])) {
1025 // $this->markTestIncomplete("civicrm_api3_{$Entity}_delete to be implemented");
1026 return;
1027 }
1028 $result = civicrm_api($Entity, 'Delete', array());
1029 $this->assertEquals(1, $result['is_error'], 'In line ' . __LINE__);
1030 $this->assertContains("Mandatory key(s) missing from params array", $result['error_message']);
1031 }
1032 /**
1033 * @dataProvider entities_delete
1034 */
1035 public function testInvalidID_delete($Entity) {
1036 // turn test off for now
1037 $this->markTestIncomplete("Entity [ $Entity ] cannot be mocked - no known DAO");
1038 return;
1039 if (in_array($Entity, $this->toBeImplemented['delete'])) {
1040 // $this->markTestIncomplete("civicrm_api3_{$Entity}_delete to be implemented");
1041 return;
1042 }
1043 $result = $this->callAPIFailure($Entity, 'Delete', array('id' => 999));
1044 }
1045 /**
1046 * @dataProvider entities
1047 */
1048 public function testDeleteWrongTypeParamTag_delete() {
1049 $result = civicrm_api("Tag", 'Delete', 'this is not a string');
1050 $this->assertEquals(1, $result['is_error'], 'In line ' . __LINE__);
1051 $this->assertEquals("Input variable `params` is not an array", $result['error_message']);
1052 }
1053
1054 /**
1055 * Create two entities and make sure delete action only deletes one!
1056 *
1057 * @dataProvider entities_delete
1058 *
1059 * limitations include the problem with avoiding loops when creating test objects -
1060 * hence FKs only set by createTestObject when required. e.g parent_id on campaign is not being followed through
1061 * Currency - only seems to support US
1062 */
1063 public function testByID_delete($entityName) {
1064 // turn test off for noew
1065 $this->markTestIncomplete("Entity [$entityName] cannot be mocked - no known DAO");
1066 return;
1067
1068 if (in_array($entityName, self::toBeSkipped_automock(TRUE))) {
1069 // $this->markTestIncomplete("civicrm_api3_{$Entity}_create to be implemented");
1070 return;
1071 }
1072 $startCount = $this->callAPISuccess($entityName, 'getcount', array());
1073 $createcount = 2;
1074 $baos = $this->getMockableBAOObjects($entityName, $createcount);
1075 list($baoObj1, $baoObj2) = $baos;
1076
1077 // make sure exactly 2 exist
1078 $result = $this->callAPISuccess($entityName, 'getcount', array(),
1079 $createcount + $startCount
1080 );
1081
1082 $this->callAPISuccess($entityName, 'delete', array('id' => $baoObj2->id));
1083 //make sure 1 less exists now
1084 $result = $this->callAPISuccess($entityName, 'getcount', array(),
1085 ($createcount + $startCount) -1
1086 );
1087
1088 //make sure id #1 exists
1089 $result = $this->callAPISuccess($entityName, 'getcount', array('id' => $baoObj1->id),
1090 1
1091 );
1092 //make sure id #2 desn't exist
1093 $result = $this->callAPISuccess($entityName, 'getcount', array('id' => $baoObj2->id),
1094 0
1095 );
1096 }
1097
1098 /**
1099 * @param $entityName
1100 * @param int $count
1101 *
1102 * @internal param $entityName
1103 *
1104 * @return array
1105 */
1106 private function getMockableBAOObjects($entityName, $count = 2) {
1107 $baoString = _civicrm_api3_get_DAO($entityName);
1108 if (empty($baoString)) {
1109 $this->markTestIncomplete("Entity [$entityName] cannot be mocked - no known DAO");
1110 return;
1111 }
1112 $baos = array();
1113 $i = 0;
1114 while($i < $count) {
1115 // create entities
1116 $baoObj = CRM_Core_DAO::createTestObject($baoString, array('currency' => 'USD'));
1117 $this->assertTrue(is_integer($baoObj->id), 'check first id');
1118 $this->deletableTestObjects[$baoString][] = $baoObj->id;
1119 $baos[] = $baoObj;
1120 $i ++;
1121 }
1122 return $baos;
1123 }
1124
1125
1126 /**
1127 * Verify that HTML metacharacters provided as inputs appear consistently
1128 * as outputs.
1129 *
1130 * At time of writing, the encoding scheme requires (for example) that an
1131 * event title be partially-HTML-escaped before writing to DB. To provide
1132 * consistency, the API must perform extra encoding and decoding on some
1133 * fields.
1134 *
1135 * In this example, the event 'title' is subject to encoding, but the
1136 * event 'description' is not.
1137 */
1138 public function testEncodeDecodeConsistency() {
1139 // Create example
1140 $createResult = civicrm_api('Event', 'Create', array(
1141 'version' => 3,
1142 'title' => 'CiviCRM <> TheRest',
1143 'description' => 'TheRest <> CiviCRM',
1144 'event_type_id' => 1,
1145 'is_public' => 1,
1146 'start_date' => 20081021,
1147 ));
1148 $this->assertAPISuccess($createResult);
1149 $eventId = $createResult['id'];
1150 $this->assertEquals('CiviCRM <> TheRest', $createResult['values'][$eventId]['title']);
1151 $this->assertEquals('TheRest <> CiviCRM', $createResult['values'][$eventId]['description']);
1152
1153 // Verify "get" handles decoding in result value
1154 $getByIdResult = civicrm_api('Event', 'Get', array(
1155 'version' => 3,
1156 'id' => $eventId,
1157 ));
1158 $this->assertAPISuccess($getByIdResult);
1159 $this->assertEquals('CiviCRM <> TheRest', $getByIdResult['values'][$eventId]['title']);
1160 $this->assertEquals('TheRest <> CiviCRM', $getByIdResult['values'][$eventId]['description']);
1161
1162 // Verify "get" handles encoding in search value
1163 $getByTitleResult = civicrm_api('Event', 'Get', array(
1164 'version' => 3,
1165 'title' => 'CiviCRM <> TheRest',
1166 ));
1167 $this->assertAPISuccess($getByTitleResult);
1168 $this->assertEquals('CiviCRM <> TheRest', $getByTitleResult['values'][$eventId]['title']);
1169 $this->assertEquals('TheRest <> CiviCRM', $getByTitleResult['values'][$eventId]['description']);
1170
1171 // Verify that "getSingle" handles decoding
1172 $getSingleResult = $this->callAPISuccess('Event', 'GetSingle', array(
1173 'id' => $eventId,
1174 ));
1175
1176 $this->assertEquals('CiviCRM <> TheRest', $getSingleResult['title']);
1177 $this->assertEquals('TheRest <> CiviCRM', $getSingleResult['description']);
1178
1179 // Verify that chaining handles decoding
1180 $chainResult = $this->callAPISuccess('Event', 'Get', array(
1181 'id' => $eventId,
1182 'api.event.get' => array(
1183 ),
1184 ));
1185 $this->assertEquals('CiviCRM <> TheRest', $chainResult['values'][$eventId]['title']);
1186 $this->assertEquals('TheRest <> CiviCRM', $chainResult['values'][$eventId]['description']);
1187 $this->assertEquals('CiviCRM <> TheRest', $chainResult['values'][$eventId]['api.event.get']['values'][0]['title']);
1188 $this->assertEquals('TheRest <> CiviCRM', $chainResult['values'][$eventId]['api.event.get']['values'][0]['description']);
1189
1190 // Verify that "setvalue" handles encoding for updates
1191 $setValueTitleResult = civicrm_api('Event', 'setvalue', array(
1192 'version' => 3,
1193 'id' => $eventId,
1194 'field' => 'title',
1195 'value' => 'setValueTitle: CiviCRM <> TheRest',
1196 ));
1197 $this->assertAPISuccess($setValueTitleResult);
1198 $this->assertEquals('setValueTitle: CiviCRM <> TheRest', $setValueTitleResult['values']['title']);
1199 $setValueDescriptionResult = civicrm_api('Event', 'setvalue', array(
1200 'version' => 3,
1201 'id' => $eventId,
1202 'field' => 'description',
1203 'value' => 'setValueDescription: TheRest <> CiviCRM',
1204 ));
1205 //$this->assertTrue((bool)$setValueDescriptionResult['is_error']); // not supported by setValue
1206 $this->assertEquals('setValueDescription: TheRest <> CiviCRM', $setValueDescriptionResult['values']['description']);
1207 }
1208
1209 /**
1210 * Verify that write operations (create/update) use partial HTML-encoding
1211 *
1212 * In this example, the event 'title' is subject to encoding, but the
1213 * event 'description' is not.
1214 */
1215 public function testEncodeWrite() {
1216 // Create example
1217 $createResult = civicrm_api('Event', 'Create', array(
1218 'version' => 3,
1219 'title' => 'createNew: CiviCRM <> TheRest',
1220 'description' => 'createNew: TheRest <> CiviCRM',
1221 'event_type_id' => 1,
1222 'is_public' => 1,
1223 'start_date' => 20081021,
1224 ));
1225 $this->assertAPISuccess($createResult);
1226 $eventId = $createResult['id'];
1227 $this->assertDBQuery('createNew: CiviCRM &lt;&gt; TheRest', 'SELECT title FROM civicrm_event WHERE id = %1', array(
1228 1 => array($eventId, 'Integer')
1229 ));
1230 $this->assertDBQuery('createNew: TheRest <> CiviCRM', 'SELECT description FROM civicrm_event WHERE id = %1', array(
1231 1 => array($eventId, 'Integer')
1232 ));
1233
1234 // Verify that "create" handles encoding for updates
1235 $createWithIdResult = civicrm_api('Event', 'Create', array(
1236 'version' => 3,
1237 'id' => $eventId,
1238 'title' => 'createWithId: CiviCRM <> TheRest',
1239 'description' => 'createWithId: TheRest <> CiviCRM',
1240 ));
1241 $this->assertAPISuccess($createWithIdResult);
1242 $this->assertDBQuery('createWithId: CiviCRM &lt;&gt; TheRest', 'SELECT title FROM civicrm_event WHERE id = %1', array(
1243 1 => array($eventId, 'Integer')
1244 ));
1245 $this->assertDBQuery('createWithId: TheRest <> CiviCRM', 'SELECT description FROM civicrm_event WHERE id = %1', array(
1246 1 => array($eventId, 'Integer')
1247 ));
1248
1249 // Verify that "setvalue" handles encoding for updates
1250 $setValueTitleResult = civicrm_api('Event', 'setvalue', array(
1251 'version' => 3,
1252 'id' => $eventId,
1253 'field' => 'title',
1254 'value' => 'setValueTitle: CiviCRM <> TheRest',
1255 ));
1256 $this->assertAPISuccess($setValueTitleResult);
1257 $this->assertDBQuery('setValueTitle: CiviCRM &lt;&gt; TheRest', 'SELECT title FROM civicrm_event WHERE id = %1', array(
1258 1 => array($eventId, 'Integer')
1259 ));
1260 $setValueDescriptionResult = civicrm_api('Event', 'setvalue', array(
1261 'version' => 3,
1262 'id' => $eventId,
1263 'field' => 'description',
1264 'value' => 'setValueDescription: TheRest <> CiviCRM',
1265 ));
1266 //$this->assertTrue((bool)$setValueDescriptionResult['is_error']); // not supported by setValue
1267 $this->assertAPISuccess($setValueDescriptionResult);
1268 $this->assertDBQuery('setValueDescription: TheRest <> CiviCRM', 'SELECT description FROM civicrm_event WHERE id = %1', array(
1269 1 => array($eventId, 'Integer')
1270 ));
1271 }
1272
1273 }