unit tests for basic CalDAV and special delete handling for CalDAV Outlook Synschronizer

This commit is contained in:
Ralf Becker 2020-03-04 22:44:56 +01:00
parent b448f9a021
commit aef614b2f0
3 changed files with 851 additions and 0 deletions

362
api/tests/CalDAVTest.php Normal file
View File

@ -0,0 +1,362 @@
<?php
/**
* CalDAV tests base class
*
* @link http://www.egroupware.org
* @author Ralf Becker
* @package api
* @subpackage caldav
* @copyright (c) 2020 by Ralf Becker <rb@egroupware.org>
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
*/
namespace EGroupware\Api;
// so tests can run standalone
require_once __DIR__.'/../src/loader/common.php'; // autoloader
use PHPUnit\Framework\TestCase;
use GuzzleHttp\Client, GuzzleHttp\RequestOptions;
use Horde_Icalendar, Horde_Icalendar_Exception;
use Psr\Http\Message\ResponseInterface;
/**
* Abstract CalDAVTest using GuzzleHttp\Client against EGroupware CalDAV/CardDAV server
*
* @see http://docs.guzzlephp.org/en/v6/quickstart.html
*
* @package EGroupware\Api
*/
abstract class CalDAVTest extends TestCase
{
/**
* Base URL of CalDAV server
*/
const CALDAV_BASE = 'http://localhost/egroupware/groupdav.php';
/**
* Get full URL for a CalDAV path
*
* @param string $path CalDAV path
* @return string URL
*/
protected function url($path='/')
{
$base = self::CALDAV_BASE;
if (!empty($GLOBALS['EGW_DOMAIN']) && $GLOBALS['EGW_DOMAIN'] !== 'default')
{
$base = str_replace('localhost', $GLOBALS['EGW_DOMAIN'], $base);
}
return $base.$path;
}
/**
* Default options for GuzzleHttp\Client
*
* @var array
* @see http://docs.guzzlephp.org/en/v6/request-options.html
*/
protected $client_options = [
RequestOptions::HTTP_ERRORS => false, // return all HTTP status, not throwing exceptions
RequestOptions::HEADERS => [
'Cookie' => 'XDEBUG_SESSION=PHPSTORM',
//'User-Agent' => 'CalDAVSynchronizer',
],
];
/**
* Get HTTP client for tests
*
* It will use by default the always existing user "demo" with password "guest" (use [] to NOT authenticate).
* Additional users need to be created with $this->createUser("name").
*
* @param string|array $user_or_options ='demo' string with account_lid of user for authentication or array of options
* @return Client
* @see http://docs.guzzlephp.org/en/v6/request-options.html
* @see http://docs.guzzlephp.org/en/v6/quickstart.html
*/
protected function getClient($user_or_options='demo')
{
if (!is_array($user_or_options))
{
$user_or_options = $this->auth($user_or_options);
}
return new Client(array_merge($this->client_options, $user_or_options));
}
/**
* Create a number of users with optional ACL rights too
*
* Example with boss granting secretary full rights on his calendar, plus one other user:
*
* $users = [
* 'boss' => [],
* 'secretary' => ['rights' => ['boss' => Acl::READ|Acl::ADD|Acl::EDIT|Acl::DELETE]],
* 'other' => [],
* ];
* self::createUsersACL($users);
*
* @param array& $users $account_lid => array with values for keys with (defaults) "firstname" ($_acount_lid), "lastname" ("User"),
* "email" ("$_account_lid@example.org"), "password" (random string), "primary_group" ("NoGroups" to not set rights)
* "rights" array with $grantee => $rights pairs (need to be created before!)
* @param string $app app to create the rights for, default "calendar"
* @throws \Exception
*/
protected function createUsersACL(array &$users, $app='calendar')
{
foreach($users as $user => $data)
{
$data['id'] = self::createUser($user, $data);
foreach($data['rights'] ?? [] as $grantee => $rights)
{
self::addAcl('calendar', $data['id'], $grantee, $rights);
}
}
}
/**
* Array to track created users for tearDown and authentication
*
* @var array $account_lid => array with other data pairs
*/
private static $created_users = [];
/**
* Create a user
*
* Created users are automatic deleted in tearDown() and can be passed to auth() or getClient() methods.
* Users have random passwords to force new/different sessions!
*
* @param string $_account_lid
* @param array& $data =[] values for keys with (defaults) "firstname" ($_acount_lid), "lastname" ("User"),
* "email" ("$_account_lid@example.org"), "password" (random string), "primary_group" ("NoGroups" to not set rights)
* on return: with defaults set
* @return int account_id of created user
* @throws \Exception
*/
protected static function createUser($_account_lid, array &$data=[])
{
// add some defaults
$data = array_merge([
'firstname' => ucfirst($_account_lid),
'lastname' => 'User',
'email' => $_account_lid.'@example.org',
'password' => 'secret',//Auth::randomstring(12),
'primary_group' => 'NoGroup',
], $data);
$data['id'] = self::getSetup()->add_account($_account_lid, $data['firstname'], $data['lastname'],
$data['password'], $data['primary_group'], false, $data['email']);
// give use run rights for CalDAV apps, as NoGroup does NOT!
self::addAcl(['groupdav','calendar','infolog','addressbook'], 'run', $data['id']);
self::$created_users[$_account_lid] = $data;
return $data['id'];
}
/**
* Get authentication information for given user to use
*
* @param string $_account_lid ='demo'
* @return array
*/
protected function auth($_account_lid='demo')
{
if ($_account_lid === 'demo')
{
$password = 'guest';
}
elseif (!isset(self::$created_users[$_account_lid]))
{
throw new \InvalidArgumentException("No user '$_account_lid' exist, need to create it with createUser('$_account_lid')");
}
else
{
$password = self::$created_users[$_account_lid]['password'];
}
return [RequestOptions::AUTH => [$_account_lid, $password]];
}
/**
* Tear down:
* - delete users created by createUser() incl. their ACL and data
*
* @ToDo: implement eg. with admin_cmd_delete_user to also delete ACL and data
*/
public static function tearDownAfterClass() : void
{
$setup = self::getSetup();
foreach(self::$created_users as $account_lid => $data)
{
// if ($id) $setup->accounts->delete($data['id']);
unset(self::$created_users[$account_lid]);
}
}
/**
* Add ACL rights
*
* @param string|array $apps app-names
* @param string $location eg. "run"
* @param int|string $account accountid or account_lid
* @param int $rights rights to set, default 1
*/
protected static function addAcl($apps, $location, $account, $rights=1)
{
return self::getSetup()->add_acl($apps, $location, $account, $rights);
}
/**
* Return instance of setup object eg. to create users
*
* @return \setup
*/
private static function getSetup()
{
if (!isset($_REQUEST['domain']))
{
$_REQUEST['domain'] = $GLOBALS['EGW_DOMAIN'] ?? 'default';
}
$_REQUEST['ConfigDomain'] = $_REQUEST['domain'];
require_once __DIR__.'/../../setup/inc/functions.inc.php';
return $GLOBALS['egw_setup'];
}
/**
* Check HTTP status in response
*
* @param int|array $expected one or more valid status codes
* @param ResponseInterface $response
* @param string $message ='' additional message to prefix result message
*/
protected function assertHttpStatus($expected, ResponseInterface $response, $message='')
{
$status = $response->getStatusCode();
$this->assertEquals(in_array($status, (array)$expected) ? $status : ((array)$expected)[0], $status,
(!empty($message) ? $message.': ' : ''). 'Expected HTTP status: '.json_encode($expected).
", Server returned: $status ".$response->getReasonPhrase());
}
/**
* Asserts an iCal file matches an expected one taking into account $_overwrites
*
* @param string $_expected
* @param string $_acctual
* @param string $_message
* @param array $_overwrites =[] eg. ['vEvent' => [['ATTENDEE' => ['mailto:boss@example.org' => ['PARTSTAT' => 'DECLINED']]]]]
* (first vEvent attendee with value 'mailto:boss@...' has param 'PARTSTAT=DECLINED')
* @throws Horde_Icalendar_Exception
*/
protected function assertIcal($_expected, $_acctual, $_message=null, $_overwrites=[])
{
// enable to see full iCals
//$this->assertEquals($_expected, (string)$_acctual, $_message.": iCal not byte-by-byte identical");
$expected = new Horde_Icalendar();
$expected->parsevCalendar($_expected);
$acctual = new Horde_Icalendar();
$acctual->parsevCalendar($_acctual);
if (($msgs = $this->checkComponentEqual($expected, $acctual, $_overwrites)))
{
$this->assertEquals($_expected, (string)$_acctual, ($_message ? $_message.":\n" : '').implode("\n", $msgs));
}
else
{
$this->assertTrue(true); // due to $_overwrite probable $_expected !== $_acctual
}
}
/**
* Check two iCal components are equal modulo overwrites / expected difference
*
* Only a whitelist of attributes per component are checked, see $component_attrs2check variable.
*
* @param Horde_Icalendar $_expected
* @param Horde_Icalendar $_acctual
* @param string $_message
* @param array $_overwrites =[] eg. ['ATTENDEE' => ['boss@example.org' => ['PARTSTAT' => 'DECLINED']]]
* @throws Horde_Icalendar_Exception
* @return array message(s) what's not equal
*/
protected function checkComponentEqual(Horde_Icalendar $_expected, Horde_Icalendar $_acctual, $_overwrites=[])
{
// only following attributes in these components are checked:
static $component_attrs2check = [
'vcalendar' => ['VERSION'],
'vTimeZone' => ['TZID'],
'vEvent' => ['UID', 'SUMMARY', 'LOCATION', 'DESCRIPTION', 'DTSTART', 'DTEND', 'ORGANIZER', 'ATTENDEE'],
];
if ($_expected->getType() !== $_acctual->getType())
{
return ["component type not equal"];
}
$msgs = [];
foreach ($component_attrs2check[$_expected->getType()] ?? [] as $attr)
{
$acctualAttrs = $_acctual->getAllAttributes($attr);
foreach($_expected->getAllAttributes($attr) as $expectedAttr)
{
$found = false;
foreach($acctualAttrs as $acctualAttr)
{
if (count($acctualAttrs) === 1 || $expectedAttr['value'] === $acctualAttr['value'])
{
$found = true;
break;
}
}
if (!$found)
{
$msgs[] = "No $attr {$expectedAttr['value']} found";
continue;
}
// remove / ignore X-parameters, eg. X-EGROUPWARE-UID in ATTENDEE or ORGANIZER
$acctualAttr['params'] = array_filter($acctualAttr['params'], function ($key) {
return substr($key, 0, 2) !== 'X-';
}, ARRAY_FILTER_USE_KEY);
if (isset($_overwrites[$attr]) && is_scalar($_overwrites[$attr]))
{
$expectedAttr = [
'name' => $attr,
'value' => $_overwrites[$attr],
'values' => [$_overwrites[$attr]],
'params' => [],
];
}
elseif (isset($_overwrites[$attr]) && is_array($_overwrites[$attr]))
{
foreach ($_overwrites[$attr] as $value => $params)
{
if ($value === $expectedAttr['value'])
{
$expectedAttr['params'] = array_merge($expectedAttr['params'], $params);
}
}
}
if ($expectedAttr != $acctualAttr)
{
$this->assertEquals($expectedAttr, $acctualAttr, "$attr not equal");
$msgs[] = "$attr not equal";
}
}
}
// check sub-components, overrites use an index by type eg. 1. vEvent: ['vEvent'=>[[<overwrites for 1. vEvent]]]
$idx_by_type = [];
foreach($_expected->getComponents() as $idx => $component)
{
if (!isset($idx_by_type[$type = $component->getType()])) $idx_by_type[$type] = 0;
$msgs = array_merge($msgs, $this->checkComponentEqual($component, $_acctual->getComponent($idx),
$_overwrites[$type][$idx_by_type[$type]] ?? []));
$idx_by_type[$type]++;
}
return $msgs;
}
}

