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