Merge branch 'pu/2013.10-caldav' into 2014.09
[tine20] / tests / tine20 / Calendar / Frontend / iMIPTest.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
7  * @copyright   Copyright (c) 2011-2014 Metaways Infosystems GmbH (http://www.metaways.de)
8  * @author      Philipp Schüle <p.schuele@metaways.de>
9  * 
10  * @todo        add tests testInvitationCancel and testOrganizerSendBy
11  */
12
13 /**
14  * Test class for Calendar_Frontend_iMIP
15  */
16 class Calendar_Frontend_iMIPTest extends TestCase
17 {
18     /**
19      * event ids that should be deleted in tearDown
20      * 
21      * @var unknown_type
22      */
23     protected $_eventIdsToDelete = array();
24     
25     /**
26      * iMIP frontent to be tested
27      * 
28      * @var Calendar_Frontend_iMIP
29      */
30     protected $_iMIPFrontend = NULL;
31     
32     /**
33      * iMIP frontent to be tested
34      * 
35      * @var Calendar_Frontend_iMIPMock
36      */
37     protected $_iMIPFrontendMock = NULL;
38     
39     /**
40     * email test class
41     *
42     * @var Felamimail_Controller_MessageTest
43     */
44     protected $_emailTestClass;
45         
46     /**
47      * Runs the test methods of this class.
48      *
49      * @access public
50      * @static
51      */
52     public static function main()
53     {
54         $suite  = new PHPUnit_Framework_TestSuite('Tine 2.0 Calendar iMIP Tests');
55         PHPUnit_TextUI_TestRunner::run($suite);
56     }
57
58     /**
59      * Sets up the fixture.
60      * This method is called before a test is executed.
61      *
62      * @access protected
63      */
64     protected function setUp()
65     {
66         Calendar_Controller_Event::getInstance()->sendNotifications(true);
67         
68         Calendar_Config::getInstance()->set(Calendar_Config::DISABLE_EXTERNAL_IMIP, false);
69         
70         $this->_iMIPFrontend = new Calendar_Frontend_iMIP();
71         $this->_iMIPFrontendMock = new Calendar_Frontend_iMIPMock();
72         
73         try {
74             $this->_emailTestClass = new Felamimail_Controller_MessageTest();
75             $this->_emailTestClass->setup();
76         } catch (Exception $e) {
77             // do nothing
78         }
79     }
80
81     /**
82      * Tears down the fixture
83      * This method is called after a test is executed.
84      *
85      * @access protected
86      */
87     protected function tearDown()
88     {
89         Calendar_Controller_Event::getInstance()->sendNotifications(false);
90         
91         if (! empty($this->_eventIdsToDelete)) {
92             Calendar_Controller_Event::getInstance()->delete($this->_eventIdsToDelete);
93         }
94         
95         if ($this->_emailTestClass instanceof Felamimail_Controller_MessageTest) {
96             $this->_emailTestClass->tearDown();
97         }
98     }
99     
100     /**
101      * testExternalInvitationRequestAutoProcess
102      */
103     public function testExternalInvitationRequestAutoProcess()
104     {
105         $ics = Calendar_Frontend_WebDAV_EventTest::getVCalendar(dirname(__FILE__) . '/files/invitation_request_external.ics' );
106         $iMIP = new Calendar_Model_iMIP(array(
107             'id'             => Tinebase_Record_Abstract::generateUID(),
108             'ics'            => $ics,
109             'method'         => 'REQUEST',
110             'originator'     => 'l.kneschke@caldav.org',
111         ));
112         
113         $this->_iMIPFrontend->autoProcess($iMIP);
114         $prepared = $this->_iMIPFrontend->prepareComponent($iMIP);
115         
116         $this->assertEmpty($prepared->existing_event, 'there should be no existing event');
117         $this->assertEmpty($prepared->preconditions, 'no preconditions should be raised');
118         $this->assertEquals(5, count($prepared->event->attendee));
119         $this->assertEquals('test mit extern', $prepared->event->summary);
120         
121         return $iMIP;
122     }
123
124     /**
125     * testSupportedPrecondition
126     */
127     public function testUnsupportedPrecondition()
128     {
129         $iMIP = $this->_getiMIP('PUBLISH');
130             
131         $prepared = $this->_iMIPFrontend->prepareComponent($iMIP);
132     
133         $this->assertEquals(1, count($prepared->preconditions));
134         $this->assertEquals('processing published events is not supported yet', $prepared->preconditions[Calendar_Model_iMIP::PRECONDITION_SUPPORTED][0]['message']);
135         $this->assertFalse($prepared->preconditions[Calendar_Model_iMIP::PRECONDITION_SUPPORTED][0]['check']);
136     }
137     
138     /**
139      * get iMIP record from internal event
140      * 
141      * @param string $_method
142      * @param boolean $_addEventToiMIP
143      * @return Calendar_Model_iMIP
144      */
145     protected function _getiMIP($_method, $_addEventToiMIP = FALSE, $_testEmptyMethod = FALSE)
146     {
147         $testConfig = Zend_Registry::get('testConfig');
148         $email = ($testConfig->email) ? $testConfig->email : Tinebase_Core::getUser()->accountEmailAddress;
149         
150         $event = $this->_getEvent();
151         $event = Calendar_Controller_Event::getInstance()->create($event);
152         $this->_eventIdsToDelete[] = $event->getId();
153         
154         if ($_method == 'REPLY') {
155             $personas = Zend_Registry::get('personas');
156             $sclever = $personas['sclever'];
157             
158             $scleverAttendee = $event->attendee
159                 ->filter('status', Calendar_Model_Attender::STATUS_NEEDSACTION)
160                 ->getFirstRecord();
161             
162             $scleverAttendee->status = Calendar_Model_Attender::STATUS_ACCEPTED;
163             Calendar_Controller_Event::getInstance()->attenderStatusUpdate($event, $scleverAttendee, $scleverAttendee->status_authkey);
164             $event = Calendar_Controller_Event::getInstance()->get($event->getId());
165             $email = $sclever->accountEmailAddress;
166         }
167         
168         // get iMIP invitation for event
169         $converter = Calendar_Convert_Event_VCalendar_Factory::factory(Calendar_Convert_Event_VCalendar_Factory::CLIENT_GENERIC);
170         $vevent = $converter->fromTine20Model($event);
171         $vevent->METHOD = $_method;
172         $ics = $vevent->serialize();
173         
174         $iMIP = new Calendar_Model_iMIP(array(
175             'id'             => Tinebase_Record_Abstract::generateUID(),
176             'ics'            => $ics,
177             'method'         => ($_testEmptyMethod) ? NULL : $_method,
178             'originator'     => $email,
179         ));
180         
181         if ($_addEventToiMIP) {
182             $iMIP->event = $event;
183         }
184         
185         return $iMIP;
186     }
187     
188     /**
189      * testInternalInvitationRequestAutoProcess
190      */
191     public function testInternalInvitationRequestAutoProcess()
192     {
193         $iMIP = $this->_getiMIP('REQUEST');
194         
195         $this->_iMIPFrontend->autoProcess($iMIP);
196         $prepared = $this->_iMIPFrontend->prepareComponent($iMIP);
197         
198         $this->assertEquals(2, count($prepared->event->attendee), 'expected 2 attendee');
199         $this->assertEquals('Sleep very long', $prepared->event->summary);
200         $this->assertTrue(empty($prepared->preconditions));
201     }
202
203     /**
204     * testInternalInvitationRequestAutoProcessOwnStatusAlreadySet
205     */
206     public function testInternalInvitationRequestPreconditionOwnStatusAlreadySet()
207     {
208         $iMIP = $this->_getiMIP('REQUEST', TRUE);
209         
210         // set own status
211         $ownAttender = Calendar_Model_Attender::getOwnAttender($iMIP->getEvent()->attendee);
212         $ownAttender->status = Calendar_Model_Attender::STATUS_TENTATIVE;
213         Calendar_Controller_Event::getInstance()->attenderStatusUpdate($iMIP->getEvent(), $ownAttender, $ownAttender->status_authkey);
214         
215         $prepared = $this->_iMIPFrontend->prepareComponent($iMIP);
216         $this->assertTrue(empty($prepared->preconditions), "it's ok to reanswer without reschedule!");
217         
218         // reschedule
219         $event = Calendar_Controller_Event::getInstance()->get($prepared->existing_event->getId());
220         $event->dtstart->addHour(2);
221         $event->dtend->addHour(2);
222         Calendar_Controller_Event::getInstance()->update($event, false);
223         
224         $iMIP->getExistingEvent(true);
225         $iMIP->preconditionsChecked = false;
226         $prepared = $this->_iMIPFrontend->prepareComponent($iMIP);
227         
228         $this->assertFalse(empty($prepared->preconditions), 'do not accept this iMIP after reshedule');
229         $this->assertTrue((isset($prepared->preconditions[Calendar_Model_iMIP::PRECONDITION_RECENT]) || array_key_exists(Calendar_Model_iMIP::PRECONDITION_RECENT, $prepared->preconditions)));
230     }
231     
232     /**
233      * returns a simple event
234      *
235      * @return Calendar_Model_Event
236      * @param bool $_now
237      * @param bool $mute
238      * @todo replace with TestCase::_getEvent
239      */
240     protected function _getEvent($now = FALSE, $mute = NULL)
241     {
242         return new Calendar_Model_Event(array(
243             'summary'     => 'Sleep very long',
244             'dtstart'     => '2012-03-25 01:00:00',
245             'dtend'       => '2012-03-25 11:15:00',
246             'description' => 'Early to bed and early to rise, makes a men healthy, wealthy and wise ... not.',
247             'attendee'    => $this->_getAttendee(),
248             'organizer'   => Tinebase_Core::getUser()->contact_id,
249             'uid'         => Calendar_Model_Event::generateUID(),
250         ));
251     }
252     
253     /**
254      * get test attendee
255      *
256      * @return Tinebase_Record_RecordSet
257      */
258     protected function _getAttendee()
259     {
260         $personas = Zend_Registry::get('personas');
261         $sclever = $personas['sclever'];
262         
263         return new Tinebase_Record_RecordSet('Calendar_Model_Attender', array(
264             array(
265                 'user_id'        => Tinebase_Core::getUser()->contact_id,
266                 'user_type'      => Calendar_Model_Attender::USERTYPE_USER,
267                 'role'           => Calendar_Model_Attender::ROLE_REQUIRED,
268                 'status'         => Calendar_Model_Attender::STATUS_ACCEPTED,
269                 'status_authkey' => Tinebase_Record_Abstract::generateUID(),
270             ),
271             array(
272                 'user_id'        => $sclever->contact_id,
273                 'user_type'      => Calendar_Model_Attender::USERTYPE_USER,
274                 'role'           => Calendar_Model_Attender::ROLE_REQUIRED,
275                 'status_authkey' => Tinebase_Record_Abstract::generateUID(),
276             ),
277         ));
278     }
279     
280     /**
281      * testExternalInvitationRequestProcess
282      */
283     public function testExternalInvitationRequestProcess()
284     {
285         $ics = Calendar_Frontend_WebDAV_EventTest::getVCalendar(dirname(__FILE__) . '/files/invitation_request_external.ics' );
286         $ics = preg_replace('#DTSTART;VALUE=DATE-TIME;TZID=Europe/Berlin:20111121T130000#', 'DTSTART;VALUE=DATE-TIME;TZID=Europe/Berlin:' . Tinebase_DateTime::now()->addHour(1)->format('Ymd\THis'), $ics);
287         $ics = preg_replace('#DTEND;VALUE=DATE-TIME;TZID=Europe/Berlin:20111121T140000#', 'DTEND;VALUE=DATE-TIME;TZID=Europe/Berlin:' . Tinebase_DateTime::now()->addHour(2)->format('Ymd\THis'), $ics);
288         
289         $iMIP = new Calendar_Model_iMIP(array(
290                 'id'             => Tinebase_Record_Abstract::generateUID(),
291                 'ics'            => $ics,
292                 'method'         => 'REQUEST',
293                 'originator'     => 'l.kneschke@caldav.org',
294         ));
295         
296         Calendar_Controller_EventNotificationsTests::flushMailer();
297         $result = $this->_iMIPFrontendMock->process($iMIP, Calendar_Model_Attender::STATUS_ACCEPTED);
298         
299         $this->_iMIPFrontend->prepareComponent($iMIP);
300         $this->_eventIdsToDelete[] = $iMIP->event->getId();
301         
302         // assert external organizer
303         $this->assertEquals('l.kneschke@caldav.org', $iMIP->event->organizer->email, 'wrong organizer');
304         $this->assertTrue(empty($iMIP->event->organizer->account_id), 'organizer must not have an account');
305         
306         // assert attendee
307         $ownAttendee = Calendar_Model_Attender::getOwnAttender($iMIP->event->attendee);
308         $this->assertTrue(!! $ownAttendee, 'own attendee missing');
309         $this->assertEquals(5, count($iMIP->event->attendee), 'all attendee must be keeped');
310         $this->assertEquals(Calendar_Model_Attender::STATUS_ACCEPTED, $ownAttendee->status, 'must be ACCEPTED');
311         
312         // assert REPLY message to organizer only
313         $smtpConfig = Tinebase_Config::getInstance()->get(Tinebase_Config::SMTP);
314         if (isset($smtpConfig->from) && ! empty($smtpConfig->from)) {
315             $messages = Calendar_Controller_EventNotificationsTests::getMessages();
316             $this->assertEquals(1, count($messages), 'exactly one mail should be send');
317             $this->assertTrue(in_array('l.kneschke@caldav.org', $messages[0]->getRecipients()), 'organizer is not a receipient');
318             $this->assertContains('accepted', $messages[0]->getSubject(), 'wrong subject');
319             $this->assertContains('METHOD:REPLY', var_export($messages[0], TRUE), 'method missing');
320             $this->assertContains('SEQUENCE:0', var_export($messages[0], TRUE), 'external sequence has not been keepted');
321         }
322     }
323     
324     /**
325      * adds new imip message to Felamimail cache
326      * 
327      * @return Felamimail_Model_Message
328      */
329     protected function _addImipMessageToEmailCache()
330     {
331         $this->_checkIMAPConfig();
332         
333         // handle message with fmail (add to cache)
334         $message = $this->_emailTestClass->messageTestHelper('calendar_request.eml', NULL, NULL, array('unittest@tine20.org', $this->_getEmailAddress()));
335         return Felamimail_Controller_Message::getInstance()->getCompleteMessage($message);
336     }
337     
338     /**
339      * testDisabledExternalImip
340      */
341     public function testDisabledExternalImip()
342     {
343         Calendar_Config::getInstance()->set(Calendar_Config::DISABLE_EXTERNAL_IMIP, true);
344         $complete = $this->_addImipMessageToEmailCache();
345         $fmailJson = new Felamimail_Frontend_Json();
346         $jsonMessage = $fmailJson->getMessage($complete->getId());
347         Calendar_Config::getInstance()->set(Calendar_Config::DISABLE_EXTERNAL_IMIP, false);
348         $this->assertEmpty($jsonMessage['preparedParts']);
349     }
350     
351     /**
352      * check IMAP config and marks test as skipped if no IMAP backend is configured
353      */
354     protected function _checkIMAPConfig()
355     {
356         $imapConfig = Tinebase_Config::getInstance()->get(Tinebase_Config::IMAP);
357         if (! $imapConfig || ! isset($imapConfig->useSystemAccount)
358             || $imapConfig->useSystemAccount != TRUE
359             || ! $this->_emailTestClass instanceof Felamimail_Controller_MessageTest
360         ) {
361             $this->markTestSkipped('IMAP backend not configured');
362         }
363     }
364
365     /**
366      * testExternalPublishProcess
367      * - uses felamimail to cache external publish message
368      * 
369      * NOTE: meetup sends REQUEST w.o. attendee. We might think of autoconvert this to PUBLISH
370      */
371     public function testExternalPublishProcess()
372     {
373         $this->_checkIMAPConfig();
374         
375         // handle message with fmail (add to cache)
376         $message = $this->_emailTestClass->messageTestHelper('meetup.eml');
377         $complete = Felamimail_Controller_Message::getInstance()->getCompleteMessage($message);
378         
379         $iMIP = $complete->preparedParts->getFirstRecord()->preparedData;
380         
381         $this->setExpectedException('Calendar_Exception_iMIP', 'iMIP preconditions failed: ATTENDEE');
382         $result = $this->_iMIPFrontend->process($iMIP);
383     }
384
385     /**
386      * testInternalInvitationRequestProcess
387      */
388     public function testInternalInvitationRequestProcess()
389     {
390         $iMIP = $this->_getiMIP('REQUEST');
391         $result = $this->_iMIPFrontendMock->process($iMIP, Calendar_Model_Attender::STATUS_TENTATIVE);
392         
393         $event = Calendar_Controller_MSEventFacade::getInstance()->lookupExistingEvent($iMIP->getEvent());
394         
395         $attender = Calendar_Model_Attender::getOwnAttender($event->attendee);
396         $this->assertEquals(Calendar_Model_Attender::STATUS_TENTATIVE, $attender->status);
397     }
398
399     /**
400      * testEmptyMethod
401      */
402     public function testEmptyMethod()
403     {
404         $iMIP = $this->_getiMIP('REQUEST', FALSE, TRUE);
405         
406         $this->assertEquals('REQUEST', $iMIP->method);
407     }
408     
409     /**
410      * testInternalInvitationReplyPreconditions
411      * 
412      * an internal reply does not need to be processed of course
413      */
414     public function testInternalInvitationReplyPreconditions()
415     {
416         $iMIP = $this->_getiMIP('REPLY');
417         $prepared = $this->_iMIPFrontend->prepareComponent($iMIP);
418         
419         $this->assertFalse(empty($prepared->preconditions), 'empty preconditions');
420         $this->assertTrue((isset($prepared->preconditions[Calendar_Model_iMIP::PRECONDITION_TOPROCESS]) || array_key_exists(Calendar_Model_iMIP::PRECONDITION_TOPROCESS, $prepared->preconditions)), 'missing PRECONDITION_TOPROCESS');
421     }
422     
423     /**
424      * testInternalInvitationReplyAutoProcess
425      * 
426      * an internal reply does not need to be processed of course
427      */
428     public function testInternalInvitationReplyAutoProcess()
429     {
430         // flush mailer
431         if (isset(Tinebase_Core::getConfig()->actionqueue)) {
432             Tinebase_ActionQueue::getInstance()->processQueue(10000);
433         }
434         Tinebase_Smtp::getDefaultTransport()->flush();
435         
436         $iMIP = $this->_getiMIP('REPLY', TRUE);
437         $event = $iMIP->getEvent();
438         
439         try {
440             $this->_iMIPFrontend->autoProcess($iMIP);
441         } catch (Exception $e) {
442             $this->assertContains('TOPROCESS', $e->getMessage());
443             return;
444         }
445         
446         $this->fail("autoProcess did not throw TOPROCESS Exception $e");
447     }
448     
449     /**
450      * testInvitationExternalReply
451      */
452     public function testInvitationExternalReply()
453     {
454         $testConfig = Zend_Registry::get('testConfig');
455         $email = ($testConfig->email) ? $testConfig->email : Tinebase_Core::getUser()->accountEmailAddress;
456         
457         $ics = file_get_contents(dirname(__FILE__) . '/files/invitation_reply_external_accepted.ics' );
458         $ics = preg_replace('/unittest@tine20\.org/', $email, $ics);
459         
460         $iMIP = new Calendar_Model_iMIP(array(
461             'id'             => Tinebase_Record_Abstract::generateUID(),
462             'ics'            => $ics,
463             'method'         => 'REPLY',
464             'originator'     => 'mail@corneliusweiss.de',
465         ));
466         
467         $this->assertEquals(1, $iMIP->getEvent()->seq);
468         $this->assertTrue(! empty($iMIP->getEvent()->last_modified_time));
469         
470         // force creation of external attendee
471         $externalAttendee = new Calendar_Model_Attender(array(
472             'user_type'     => Calendar_Model_Attender::USERTYPE_USER,
473             'user_id'       => $iMIP->getEvent()->attendee->getFirstRecord()->user_id,
474             'status'        => Calendar_Model_Attender::STATUS_NEEDSACTION
475         ));
476         
477         // create matching event
478         $event = new Calendar_Model_Event(array(
479             'summary'     => 'TEST7',
480             'dtstart'     => '2011-11-30 14:00:00',
481             'dtend'       => '2011-11-30 15:00:00',
482             'description' => 'Early to bed and early to rise, makes a men healthy, wealthy and wise ...',
483             'attendee'    => $this->_getAttendee(),
484             'organizer'   => Tinebase_Core::getUser()->contact_id,
485             'uid'         => 'a8d10369e051094ae9322bd65e8afecac010bfc8',
486         ));
487         $event->attendee->addRecord($externalAttendee);
488         $event = Calendar_Controller_Event::getInstance()->create($event);
489         $this->_eventIdsToDelete[] = $event->getId();
490         
491         // TEST NORMAL REPLY
492         try {
493             $this->_iMIPFrontend->autoProcess($iMIP);
494         } catch (Exception $e) {
495             $this->fail('TEST NORMAL REPLY autoProcess throws Exception: ' . $e);
496         }
497         unset($iMIP->existing_event);
498         
499         $updatedEvent = Calendar_Controller_Event::getInstance()->get($event->getId());
500         $updatedExternalAttendee = Calendar_Model_Attender::getAttendee($updatedEvent->attendee, $externalAttendee);
501         
502         $this->assertEquals(3, count($updatedEvent->attendee));
503         $this->assertEquals(Calendar_Model_Attender::STATUS_ACCEPTED, $updatedExternalAttendee->status, 'status not updated');
504         
505         // TEST ACCEPTABLE NON RECENT REPLY
506         $updatedExternalAttendee->status = Calendar_Model_Attender::STATUS_NEEDSACTION;
507         Calendar_Controller_Event::getInstance()->attenderStatusUpdate($updatedEvent, $updatedExternalAttendee, $updatedExternalAttendee->status_authkey);
508         try {
509             $iMIP->preconditionsChecked = false;
510             $this->_iMIPFrontend->autoProcess($iMIP);
511         } catch (Exception $e) {
512             $this->fail('TEST ACCEPTABLE NON RECENT REPLY autoProcess throws Exception: ' . $e);
513         }
514         unset($iMIP->existing_event);
515         
516         $updatedEvent = Calendar_Controller_Event::getInstance()->get($event->getId());
517         $updatedExternalAttendee = Calendar_Model_Attender::getAttendee($updatedEvent->attendee, $externalAttendee);
518         
519         $this->assertEquals(3, count($updatedEvent->attendee));
520         $this->assertEquals(Calendar_Model_Attender::STATUS_ACCEPTED, $updatedExternalAttendee->status, 'status not updated');
521         
522         // check if attendee are resolved
523         $existingEvent = $iMIP->getExistingEvent();
524         $this->assertTrue($iMIP->existing_event->attendee instanceof Tinebase_Record_RecordSet);
525         $this->assertEquals(3, count($iMIP->existing_event->attendee));
526         
527         // TEST NON ACCEPTABLE NON RECENT REPLY
528         $iMIP->preconditionsChecked = false;
529         try {
530             $this->_iMIPFrontend->autoProcess($iMIP);
531             $this->fail('autoProcess should throw Calendar_Exception_iMIP');
532         } catch (Calendar_Exception_iMIP $cei) {
533             $this->assertContains('iMIP preconditions failed: RECENT', $cei->getMessage());
534         }
535     }
536
537     /**
538      * testInvitationCancel
539      * 
540       * @todo implement
541       */
542      public function testInvitationCancel()
543      {
544         $this->markTestIncomplete('implement me');
545      }
546     
547     /**
548       * testInvitationCancel
549       * 
550       * @todo implement
551       */
552      public function testOrganizerSendBy()
553      {
554          $this->markTestIncomplete('implement me');
555      }
556 }