View File

@ -0,0 +1,121 @@
<?php
/**
* CalDAV tests: create, read and delete an event
*
* @link https://www.egroupware.org
* @author Ralf Becker <rb@egroupware.org>
* @package calendar
* @subpackage tests
* @copyright (c) 2020 by Ralf Becker <rb@egroupware.org>
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
*/
namespace EGroupware\calendar;
require_once __DIR__.'/../../../api/tests/CalDAVTest.php';
use EGroupware\Api\CalDAVTest;
use GuzzleHttp\RequestOptions;
class CalDAVcreateReadDelete extends CalDAVTest
{
/**
* Test accessing CalDAV without authentication
*/
public function testNoAuth()
{
$response = $this->getClient([])->get($this->url('/'));
$this->assertHttpStatus(401, $response);
}
/**
* Test accessing CalDAV with authentication
*/
public function testAuth()
{
$response = $this->getClient()->get($this->url('/'));
$this->assertHttpStatus(200, $response);
}
const EVENT_URL = '/demo/calendar/new-event-1233456789-new.ics';
const EVENT_ICAL = <<<EOICAL
BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VTIMEZONE
TZID:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID=Europe/Berlin:20110406T210000
DTEND;TZID=Europe/Berlin:20110406T220000
DTSTAMP:20110406T183747Z
LAST-MODIFIED:20110406T183747Z
LOCATION:Somewhere
SUMMARY:Tonight
UID:new-event-1233456789-new
END:VEVENT
END:VCALENDAR
EOICAL;
/**
* Create an event
*/
public function testCreate()
{
$response = $this->getClient()->put($this->url(self::EVENT_URL), [
RequestOptions::HEADERS => [
'Content-Type' => 'text/calendar',
'If-None-Match' => '*',
],
RequestOptions::BODY => self::EVENT_ICAL,
]);
$this->assertHttpStatus(201, $response);
}
/**
* Read created event
*/
public function testRead()
{
$response = $this->getClient()->get($this->url(self::EVENT_URL));
$this->assertHttpStatus(200, $response);
$this->assertIcal(self::EVENT_ICAL, $response->getBody());
}
/**
* Delete created event
*/
public function testDelete()
{
$response = $this->getClient()->delete($this->url(self::EVENT_URL));
$this->assertHttpStatus(204, $response);
}
/**
* Read created event
*/
public function testReadDeleted()
{
$response = $this->getClient()->get($this->url(self::EVENT_URL));
$this->assertHttpStatus(404, $response);
}
}

