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