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