Merge branch '2015.11-develop' into 2016.11
[tine20] / tests / tine20 / Calendar / JsonTests.php
1 <?php
2 /**
3  * Tine 2.0 - http://www.tine20.org
4  * 
5  * @package     Calendar
6  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
7  * @copyright   Copyright (c) 2009-2014 Metaways Infosystems GmbH (http://www.metaways.de)
8  * @author      Cornelius Weiss <c.weiss@metaways.de>
9  */
10
11 /**
12  * Test class for Json Frontend
13  * 
14  * @package     Calendar
15  */
16 class Calendar_JsonTests extends Calendar_TestCase
17 {
18     /**
19      * Calendar Json Object
20      *
21      * @var Calendar_Frontend_Json
22      */
23     protected $_uit = null;
24     
25     /**
26      * (non-PHPdoc)
27      * @see Calendar/Calendar_TestCase::setUp()
28      */
29     public function setUp()
30     {
31         parent::setUp();
32         
33         Calendar_Controller_Event::getInstance()->doContainerACLChecks(true);
34         
35         $this->_uit = new Calendar_Frontend_Json();
36     }
37     
38     /**
39      * testGetRegistryData
40      */
41     public function testGetRegistryData()
42     {
43         // enforce fresh instance of calendar preferences
44         Tinebase_Core::set(Tinebase_Core::PREFERENCES, array());
45         
46         $registryData = $this->_uit->getRegistryData();
47         
48         $this->assertTrue(is_array($registryData['defaultContainer']['account_grants']));
49     }
50     
51     /**
52      * testCreateEvent
53      * 
54      * @param $now should the current date be used
55      */
56     public function testCreateEvent($now = FALSE)
57     {
58         $scleverDisplayContainerId = Tinebase_Core::getPreference('Calendar')->getValueForUser(Calendar_Preference::DEFAULTCALENDAR, $this->_getPersona('sclever')->getId());
59         $contentSeqBefore = Tinebase_Container::getInstance()->getContentSequence($scleverDisplayContainerId);
60         
61         $eventData = $this->_getEvent($now)->toArray();
62         
63         $tag = Tinebase_Tags::getInstance()->createTag(new Tinebase_Model_Tag(array(
64             'name' => 'phpunit-' . substr(Tinebase_Record_Abstract::generateUID(), 0, 10),
65             'type' => Tinebase_Model_Tag::TYPE_PERSONAL
66         )));
67         $eventData['tags'] = array($tag->toArray());
68         
69         $note = new Tinebase_Model_Note(array(
70             'note'         => 'very important note!',
71             'note_type_id' => Tinebase_Notes::getInstance()->getNoteTypes()->getFirstRecord()->getId(),
72         ));
73         $eventData['notes'] = array($note->toArray());
74         $eventData['etag'] = Tinebase_Record_Abstract::generateUID();
75         
76         $persistentEventData = $this->_uit->saveEvent($eventData);
77         $loadedEventData = $this->_uit->getEvent($persistentEventData['id']);
78         
79         $this->_assertJsonEvent($eventData, $loadedEventData, 'failed to create/load event');
80         $this->assertEquals($eventData['etag'], $loadedEventData['etag']);
81         
82         $contentSeqAfter = Tinebase_Container::getInstance()->getContentSequence($scleverDisplayContainerId);
83         $this->assertEquals($contentSeqBefore + 1, $contentSeqAfter,
84             'content sequence of display container should be increased by 1:' . $contentSeqAfter);
85         $this->assertEquals($contentSeqAfter, Tinebase_Container::getInstance()->get($scleverDisplayContainerId)->content_seq);
86         
87         return $loadedEventData;
88     }
89     
90     public function testStripWindowsLinebreaks()
91     {
92         $e = $this->_getEvent(TRUE);
93         $e->description = 'Hello my friend,' . chr(13) . chr(10) .'bla bla bla.'  . chr(13) . chr(10) .'good bye.';
94         $persistentEventData = $this->_uit->saveEvent($e->toArray());
95         $loadedEventData = $this->_uit->getEvent($persistentEventData['id']);
96         $this->assertEquals($loadedEventData['description'], 'Hello my friend,' . chr(10) . 'bla bla bla.' . chr(10) . 'good bye.');
97     }
98
99     /**
100     * testCreateEventWithNonExistantAttender
101     */
102     public function testCreateEventWithNonExistantAttender()
103     {
104         $testEmail = 'unittestnotexists@example.org';
105         $eventData = $this->_getEvent(TRUE)->toArray();
106         $eventData['attendee'][] = $this->_getUserTypeAttender($testEmail);
107         
108         $persistentEventData = $this->_uit->saveEvent($eventData);
109         $found = FALSE;
110         foreach ($persistentEventData['attendee'] as $attender) {
111             if ($attender['user_id']['email'] === $testEmail) {
112                 $this->assertEquals($testEmail, $attender['user_id']['n_fn']);
113                 $found = TRUE;
114             }
115         }
116         $this->assertTrue($found);
117     }
118     
119     /**
120      * get single attendee array
121      * 
122      * @param string $email
123      * @return array
124      */
125     protected function _getUserTypeAttender($email = 'unittestnotexists@example.org')
126     {
127         return array(
128             'user_id'        => $email,
129             'user_type'      => Calendar_Model_Attender::USERTYPE_USER,
130             'role'           => Calendar_Model_Attender::ROLE_REQUIRED,
131         );
132     }
133     
134     /**
135      * test create event with alarm
136      *
137      * @todo add testUpdateEventWithAlarm
138      */
139     public function testCreateEventWithAlarm()
140     {
141         $eventData = $this->_getEventWithAlarm(TRUE)->toArray();
142         $persistentEventData = $this->_uit->saveEvent($eventData);
143         $loadedEventData = $this->_uit->getEvent($persistentEventData['id']);
144         
145         //print_r($loadedEventData);
146         
147         // check if alarms are created / returned
148         $this->assertGreaterThan(0, count($loadedEventData['alarms']));
149         $this->assertEquals('Calendar_Model_Event', $loadedEventData['alarms'][0]['model']);
150         $this->assertEquals(Tinebase_Model_Alarm::STATUS_PENDING, $loadedEventData['alarms'][0]['sent_status']);
151         $this->assertTrue((isset($loadedEventData['alarms'][0]['minutes_before']) || array_key_exists('minutes_before', $loadedEventData['alarms'][0])), 'minutes_before is missing');
152         
153         $scheduler = Tinebase_Core::getScheduler();
154         $scheduler->addTask('Tinebase_Alarm', $this->createTask());
155         $scheduler->run();
156         
157         // check alarm status
158         $loadedEventData = $this->_uit->getEvent($persistentEventData['id']);
159         $this->assertEquals(Tinebase_Model_Alarm::STATUS_SUCCESS, $loadedEventData['alarms'][0]['sent_status']);
160     }
161     
162     /**
163      * createTask
164      */
165     public function createTask()
166     {
167         $request = new Zend_Controller_Request_Http();
168         $request->setControllerName('Tinebase_Alarm');
169         $request->setActionName('sendPendingAlarms');
170         $request->setParam('eventName', 'Tinebase_Event_Async_Minutely');
171         
172         $task = new Tinebase_Scheduler_Task();
173         $task->setMonths("Jan-Dec");
174         $task->setWeekdays("Sun-Sat");
175         $task->setDays("1-31");
176         $task->setHours("0-23");
177         $task->setMinutes("0/1");
178         $task->setRequest($request);
179         return $task;
180     }
181     
182     /**
183      * testUpdateEvent
184      */
185     public function testUpdateEvent()
186     {
187         $event = new Calendar_Model_Event($this->testCreateEvent(), true);
188         $event->dtstart->addHour(5);
189         $event->dtend->addHour(5);
190         $event->description = 'are you kidding?';
191         
192         $eventData = $event->toArray();
193         foreach ($eventData['attendee'] as $key => $attenderData) {
194             if ($eventData['attendee'][$key]['user_id'] != $this->_getTestUserContact()->getId()) {
195                 unset($eventData['attendee'][$key]);
196             }
197         }
198         
199         $updatedEventData = $this->_uit->saveEvent($eventData);
200         
201         $this->_assertJsonEvent($eventData, $updatedEventData, 'failed to update event');
202         
203         return $updatedEventData;
204     }
205
206     /**
207      * testDeleteEvent
208      */
209     public function testDeleteEvent() {
210         $eventData = $this->testCreateEvent();
211         
212         $this->_uit->deleteEvents(array($eventData['id']));
213         
214         $this->setExpectedException('Tinebase_Exception_NotFound');
215         $this->_uit->getEvent($eventData['id']);
216     }
217     
218     /**
219      * testSearchEvents
220      */
221     public function testSearchEvents()
222     {
223         $eventData = $this->testCreateEvent(TRUE); 
224         
225         $filter = $this->_getEventFilterArray();
226         $searchResultData = $this->_uit->searchEvents($filter, array());
227         
228         $this->assertTrue(! empty($searchResultData['results']));
229         $resultEventData = $searchResultData['results'][0];
230         
231         $this->_assertJsonEvent($eventData, $resultEventData, 'failed to search event');
232         return $searchResultData;
233     }
234     
235     /**
236      * get filter array with container and period filter
237      * 
238      * @param string|int $containerId
239      * @return multitype:multitype:string Ambigous <number, multitype:>  multitype:string multitype:string
240      */
241     protected function _getEventFilterArray($containerId = NULL)
242     {
243         $containerId = ($containerId) ? $containerId : $this->_getTestCalendar()->getId();
244         return array(
245             array('field' => 'container_id', 'operator' => 'equals', 'value' => $containerId),
246             array('field' => 'period', 'operator' => 'within', 'value' =>
247                 array("from" => '2009-03-20 06:15:00', "until" => Tinebase_DateTime::now()->addDay(1)->toString())
248             )
249         );
250     }
251     
252     /**
253      * testSearchEvents with period filter
254      * 
255      * @todo add an event that is in result set of Calendar_Controller_Event::search() 
256      *       but should be removed in Calendar_Frontend_Json::_multipleRecordsToJson()
257      */
258     public function testSearchEventsWithPeriodFilter()
259     {
260         $eventData = $this->testCreateRecurEvent();
261         
262         $filter = array(
263             array('field' => 'period', 'operator' => 'within', 'value' => array(
264                 'from'  => '2009-03-25 00:00:00',
265                 'until' => '2009-03-25 23:59:59',
266             )),
267             array('field' => 'container_id', 'operator' => 'equals', 'value' => $this->_getTestCalendar()->getId()),
268         );
269         
270         $searchResultData = $this->_uit->searchEvents($filter, array());
271         
272         $this->assertTrue(isset($searchResultData['results'][0]), 'event not found in result: ' . print_r($searchResultData['results'], true));
273         $resultEventData = $searchResultData['results'][0];
274         
275         $this->_assertJsonEvent($eventData, $resultEventData, 'failed to search event');
276     }
277     
278     /**
279      * #7688: Internal Server Error on calendar search
280      * 
281      * add period filter if none is given
282      * 
283      * https://forge.tine20.org/mantisbt/view.php?id=7688
284      */
285     public function testSearchEventsWithOutPeriodFilter()
286     {
287         $eventData = $this->testCreateRecurEvent();
288         $filter = array(array('field' => 'container_id', 'operator' => 'equals', 'value' => $this->_getTestCalendar()->getId()));
289         
290         $searchResultData = $this->_uit->searchEvents($filter, array());
291         $returnedFilter = $searchResultData['filter'];
292         $this->assertEquals(2, count($returnedFilter), 'Two filters shoud have been returned!');
293         $this->assertTrue($returnedFilter[1]['field'] == 'period' || $returnedFilter[0]['field'] == 'period', 'One returned filter shoud be a period filter');
294     }
295     
296     /**
297      * add period filter if none is given / configure from+until
298      * 
299      * @see 0009688: allow to configure default period filter in json frontend
300      */
301     public function testSearchEventsWithOutPeriodFilterConfiguredFromAndUntil()
302     {
303         Calendar_Config::getInstance()->set(Calendar_Config::MAX_JSON_DEFAULT_FILTER_PERIOD_FROM, 12);
304         
305         $filter = array(array('field' => 'container_id', 'operator' => 'equals', 'value' => $this->_getTestCalendar()->getId()));
306         $searchResultData = $this->_uit->searchEvents($filter, array());
307         
308         $now = Tinebase_DateTime::now()->setTime(0,0,0);
309         foreach ($searchResultData['filter'] as $filter) {
310             if ($filter['field'] === 'period') {
311                 $this->assertEquals($now->getClone()->subYear(1)->toString(), $filter['value']['from']);
312                 $this->assertEquals($now->getClone()->addMonth(1)->toString(), $filter['value']['until']);
313             }
314         }
315     }
316     
317     /**
318      * testSearchEvents with organizer = me filter
319      * 
320      * @see #6716: default favorite "me" is not resolved properly
321      */
322     public function testSearchEventsWithOrganizerMeFilter()
323     {
324         $eventData = $this->testCreateEvent(TRUE);
325         
326         $filter = $this->_getEventFilterArray();
327         $filter[] = array('field' => 'organizer', 'operator' => 'equals', 'value' => Addressbook_Model_Contact::CURRENTCONTACT);
328         
329         $searchResultData = $this->_uit->searchEvents($filter, array());
330         $this->assertTrue(! empty($searchResultData['results']));
331         $resultEventData = $searchResultData['results'][0];
332         $this->_assertJsonEvent($eventData, $resultEventData, 'failed to search event');
333         
334         // check organizer filter resolving
335         $organizerfilter = $searchResultData['filter'][2];
336         $this->assertTrue(is_array($organizerfilter['value']), 'organizer should be resolved: ' . print_r($organizerfilter, TRUE));
337         $this->assertEquals(Tinebase_Core::getUser()->contact_id, $organizerfilter['value']['id']);
338     }
339
340     /**
341      * testSearchEventsWithSharedContainerFilter
342      *
343      * @see 0011968: shared calendars filter leads to sql error with pgsql
344      */
345     public function testSearchEventsWithSharedContainerFilter()
346     {
347         $filter = $this->_getEventFilterArray();
348         $pathFilterValue = array("path" => "/shared");
349         $filter[0]['value'] = $pathFilterValue;
350         $searchResultData = $this->_uit->searchEvents($filter, array());
351
352         $this->assertEquals($pathFilterValue, $searchResultData['filter'][0]['value'], print_r($searchResultData['filter'], true));
353     }
354
355     /**
356      * search event with alarm
357      */
358     public function testSearchEventsWithAlarm()
359     {
360         $eventData = $this->_getEventWithAlarm(TRUE)->toArray();
361         $persistentEventData = $this->_uit->saveEvent($eventData);
362         
363         $searchResultData = $this->_uit->searchEvents($this->_getEventFilterArray(), array());
364         $this->assertTrue(! empty($searchResultData['results']));
365         $resultEventData = $searchResultData['results'][0];
366         
367         $this->_assertJsonEvent($persistentEventData, $resultEventData, 'failed to search event with alarm');
368     }
369     
370     /**
371      * testSetAttenderStatus
372      */
373     public function testSetAttenderStatus()
374     {
375         $eventData = $this->testCreateEvent();
376         $numAttendee = count($eventData['attendee']);
377         $eventData['attendee'][$numAttendee] = array(
378             'user_id' => $this->_getPersonasContacts('pwulf')->getId(),
379         );
380         
381         $updatedEventData = $this->_uit->saveEvent($eventData);
382         $pwulf = $this->_findAttender($updatedEventData['attendee'], 'pwulf');
383         
384         // he he, we don't have his authkey, cause json class sorts it out due to rights restrictions.
385         $attendeeBackend = new Calendar_Backend_Sql_Attendee();
386         $pwulf['status_authkey'] = $attendeeBackend->get($pwulf['id'])->status_authkey;
387         
388         $updatedEventData['container_id'] = $updatedEventData['container_id']['id'];
389         
390         $pwulf['status'] = Calendar_Model_Attender::STATUS_ACCEPTED;
391         $this->_uit->setAttenderStatus($updatedEventData, $pwulf, $pwulf['status_authkey']);
392         
393         $loadedEventData = $this->_uit->getEvent($eventData['id']);
394         $loadedPwulf = $this->_findAttender($loadedEventData['attendee'], 'pwulf');
395         $this->assertEquals(Calendar_Model_Attender::STATUS_ACCEPTED, $loadedPwulf['status']);
396     }
397     
398     /**
399      * testCreateRecurEvent
400      */
401     public function testCreateRecurEvent()
402     {
403         $eventData = $this->testCreateEvent();
404         $eventData['rrule'] = array(
405             'freq'     => 'WEEKLY',
406             'interval' => 1,
407             'byday'    => 'WE'
408         );
409         
410         $updatedEventData = $this->_uit->saveEvent($eventData);
411         $this->assertTrue(is_array($updatedEventData['rrule']));
412
413         return $updatedEventData;
414     }
415
416     /**
417      * testCreateRecurEventYearly
418      * 
419      * @see 0010610: yearly event is not shown in week view
420      */
421     public function testCreateRecurEventYearly()
422     {
423         $eventData = $this->_getEvent()->toArray();
424         $eventData['is_all_day_event'] = true;
425         $eventData['dtstart'] = '2015-01-04 00:00:00';
426         $eventData['dtend'] = '2015-01-04 23:59:59';
427         $eventData['rrule'] = array(
428             'freq'       => 'YEARLY',
429             'interval'   => 1,
430             'bymonthday' => 4,
431             'bymonth'    => 1,
432         );
433         
434         $updatedEventData = $this->_uit->saveEvent($eventData);
435         $this->assertTrue(is_array($updatedEventData['rrule']));
436         
437         $filter = array(
438             array('field' => 'container_id', 'operator' => 'equals', 'value' => $eventData['container_id']),
439             array('field' => 'period', 'operator' => 'within', 'value' =>
440                 array("from" => '2014-12-29 00:00:00', "until" => '2015-01-05 00:00:00')
441             )
442         );
443         $searchResultData = $this->_uit->searchEvents($filter, array());
444         $this->assertEquals(1, $searchResultData['totalcount'], 'event not found');
445     }
446     
447     /**
448      * testCreateRecurEventWithRruleUntil
449      * 
450      * @see 0008906: rrule_until is saved in usertime
451      */
452     public function testCreateRecurEventWithRruleUntil()
453     {
454         $eventData = $this->testCreateRecurEvent();
455         $localMidnight = Tinebase_DateTime::now()->setTime(23,59,59)->toString();
456         $eventData['rrule']['until'] = $localMidnight;
457         //$eventData['rrule']['freq']  = 'WEEKLY';
458         
459         $updatedEventData = $this->_uit->saveEvent($eventData);
460         $this->assertGreaterThanOrEqual($localMidnight, $updatedEventData['rrule']['until']);
461         
462         // check db record
463         $calbackend = new Calendar_Backend_Sql();
464         $db = $calbackend->getAdapter();
465         $select = $db->select();
466         $select->from(array($calbackend->getTableName() => $calbackend->getTablePrefix() . $calbackend->getTableName()), array('rrule_until', 'rrule'))->limit(1);
467         $select->where($db->quoteIdentifier($calbackend->getTableName() . '.id') . ' = ?', $updatedEventData['id']);
468         
469         $stmt = $db->query($select);
470         $queryResult = $stmt->fetch();
471         
472 //         echo Tinebase_Core::getUserTimezone();
473 //         echo date_default_timezone_get();
474         
475         $midnightInUTC = new Tinebase_DateTime($queryResult['rrule_until']);
476         $this->assertEquals(Tinebase_DateTime::now()->setTime(23,59,59)->toString(), $midnightInUTC->setTimezone(Tinebase_Core::getUserTimezone(), TRUE)->toString());
477     }
478
479     /**
480      * testCreateRecurEventWithConstrains
481      */
482     public function testCreateRecurEventWithConstrains()
483     {
484         $conflictEventData = $this->testCreateEvent();
485
486         $eventData = $this->testCreateEvent();
487         $eventData['rrule'] = array(
488             'freq'       => 'WEEKLY',
489             'interval'   => 1,
490             'byday'      => 'WE',
491         );
492         $eventData['rrule_constraints'] = array(
493             array('field' => 'container_id', 'operator' => 'in', 'value' => array($eventData['container_id'])),
494         );
495
496         $updatedEventData = $this->_uit->saveEvent($eventData);
497
498         $this->assertTrue(is_array($updatedEventData['rrule_constraints']));
499         $this->assertEquals('personal',$updatedEventData['rrule_constraints'][0]['value'][0]['type'], 'filter is not resolved');
500         $this->assertEquals(1, count($updatedEventData['exdate']));
501         $this->assertEquals('2009-03-25 06:00:00', $updatedEventData['exdate'][0]);
502
503         return $updatedEventData;
504     }
505
506     /**
507     * testSearchRecuringIncludes
508     */
509     public function testSearchRecuringIncludes()
510     {
511         $recurEvent = $this->testCreateRecurEvent();
512     
513         $from = $recurEvent['dtstart'];
514         $until = new Tinebase_DateTime($from);
515         $until->addWeek(5)->addHour(10);
516         $until = $until->get(Tinebase_Record_Abstract::ISO8601LONG);
517     
518         $filter = array(
519         array('field' => 'container_id', 'operator' => 'equals', 'value' => $this->_getTestCalendar()->getId()),
520         array('field' => 'period',       'operator' => 'within', 'value' => array('from' => $from, 'until' => $until)),
521         );
522     
523         $searchResultData = $this->_uit->searchEvents($filter, array());
524     
525         $this->assertEquals(6, $searchResultData['totalcount']);
526         
527         // test appending tags to recurring instances
528         $this->assertTrue(isset($searchResultData['results'][4]['tags'][0]), 'tags not set: ' . print_r($searchResultData['results'][4], true));
529         $this->assertEquals('phpunit-', substr($searchResultData['results'][4]['tags'][0]['name'], 0, 8));
530     
531         return $searchResultData;
532     }
533     
534     /**
535      * testSearchRecuringIncludesAndSort
536      */
537     public function testSearchRecuringIncludesAndSort()
538     {
539         $recurEvent = $this->testCreateRecurEvent();
540         
541         $from = $recurEvent['dtstart'];
542         $until = new Tinebase_DateTime($from);
543         $until->addWeek(5)->addHour(10);
544         $until = $until->get(Tinebase_Record_Abstract::ISO8601LONG);
545         
546         $filter = array(
547             array('field' => 'container_id', 'operator' => 'equals', 'value' => $this->_getTestCalendar()->getId()),
548             array('field' => 'period',       'operator' => 'within', 'value' => array('from' => $from, 'until' => $until)),
549         );
550         
551         $searchResultData = $this->_uit->searchEvents($filter, array('sort' => 'dtstart', 'dir' => 'DESC'));
552         
553         $this->assertEquals(6, $searchResultData['totalcount']);
554         
555         // check sorting
556         $this->assertEquals('2009-04-29 06:00:00', $searchResultData['results'][0]['dtstart']);
557         $this->assertEquals('2009-04-22 06:00:00', $searchResultData['results'][1]['dtstart']);
558     }
559     
560     /**
561      * testCreateRecurException
562      */
563     public function testCreateRecurException()
564     {
565         $recurSet = Tinebase_Helper::array_value('results', $this->testSearchRecuringIncludes());
566         
567         $persistentException = $recurSet[1];
568         $persistentException['summary'] = 'go sleeping';
569         
570         // create persistent exception
571         $this->_uit->createRecurException($persistentException, FALSE, FALSE);
572         
573         // create exception date
574         $updatedBaseEvent = Calendar_Controller_Event::getInstance()->getRecurBaseEvent(new Calendar_Model_Event($recurSet[2]));
575         $recurSet[2]['last_modified_time'] = $updatedBaseEvent->last_modified_time;
576         $this->_uit->createRecurException($recurSet[2], TRUE, FALSE);
577         
578         // delete all following (including this)
579         $updatedBaseEvent = Calendar_Controller_Event::getInstance()->getRecurBaseEvent(new Calendar_Model_Event($recurSet[4]));
580         $recurSet[4]['last_modified_time'] = $updatedBaseEvent->last_modified_time;
581         $this->_uit->createRecurException($recurSet[4], TRUE, TRUE);
582         
583         $from = $recurSet[0]['dtstart'];
584         $until = new Tinebase_DateTime($from);
585         $until->addWeek(5)->addHour(10);
586         $until = $until->get(Tinebase_Record_Abstract::ISO8601LONG);
587         
588         $filter = array(
589             array('field' => 'container_id', 'operator' => 'equals', 'value' => $this->_getTestCalendar()->getId()),
590             array('field' => 'period',       'operator' => 'within', 'value' => array('from' => $from, 'until' => $until)),
591         );
592         
593         $searchResultData = $this->_uit->searchEvents($filter, array('sort' => 'dtstart'));
594         
595         // we deleted one and cropped
596         $this->assertEquals(3, count($searchResultData['results']));
597         
598         $summaryMap = array();
599         foreach ($searchResultData['results'] as $event) {
600             $summaryMap[$event['dtstart']] = $event['summary'];
601         }
602         $this->assertTrue((isset($summaryMap['2009-04-01 06:00:00']) || array_key_exists('2009-04-01 06:00:00', $summaryMap)));
603         $this->assertEquals($persistentException['summary'], $summaryMap['2009-04-01 06:00:00']);
604         
605         return $searchResultData;
606     }
607     
608     /**
609      * testCreateRecurExceptionWithOtherUser
610      * 
611      * @see 0008172: displaycontainer_id not set when recur exception is created
612      */
613     public function testCreateRecurExceptionWithOtherUser()
614     {
615         $recurSet = Tinebase_Helper::array_value('results', $this->testSearchRecuringIncludes());
616         
617         // create persistent exception (just status update)
618         $persistentException = $recurSet[1];
619         $scleverAttender = $this->_findAttender($persistentException['attendee'], 'sclever');
620         $attendeeBackend = new Calendar_Backend_Sql_Attendee();
621         $status_authkey = $attendeeBackend->get($scleverAttender['id'])->status_authkey;
622         $scleverAttender['status'] = Calendar_Model_Attender::STATUS_ACCEPTED;
623         $scleverAttender['status_authkey'] = $status_authkey;
624         foreach ($persistentException['attendee'] as $key => $attender) {
625             if ($attender['id'] === $scleverAttender['id']) {
626                 $persistentException['attendee'][$key] = $scleverAttender;
627                 break;
628             }
629         }
630         
631         // sclever has only READ grant
632         Tinebase_Container::getInstance()->setGrants($this->_getTestCalendar(), new Tinebase_Record_RecordSet('Tinebase_Model_Grants', array(array(
633             'account_id'    => $this->_getTestUser()->getId(),
634             'account_type'  => 'user',
635             Tinebase_Model_Grants::GRANT_READ     => true,
636             Tinebase_Model_Grants::GRANT_ADD      => true,
637             Tinebase_Model_Grants::GRANT_EDIT     => true,
638             Tinebase_Model_Grants::GRANT_DELETE   => true,
639             Tinebase_Model_Grants::GRANT_PRIVATE  => true,
640             Tinebase_Model_Grants::GRANT_ADMIN    => true,
641             Tinebase_Model_Grants::GRANT_FREEBUSY => true,
642         ), array(
643             'account_id'    => $this->_getPersona('sclever')->getId(),
644             'account_type'  => 'user',
645             Tinebase_Model_Grants::GRANT_READ     => true,
646             Tinebase_Model_Grants::GRANT_FREEBUSY => true,
647         ))), TRUE);
648         
649         $unittestUser = Tinebase_Core::getUser();
650         Tinebase_Core::set(Tinebase_Core::USER, $this->_getPersona('sclever'));
651         
652         // create persistent exception
653         $createdException = $this->_uit->createRecurException($persistentException, FALSE, FALSE);
654         Tinebase_Core::set(Tinebase_Core::USER, $this->_originalTestUser);
655         
656         $sclever = $this->_findAttender($createdException['attendee'], 'sclever');
657         $defaultCal = $this->_getPersonasDefaultCals('sclever');
658         $this->assertEquals('Susan Clever', $sclever['user_id']['n_fn']);
659         $this->assertEquals(Calendar_Model_Attender::STATUS_ACCEPTED, $sclever['status'], 'status mismatch: ' . print_r($sclever, TRUE));
660         $this->assertTrue(is_array($sclever['displaycontainer_id']));
661         $this->assertEquals($defaultCal['id'], $sclever['displaycontainer_id']['id']);
662     }
663     
664     /**
665      * testUpdateRecurSeries
666      */
667     public function testUpdateRecurSeries()
668     {
669         $recurSet = Tinebase_Helper::array_value('results', $this->testSearchRecuringIncludes());
670         
671         $persistentException = $recurSet[1];
672         $persistentException['summary'] = 'go sleeping';
673         $persistentException['dtstart'] = '2009-04-01 20:00:00';
674         $persistentException['dtend']   = '2009-04-01 20:30:00';
675         
676         // create persistent exception
677         $recurResult = $this->_uit->createRecurException($persistentException, FALSE, FALSE);
678         
679         // update recurseries 
680         $someRecurInstance = $recurSet[2];
681         $someRecurInstance['summary'] = 'go fishing';
682         $someRecurInstance['dtstart'] = '2009-04-08 10:00:00';
683         $someRecurInstance['dtend']   = '2009-04-08 12:30:00';
684         
685         $someRecurInstance['seq'] = 3;
686         $this->_uit->updateRecurSeries($someRecurInstance, FALSE, FALSE);
687         
688         $searchResultData = $this->_searchRecurSeries($recurSet[0]);
689         $this->assertEquals(6, count($searchResultData['results']));
690         
691         $summaryMap = array();
692         foreach ($searchResultData['results'] as $event) {
693             $summaryMap[$event['dtstart']] = $event['summary'];
694         }
695         
696         $this->assertTrue((isset($summaryMap['2009-04-01 20:00:00']) || array_key_exists('2009-04-01 20:00:00', $summaryMap)));
697         $this->assertEquals('go sleeping', $summaryMap['2009-04-01 20:00:00']);
698         
699         $fishings = array_keys($summaryMap, 'go fishing');
700         $this->assertEquals(5, count($fishings));
701         foreach ($fishings as $dtstart) {
702             $this->assertEquals('10:00:00', substr($dtstart, -8), 'all fishing events should start at 10:00');
703         }
704     }
705     
706     /**
707      * search updated recur set
708      * 
709      * @param array $firstInstance
710      * @return array
711      */
712     protected function _searchRecurSeries($firstInstance)
713     {
714         $from = $firstInstance['dtstart'];
715         $until = new Tinebase_DateTime($from);
716         $until->addWeek(5)->addHour(10);
717         $until = $until->get(Tinebase_Record_Abstract::ISO8601LONG);
718         
719         $filter = array(
720             array('field' => 'container_id', 'operator' => 'equals', 'value' => $this->_getTestCalendar()->getId()),
721             array('field' => 'period',       'operator' => 'within', 'value' => array('from' => $from, 'until' => $until)),
722         );
723         
724         return $this->_uit->searchEvents($filter, array());
725     }
726     
727     /**
728      * testUpdateRecurExceptionsFromSeriesOverDstMove
729      * 
730      * @todo implement
731      */
732     public function testUpdateRecurExceptionsFromSeriesOverDstMove()
733     {
734         /*
735          * 1. create recur event 1 day befor dst move
736          * 2. create an exception and exdate
737          * 3. move dtstart from 1 over dst boundary
738          * 4. test recurid and exdate by calculating series
739          */
740     }
741     
742     /**
743      * testDeleteRecurSeries
744      */
745     public function testDeleteRecurSeries()
746     {
747         $recurSet = Tinebase_Helper::array_value('results', $this->testSearchRecuringIncludes());
748         
749         $persistentException = $recurSet[1];
750         $persistentException['summary'] = 'go sleeping';
751         
752         // create persistent exception
753         $this->_uit->createRecurException($persistentException, FALSE, FALSE);
754         
755         // delete recurseries 
756         $someRecurInstance = $persistentException = $recurSet[2];
757         $this->_uit->deleteRecurSeries($someRecurInstance);
758         
759         $from = $recurSet[0]['dtstart'];
760         $until = new Tinebase_DateTime($from);
761         $until->addWeek(5)->addHour(10);
762         $until = $until->get(Tinebase_Record_Abstract::ISO8601LONG);
763         
764         $filter = array(
765             array('field' => 'container_id', 'operator' => 'equals', 'value' => $this->_getTestCalendar()->getId()),
766             array('field' => 'period',       'operator' => 'within', 'value' => array('from' => $from, 'until' => $until)),
767         );
768         
769         $searchResultData = $this->_uit->searchEvents($filter, array());
770         
771         $this->assertEquals(0, count($searchResultData['results']));
772     }
773     
774     /**
775      * testMeAsAttenderFilter
776      */
777     public function testMeAsAttenderFilter()
778     {
779         $eventData = $this->testCreateEvent(TRUE);
780         
781         $filter = $this->_getEventFilterArray();
782         $filter[] = array('field' => 'attender'    , 'operator' => 'equals', 'value' => array(
783             'user_type' => Calendar_Model_Attender::USERTYPE_USER,
784             'user_id'   => Addressbook_Model_Contact::CURRENTCONTACT,
785         ));
786         
787         $searchResultData = $this->_uit->searchEvents($filter, array());
788         $this->assertTrue(! empty($searchResultData['results']));
789         $resultEventData = $searchResultData['results'][0];
790         
791         $this->_assertJsonEvent($eventData, $resultEventData, 'failed to filter for me as attender');
792     }
793     
794     /**
795      * testFreeBusyCleanup
796      * 
797      * @return array event data
798      */
799     public function testFreeBusyCleanup()
800     {
801         // give fb grants from sclever
802         $scleverCal = Tinebase_Container::getInstance()->getContainerById($this->_getPersonasDefaultCals('sclever'));
803         Tinebase_Container::getInstance()->setGrants($scleverCal->getId(), new Tinebase_Record_RecordSet('Tinebase_Model_Grants', array(array(
804             'account_id'    => $this->_getPersona('sclever')->getId(),
805             'account_type'  => 'user',
806             Tinebase_Model_Grants::GRANT_READ     => true,
807             Tinebase_Model_Grants::GRANT_ADD      => true,
808             Tinebase_Model_Grants::GRANT_EDIT     => true,
809             Tinebase_Model_Grants::GRANT_DELETE   => true,
810             Tinebase_Model_Grants::GRANT_PRIVATE  => true,
811             Tinebase_Model_Grants::GRANT_ADMIN    => true,
812             Tinebase_Model_Grants::GRANT_FREEBUSY => true,
813         ), array(
814             'account_id'    => $this->_getTestUser()->getId(),
815             'account_type'  => 'user',
816             Tinebase_Model_Grants::GRANT_FREEBUSY => true,
817         ))), TRUE);
818         
819         Tinebase_Core::set(Tinebase_Core::USER, $this->_getPersona('sclever'));
820         $eventData = $this->_getEvent()->toArray();
821         unset($eventData['organizer']);
822         $eventData['container_id'] = $scleverCal->getId();
823         $eventData['attendee'] = array(array(
824             'user_id' => $this->_getPersonasContacts('sclever')->getId()
825         ));
826         $eventData['organizer'] = $this->_getPersonasContacts('sclever')->getId();
827         $eventData = $this->_uit->saveEvent($eventData);
828         $filter = $this->_getEventFilterArray($this->_getPersonasDefaultCals('sclever')->getId());
829         $filter[] = array('field' => 'summary', 'operator' => 'equals', 'value' => 'Wakeup');
830         $searchResultData = $this->_uit->searchEvents($filter, array());
831         $this->assertTrue(! empty($searchResultData['results']), 'expected event in search result (search by sclever): ' 
832             . print_r($eventData, TRUE) . 'search filter: ' . print_r($filter, TRUE));
833         
834         Tinebase_Core::set(Tinebase_Core::USER, $this->_getTestUser());
835         $searchResultData = $this->_uit->searchEvents($filter, array());
836         $this->assertTrue(! empty($searchResultData['results']), 'expected (freebusy cleanup) event in search result: ' 
837             . print_r($eventData, TRUE) . 'search filter: ' . print_r($filter, TRUE));
838         $eventData = $searchResultData['results'][0];
839         
840         $this->assertFalse((isset($eventData['summary']) || array_key_exists('summary', $eventData)), 'summary not empty: ' . print_r($eventData, TRUE));
841         $this->assertFalse((isset($eventData['description']) || array_key_exists('description', $eventData)), 'description not empty');
842         $this->assertFalse((isset($eventData['tags']) || array_key_exists('tags', $eventData)), 'tags not empty');
843         $this->assertFalse((isset($eventData['notes']) || array_key_exists('notes', $eventData)), 'notes not empty');
844         $this->assertFalse((isset($eventData['attendee']) || array_key_exists('attendee', $eventData)), 'attendee not empty');
845         $this->assertFalse((isset($eventData['organizer']) || array_key_exists('organizer', $eventData)), 'organizer not empty');
846         $this->assertFalse((isset($eventData['alarms']) || array_key_exists('alarms', $eventData)), 'alarms not empty');
847         
848         return $eventData;
849     }
850
851     /**
852      * testFreeBusyCleanupOfNotes
853      * 
854      * @see 0009918: shared (only free/busy) calendar is showing event details within the history tab.
855      */
856     public function testFreeBusyCleanupOfNotes()
857     {
858         $eventData = $this->testFreeBusyCleanup();
859         
860         $tinebaseJson = new Tinebase_Frontend_Json();
861         $filter = array(array(
862             'field' => "record_model",
863             'operator' => "equals",
864             'value' => "Calendar_Model_Event"
865         ), array(
866             'field' => 'record_id',
867             'operator' => 'equals',
868             'value' => $eventData['id']
869         ));
870         $notes = $tinebaseJson->searchNotes($filter, array());
871         
872         $this->assertEquals(0, $notes['totalcount'], 'should not find any notes of record');
873         $this->assertEquals(0, count($notes['results']), 'should not find any notes of record');
874     }
875     
876     /**
877      * test deleting container and the containing events
878      * #6704: events do not disappear when shared calendar got deleted
879      * https://forge.tine20.org/mantisbt/view.php?id=6704
880      */
881     public function testDeleteContainerAndEvents()
882     {
883         $fe = new Tinebase_Frontend_Json_Container();
884         $container = $fe->addContainer('Calendar', 'testdeletecontacts', Tinebase_Model_Container::TYPE_SHARED, '');
885         // test if model is set automatically
886         $this->assertEquals($container['model'], 'Calendar_Model_Event');
887         
888         $date = new Tinebase_DateTime();
889         $event = new Calendar_Model_Event(array(
890             'dtstart' => $date,
891             'dtend'    => $date->subHour(1),
892             'summary' => 'bla bla',
893             'class'    => 'PUBLIC',
894             'transp'    => 'OPAQUE',
895             'container_id' => $container['id'],
896             'organizer' => Tinebase_Core::getUser()->contact_id
897             ));
898         $event = Calendar_Controller_Event::getInstance()->create($event);
899         $this->assertEquals($container['id'], $event->container_id);
900         
901         $fe->deleteContainer($container['id']);
902         
903         $e = new Exception('dummy');
904         
905         $cb = new Calendar_Backend_Sql();
906         $deletedEvent = $cb->get($event->getId(), true);
907         // record should be deleted
908         $this->assertEquals($deletedEvent->is_deleted, 1);
909         
910         try {
911             Calendar_Controller_Event::getInstance()->get($event->getId(), $container['id']);
912             $this->fail('The expected exception was not thrown');
913         } catch (Tinebase_Exception_NotFound $e) {
914             // ok;
915         }
916         // record should not be found
917         $this->assertEquals($e->getMessage(), 'Calendar_Model_Event record with id '.$event->getId().' not found!');
918     }
919     
920     /**
921      * compare expected event data with test event
922      *
923      * @param array $expectedEventData
924      * @param array $eventData
925      * @param string $msg
926      */
927     protected function _assertJsonEvent($expectedEventData, $eventData, $msg)
928     {
929         $this->assertEquals($expectedEventData['summary'], $eventData['summary'], $msg . ': failed to create/load event');
930         
931         // assert effective grants are set
932         $this->assertEquals((bool) $expectedEventData[Tinebase_Model_Grants::GRANT_EDIT], (bool) $eventData[Tinebase_Model_Grants::GRANT_EDIT], $msg . ': effective grants mismatch');
933         // container, assert attendee, tags
934         $this->assertEquals($expectedEventData['dtstart'], $eventData['dtstart'], $msg . ': dtstart mismatch');
935         $this->assertTrue(is_array($eventData['container_id']), $msg . ': failed to "resolve" container');
936         $this->assertTrue(is_array($eventData['container_id']['account_grants']), $msg . ': failed to "resolve" container account_grants');
937         $this->assertGreaterThan(0, count($eventData['attendee']));
938         $this->assertEquals(count($eventData['attendee']), count($expectedEventData['attendee']), $msg . ': failed to append attendee');
939         $this->assertTrue(is_array($eventData['attendee'][0]['user_id']), $msg . ': failed to resolve attendee user_id');
940         // NOTE: due to sorting isshues $eventData['attendee'][0] may be a non resolvable container (due to rights restrictions)
941         $this->assertTrue(is_array($eventData['attendee'][0]['displaycontainer_id']) || (isset($eventData['attendee'][1]) && is_array($eventData['attendee'][1]['displaycontainer_id'])), $msg . ': failed to resolve attendee displaycontainer_id');
942         $this->assertEquals(count($expectedEventData['tags']), count($eventData['tags']), $msg . ': failed to append tag');
943         $this->assertEquals(count($expectedEventData['notes']), count($eventData['notes']), $msg . ': failed to create note or wrong number of notes');
944         
945         if ((isset($expectedEventData['alarms']) || array_key_exists('alarms', $expectedEventData))) {
946             $this->assertTrue((isset($eventData['alarms']) || array_key_exists('alarms', $eventData)), ': failed to create alarms');
947             $this->assertEquals(count($expectedEventData['alarms']), count($eventData['alarms']), $msg . ': failed to create correct number of alarms');
948             if (count($expectedEventData['alarms']) > 0) {
949                 $this->assertTrue((isset($eventData['alarms'][0]['minutes_before']) || array_key_exists('minutes_before', $eventData['alarms'][0])));
950             }
951         }
952     }
953     
954     /**
955      * find attender 
956      *
957      * @param array $attendeeData
958      * @param string $name
959      * @return array
960      */
961     protected function _findAttender($attendeeData, $name) {
962         $attenderData = false;
963         $searchedId = $this->_getPersonasContacts($name)->getId();
964         
965         foreach ($attendeeData as $key => $attender) {
966             if ($attender['user_type'] == Calendar_Model_Attender::USERTYPE_USER) {
967                 if (is_array($attender['user_id']) && (isset($attender['user_id']['id']) || array_key_exists('id', $attender['user_id']))) {
968                     if ($attender['user_id']['id'] == $searchedId) {
969                         $attenderData = $attendeeData[$key];
970                     }
971                 }
972             }
973         }
974         
975         return $attenderData;
976     }
977     
978     /**
979      * test filter with hidden group -> should return empty result
980      * 
981      * @see 0006934: setting a group that is hidden from adb as attendee filter throws exception
982      */
983     public function testHiddenGroupFilter()
984     {
985         $hiddenGroup = new Tinebase_Model_Group(array(
986             'name'          => 'hiddengroup',
987             'description'   => 'hidden group',
988             'visibility'     => Tinebase_Model_Group::VISIBILITY_HIDDEN
989         ));
990         $hiddenGroup = Admin_Controller_Group::getInstance()->create($hiddenGroup);
991         $this->_groupIdsToDelete[] = $hiddenGroup->getId();
992         
993         $filter = array(array(
994             'field'    => 'attender',
995             'operator' => 'equals',
996             'value'    => array(
997                 'user_id'   => $hiddenGroup->list_id,
998                 'user_type' => 'group',
999             ),
1000         ));
1001         $result = $this->_uit->searchEvents($filter, array());
1002         $this->assertEquals(0, $result['totalcount']);
1003     }
1004     
1005     /**
1006      * testExdateDeleteAll
1007      * 
1008      * @see 0007382: allow to edit / delete the whole series / thisandfuture when editing/deleting recur exceptions
1009      */
1010     public function testExdateDeleteAll()
1011     {
1012         $events = $this->testCreateRecurException();
1013         $exception = $this->_getException($events);
1014         $this->_uit->deleteEvents(array($exception['id']), Calendar_Model_Event::RANGE_ALL);
1015         
1016         $search = $this->_uit->searchEvents($events['filter'], NULL);
1017         $this->assertEquals(0, $search['totalcount'], 'all events should be deleted: ' . print_r($search,TRUE));
1018     }
1019     
1020     /**
1021      * get exception from event resultset
1022      * 
1023      * @param array $events
1024      * @param integer $index (1 = picks first, 2 = picks second, ...)
1025      * @return array|NULL
1026      */
1027     protected function _getException($events, $index = 1)
1028     {
1029         $event = NULL;
1030         $found = 0;
1031         foreach ($events['results'] as $event) {
1032             if (! empty($event['recurid'])) {
1033                 $found++;
1034                 if ($index === $found) {
1035                     return $event;
1036                 }
1037             }
1038         }
1039         
1040         return $event;
1041     }
1042     
1043     /**
1044      * testExdateDeleteThis
1045      * 
1046      * @see 0007382: allow to edit / delete the whole series / thisandfuture when editing/deleting recur exceptions
1047      */
1048     public function testExdateDeleteThis()
1049     {
1050         $events = $this->testCreateRecurException();
1051         $exception = $this->_getException($events);
1052         $this->_uit->deleteEvents(array($exception['id']));
1053         
1054         $search = $this->_uit->searchEvents($events['filter'], NULL);
1055         $this->assertEquals(2, $search['totalcount'], '2 events should remain: ' . print_r($search,TRUE));
1056     }
1057     
1058     /**
1059      * testExdateDeleteThisAndFuture
1060      * 
1061      * @see 0007382: allow to edit / delete the whole series / thisandfuture when editing/deleting recur exceptions
1062      */
1063     public function testExdateDeleteThisAndFuture()
1064     {
1065         $events = $this->testCreateRecurException();
1066         $exception = $this->_getException($events, 1);
1067         $this->_uit->deleteEvents(array($exception['id']), Calendar_Model_Event::RANGE_THISANDFUTURE);
1068         
1069         $search = $this->_uit->searchEvents($events['filter'], NULL);
1070         $this->assertEquals(1, $search['totalcount'], '1 event should remain: ' . print_r($search,TRUE));
1071     }
1072     
1073     /**
1074      * assert grant handling
1075      */
1076     public function testSaveResource($grants = array('readGrant' => true,'editGrant' => true))
1077     {
1078         $resoureData = array(
1079             'name'  => Tinebase_Record_Abstract::generateUID(),
1080             'email' => Tinebase_Record_Abstract::generateUID() . '@unittest.com',
1081             'grants' => array(array_merge($grants, array(
1082                 'account_id' => Tinebase_Core::getUser()->getId(),
1083                 'account_type' => 'user'
1084             )))
1085         );
1086         
1087         $resoureData = $this->_uit->saveResource($resoureData);
1088         $this->assertTrue(is_array($resoureData['grants']), 'grants are not resolved');
1089         
1090         return $resoureData;
1091     }
1092     
1093     /**
1094      * assert only resources with read grant are returned if the user has no manage right
1095      */
1096     public function testSearchResources()
1097     {
1098         $readableResoureData = $this->testSaveResource();
1099         $nonReadableResoureData = $this->testSaveResource(array());
1100         
1101         $filer = array(
1102             array('field' => 'name', 'operator' => 'in', 'value' => array(
1103                 $readableResoureData['name'],
1104                 $nonReadableResoureData['name'],
1105             ))
1106         );
1107         
1108         $searchResultManager = $this->_uit->searchResources($filer, array());
1109         $this->assertEquals(2, count($searchResultManager['results']), 'with manage grants all records should be found');
1110         
1111         // steal manage right and reactivate container checks
1112         $roleManager = Tinebase_Acl_Roles::getInstance();
1113         $roleManager->deleteRoles(array(
1114                 $roleManager->getRoleByName('manager role')->getId(),
1115                 $roleManager->getRoleByName('admin role')->getId()
1116                 ));
1117         
1118         Calendar_Controller_Resource::getInstance()->doContainerACLChecks(TRUE);
1119         
1120         $searchResult = $this->_uit->searchResources($filer, array());
1121         $this->assertEquals(1, count($searchResult['results']), 'without manage grants only one record should be found');
1122     }
1123     
1124     /**
1125      * assert status authkey with editGrant
1126      * assert stauts can be set with editGrant
1127      * assert stauts can't be set without editGrant
1128      */
1129     public function testResourceAttendeeGrants()
1130     {
1131         $editableResoureData = $this->testSaveResource();
1132         $nonEditableResoureData = $this->testSaveResource(array('readGrant'));
1133         
1134         $event = $this->_getEvent(TRUE);
1135         $event->attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender', array(
1136             array(
1137                 'user_type'  => Calendar_Model_Attender::USERTYPE_RESOURCE,
1138                 'user_id'    => $editableResoureData['id'],
1139                 'status'     => Calendar_Model_Attender::STATUS_ACCEPTED
1140             ),
1141             array(
1142                 'user_type'  => Calendar_Model_Attender::USERTYPE_RESOURCE,
1143                 'user_id'    => $nonEditableResoureData['id'],
1144                 'status'     => Calendar_Model_Attender::STATUS_ACCEPTED
1145             )
1146         ));
1147         
1148         $persistentEventData = $this->_uit->saveEvent($event->toArray());
1149         
1150         $attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender', $persistentEventData['attendee']);
1151         $this->assertEquals(1, count($attendee->filter('status', Calendar_Model_Attender::STATUS_ACCEPTED)), 'one accepted');
1152         $this->assertEquals(1, count($attendee->filter('status', Calendar_Model_Attender::STATUS_NEEDSACTION)), 'one needs action');
1153         
1154         $this->assertEquals(1, count($attendee->filter('status_authkey', '/[a-z0-9]+/', TRUE)), 'one has authkey');
1155         
1156         $attendee->status = Calendar_Model_Attender::STATUS_TENTATIVE;
1157         $persistentEventData['attendee'] = $attendee->toArray();
1158         
1159         $updatedEventData = $this->_uit->saveEvent($persistentEventData);
1160         $attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender', $updatedEventData['attendee']);
1161         $this->assertEquals(1, count($attendee->filter('status', Calendar_Model_Attender::STATUS_TENTATIVE)), 'one tentative');
1162     }
1163
1164     /**
1165      * testExdateUpdateAllSummary
1166      * 
1167      * @see 0007690: allow to update the whole series / thisandfuture when updating recur exceptions
1168      */
1169     public function testExdateUpdateAllSummary()
1170     {
1171         $events = $this->testCreateRecurException();
1172         $exception = $this->_getException($events, 1);
1173         $exception['summary'] = 'new summary';
1174         
1175         $event = $this->_uit->saveEvent($exception, FALSE, Calendar_Model_Event::RANGE_ALL);
1176         
1177         $search = $this->_uit->searchEvents($events['filter'], NULL);
1178         foreach ($search['results'] as $event) {
1179             $this->assertEquals('new summary', $event['summary']);
1180         }
1181     }
1182
1183     /**
1184      * testExdateUpdateAllDtStart
1185      * 
1186      * @see 0007690: allow to update the whole series / thisandfuture when updating recur exceptions
1187      * 
1188      * @todo finish
1189      */
1190     public function testExdateUpdateAllDtStart()
1191     {
1192         $events = $this->testCreateRecurException();
1193         $exception = $this->_getException($events, 1);
1194         $exception['dtstart'] = '2009-04-01 08:00:00';
1195         $exception['dtend'] = '2009-04-01 08:15:00';
1196         
1197         $event = $this->_uit->saveEvent($exception, FALSE, Calendar_Model_Event::RANGE_ALL);
1198         
1199         $search = $this->_uit->searchEvents($events['filter'], NULL);
1200         foreach ($search['results'] as $event) {
1201             $this->assertContains('08:00:00', $event['dtstart'], 'wrong dtstart: ' . print_r($event, TRUE));
1202             $this->assertContains('08:15:00', $event['dtend']);
1203         }
1204     }
1205     
1206     /**
1207      * testExdateUpdateThis
1208      * 
1209      * @see 0007690: allow to update the whole series / thisandfuture when updating recur exceptions
1210      */
1211     public function testExdateUpdateThis()
1212     {
1213         $events = $this->testCreateRecurException();
1214         $exception = $this->_getException($events, 1);
1215         $exception['summary'] = 'exception';
1216         
1217         $event = $this->_uit->saveEvent($exception);
1218         $this->assertEquals('exception', $event['summary']);
1219         
1220         // check for summary (only changed in one event)
1221         $search = $this->_uit->searchEvents($events['filter'], NULL);
1222         foreach ($search['results'] as $event) {
1223             if (! empty($event['recurid']) && ! preg_match('/^fakeid/', $event['id'])) {
1224                 $this->assertEquals('exception', $event['summary'], 'summary not changed in exception: ' . print_r($event, TRUE));
1225             } else {
1226                 $this->assertEquals('Wakeup', $event['summary']);
1227             }
1228         }
1229     }
1230
1231     /**
1232      * testExdateUpdateThisAndFuture
1233      * 
1234      * @see 0007690: allow to update the whole series / thisandfuture when updating recur exceptions
1235      */
1236     public function testExdateUpdateThisAndFuture()
1237     {
1238         $events = $this->testCreateRecurException();
1239         $exception = $this->_getException($events, 1);
1240         $exception['summary'] = 'new summary';
1241         
1242         $updatedEvent = $this->_uit->saveEvent($exception, FALSE, Calendar_Model_Event::RANGE_THISANDFUTURE);
1243         $this->assertEquals('new summary', $updatedEvent['summary'], 'summary not changed in exception: ' . print_r($updatedEvent, TRUE));
1244         
1245         $search = $this->_uit->searchEvents($events['filter'], NULL);
1246         foreach ($search['results'] as $event) {
1247             if ($event['dtstart'] >= $updatedEvent['dtstart']) {
1248                 $this->assertEquals('new summary', $event['summary'], 'summary not changed in event: ' . print_r($event, TRUE));
1249             } else {
1250                 $this->assertEquals('Wakeup', $event['summary']);
1251             }
1252         }
1253     }
1254
1255     /**
1256      * testExdateUpdateThisAndFutureWithRruleUntil
1257      * 
1258      * @see 0008244: "rrule until must not be before dtstart" when updating recur exception (THISANDFUTURE)
1259      */
1260     public function testExdateUpdateThisAndFutureWithRruleUntil()
1261     {
1262         $events = $this->testCreateRecurException();
1263         
1264         $exception = $this->_getException($events, 1);
1265         $exception['dtstart'] = Tinebase_DateTime::now()->toString();
1266         $exception['dtend'] = Tinebase_DateTime::now()->addHour(1)->toString();
1267         
1268         // move exception
1269         $updatedEvent = $this->_uit->saveEvent($exception);
1270         // try to update the whole series
1271         $updatedEvent['summary'] = 'new summary';
1272         $updatedEvent = $this->_uit->saveEvent($updatedEvent, FALSE, Calendar_Model_Event::RANGE_THISANDFUTURE);
1273         
1274         $this->assertEquals('new summary', $updatedEvent['summary'], 'summary not changed in event: ' . print_r($updatedEvent, TRUE));
1275     }
1276     
1277     /**
1278      * testExdateUpdateThisAndFutureRemoveAttendee
1279      * 
1280      * @see 0007690: allow to update the whole series / thisandfuture when updating recur exceptions
1281      */
1282     public function testExdateUpdateThisAndFutureRemoveAttendee()
1283     {
1284         $events = $this->testCreateRecurException();
1285         $exception = $this->_getException($events, 1);
1286         // remove susan from attendee
1287         unset($exception['attendee'][0]);
1288         
1289         $updatedEvent = $this->_uit->saveEvent($exception, FALSE, Calendar_Model_Event::RANGE_THISANDFUTURE);
1290         $this->assertEquals(1, count($updatedEvent['attendee']), 'attender not removed from exception: ' . print_r($updatedEvent, TRUE));
1291         
1292         $search = $this->_uit->searchEvents($events['filter'], NULL);
1293         foreach ($search['results'] as $event) {
1294             if ($event['dtstart'] >= $updatedEvent['dtstart']) {
1295                 $this->assertEquals(1, count($event['attendee']), 'attendee count mismatch: ' . print_r($event, TRUE));
1296             } else {
1297                 $this->assertEquals(2, count($event['attendee']), 'attendee count mismatch: ' . print_r($event, TRUE));
1298             }
1299         }
1300     }
1301
1302     /**
1303      * testExdateUpdateAllAddAttendee
1304      * 
1305      * @see 0007690: allow to update the whole series / thisandfuture when updating recur exceptions
1306      */
1307     public function testExdateUpdateAllAddAttendee()
1308     {
1309         $events = $this->testCreateRecurException();
1310         $exception = $this->_getException($events, 1);
1311         // add new attender
1312         $exception['attendee'][] = $this->_getUserTypeAttender();
1313         
1314         $updatedEvent = $this->_uit->saveEvent($exception, FALSE, Calendar_Model_Event::RANGE_ALL);
1315         $this->assertEquals(3, count($updatedEvent['attendee']), 'attender not added to exception: ' . print_r($updatedEvent, TRUE));
1316         
1317         $search = $this->_uit->searchEvents($events['filter'], NULL);
1318         foreach ($search['results'] as $event) {
1319             $this->assertEquals(3, count($event['attendee']), 'attendee count mismatch: ' . print_r($event, TRUE));
1320         }
1321     }
1322     
1323     /**
1324      * testExdateUpdateThisAndFutureChangeDtstart
1325      * 
1326      * @see 0007690: allow to update the whole series / thisandfuture when updating recur exceptions
1327      */
1328     public function testExdateUpdateThisAndFutureChangeDtstart()
1329     {
1330         $events = $this->testCreateRecurException();
1331         $exception = $this->_getException($events, 1);
1332         $exception['dtstart'] = '2009-04-01 08:00:00';
1333         $exception['dtend'] = '2009-04-01 08:15:00';
1334         
1335         $updatedEvent = $this->_uit->saveEvent($exception, FALSE, Calendar_Model_Event::RANGE_THISANDFUTURE);
1336         
1337         $search = $this->_uit->searchEvents($events['filter'], NULL);
1338         foreach ($search['results'] as $event) {
1339             if ($event['dtstart'] >= $updatedEvent['dtstart']) {
1340                 $this->assertContains('08:00:00', $event['dtstart'], 'wrong dtstart: ' . print_r($event, TRUE));
1341                 $this->assertContains('08:15:00', $event['dtend']);
1342             } else {
1343                 $this->assertContains('06:00:00', $event['dtstart'], 'wrong dtstart: ' . print_r($event, TRUE));
1344                 $this->assertContains('06:15:00', $event['dtend']);
1345             }
1346         }
1347     }
1348     
1349     /**
1350      * testExdateUpdateAllWithModlog
1351      * - change base event, then update all
1352      * 
1353      * @see 0007690: allow to update the whole series / thisandfuture when updating recur exceptions
1354      * @see 0009340: fix Calendar_JsonTests::testExdateUpdateAllWithModlog*
1355      */
1356     public function testExdateUpdateAllWithModlog()
1357     {
1358         $this->markTestSkipped('this test is broken: see 0009340: fix Calendar_JsonTests::testExdateUpdateAllWithModlog*');
1359         
1360         $events = $this->testCreateRecurException();
1361         $baseEvent = $events['results'][0];
1362         $exception = $this->_getException($events, 1);
1363         
1364         $baseEvent['summary'] = 'Get up, lazyboy!';
1365         $baseEvent = $this->_uit->saveEvent($baseEvent);
1366         sleep(1);
1367         
1368         $exception['summary'] = 'new summary';
1369         $updatedEvent = $this->_uit->saveEvent($exception, FALSE, Calendar_Model_Event::RANGE_ALL);
1370         
1371         $search = $this->_uit->searchEvents($events['filter'], NULL);
1372         foreach ($search['results'] as $event) {
1373             if ($event['dtstart'] == $updatedEvent['dtstart']) {
1374                 $this->assertEquals('new summary', $event['summary'], 'Recur exception should have the new summary');
1375             } else {
1376                 $this->assertEquals('Get up, lazyboy!', $event['summary'], 'Wrong summary in base/recur event: ' . print_r($event, TRUE));
1377             }
1378         }
1379     }
1380
1381     /**
1382      * testExdateUpdateAllWithModlogAddAttender
1383      * - change base event, then update all
1384      * 
1385      * @see 0007690: allow to update the whole series / thisandfuture when updating recur exceptions
1386      * @see 0007826: add attendee changes to modlog
1387      * @see 0009340: fix Calendar_JsonTests::testExdateUpdateAllWithModlog*
1388      */
1389     public function testExdateUpdateAllWithModlogAddAttender()
1390     {
1391         $this->markTestSkipped('0009340: fix Calendar_JsonTests::testExdateUpdateAllWithModlogAddAttender');
1392         
1393         $events = $this->testCreateRecurException();
1394         $baseEvent = $events['results'][0];
1395         $exception = $this->_getException($events, 1);
1396         
1397         // add new attender
1398         $baseEvent['attendee'][] = $this->_getUserTypeAttender();
1399         $baseEvent = $this->_uit->saveEvent($baseEvent);
1400         $this->assertEquals(3, count($baseEvent['attendee']), 'Attendee count mismatch in baseEvent: ' . print_r($baseEvent, TRUE));
1401         sleep(1);
1402         
1403         // check recent changes (needs to contain attendee change)
1404         $exdate = Calendar_Controller_Event::getInstance()->get($exception['id']);
1405         $recentChanges = Tinebase_Timemachine_ModificationLog::getInstance()->getModifications('Calendar', $baseEvent['id'], NULL, 'Sql', $exdate->creation_time);
1406         $this->assertGreaterThan(2, count($recentChanges), 'Did not get all recent changes: ' . print_r($recentChanges->toArray(), TRUE));
1407         $this->assertTrue(in_array('attendee', $recentChanges->modified_attribute), 'Attendee change missing: ' . print_r($recentChanges->toArray(), TRUE));
1408         
1409         $exception['attendee'][] = $this->_getUserTypeAttender('unittestnotexists@example.com');
1410         $updatedEvent = $this->_uit->saveEvent($exception, FALSE, Calendar_Model_Event::RANGE_ALL);
1411         
1412         $search = $this->_uit->searchEvents($events['filter'], NULL);
1413         foreach ($search['results'] as $event) {
1414             if ($event['dtstart'] == $updatedEvent['dtstart']) {
1415                 $this->assertEquals(3, count($event['attendee']), 'Attendee count mismatch in exdate: ' . print_r($event, TRUE));
1416             } else {
1417                 $this->assertEquals(4, count($event['attendee']), 'Attendee count mismatch: ' . print_r($event, TRUE));
1418             }
1419         }
1420     }
1421
1422     /**
1423      * testConcurrentAttendeeChangeAdd
1424      * 
1425      * @see 0008078: concurrent attendee change should be merged
1426      */
1427     public function testConcurrentAttendeeChangeAdd()
1428     {
1429         $eventData = $this->testCreateEvent();
1430         $numAttendee = count($eventData['attendee']);
1431         $eventData['attendee'][$numAttendee] = array(
1432             'user_id' => $this->_getPersonasContacts('pwulf')->getId(),
1433         );
1434         $this->_uit->saveEvent($eventData);
1435         
1436         $eventData['attendee'][$numAttendee] = array(
1437             'user_id' => $this->_getPersonasContacts('jsmith')->getId(),
1438         );
1439         $event = $this->_uit->saveEvent($eventData);
1440         
1441         $this->assertEquals(4, count($event['attendee']), 'both new attendee (pwulf + jsmith) should be added: ' . print_r($event['attendee'], TRUE));
1442     }
1443
1444     /**
1445      * testConcurrentAttendeeChangeRemove
1446      * 
1447      * @see 0008078: concurrent attendee change should be merged
1448      */
1449     public function testConcurrentAttendeeChangeRemove()
1450     {
1451         $eventData = $this->testCreateEvent();
1452         $currentAttendee = $eventData['attendee'];
1453         unset($eventData['attendee'][1]);
1454         $event = $this->_uit->saveEvent($eventData);
1455         
1456         $eventData['attendee'] = $currentAttendee;
1457         $numAttendee = count($eventData['attendee']);
1458         $eventData['attendee'][$numAttendee] = array(
1459             'user_id' => $this->_getPersonasContacts('pwulf')->getId(),
1460         );
1461         $event = $this->_uit->saveEvent($eventData);
1462         
1463         $this->assertEquals(2, count($event['attendee']), 'one attendee should added and one removed: ' . print_r($event['attendee'], TRUE));
1464     }
1465
1466     /**
1467      * testConcurrentAttendeeChangeUpdate
1468      * 
1469      * @see 0008078: concurrent attendee change should be merged
1470      */
1471     public function testConcurrentAttendeeChangeUpdate()
1472     {
1473         $eventData = $this->testCreateEvent();
1474         $currentAttendee = $eventData['attendee'];
1475         $adminIndex = ($eventData['attendee'][0]['user_id']['n_fn'] === 'Susan Clever') ? 1 : 0;
1476         $eventData['attendee'][$adminIndex]['status'] = Calendar_Model_Attender::STATUS_TENTATIVE;
1477         $event = $this->_uit->saveEvent($eventData);
1478         
1479         $loggedMods = Tinebase_Timemachine_ModificationLog::getInstance()->getModificationsBySeq(
1480             Tinebase_Application::getInstance()->getApplicationByName('Calendar')->getId(),
1481             new Calendar_Model_Attender($eventData['attendee'][$adminIndex]), 2);
1482         $this->assertEquals(1, count($loggedMods), 'attender modification has not been logged');
1483         
1484         $eventData['attendee'] = $currentAttendee;
1485         $scleverIndex = ($adminIndex === 1) ? 0 : 1;
1486         $attendeeBackend = new Calendar_Backend_Sql_Attendee();
1487         $eventData['attendee'][$scleverIndex]['status_authkey'] = $attendeeBackend->get($eventData['attendee'][$scleverIndex]['id'])->status_authkey;
1488         $eventData['attendee'][$scleverIndex]['status'] = Calendar_Model_Attender::STATUS_TENTATIVE;
1489         $event = $this->_uit->saveEvent($eventData);
1490
1491         foreach ($event['attendee'] as $attender) {
1492             $this->assertEquals(Calendar_Model_Attender::STATUS_TENTATIVE, $attender['status'], 'both attendee status should be TENTATIVE: ' . print_r($attender, TRUE));
1493         }
1494     }
1495
1496     /**
1497      * testFreeBusyCheckForExdates
1498      * 
1499      * @see 0008464: freebusy check does not work when creating recur exception
1500      */
1501     public function testFreeBusyCheckForExdates()
1502     {
1503         $events = $this->testCreateRecurException();
1504         $exception = $this->_getException($events, 1);
1505         
1506         $anotherEvent = $this->_getEvent(TRUE);
1507         $anotherEvent = $this->_uit->saveEvent($anotherEvent->toArray());
1508         
1509         $exception['dtstart'] = $anotherEvent['dtstart'];
1510         $exception['dtend'] = $anotherEvent['dtend'];
1511         
1512         try {
1513             $event = $this->_uit->saveEvent($exception, TRUE);
1514             $this->fail('Calendar_Exception_AttendeeBusy expected when saving exception: ' . print_r($exception, TRUE));
1515         } catch (Calendar_Exception_AttendeeBusy $ceab) {
1516             $this->assertEquals('Calendar_Exception_AttendeeBusy', get_class($ceab));
1517         }
1518     }
1519     
1520     /**
1521      * testAddAttachmentToRecurSeries
1522      * 
1523      * @see 0005024: allow to attach external files to records
1524      */
1525     public function testAddAttachmentToRecurSeries()
1526     {
1527         $tempFile = $this->_getTempFile();
1528         $recurSet = Tinebase_Helper::array_value('results', $this->testSearchRecuringIncludes());
1529         // update recurseries 
1530         $someRecurInstance = $recurSet[2];
1531         $someRecurInstance['attachments'] = array(array('tempFile' => array('id' => $tempFile->getId())));
1532         $someRecurInstance['seq'] = 2;
1533         $this->_uit->updateRecurSeries($someRecurInstance, FALSE, FALSE);
1534         
1535         $searchResultData = $this->_searchRecurSeries($recurSet[0]);
1536         foreach ($searchResultData['results'] as $recurInstance) {
1537             $this->assertTrue(isset($recurInstance['attachments']), 'no attachments found in event: ' . print_r($recurInstance, TRUE));
1538             $this->assertEquals(1, count($recurInstance['attachments']));
1539             $attachment = $recurInstance['attachments'][0];
1540             $this->assertEquals('text/plain', $attachment['contenttype'], print_r($attachment, TRUE));
1541         }
1542     }
1543     
1544     /**
1545      * checks if manipulated dtend and dtstart gets set to the correct values on creating or updating an event
1546      * 
1547      * @see 0009696: time is not grayed out for all-day events
1548      */
1549     public function testWholedayEventTimes()
1550     {
1551         $event = $this->_getEvent(TRUE);
1552         $event->is_all_day_event = TRUE;
1553         
1554         $event = Calendar_Controller_Event::getInstance()->create($event);
1555         $event->setTimezone(Tinebase_Core::getUserTimezone());
1556         
1557         $this->assertEquals('00:00:00', $event->dtstart->format('H:i:s'));
1558         $this->assertEquals('23:59:59', $event->dtend->format('H:i:s'));
1559         
1560         $event->dtstart = Tinebase_DateTime::now();
1561         $event->dtend   = Tinebase_DateTime::now()->addHour(1);
1562         
1563         $event = Calendar_Controller_Event::getInstance()->update($event);
1564         $event->setTimezone(Tinebase_Core::getUserTimezone());
1565         
1566         $this->assertEquals('00:00:00', $event->dtstart->format('H:i:s'));
1567         $this->assertEquals('23:59:59', $event->dtend->format('H:i:s'));
1568     }
1569     
1570      /**
1571      * testAttendeeChangeQuantityToInvalid
1572      * 
1573      * @see 9630: sanitize attender quantity
1574      */
1575     public function testAttendeeChangeQuantityToInvalid()
1576     {
1577         $eventData = $this->testCreateEvent();
1578         $currentAttendee = $eventData['attendee'];
1579         $eventData['attendee'][1]['quantity'] = '';
1580         $event = $this->_uit->saveEvent($eventData);
1581         $this->assertEquals(1, $event['attendee'][1]['quantity'], 'The invalid quantity should be saved as 1');
1582     }
1583
1584     /**
1585      * trigger caldav import by json frontend
1586      * 
1587      * @todo use mock as fallback (if server can not be reached by curl)
1588      * @todo get servername from unittest config / skip or mock if no servername found
1589      */
1590     public function testCalDAVImport()
1591     {
1592         // Skip if tine20.com.local could not be resolved
1593         if (gethostbyname('tine20.com.local') == 'tine20.com.local') {
1594             $this->markTestSkipped('Can\'t perform test, because instance is not reachable.');
1595         }
1596
1597         $this->_testNeedsTransaction();
1598         
1599         $event = $this->testCreateEvent(/* $now = */ true);
1600         
1601         $fe = new Calendar_Frontend_Json();
1602         $testUserCredentials = TestServer::getInstance()->getTestCredentials();
1603         $fe->importRemoteEvents(
1604             'http://tine20.com.local/calendars/' . Tinebase_Core::getUser()->contact_id . '/' . $event['container_id']['id'],
1605             Tinebase_Model_Import::INTERVAL_DAILY,
1606             array(
1607                 'container_id'          => 'remote_caldav_calendar',
1608                 'sourceType'            => 'remote_caldav',
1609                 'importFileByScheduler' => false,
1610                 'allowDuplicateEvents'  => true,
1611                 'username'              => $testUserCredentials['username'],
1612                 'password'              => $testUserCredentials['password'],
1613             ));
1614
1615         $importScheduler = Tinebase_Controller_ScheduledImport::getInstance();
1616         $record = $importScheduler->runNextScheduledImport();
1617
1618         $container = Tinebase_Container::getInstance()->getContainerByName('Calendar', 'remote_caldav_calendar', Tinebase_Model_Container::TYPE_PERSONAL, Tinebase_Core::getUser()->getId());
1619         $this->_testCalendars[] = $container;
1620         $this->assertTrue($container instanceof Tinebase_Model_Container, 'Container was not created');
1621
1622         $this->assertNotEquals($record, null, 'The import could not start!');
1623         
1624         $filter = $this->_getEventFilterArray($container->getId());
1625         $result = $this->_uit->searchEvents($filter, array());
1626         $this->assertEquals(1, $result['totalcount']);
1627     }
1628     
1629     /**
1630      * testGetRelations
1631      * 
1632      * @see 0009542: load event relations on demand
1633      */
1634     public function testGetRelations()
1635     {
1636         $contact = Addressbook_Controller_Contact::getInstance()->create(new Addressbook_Model_Contact(array(
1637             'n_family' => 'Contact for relations test'
1638         )));
1639         $eventData = $this->_getEvent()->toArray();
1640         $eventData['relations'] = array(
1641             array(
1642                 'own_model' => 'Calendar_Model_Event',
1643                 'own_backend' => 'Sql',
1644                 'own_id' => 0,
1645                 'related_degree' => Tinebase_Model_Relation::DEGREE_SIBLING,
1646                 'type' => '',
1647                 'related_backend' => 'Sql',
1648                 'related_id' => $contact->getId(),
1649                 'related_model' => 'Addressbook_Model_Contact',
1650                 'remark' => NULL,
1651             ));
1652         $event = $this->_uit->saveEvent($eventData);
1653
1654         $tfj = new Tinebase_Frontend_Json();
1655         $relations = $tfj->getRelations('Calendar_Model_Event', $event['id']);
1656
1657         $this->assertEquals(1, $relations['totalcount']);
1658         $this->assertEquals($contact->n_fn, $relations['results'][0]['related_record']['n_family'], print_r($relations['results'], true));
1659     }
1660 }