View File

@ -0,0 +1,368 @@
<?php
/**
* CalDAV tests: DELETE requests for non-series by Outlook CalDAV Synchronizer and other clients
*
* @link https://www.egroupware.org
* @author Ralf Becker <rb@egroupware.org>
* @package calendar
* @subpackage tests
* @copyright (c) 2020 by Ralf Becker <rb@egroupware.org>
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
*/
namespace EGroupware\calendar;
require_once __DIR__.'/../../../api/tests/CalDAVTest.php';
use EGroupware\Api\CalDAVTest;
use GuzzleHttp\RequestOptions;
use EGroupware\Api\Acl;
/**
* Class CalDAVsingleDELETE
*
* This tests check all sorts of DELETE requests by organizer and attendees, with and without (delete) rights on the organizer.
*
* For CalDAV Synchronizer, which does not distingues between deleting and rejecting events, we only allow the
* organizer to delete an event.
*
* @package EGroupware\calendar
* @covers \calendar_groupdav::delete()
* @uses \calendar_groupdav::put()
* @uses \calendar_groupdav::get()
*/
class CalDAVsingleDELETE extends CalDAVTest
{
/**
* Users and their ACL for the test
*
* @var array
*/
protected static $users = [
'boss' => [],
'secretary' => [
'rights' => [
'boss' => Acl::READ|Acl::ADD|Acl::EDIT|Acl::DELETE,
]
],
'other' => [],
];
/**
* Create some users incl. ACL
*/
public static function setUpBeforeClass() : void
{
parent::setUpBeforeClass();
self::createUsersACL(self::$users, 'calendar');
}
/**
* Check created users
*/
public function testPrincipals()
{
foreach(self::$users as $user => &$data)
{
$response = $this->getClient()->propfind($this->url('/principals/users/'.$user.'/'), [
RequestOptions::HEADERS => [
'Depth' => 0,
],
]);
$this->assertHttpStatus(207, $response);
}
}
const EVENT_BOSS_ATTENDEE_ORGANIZER_URL = '/other/calendar/new-event-boss-attendee-123456789-new.ics';
const EVENT_BOSS_ATTENDEE_URL = '/boss/calendar/new-event-boss-attendee-123456789-new.ics';
const EVENT_BOSS_ATTENDEE_ICAL = <<<EOICAL
BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VTIMEZONE
TZID:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID=Europe/Berlin:20110406T210000
DTEND;TZID=Europe/Berlin:20110406T220000
DTSTAMP:20110406T183747Z
LAST-MODIFIED:20110406T183747Z
LOCATION:Somewhere
SUMMARY:Tonight
ORGANIZER;CN="Other User":mailto:other@example.org
ATTENDEE;CN="Other User";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:other@example.org
ATTENDEE;CN="Boss User";CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:boss@example.org
UID:new-event-boss-attendee-123456789-new
END:VEVENT
END:VCALENDAR
EOICAL;
/**
* Check secretary deletes in boss's calendar event he is an attendee / invited
*
* @throws \Horde_Icalendar_Exception
*/
public function testSecretaryDeletesBossAttendee()
{
// create invitation by organizer
$response = $this->getClient('other')->put($this->url(self::EVENT_BOSS_ATTENDEE_ORGANIZER_URL), [
RequestOptions::HEADERS => [
'Content-Type' => 'text/calendar',
'Prefer' => 'return=representation'
],
RequestOptions::BODY => self::EVENT_BOSS_ATTENDEE_ICAL,
]);
$this->assertHttpStatus([200,201], $response);
$this->assertIcal(self::EVENT_BOSS_ATTENDEE_ICAL, $response->getBody());
// secretrary deletes event in boss's calendar
$response = $this->getClient('secretary')->delete($this->url(self::EVENT_BOSS_ATTENDEE_URL));
$this->assertHttpStatus(204, $response, 'Secretary delete/rejects for boss');
// use organizer to check event still exists and boss rejected
$response = $this->getClient('other')->get($this->url(self::EVENT_BOSS_ATTENDEE_ORGANIZER_URL));
$this->assertHttpStatus(200, $response, 'Check event still exists after DELETE in attendee calendar');
$this->assertIcal(self::EVENT_BOSS_ATTENDEE_ICAL, $response->getBody(),
'Boss should have declined the invitation',
['vEvent' => [['ATTENDEE' => ['mailto:boss@example.org' => ['PARTSTAT' => 'DECLINED', 'RSVP' => 'FALSE']]]]]
);
// secretary tries to delete event in organizers calendar
$response = $this->getClient('secretary')->delete($this->url(self::EVENT_BOSS_ATTENDEE_ORGANIZER_URL));
$this->assertHttpStatus(403, $response, 'Secretary not allowed to delete for organizer');
// boss deletes/rejects event in his calendar
$response = $this->getClient('boss')->delete($this->url(self::EVENT_BOSS_ATTENDEE_URL));
$this->assertHttpStatus(204, $response, 'Boss deletes/rejects in his calendar');
// boss deletes/rejects event in organizers calendar
$response = $this->getClient('boss')->delete($this->url(self::EVENT_BOSS_ATTENDEE_ORGANIZER_URL));
$this->assertHttpStatus(204, $response, 'Boss deletes/rejects in organizers calendar');
// use organizer to delete event
$response = $this->getClient('other')->delete($this->url(self::EVENT_BOSS_ATTENDEE_ORGANIZER_URL));
$this->assertHttpStatus(204, $response);
// use organizer to check event deleted
$response = $this->getClient('other')->get($this->url(self::EVENT_BOSS_ATTENDEE_ORGANIZER_URL));
$this->assertHttpStatus(404, $response, "Check event deleted by organizer");
}
const EVENT_BOSS_ORGANIZER_URL = '/boss/calendar/new-event-boss-organizer-123456789-new.ics';
const EVENT_BOSS_ORGANIZER_OTHER_URL = '/other/calendar/new-event-boss-organizer-123456789-new.ics';
const EVENT_BOSS_ORGANIZER_ICAL = <<<EOICAL
BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VTIMEZONE
TZID:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID=Europe/Berlin:20110406T210000
DTEND;TZID=Europe/Berlin:20110406T220000
DTSTAMP:20110406T183747Z
LAST-MODIFIED:20110406T183747Z
LOCATION:Somewhere
SUMMARY:Tonight
ORGANIZER;CN="Boss User":mailto:boss@example.org
ATTENDEE;CN="Boss User";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:boss@example.org
ATTENDEE;CN="Other User";CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:other@example.org
UID:new-event-boss-organizer-123456789-new
END:VEVENT
END:VCALENDAR
EOICAL;
/**
* Check secretary deletes for boss, which is organizer of event
*
* @throws \Horde_Icalendar_Exception
*/
public function testSecretaryDeletesBossOrganizer()
{
// create invitation by boss as organizer
$response = $this->getClient('boss')->put($this->url(self::EVENT_BOSS_ORGANIZER_URL), [
RequestOptions::HEADERS => [
'Content-Type' => 'text/calendar',
'Prefer' => 'return=representation'
],
RequestOptions::BODY => self::EVENT_BOSS_ORGANIZER_ICAL,
]);
$this->assertHttpStatus([200,201], $response);
$this->assertIcal(self::EVENT_BOSS_ORGANIZER_ICAL, $response->getBody());
// attendee deletes/rejects event in his calendar
$response = $this->getClient('other')->delete($this->url(self::EVENT_BOSS_ORGANIZER_OTHER_URL));
$this->assertHttpStatus(204, $response);
// secretrary deletes event in boss's calendar
$response = $this->getClient('secretary')->delete($this->url(self::EVENT_BOSS_ORGANIZER_URL));
$this->assertHttpStatus(204, $response, 'Secretary deletes for boss');
// use organizer/boss to check event deleted
$response = $this->getClient('boss')->get($this->url(self::EVENT_BOSS_ORGANIZER_URL));
$this->assertHttpStatus(404, $response, "Check event deleted by secretary");
}
/**
* Check organizer (boss) can delete event in his calendar
*
* @throws \Horde_Icalendar_Exception
*/
public function testOrganizerDeletes()
{
// create invitation by boss as organizer
$response = $this->getClient('boss')->put($this->url(self::EVENT_BOSS_ORGANIZER_URL), [
RequestOptions::HEADERS => [
'Content-Type' => 'text/calendar',
'Prefer' => 'return=representation'
],
RequestOptions::BODY => self::EVENT_BOSS_ORGANIZER_ICAL,
]);
$this->assertHttpStatus([200,201], $response);
$this->assertIcal(self::EVENT_BOSS_ORGANIZER_ICAL, $response->getBody());
// organizer deletes event in his calendar
$response = $this->getClient('boss')->delete($this->url(self::EVENT_BOSS_ORGANIZER_URL));
$this->assertHttpStatus(204, $response, 'Organizer deletes');
// use organizer/boss to check event deleted
$response = $this->getClient('boss')->get($this->url(self::EVENT_BOSS_ORGANIZER_URL));
$this->assertHttpStatus(404, $response, "Check event deleted by organizer");
// use attendee to check event deleted
$response = $this->getClient('other')->get($this->url(self::EVENT_BOSS_ORGANIZER_OTHER_URL));
$this->assertHttpStatus(404, $response, "Check event deleted by organizer");
}
const EVENT_SECRETARY_ATTENDEE_URL = '/secretary/calendar/new-event-secreatary-attendee-123456789-new.ics';
const EVENT_SECRETARY_ATTENDEE_ORGANIZER_URL = '/boss/calendar/new-event-secreatary-attendee-123456789-new.ics';
const EVENT_SECRETARY_ATTENDEE_ICAL = <<<EOICAL
BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VTIMEZONE
TZID:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID=Europe/Berlin:20110406T210000
DTEND;TZID=Europe/Berlin:20110406T220000
DTSTAMP:20110406T183747Z
LAST-MODIFIED:20110406T183747Z
LOCATION:Somewhere
SUMMARY:Tonight
ORGANIZER;CN="Boss User":mailto:boss@example.org
ATTENDEE;CN="Boss User";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:boss@example.org
ATTENDEE;CN="Secretary User";CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:secretary@example.org
UID:new-event-secreatary-attendee-123456789-new
END:VEVENT
END:VCALENDAR
EOICAL;
/**
* Check secretary as attendee deletes event
*
* @throws \Horde_Icalendar_Exception
*/
public function testSecretaryAttendeeDeletes()
{
// create invitation by boss as organizer
$response = $this->getClient('boss')->put($this->url(self::EVENT_SECRETARY_ATTENDEE_ORGANIZER_URL), [
RequestOptions::HEADERS => [
'Content-Type' => 'text/calendar',
'Prefer' => 'return=representation'
],
RequestOptions::BODY => self::EVENT_SECRETARY_ATTENDEE_ICAL,
]);
$this->assertHttpStatus([200,201], $response);
$this->assertIcal(self::EVENT_SECRETARY_ATTENDEE_ICAL, $response->getBody());
// secretary deletes in her calendar
$response = $this->getClient('secretary')->delete($this->url(self::EVENT_SECRETARY_ATTENDEE_URL));
$this->assertHttpStatus(204, $response, 'Secretary (attendee) deletes');
// use organizer to check it's really deleted
$response = $this->getClient('boss')->get($this->url(self::EVENT_SECRETARY_ATTENDEE_ORGANIZER_URL));
$this->assertHttpStatus(404, $response, "Check event deleted by secretary");
}
/**
* Check secretary as attendee deletes event with CalDAVSynchronizer
*
* @throws \Horde_Icalendar_Exception
*/
public function testSecretaryAttendeeDeletesCalDAVSynchronizer()
{
// create invitation by boss as organizer
$response = $this->getClient('boss')->put($this->url(self::EVENT_SECRETARY_ATTENDEE_ORGANIZER_URL), [
RequestOptions::HEADERS => [
'Content-Type' => 'text/calendar',
'Prefer' => 'return=representation'
],
RequestOptions::BODY => self::EVENT_SECRETARY_ATTENDEE_ICAL,
]);
$this->assertHttpStatus([200,201], $response);
$this->assertIcal(self::EVENT_SECRETARY_ATTENDEE_ICAL, $response->getBody());
// secretary deletes in her calendar with CalDAVSynchronizer
$response = $this->getClient('secretary')->delete($this->url(self::EVENT_SECRETARY_ATTENDEE_URL),
[RequestOptions::HEADERS => ['User-Agent' => 'CalDAVSynchronizer']]);
$this->assertHttpStatus(204, $response, 'Secretary (attendee) deletes/rejects');
// use organizer to check it's NOT deleted, as CalDAVSynchronizer / Outlook does not distinguish between reject and delete
$response = $this->getClient('boss')->get($this->url(self::EVENT_SECRETARY_ATTENDEE_ORGANIZER_URL));
$this->assertHttpStatus(200, $response, "Check event NOT deleted by secretary");
$this->assertIcal(self::EVENT_SECRETARY_ATTENDEE_ICAL, $response->getBody(),
'Secretary should have declined the invitation',
['vEvent' => [['ATTENDEE' => ['mailto:secretary@example.org' => ['PARTSTAT' => 'DECLINED', 'RSVP' => 'FALSE']]]]]
);
// organizer deletes in his calendar with CalDAVSynchronizer
$response = $this->getClient('boss')->delete($this->url(self::EVENT_SECRETARY_ATTENDEE_ORGANIZER_URL),
[RequestOptions::HEADERS => ['User-Agent' => 'CalDAVSynchronizer']]);
$this->assertHttpStatus(204, $response, 'Organizer deletes');
// use organizer to check it's deleted, as CalDAVSynchronizer / Outlook should still delete for organizer
$response = $this->getClient('boss')->get($this->url(self::EVENT_SECRETARY_ATTENDEE_ORGANIZER_URL));
$this->assertHttpStatus(404, $response, "Check event deleted by organizer");
}
}