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