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