fix for external invitations
[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     */
237     protected function _getEvent()
238     {
239         return new Calendar_Model_Event(array(
240             'summary'     => 'Sleep very long',
241             'dtstart'     => '2012-03-25 01:00:00',
242             'dtend'       => '2012-03-25 11:15:00',
243             'description' => 'Early to bed and early to rise, makes a men healthy, wealthy and wise ... not.',
244             'attendee'    => $this->_getAttendee(),
245             'organizer'   => Tinebase_Core::getUser()->contact_id,
246             'uid'         => Calendar_Model_Event::generateUID(),
247         ));
248     }
249     
250     /**
251      * get test attendee
252      *
253      * @return Tinebase_Record_RecordSet
254      */
255     protected function _getAttendee()
256     {
257         $personas = Zend_Registry::get('personas');
258         $sclever = $personas['sclever'];
259         
260         return new Tinebase_Record_RecordSet('Calendar_Model_Attender', array(
261             array(
262                 'user_id'        => Tinebase_Core::getUser()->contact_id,
263                 'user_type'      => Calendar_Model_Attender::USERTYPE_USER,
264                 'role'           => Calendar_Model_Attender::ROLE_REQUIRED,
265                 'status'         => Calendar_Model_Attender::STATUS_ACCEPTED,
266                 'status_authkey' => Tinebase_Record_Abstract::generateUID(),
267             ),
268             array(
269                 'user_id'        => $sclever->contact_id,
270                 'user_type'      => Calendar_Model_Attender::USERTYPE_USER,
271                 'role'           => Calendar_Model_Attender::ROLE_REQUIRED,
272                 'status_authkey' => Tinebase_Record_Abstract::generateUID(),
273             ),
274         ));
275     }
276     
277     /**
278      * testExternalInvitationRequestProcess
279      */
280     public function testExternalInvitationRequestProcess()
281     {
282         $ics = Calendar_Frontend_WebDAV_EventTest::getVCalendar(dirname(__FILE__) . '/files/invitation_request_external.ics' );
283         $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);
284         $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);
285         
286         $iMIP = new Calendar_Model_iMIP(array(
287                 'id'             => Tinebase_Record_Abstract::generateUID(),
288                 'ics'            => $ics,
289                 'method'         => 'REQUEST',
290                 'originator'     => 'l.kneschke@caldav.org',
291         ));
292         
293         Calendar_Controller_EventNotificationsTests::flushMailer();
294         $result = $this->_iMIPFrontendMock->process($iMIP, Calendar_Model_Attender::STATUS_ACCEPTED);
295         
296         $this->_iMIPFrontend->prepareComponent($iMIP);
297         $this->_eventIdsToDelete[] = $iMIP->event->getId();
298         
299         // assert external organizer
300         $this->assertEquals('l.kneschke@caldav.org', $iMIP->event->organizer->email, 'wrong organizer');
301         $this->assertTrue(empty($iMIP->event->organizer->account_id), 'organizer must not have an account');
302         
303         // assert attendee
304         $ownAttendee = Calendar_Model_Attender::getOwnAttender($iMIP->event->attendee);
305         $this->assertTrue(!! $ownAttendee, 'own attendee missing');
306         $this->assertEquals(5, count($iMIP->event->attendee), 'all attendee must be keeped');
307         $this->assertEquals(Calendar_Model_Attender::STATUS_ACCEPTED, $ownAttendee->status, 'must be ACCEPTED');
308         
309         // assert REPLY message to organizer only
310         $messages = Calendar_Controller_EventNotificationsTests::getMessages();
311         $this->assertEquals(1, count($messages), 'exactly one mail should be send');
312         $this->assertTrue(in_array('l.kneschke@caldav.org', $messages[0]->getRecipients()), 'organizer is not a receipient');
313         $this->assertContains('accepted', $messages[0]->getSubject(), 'wrong subject');
314         $this->assertContains('METHOD:REPLY', var_export($messages[0], TRUE), 'method missing');
315         $this->assertContains('SEQUENCE:0', var_export($messages[0], TRUE), 'external sequence has not been keepted');
316     }
317     
318     /**
319      * adds new imip message to Felamimail cache
320      * 
321      * @return Felamimail_Model_Message
322      */
323     protected function _addImipMessageToEmailCache()
324     {
325         $this->_checkIMAPConfig();
326         
327         // handle message with fmail (add to cache)
328         $message = $this->_emailTestClass->messageTestHelper('calendar_request.eml', NULL, NULL, array('unittest@tine20.org', $this->_getEmailAddress()));
329         return Felamimail_Controller_Message::getInstance()->getCompleteMessage($message);
330     }
331     
332     /**
333      * testDisabledExternalImip
334      */
335     public function testDisabledExternalImip()
336     {
337         Calendar_Config::getInstance()->set(Calendar_Config::DISABLE_EXTERNAL_IMIP, true);
338         $complete = $this->_addImipMessageToEmailCache();
339         $fmailJson = new Felamimail_Frontend_Json();
340         $jsonMessage = $fmailJson->getMessage($complete->getId());
341         Calendar_Config::getInstance()->set(Calendar_Config::DISABLE_EXTERNAL_IMIP, false);
342         $this->assertEmpty($jsonMessage['preparedParts']);
343     }
344     
345     /**
346      * check IMAP config and marks test as skipped if no IMAP backend is configured
347      */
348     protected function _checkIMAPConfig()
349     {
350         $imapConfig = Tinebase_Config::getInstance()->get(Tinebase_Config::IMAP);
351         if (! $imapConfig || ! isset($imapConfig->useSystemAccount)
352             || $imapConfig->useSystemAccount != TRUE
353             || ! $this->_emailTestClass instanceof Felamimail_Controller_MessageTest
354         ) {
355             $this->markTestSkipped('IMAP backend not configured');
356         }
357     }
358
359     /**
360      * testExternalPublishProcess
361      * - uses felamimail to cache external publish message
362      * 
363      * NOTE: meetup sends REQUEST w.o. attendee. We might think of autoconvert this to PUBLISH
364      */
365     public function testExternalPublishProcess()
366     {
367         $this->_checkIMAPConfig();
368         
369         // handle message with fmail (add to cache)
370         $message = $this->_emailTestClass->messageTestHelper('meetup.eml');
371         $complete = Felamimail_Controller_Message::getInstance()->getCompleteMessage($message);
372         
373         $iMIP = $complete->preparedParts->getFirstRecord()->preparedData;
374         
375         $this->setExpectedException('Calendar_Exception_iMIP', 'iMIP preconditions failed: ATTENDEE');
376         $result = $this->_iMIPFrontend->process($iMIP);
377     }
378
379     /**
380      * testInternalInvitationRequestProcess
381      */
382     public function testInternalInvitationRequestProcess()
383     {
384         $iMIP = $this->_getiMIP('REQUEST');
385         $result = $this->_iMIPFrontendMock->process($iMIP, Calendar_Model_Attender::STATUS_TENTATIVE);
386         
387         $event = Calendar_Controller_MSEventFacade::getInstance()->lookupExistingEvent($iMIP->getEvent());
388         
389         $attender = Calendar_Model_Attender::getOwnAttender($event->attendee);
390         $this->assertEquals(Calendar_Model_Attender::STATUS_TENTATIVE, $attender->status);
391     }
392
393     /**
394      * testEmptyMethod
395      */
396     public function testEmptyMethod()
397     {
398         $iMIP = $this->_getiMIP('REQUEST', FALSE, TRUE);
399         
400         $this->assertEquals('REQUEST', $iMIP->method);
401     }
402     
403     /**
404      * testInternalInvitationReplyPreconditions
405      * 
406      * an internal reply does not need to be processed of course
407      */
408     public function testInternalInvitationReplyPreconditions()
409     {
410         $iMIP = $this->_getiMIP('REPLY');
411         $prepared = $this->_iMIPFrontend->prepareComponent($iMIP);
412         
413         $this->assertFalse(empty($prepared->preconditions), 'empty preconditions');
414         $this->assertTrue((isset($prepared->preconditions[Calendar_Model_iMIP::PRECONDITION_TOPROCESS]) || array_key_exists(Calendar_Model_iMIP::PRECONDITION_TOPROCESS, $prepared->preconditions)), 'missing PRECONDITION_TOPROCESS');
415     }
416     
417     /**
418      * testInternalInvitationReplyAutoProcess
419      * 
420      * an internal reply does not need to be processed of course
421      */
422     public function testInternalInvitationReplyAutoProcess()
423     {
424         // flush mailer
425         if (isset(Tinebase_Core::getConfig()->actionqueue)) {
426             Tinebase_ActionQueue::getInstance()->processQueue(10000);
427         }
428         Tinebase_Smtp::getDefaultTransport()->flush();
429         
430         $iMIP = $this->_getiMIP('REPLY', TRUE);
431         $event = $iMIP->getEvent();
432         
433         try {
434             $this->_iMIPFrontend->autoProcess($iMIP);
435         } catch (Exception $e) {
436             $this->assertContains('TOPROCESS', $e->getMessage());
437             return;
438         }
439         
440         $this->fail("autoProcess did not throw TOPROCESS Exception $e");
441     }
442     
443     /**
444      * testInvitationExternalReply
445      */
446     public function testInvitationExternalReply()
447     {
448         $testConfig = Zend_Registry::get('testConfig');
449         $email = ($testConfig->email) ? $testConfig->email : Tinebase_Core::getUser()->accountEmailAddress;
450         
451         $ics = file_get_contents(dirname(__FILE__) . '/files/invitation_reply_external_accepted.ics' );
452         $ics = preg_replace('/unittest@tine20\.org/', $email, $ics);
453         
454         $iMIP = new Calendar_Model_iMIP(array(
455             'id'             => Tinebase_Record_Abstract::generateUID(),
456             'ics'            => $ics,
457             'method'         => 'REPLY',
458             'originator'     => 'mail@corneliusweiss.de',
459         ));
460         
461         $this->assertEquals(1, $iMIP->getEvent()->seq);
462         $this->assertTrue(! empty($iMIP->getEvent()->last_modified_time));
463         
464         // force creation of external attendee
465         $externalAttendee = new Calendar_Model_Attender(array(
466             'user_type'     => Calendar_Model_Attender::USERTYPE_USER,
467             'user_id'       => $iMIP->getEvent()->attendee->getFirstRecord()->user_id,
468             'status'        => Calendar_Model_Attender::STATUS_NEEDSACTION
469         ));
470         
471         // create matching event
472         $event = new Calendar_Model_Event(array(
473             'summary'     => 'TEST7',
474             'dtstart'     => '2011-11-30 14:00:00',
475             'dtend'       => '2011-11-30 15:00:00',
476             'description' => 'Early to bed and early to rise, makes a men healthy, wealthy and wise ...',
477             'attendee'    => $this->_getAttendee(),
478             'organizer'   => Tinebase_Core::getUser()->contact_id,
479             'uid'         => 'a8d10369e051094ae9322bd65e8afecac010bfc8',
480         ));
481         $event->attendee->addRecord($externalAttendee);
482         $event = Calendar_Controller_Event::getInstance()->create($event);
483         $this->_eventIdsToDelete[] = $event->getId();
484         
485         // TEST NORMAL REPLY
486         try {
487             $this->_iMIPFrontend->autoProcess($iMIP);
488         } catch (Exception $e) {
489             $this->fail('TEST NORMAL REPLY autoProcess throws Exception: ' . $e);
490         }
491         unset($iMIP->existing_event);
492         
493         $updatedEvent = Calendar_Controller_Event::getInstance()->get($event->getId());
494         $updatedExternalAttendee = Calendar_Model_Attender::getAttendee($updatedEvent->attendee, $externalAttendee);
495         
496         $this->assertEquals(3, count($updatedEvent->attendee));
497         $this->assertEquals(Calendar_Model_Attender::STATUS_ACCEPTED, $updatedExternalAttendee->status, 'status not updated');
498     
499         // TEST ACCEPTABLE NON RECENT REPLY
500         $updatedExternalAttendee->status = Calendar_Model_Attender::STATUS_NEEDSACTION;
501         Calendar_Controller_Event::getInstance()->attenderStatusUpdate($updatedEvent, $updatedExternalAttendee, $updatedExternalAttendee->status_authkey);
502         try {
503             $iMIP->preconditionsChecked = false;
504             $this->_iMIPFrontend->autoProcess($iMIP);
505         } catch (Exception $e) {
506             $this->fail('TEST ACCEPTABLE NON RECENT REPLY autoProcess throws Exception: ' . $e);
507         }
508         unset($iMIP->existing_event);
509         
510         $updatedEvent = Calendar_Controller_Event::getInstance()->get($event->getId());
511         $updatedExternalAttendee = Calendar_Model_Attender::getAttendee($updatedEvent->attendee, $externalAttendee);
512         
513         $this->assertEquals(3, count($updatedEvent->attendee));
514         $this->assertEquals(Calendar_Model_Attender::STATUS_ACCEPTED, $updatedExternalAttendee->status, 'status not updated');
515     
516         // TEST NON ACCEPTABLE NON RECENT REPLY
517         $this->setExpectedException('Calendar_Exception_iMIP', 'iMIP preconditions failed: RECENT');
518         $iMIP->preconditionsChecked = false;
519         $this->_iMIPFrontend->autoProcess($iMIP);
520     }
521
522     /**
523      * testInvitationCancel
524      * 
525       * @todo implement
526       */
527      public function testInvitationCancel()
528      {
529         $this->markTestIncomplete('implement me');
530      }
531     
532     /**
533       * testInvitationCancel
534       * 
535       * @todo implement
536       */
537      public function testOrganizerSendBy()
538      {
539          $this->markTestIncomplete('implement me');
540      }
541 }