diff plugins/libcalendaring/tests/libvcalendar.php @ 4:888e774ee983

libcalendar plugin as distributed
author Charlie Root
date Sat, 13 Jan 2018 08:57:56 -0500
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/tests/libvcalendar.php	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,599 @@
+<?php
+
+/**
+ * libcalendaring plugin's iCalendar functions tests
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class libvcalendar_test extends PHPUnit_Framework_TestCase
+{
+    function setUp()
+    {
+        require_once __DIR__ . '/../libvcalendar.php';
+        require_once __DIR__ . '/../libcalendaring.php';
+    }
+
+    /**
+     * Simple iCal parsing test
+     */
+    function test_import()
+    {
+        $ical = new libvcalendar();
+        $ics = file_get_contents(__DIR__ . '/resources/snd.ics');
+        $events = $ical->import($ics, 'UTF-8');
+
+        $this->assertEquals(1, count($events));
+        $event = $events[0];
+
+        $this->assertInstanceOf('DateTime', $event['created'], "'created' property is DateTime object");
+        $this->assertInstanceOf('DateTime', $event['changed'], "'changed' property is DateTime object");
+        $this->assertEquals('UTC', $event['created']->getTimezone()->getName(), "'created' date is in UTC");
+
+        $this->assertInstanceOf('DateTime', $event['start'], "'start' property is DateTime object");
+        $this->assertInstanceOf('DateTime', $event['end'], "'end' property is DateTime object");
+        $this->assertEquals('08-01', $event['start']->format('m-d'), "Start date is August 1st");
+        $this->assertTrue($event['allday'], "All-day event flag");
+
+        $this->assertEquals('B968B885-08FB-40E5-B89E-6DA05F26AA79', $event['uid'], "Event UID");
+        $this->assertEquals('Swiss National Day', $event['title'], "Event title");
+        $this->assertEquals('http://en.wikipedia.org/wiki/Swiss_National_Day', $event['url'], "URL property");
+        $this->assertEquals(2, $event['sequence'], "Sequence number");
+
+        $desclines = explode("\n", $event['description']);
+        $this->assertEquals(4, count($desclines), "Multiline description");
+        $this->assertEquals("French: FĂȘte nationale Suisse", rtrim($desclines[1]), "UTF-8 encoding");
+    }
+
+    /**
+     * Test parsing from files
+     */
+    function test_import_from_file()
+    {
+        $ical = new libvcalendar();
+
+        $events = $ical->import_from_file(__DIR__ . '/resources/multiple.ics', 'UTF-8');
+        $this->assertEquals(2, count($events));
+
+        $events = $ical->import_from_file(__DIR__ . '/resources/invalid.txt', 'UTF-8');
+        $this->assertEmpty($events);
+    }
+
+    /**
+     * Test parsing from files with multiple VCALENDAR blocks (#2884)
+     */
+    function test_import_from_file_multiple()
+    {
+        $ical = new libvcalendar();
+        $ical->fopen(__DIR__ . '/resources/multiple-rdate.ics', 'UTF-8');
+        $events = array();
+        foreach ($ical as $event) {
+            $events[] = $event;
+        }
+
+        $this->assertEquals(2, count($events));
+        $this->assertEquals("AAAA6A8C3CCE4EE2C1257B5C00FFFFFF-Lotus_Notes_Generated", $events[0]['uid']);
+        $this->assertEquals("AAAA1C572093EC3FC125799C004AFFFF-Lotus_Notes_Generated", $events[1]['uid']);
+    }
+
+    function test_invalid_dates()
+    {
+        $ical = new libvcalendar();
+        $events = $ical->import_from_file(__DIR__ . '/resources/invalid-dates.ics', 'UTF-8');
+        $event = $events[0];
+
+        $this->assertEquals(1, count($events), "Import event data");
+        $this->assertInstanceOf('DateTime', $event['created'], "Created date field");
+        $this->assertFalse(array_key_exists('changed', $event), "No changed date field");
+    }
+
+    function test_invalid_vevent()
+    {
+        $this->setExpectedException('\Sabre\VObject\ParseException');
+
+        $ical = new libvcalendar();
+        $events = $ical->import_from_file(__DIR__ . '/resources/invalid-event.ics', 'UTF-8', true);
+    }
+
+    /**
+     * Test some extended ical properties such as attendees, recurrence rules, alarms and attachments
+     */
+    function test_extended()
+    {
+        $ical = new libvcalendar();
+
+        $events = $ical->import_from_file(__DIR__ . '/resources/itip.ics', 'UTF-8');
+        $event = $events[0];
+        $this->assertEquals('REQUEST', $ical->method, "iTip method");
+
+        // attendees
+        $this->assertEquals(3, count($event['attendees']), "Attendees list (including organizer)");
+        $organizer = $event['attendees'][0];
+        $this->assertEquals('ORGANIZER', $organizer['role'], 'Organizer ROLE');
+        $this->assertEquals('Rolf Test', $organizer['name'], 'Organizer name');
+
+        $attendee = $event['attendees'][1];
+        $this->assertEquals('REQ-PARTICIPANT', $attendee['role'], 'Attendee ROLE');
+        $this->assertEquals('NEEDS-ACTION', $attendee['status'], 'Attendee STATUS');
+        $this->assertEquals('rolf2@mykolab.com', $attendee['email'], 'Attendee mailto:');
+        $this->assertEquals('carl@mykolab.com', $attendee['delegated-from'], 'Attendee delegated-from');
+        $this->assertTrue($attendee['rsvp'], 'Attendee RSVP');
+
+        $delegator = $event['attendees'][2];
+        $this->assertEquals('NON-PARTICIPANT',   $delegator['role'], 'Delegator ROLE');
+        $this->assertEquals('DELEGATED',         $delegator['status'], 'Delegator STATUS');
+        $this->assertEquals('INDIVIDUAL',        $delegator['cutype'], 'Delegator CUTYPE');
+        $this->assertEquals('carl@mykolab.com',  $delegator['email'], 'Delegator mailto:');
+        $this->assertEquals('rolf2@mykolab.com', $delegator['delegated-to'], 'Delegator delegated-to');
+        $this->assertFalse($delegator['rsvp'],   'Delegator RSVP');
+
+        // attachments
+        $this->assertEquals(1, count($event['attachments']), "Embedded attachments");
+        $attachment = $event['attachments'][0];
+        $this->assertEquals('text/html',                 $attachment['mimetype'], "Attachment mimetype attribute");
+        $this->assertEquals('calendar.html',             $attachment['name'],     "Attachment filename (X-LABEL) attribute");
+        $this->assertContains('<title>Kalender</title>', $attachment['data'],     "Attachment content (decoded)");
+
+        // recurrence rules
+        $events = $ical->import_from_file(__DIR__ . '/resources/recurring.ics', 'UTF-8');
+        $event = $events[0];
+
+        $this->assertTrue(is_array($event['recurrence']), 'Recurrences rule as hash array');
+        $rrule = $event['recurrence'];
+        $this->assertEquals('MONTHLY',      $rrule['FREQ'],     "Recurrence frequency");
+        $this->assertEquals('1',            $rrule['INTERVAL'], "Recurrence interval");
+        $this->assertEquals('3WE',          $rrule['BYDAY'],    "Recurrence frequency");
+        $this->assertInstanceOf('DateTime', $rrule['UNTIL'],    "Recurrence end date");
+
+        $this->assertEquals(2, count($rrule['EXDATE']),          "Recurrence EXDATEs");
+        $this->assertInstanceOf('DateTime', $rrule['EXDATE'][0], "Recurrence EXDATE as DateTime");
+
+        $this->assertTrue(is_array($rrule['EXCEPTIONS']));
+        $this->assertEquals(1, count($rrule['EXCEPTIONS']), "Recurrence Exceptions");
+
+        $exception = $rrule['EXCEPTIONS'][0];
+        $this->assertEquals($event['uid'],  $event['uid'], "Exception UID");
+        $this->assertEquals('Recurring Test (Exception)',  $exception['title'], "Exception title");
+        $this->assertInstanceOf('DateTime', $exception['start'], "Exception start");
+
+        // categories, class
+        $this->assertEquals('libcalendaring tests', join(',', (array)$event['categories']), "Event categories");
+        $this->assertEquals('confidential', $event['sensitivity'], "Class/sensitivity = confidential");
+
+        // parse a recurrence chain instance
+        $events = $ical->import_from_file(__DIR__ . '/resources/recurrence-id.ics', 'UTF-8');
+        $this->assertEquals(1, count($events), "Fall back to Component::getComponents() when getBaseComponents() is empty");
+        $this->assertInstanceOf('DateTime', $events[0]['recurrence_date'], "Recurrence-ID as date");
+        $this->assertTrue($events[0]['thisandfuture'], "Range=THISANDFUTURE");
+
+        $this->assertEquals(count($events[0]['exceptions']), 1, "Second VEVENT as exception");
+        $this->assertEquals($events[0]['exceptions'][0]['uid'], $events[0]['uid'], "Exception UID match");
+        $this->assertEquals($events[0]['exceptions'][0]['sequence'], '2', "Exception sequence");
+    }
+
+    /**
+     * 
+     */
+    function test_alarms()
+    {
+        $ical = new libvcalendar();
+
+        $events = $ical->import_from_file(__DIR__ . '/resources/recurring.ics', 'UTF-8');
+        $event = $events[0];
+
+        $this->assertEquals('-12H:DISPLAY', $event['alarms'], "Serialized alarms string");
+        $alarm = libcalendaring::parse_alarm_value($event['alarms']);
+        $this->assertEquals('12', $alarm[0], "Alarm value");
+        $this->assertEquals('-H', $alarm[1], "Alarm unit");
+
+        $this->assertEquals('DISPLAY', $event['valarms'][0]['action'],  "Full alarm item (action)");
+        $this->assertEquals('-PT12H',  $event['valarms'][0]['trigger'], "Full alarm item (trigger)");
+        $this->assertEquals('END',  $event['valarms'][0]['related'], "Full alarm item (related)");
+
+        // alarm trigger with 0 values
+        $events = $ical->import_from_file(__DIR__ . '/resources/alarms.ics', 'UTF-8');
+        $event = $events[0];
+
+        $this->assertEquals('-30M:DISPLAY', $event['alarms'], "Stripped alarm string");
+        $alarm = libcalendaring::parse_alarm_value($event['alarms']);
+        $this->assertEquals('30', $alarm[0], "Alarm value");
+        $this->assertEquals('-M', $alarm[1], "Alarm unit");
+        $this->assertEquals('-30M', $alarm[2], "Alarm string");
+        $this->assertEquals('-PT30M', $alarm[3], "Unified alarm string (stripped zero-values)");
+
+        $this->assertEquals('DISPLAY', $event['valarms'][0]['action'],  "First alarm action");
+        $this->assertEquals('', $event['valarms'][0]['related'],  "First alarm related property");
+        $this->assertEquals('This is the first event reminder', $event['valarms'][0]['description'],  "First alarm text");
+
+        $this->assertEquals(3, count($event['valarms']), "List all VALARM blocks");
+
+        $valarm = $event['valarms'][1];
+        $this->assertEquals(1, count($valarm['attendees']), "Email alarm attendees");
+        $this->assertEquals('EMAIL', $valarm['action'],  "Second alarm item (action)");
+        $this->assertEquals('-P1D',  $valarm['trigger'], "Second alarm item (trigger)");
+        $this->assertEquals('This is the reminder message',  $valarm['summary'], "Email alarm text");
+        $this->assertInstanceOf('DateTime', $event['valarms'][2]['trigger'], "Absolute trigger date/time");
+
+        // test alarms export
+        $ics = $ical->export(array($event));
+        $this->assertContains('ACTION:DISPLAY',   $ics, "Display alarm block");
+        $this->assertContains('ACTION:EMAIL',     $ics, "Email alarm block");
+        $this->assertContains('DESCRIPTION:This is the first event reminder',    $ics, "Alarm description");
+        $this->assertContains('SUMMARY:This is the reminder message',            $ics, "Email alarm summary");
+        $this->assertContains('ATTENDEE:mailto:reminder-recipient@example.org',  $ics, "Email alarm recipient");
+        $this->assertContains('TRIGGER;VALUE=DATE-TIME:20130812',  $ics, "Date-Time trigger");
+    }
+
+    /**
+     * @depends test_import_from_file
+     */
+    function test_attachment()
+    {
+        $ical = new libvcalendar();
+
+        $events = $ical->import_from_file(__DIR__ . '/resources/attachment.ics', 'UTF-8');
+        $event = $events[0];
+
+        $this->assertEquals(2, count($events));
+        $this->assertEquals(1, count($event['attachments']));
+        $this->assertEquals('image/png', $event['attachments'][0]['mimetype']);
+        $this->assertEquals('500px-Opensource.svg.png', $event['attachments'][0]['name']);
+    }
+
+    /**
+     * @depends test_import
+     */
+    function test_apple_alarms()
+    {
+        $ical = new libvcalendar();
+        $events = $ical->import_from_file(__DIR__ . '/resources/apple-alarms.ics', 'UTF-8');
+        $event = $events[0];
+
+        // alarms
+        $this->assertEquals('-45M:AUDIO', $event['alarms'], "Relative alarm string");
+        $alarm = libcalendaring::parse_alarm_value($event['alarms']);
+        $this->assertEquals('45', $alarm[0], "Alarm value");
+        $this->assertEquals('-M', $alarm[1], "Alarm unit");
+
+        $this->assertEquals(1, count($event['valarms']), "Ignore invalid alarm blocks");
+        $this->assertEquals('AUDIO', $event['valarms'][0]['action'],   "Full alarm item (action)");
+        $this->assertEquals('-PT45M', $event['valarms'][0]['trigger'], "Full alarm item (trigger)");
+        $this->assertEquals('Basso',  $event['valarms'][0]['uri'],     "Full alarm item (attachment)");
+    }
+
+    /**
+     * 
+     */
+    function test_escaped_values()
+    {
+        $ical = new libvcalendar();
+        $events = $ical->import_from_file(__DIR__ . '/resources/escaped.ics', 'UTF-8');
+        $event = $events[0];
+
+        $this->assertEquals("House, Street, Zip Place", $event['location'], "Decode escaped commas in location value");
+        $this->assertEquals("Me, meets Them\nThem, meet Me", $event['description'], "Decode description value");
+        $this->assertEquals("Kolab, Thomas", $event['attendees'][3]['name'], "Unescaped");
+
+        $ics = $ical->export($events);
+        $this->assertContains('ATTENDEE;CN="Kolab, Thomas";PARTSTAT=', $ics, "Quoted attendee parameters");
+    }
+
+    /**
+     * Parse RDATE properties (#2885)
+     */
+    function test_rdate()
+    {
+        $ical = new libvcalendar();
+        $events = $ical->import_from_file(__DIR__ . '/resources/multiple-rdate.ics', 'UTF-8');
+        $event = $events[0];
+
+        $this->assertEquals(9, count($event['recurrence']['RDATE']));
+        $this->assertInstanceOf('DateTime', $event['recurrence']['RDATE'][0]);
+        $this->assertInstanceOf('DateTime', $event['recurrence']['RDATE'][1]);
+    }
+
+    /**
+     * @depends test_import
+     */
+    function test_freebusy()
+    {
+        $ical = new libvcalendar();
+        $ical->import_from_file(__DIR__ . '/resources/freebusy.ifb', 'UTF-8');
+        $freebusy = $ical->freebusy;
+
+        $this->assertInstanceOf('DateTime', $freebusy['start'], "'start' property is DateTime object");
+        $this->assertInstanceOf('DateTime', $freebusy['end'], "'end' property is DateTime object");
+        $this->assertEquals(11, count($freebusy['periods']), "Number of freebusy periods defined");
+        $periods = $ical->get_busy_periods();
+        $this->assertEquals(9, count($periods), "Number of busy periods found");
+        $this->assertEquals('BUSY-TENTATIVE', $periods[8][2], "FBTYPE=BUSY-TENTATIVE");
+    }
+
+    /**
+     * @depends test_import
+     */
+    function test_freebusy_dummy()
+    {
+        $ical = new libvcalendar();
+        $ical->import_from_file(__DIR__ . '/resources/dummy.ifb', 'UTF-8');
+        $freebusy = $ical->freebusy;
+
+        $this->assertEquals(0, count($freebusy['periods']), "Ignore 0-length freebudy periods");
+        $this->assertContains('dummy', $freebusy['comment'], "Parse comment");
+    }
+
+    function test_vtodo()
+    {
+        $ical = new libvcalendar();
+        $tasks = $ical->import_from_file(__DIR__ . '/resources/vtodo.ics', 'UTF-8', true);
+        $task = $tasks[0];
+
+        $this->assertInstanceOf('DateTime', $task['start'],   "'start' property is DateTime object");
+        $this->assertInstanceOf('DateTime', $task['due'],     "'due' property is DateTime object");
+        $this->assertEquals('-1D:DISPLAY',  $task['alarms'],  "Taks alarm value");
+        $this->assertEquals('IN-PROCESS',   $task['status'],  "Task status property");
+        $this->assertEquals(1, count($task['x-custom']),      "Custom properties");
+        $this->assertEquals(4, count($task['categories']));
+        $this->assertEquals('1234567890-12345678-PARENT', $task['parent_id'], "Parent Relation");
+
+        $completed = $tasks[1];
+        $this->assertEquals('COMPLETED', $completed['status'], "Task status=completed when COMPLETED property is present");
+        $this->assertEquals(100, $completed['complete'], "Task percent complete value");
+
+        $ics = $ical->export(array($completed));
+        $this->assertRegExp('/COMPLETED(;VALUE=DATE-TIME)?:[0-9TZ]+/', $ics, "Export COMPLETED property");
+    }
+
+    /**
+     * Test for iCal export from internal hash array representation
+     *
+     * 
+     */
+    function test_export()
+    {
+        $ical = new libvcalendar();
+
+        $events = $ical->import_from_file(__DIR__ . '/resources/itip.ics', 'UTF-8');
+        $event = $events[0];
+        $events = $ical->import_from_file(__DIR__ . '/resources/recurring.ics', 'UTF-8');
+        $event += $events[0];
+
+        $this->attachment_data = $event['attachments'][0]['data'];
+        unset($event['attachments'][0]['data']);
+        $event['attachments'][0]['id'] = '1';
+        $event['description'] = '*Exported by libvcalendar*';
+
+        $event['start']->setTimezone(new DateTimezone('America/Montreal'));
+        $event['end']->setTimezone(new DateTimezone('Europe/Berlin'));
+
+        $ics = $ical->export(array($event), 'REQUEST', false, array($this, 'get_attachment_data'), true);
+
+        $this->assertContains('BEGIN:VCALENDAR',    $ics, "VCALENDAR encapsulation BEGIN");
+
+        $this->assertContains('BEGIN:VTIMEZONE', $ics, "VTIMEZONE encapsulation BEGIN");
+        $this->assertContains('TZID:Europe/Berlin', $ics, "Timezone ID");
+        $this->assertContains('TZOFFSETFROM:+0100', $ics, "Timzone transition FROM");
+        $this->assertContains('TZOFFSETTO:+0200', $ics, "Timzone transition TO");
+        $this->assertContains('TZOFFSETFROM:-0400', $ics, "TZOFFSETFROM with negative offset (Bug T428)");
+        $this->assertContains('TZOFFSETTO:-0500', $ics, "TZOFFSETTO with negative offset (Bug T428)");
+        $this->assertContains('END:VTIMEZONE', $ics, "VTIMEZONE encapsulation END");
+
+        $this->assertContains('BEGIN:VEVENT',       $ics, "VEVENT encapsulation BEGIN");
+        $this->assertSame(2, substr_count($ics, 'DTSTAMP'), "Duplicate DTSTAMP (T1148)");
+        $this->assertContains('UID:ac6b0aee-2519-4e5c-9a25-48c57064c9f0', $ics, "Event UID");
+        $this->assertContains('SEQUENCE:' . $event['sequence'],           $ics, "Export Sequence number");
+        $this->assertContains('CLASS:CONFIDENTIAL',                       $ics, "Sensitivity => Class");
+        $this->assertContains('DESCRIPTION:*Exported by',                 $ics, "Export Description");
+        $this->assertContains('ORGANIZER;CN=Rolf Test:mailto:rolf@',      $ics, "Export organizer");
+        $this->assertRegExp('/ATTENDEE.*;ROLE=REQ-PARTICIPANT/',          $ics, "Export Attendee ROLE");
+        $this->assertRegExp('/ATTENDEE.*;PARTSTAT=NEEDS-ACTION/',         $ics, "Export Attendee Status");
+        $this->assertRegExp('/ATTENDEE.*;RSVP=TRUE/',                     $ics, "Export Attendee RSVP");
+        $this->assertRegExp('/:mailto:rolf2@/',                           $ics, "Export Attendee mailto:");
+
+        $rrule = $event['recurrence'];
+        $this->assertRegExp('/RRULE:.*FREQ='.$rrule['FREQ'].'/',          $ics, "Export Recurrence Frequence");
+        $this->assertRegExp('/RRULE:.*INTERVAL='.$rrule['INTERVAL'].'/',  $ics, "Export Recurrence Interval");
+        $this->assertRegExp('/RRULE:.*UNTIL=20140718T215959Z/',           $ics, "Export Recurrence End date");
+        $this->assertRegExp('/RRULE:.*BYDAY='.$rrule['BYDAY'].'/',        $ics, "Export Recurrence BYDAY");
+        $this->assertRegExp('/EXDATE.*:20131218/',     $ics, "Export Recurrence EXDATE");
+
+        $this->assertContains('BEGIN:VALARM',   $ics, "Export VALARM");
+        $this->assertContains('TRIGGER;RELATED=END:-PT12H', $ics, "Export Alarm trigger");
+
+        $this->assertRegExp('/ATTACH.*;VALUE=BINARY/',                    $ics, "Embed attachment");
+        $this->assertRegExp('/ATTACH.*;ENCODING=BASE64/',                 $ics, "Attachment B64 encoding");
+        $this->assertRegExp('!ATTACH.*;FMTTYPE=text/html!',               $ics, "Attachment mimetype");
+        $this->assertRegExp('!ATTACH.*;X-LABEL=calendar.html!',           $ics, "Attachment filename with X-LABEL");
+
+        $this->assertContains('END:VEVENT',     $ics, "VEVENT encapsulation END");
+        $this->assertContains('END:VCALENDAR',  $ics, "VCALENDAR encapsulation END");
+    }
+
+    /**
+     * @depends test_extended
+     * @depends test_export
+     */
+    function test_export_multiple()
+    {
+        $ical = new libvcalendar();
+        $events = array_merge(
+            $ical->import_from_file(__DIR__ . '/resources/snd.ics', 'UTF-8'),
+            $ical->import_from_file(__DIR__ . '/resources/multiple.ics', 'UTF-8')
+        );
+
+        $num = count($events);
+        $ics = $ical->export($events, null, false);
+
+        $this->assertContains('BEGIN:VCALENDAR', $ics, "VCALENDAR encapsulation BEGIN");
+        $this->assertContains('END:VCALENDAR',   $ics, "VCALENDAR encapsulation END");
+        $this->assertEquals($num, substr_count($ics, 'BEGIN:VEVENT'), "VEVENT encapsulation BEGIN");
+        $this->assertEquals($num, substr_count($ics, 'END:VEVENT'),   "VEVENT encapsulation END");
+    }
+
+    /**
+     * @depends test_export
+     */
+    function test_export_recurrence_exceptions()
+    {
+        $ical = new libvcalendar();
+        $events = $ical->import_from_file(__DIR__ . '/resources/recurring.ics', 'UTF-8');
+
+        // add exceptions
+        $event = $events[0];
+        unset($event['recurrence']['EXCEPTIONS']);
+
+        $exception1 = $event;
+        $exception1['start'] = clone $event['start'];
+        $exception1['start']->setDate(2013, 8, 14);
+        $exception1['end'] = clone $event['end'];
+        $exception1['end']->setDate(2013, 8, 14);
+
+        $exception2 = $event;
+        $exception2['start'] = clone $event['start'];
+        $exception2['start']->setDate(2013, 11, 13);
+        $exception2['end'] = clone $event['end'];
+        $exception2['end']->setDate(2013, 11, 13);
+        $exception2['title'] = 'Recurring Exception';
+
+        $events[0]['recurrence']['EXCEPTIONS'] = array($exception1, $exception2);
+
+        $ics = $ical->export($events, null, false);
+
+        $num = count($events[0]['recurrence']['EXCEPTIONS']) + 1;
+        $this->assertEquals($num, substr_count($ics, 'BEGIN:VEVENT'),       "VEVENT encapsulation BEGIN");
+        $this->assertEquals($num, substr_count($ics, 'UID:'.$event['uid']), "Recurrence Exceptions with same UID");
+        $this->assertEquals($num, substr_count($ics, 'END:VEVENT'),         "VEVENT encapsulation END");
+
+        $this->assertContains('RECURRENCE-ID;TZID=Europe/Zurich:20130814', $ics, "Recurrence-ID (1) being the exception date");
+        $this->assertContains('RECURRENCE-ID;TZID=Europe/Zurich:20131113', $ics, "Recurrence-ID (2) being the exception date");
+        $this->assertContains('SUMMARY:'.$exception2['title'], $ics, "Exception title");
+    }
+
+    function test_export_valid_rrules()
+    {
+        $event = array(
+            'uid' => '1234567890',
+            'start' => new DateTime('now'),
+            'end' => new DateTime('now + 30min'),
+            'title' => 'test_export_valid_rrules',
+            'recurrence' => array(
+                'FREQ' => 'DAILY',
+                'COUNT' => 5,
+                'EXDATE' => array(),
+                'RDATE' => array(),
+            ),
+        );
+        $ical = new libvcalendar();
+        $ics = $ical->export(array($event), null, false, null, false);
+
+        $this->assertNotContains('EXDATE=', $ics);
+        $this->assertNotContains('RDATE=', $ics);
+    }
+
+    /**
+     *
+     */
+    function test_export_rdate()
+    {
+        $ical = new libvcalendar();
+        $events = $ical->import_from_file(__DIR__ . '/resources/multiple-rdate.ics', 'UTF-8');
+        $ics = $ical->export($events, null, false);
+
+        $this->assertContains('RDATE:20140520T020000Z', $ics, "VALUE=PERIOD is translated into single DATE-TIME values");
+    }
+
+    /**
+     * @depends test_export
+     */
+    function test_export_direct()
+    {
+        $ical = new libvcalendar();
+        $events = $ical->import_from_file(__DIR__ . '/resources/multiple.ics', 'UTF-8');
+        $num = count($events);
+
+        ob_start();
+        $return = $ical->export($events, null, true);
+        $output = ob_get_contents();
+        ob_end_clean();
+
+        $this->assertTrue($return, "Return true on successful writing");
+        $this->assertContains('BEGIN:VCALENDAR', $output, "VCALENDAR encapsulation BEGIN");
+        $this->assertContains('END:VCALENDAR',   $output, "VCALENDAR encapsulation END");
+        $this->assertEquals($num, substr_count($output, 'BEGIN:VEVENT'), "VEVENT encapsulation BEGIN");
+        $this->assertEquals($num, substr_count($output, 'END:VEVENT'),   "VEVENT encapsulation END");
+    }
+
+    function test_datetime()
+    {
+        $ical = new libvcalendar();
+        $cal  = new \Sabre\VObject\Component\VCalendar();
+        $localtime = $ical->datetime_prop($cal, 'DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('Europe/Berlin')));
+        $localdate = $ical->datetime_prop($cal, 'DTSTART', new DateTime('2013-09-01', new DateTimeZone('Europe/Berlin')), false, true);
+        $utctime   = $ical->datetime_prop($cal, 'DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('UTC')));
+        $asutctime = $ical->datetime_prop($cal, 'DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('Europe/Berlin')), true);
+
+        $this->assertContains('TZID=Europe/Berlin', $localtime->serialize());
+        $this->assertContains('VALUE=DATE', $localdate->serialize());
+        $this->assertContains('20130901T120000Z', $utctime->serialize());
+        $this->assertContains('20130901T100000Z', $asutctime->serialize());
+    }
+
+    function test_get_vtimezone()
+    {
+        $vtz = libvcalendar::get_vtimezone('Europe/Berlin', strtotime('2014-08-22T15:00:00+02:00'));
+        $this->assertInstanceOf('\Sabre\VObject\Component', $vtz, "VTIMEZONE is a Component object");
+        $this->assertEquals('Europe/Berlin', $vtz->TZID);
+        $this->assertEquals('4', $vtz->{'X-MICROSOFT-CDO-TZID'});
+
+        // check for transition to daylight saving time which is BEFORE the given date
+        $dst = reset($vtz->select('DAYLIGHT'));
+        $this->assertEquals('DAYLIGHT', $dst->name);
+        $this->assertEquals('20140330T010000', $dst->DTSTART);
+        $this->assertEquals('+0100', $dst->TZOFFSETFROM);
+        $this->assertEquals('+0200', $dst->TZOFFSETTO);
+        $this->assertEquals('CEST', $dst->TZNAME);
+
+        // check (last) transition to standard time which is AFTER the given date
+        $std = end($vtz->select('STANDARD'));
+        $this->assertEquals('STANDARD', $std->name);
+        $this->assertEquals('20141026T010000', $std->DTSTART);
+        $this->assertEquals('+0200', $std->TZOFFSETFROM);
+        $this->assertEquals('+0100', $std->TZOFFSETTO);
+        $this->assertEquals('CET', $std->TZNAME);
+
+        // unknown timezone
+        $vtz = libvcalendar::get_vtimezone('America/Foo Bar');
+        $this->assertEquals(false, $vtz);
+
+        // invalid input data
+        $vtz = libvcalendar::get_vtimezone(new DateTime());
+        $this->assertEquals(false, $vtz);
+
+        // DateTimezone as input data
+        $vtz = libvcalendar::get_vtimezone(new DateTimezone('Pacific/Chatham'));
+        $this->assertInstanceOf('\Sabre\VObject\Component', $vtz);
+        $this->assertContains('TZOFFSETFROM:+1245', $vtz->serialize());
+        $this->assertContains('TZOFFSETTO:+1345', $vtz->serialize());
+    }
+
+    function get_attachment_data($id, $event)
+    {
+        return $this->attachment_data;
+    }
+}
+