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