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