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