--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2022 Christopher Ng <chrng8@gmail.com>
+ *
+ * @author Christopher Ng <chrng8@gmail.com>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * 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/>.
+ *
+ */
+
+namespace OCA\DAV\Tests\integration\UserMigration;
+
+use function Safe\scandir;
+use OCA\DAV\AppInfo\Application;
+use OCA\DAV\UserMigration\CalendarMigrator;
+use OCP\AppFramework\App;
+use OCP\IUserManager;
+use Sabre\VObject\Component as VObjectComponent;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Property as VObjectProperty;
+use Sabre\VObject\Reader as VObjectReader;
+use Sabre\VObject\UUIDUtil;
+use Test\TestCase;
+
+/**
+ * @group DB
+ */
+class CalendarMigratorTest extends TestCase {
+
+ private IUserManager $userManager;
+
+ private CalendarMigrator $migrator;
+
+ private const ASSETS_DIR = __DIR__ . '/assets/';
+
+ protected function setUp(): void {
+ $app = new App(Application::APP_ID);
+ $container = $app->getContainer();
+
+ $this->userManager = $container->get(IUserManager::class);
+ $this->migrator = $container->get(CalendarMigrator::class);
+ }
+
+ public function dataAssets(): array {
+ return array_map(
+ function (string $filename) {
+ /** @var VCalendar $vCalendar */
+ $vCalendar = VObjectReader::read(
+ fopen(self::ASSETS_DIR . $filename, 'r'),
+ VObjectReader::OPTION_FORGIVING,
+ );
+ [$initialCalendarUri, $ext] = explode('.', $filename, 2);
+ return [UUIDUtil::getUUID(), $filename, $initialCalendarUri, $vCalendar];
+ },
+ array_diff(
+ scandir(self::ASSETS_DIR),
+ // Exclude current and parent directories
+ ['.', '..'],
+ ),
+ );
+ }
+
+ private function getProperties(VCalendar $vCalendar): array {
+ return array_map(
+ fn (VObjectProperty $property) => $property->serialize(),
+ array_values(array_filter(
+ $vCalendar->children(),
+ fn (mixed $child) => $child instanceof VObjectProperty,
+ )),
+ );
+ }
+
+ private function getComponents(VCalendar $vCalendar): array {
+ return array_map(
+ // Elements of the serialized blob are sorted
+ fn (VObjectComponent $component) => $component->serialize(),
+ $vCalendar->getComponents(),
+ );
+ }
+
+ private function getSanitizedComponents(VCalendar $vCalendar): array {
+ return array_map(
+ // Elements of the serialized blob are sorted
+ fn (VObjectComponent $component) => $this->migrator->sanitizeComponent($component)->serialize(),
+ $vCalendar->getComponents(),
+ );
+ }
+
+ /**
+ * @dataProvider dataAssets
+ */
+ public function testImportExportAsset(string $userId, string $filename, string $initialCalendarUri, VCalendar $importCalendar): void {
+ $user = $this->userManager->createUser($userId, 'topsecretpassword');
+
+ $problems = $importCalendar->validate();
+ $this->assertEmpty($problems);
+
+ $this->migrator->importCalendar($user, $filename, $initialCalendarUri, $importCalendar);
+
+ $calendarExports = $this->migrator->getCalendarExports($user);
+ $this->assertCount(1, $calendarExports);
+
+ /** @var VCalendar $exportCalendar */
+ ['vCalendar' => $exportCalendar] = reset($calendarExports);
+
+ $this->assertEqualsCanonicalizing(
+ $this->getProperties($importCalendar),
+ $this->getProperties($exportCalendar),
+ );
+
+ $this->assertEqualsCanonicalizing(
+ // Components are sanitized on import
+ $this->getSanitizedComponents($importCalendar),
+ $this->getComponents($exportCalendar),
+ );
+ }
+}
--- /dev/null
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//SabreDAV//SabreDAV//EN
+CALSCALE:GREGORIAN
+X-WR-CALNAME:Alarms
+X-APPLE-CALENDAR-COLOR:#0082c9
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+DTSTART:19810329T020000
+TZNAME:GMT+2
+TZOFFSETTO:+0200
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+DTSTART:19961027T030000
+TZNAME:GMT+1
+TZOFFSETTO:+0100
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20160809T163629Z
+UID:0AD16F58-01B3-463B-A215-FD09FC729A02
+DTEND;TZID=Europe/Berlin:20160816T100000
+TRANSP:OPAQUE
+SUMMARY:Test Europe Berlin
+DTSTART;TZID=Europe/Berlin:20160816T090000
+DTSTAMP:20160809T163632Z
+SEQUENCE:0
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER;RELATED=START:P1DT9H
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER;VALUE=DATE-TIME:20200306T083000Z
+END:VALARM
+END:VEVENT
+END:VCALENDAR
--- /dev/null
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//SabreDAV//SabreDAV//EN
+CALSCALE:GREGORIAN
+X-WR-CALNAME:Attendees
+X-APPLE-CALENDAR-COLOR:#0082c9
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+DTSTART:19810329T020000
+TZNAME:GMT+2
+TZOFFSETTO:+0200
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+DTSTART:19961027T030000
+TZNAME:GMT+1
+TZOFFSETTO:+0100
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20160809T163629Z
+UID:0AD16F58-01B3-463B-A215-FD09FC729A02
+DTEND;TZID=Europe/Berlin:20160816T100000
+TRANSP:OPAQUE
+SUMMARY:Test Europe Berlin
+DTSTART;TZID=Europe/Berlin:20160816T090000
+DTSTAMP:20160809T163632Z
+SEQUENCE:0
+ORGANIZER;CN=John Smith:mailto:jsmith@example.com
+ATTENDEE;RSVP=TRUE;ROLE=REQ-PARTICIPANT:mailto:jsmith@example.com
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=TENTATIVE;CN=Henry Cabot:mailto:hcabot@example.com
+ATTENDEE;ROLE=REQ-PARTICIPANT;DELEGATED-FROM="mailto:bob@example.com";PARTSTAT=ACCEPTED;CN=Jane Doe:mailto:jdoe@example.com
+ATTENDEE;ROLE=NON-PARTICIPANT;PARTSTAT=DELEGATED;DELEGATED-TO="mailto:hcabot@example.com";CN=The Big Cheese:mailto:iamboss@example.com
+END:VEVENT
+END:VCALENDAR
--- /dev/null
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//SabreDAV//SabreDAV//EN
+CALSCALE:GREGORIAN
+X-WR-CALNAME:Categories
+X-APPLE-CALENDAR-COLOR:#0082c9
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+DTSTART:19810329T020000
+TZNAME:GMT+2
+TZOFFSETTO:+0200
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+DTSTART:19961027T030000
+TZNAME:GMT+1
+TZOFFSETTO:+0100
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20160809T163629Z
+UID:0AD16F58-01B3-463B-A215-FD09FC729A02
+DTEND;TZID=Europe/Berlin:20160816T100000
+TRANSP:OPAQUE
+SUMMARY:Test Europe Berlin
+DTSTART;TZID=Europe/Berlin:20160816T090000
+DTSTAMP:20160809T163632Z
+SEQUENCE:0
+CATEGORIES:BUSINESS,HUMAN RESOURCES
+END:VEVENT
+END:VCALENDAR
--- /dev/null
+BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//SabreDAV//SabreDAV//EN
+X-WR-CALNAME:Complex alarm recurring
+X-APPLE-CALENDAR-COLOR:#0082c9
+BEGIN:VEVENT
+CREATED:20220218T031205Z
+DTSTAMP:20220218T031409Z
+LAST-MODIFIED:20220218T031409Z
+SEQUENCE:2
+UID:b78f3a65-413d-4fa7-b125-1232bc6a2c72
+DTSTART;VALUE=DATE:20220217
+DTEND;VALUE=DATE:20220218
+STATUS:TENTATIVE
+SUMMARY:Complex recurring event
+LOCATION:Antarctica
+DESCRIPTION:Event description
+CLASS:CONFIDENTIAL
+TRANSP:TRANSPARENT
+CATEGORIES:Personal,Travel,Special occasion
+COLOR:khaki
+RRULE:FREQ=MONTHLY;COUNT=10;BYDAY=WE;BYSETPOS=2
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER;RELATED=START:-P6DT15H
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER;RELATED=START:-PT15H
+END:VALARM
+END:VEVENT
+END:VCALENDAR
--- /dev/null
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//SabreDAV//SabreDAV//EN
+CALSCALE:GREGORIAN
+X-WR-CALNAME:Complex recurrence
+X-APPLE-CALENDAR-COLOR:#0082c9
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+DTSTART:19810329T020000
+TZNAME:GMT+2
+TZOFFSETTO:+0200
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+DTSTART:19961027T030000
+TZNAME:GMT+1
+TZOFFSETTO:+0100
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20160809T163629Z
+DTSTAMP:20160809T163629Z
+UID:0AD16F58-01B3-463B-A215-FD09FC729A02
+SUMMARY:TEST
+RRULE:FREQ=WEEKLY
+DTSTART;TZID=Europe/Berlin:20200301T150000
+DTEND;TZID=Europe/Berlin:20200301T160000
+END:VEVENT
+BEGIN:VEVENT
+CREATED:20160809T163629Z
+DTSTAMP:20160809T163629Z
+UID:0AD16F58-01B3-463B-A215-FD09FC729A02
+SUMMARY:TEST EX 1
+RECURRENCE-ID;TZID=Europe/Berlin:20200308T150000
+DTSTART;TZID=Europe/Berlin:20200401T150000
+DTEND;TZID=Europe/Berlin:20200401T160000
+END:VEVENT
+BEGIN:VEVENT
+CREATED:20160809T163629Z
+DTSTAMP:20160809T163629Z
+UID:0AD16F58-01B3-463B-A215-FD09FC729A02
+SUMMARY:TEST EX 2
+RECURRENCE-ID;TZID=Europe/Berlin:20200315T150000
+DTSTART;TZID=Europe/Berlin:20201101T150000
+DTEND;TZID=Europe/Berlin:20201101T160000
+END:VEVENT
+BEGIN:VEVENT
+CREATED:20160809T163629Z
+DTSTAMP:20160809T163629Z
+UID:0AD16F58-01B3-463B-A215-FD09FC729A02
+SUMMARY:TEST EX 3
+RECURRENCE-ID;TZID=Europe/Berlin:20200405T150000
+DTSTART;TZID=Europe/Berlin:20200406T150000
+DTEND;TZID=Europe/Berlin:20200406T160000
+END:VEVENT
+BEGIN:VEVENT
+CREATED:20160809T163629Z
+DTSTAMP:20160809T163629Z
+UID:0AD16F58-01B3-463B-A215-FD09FC729A02
+SUMMARY:TEST EX 4
+RECURRENCE-ID;TZID=Europe/Berlin:20200412T150000
+DTSTART;TZID=Europe/Berlin:20201201T150000
+DTEND;TZID=Europe/Berlin:20201201T160000
+END:VEVENT
+BEGIN:VEVENT
+CREATED:20160809T163629Z
+DTSTAMP:20160809T163629Z
+UID:0AD16F58-01B3-463B-A215-FD09FC729A02
+SUMMARY:TEST EX 5
+RECURRENCE-ID;TZID=Europe/Berlin:20200426T150000
+DTSTART;TZID=Europe/Berlin:20200410T150000
+DTEND;TZID=Europe/Berlin:20200410T160000
+END:VEVENT
+BEGIN:VEVENT
+CREATED:20160809T163629Z
+DTSTAMP:20160809T163629Z
+UID:0AD16F58-01B3-463B-A215-FD09FC729A02
+SUMMARY:INVALID RECURRENCE-ID
+RECURRENCE-ID;TZID=Europe/Berlin:20200427T150000
+DTSTART;TZID=Europe/Berlin:20200420T150000
+DTEND;TZID=Europe/Berlin:20200420T160000
+END:VEVENT
+END:VCALENDAR
--- /dev/null
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//SabreDAV//SabreDAV//EN
+X-WR-CALNAME:Personal
+X-APPLE-CALENDAR-COLOR:#f264ab
+CALSCALE:GREGORIAN
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+DTSTART:19810329T020000
+TZNAME:GMT+2
+TZOFFSETTO:+0200
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+DTSTART:19961027T030000
+TZNAME:GMT+1
+TZOFFSETTO:+0100
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20160809T163629Z
+UID:0AD16F58-01B3-463B-A215-FD09FC729A02
+DTEND;TZID=Europe/Berlin:20160816T100000
+TRANSP:OPAQUE
+SUMMARY:Test Europe Berlin
+DTSTART;TZID=Europe/Berlin:20160816T090000
+DTSTAMP:20160809T163632Z
+SEQUENCE:0
+END:VEVENT
+END:VCALENDAR
--- /dev/null
+BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//SabreDAV//SabreDAV//EN
+X-WR-CALNAME:Multiple and recurring
+X-APPLE-CALENDAR-COLOR:#795AAB
+BEGIN:VEVENT
+CREATED:20220218T044833Z
+DTSTAMP:20220218T044837Z
+LAST-MODIFIED:20220218T044837Z
+SEQUENCE:2
+UID:dc343863-b57c-43a5-9ba4-19ae2740cd7e
+DTSTART;VALUE=DATE:20220607
+DTEND;VALUE=DATE:20220608
+STATUS:CONFIRMED
+SUMMARY:Event 4
+END:VEVENT
+BEGIN:VEVENT
+CREATED:20220218T044806Z
+DTSTAMP:20220218T044809Z
+LAST-MODIFIED:20220218T044809Z
+SEQUENCE:2
+UID:ae28b642-7e11-4e16-818a-06c89ae74f44
+DTSTART;VALUE=DATE:20220218
+DTEND;VALUE=DATE:20220219
+STATUS:CONFIRMED
+SUMMARY:Event 1
+END:VEVENT
+BEGIN:VEVENT
+CREATED:20220218T044820Z
+DTSTAMP:20220218T044827Z
+LAST-MODIFIED:20220218T044827Z
+SEQUENCE:2
+UID:5edfb90e-44b3-47c6-863b-f632327f46f0
+DTSTART;VALUE=DATE:20220518
+DTEND;VALUE=DATE:20220519
+STATUS:CONFIRMED
+SUMMARY:Event 3
+END:VEVENT
+BEGIN:VEVENT
+CREATED:20220218T044810Z
+DTSTAMP:20220218T044814Z
+LAST-MODIFIED:20220218T044814Z
+SEQUENCE:2
+UID:9789f684-1cf9-4ee7-90cb-54cdec6a6f03
+DTSTART;VALUE=DATE:20220223
+DTEND;VALUE=DATE:20220224
+STATUS:CONFIRMED
+SUMMARY:Event 2
+END:VEVENT
+BEGIN:VEVENT
+CREATED:20220218T044932Z
+DTSTAMP:20220218T044945Z
+LAST-MODIFIED:20220218T044945Z
+SEQUENCE:2
+UID:d5bdaf0e-d6c7-4e30-a730-04928976a1d2
+DTSTART;VALUE=DATE:20221102
+DTEND;VALUE=DATE:20221103
+STATUS:CONFIRMED
+SUMMARY:Recurring event
+RRULE:FREQ=WEEKLY;BYDAY=WE
+END:VEVENT
+BEGIN:VEVENT
+CREATED:20220218T044915Z
+DTSTAMP:20220218T044918Z
+LAST-MODIFIED:20220218T044918Z
+SEQUENCE:2
+UID:11c3d9fd-fb54-4384-ab68-40b5d6acef6f
+DTSTART;VALUE=DATE:20221010
+DTEND;VALUE=DATE:20221011
+STATUS:CONFIRMED
+SUMMARY:Event 5
+END:VEVENT
+END:VCALENDAR
--- /dev/null
+BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//SabreDAV//SabreDAV//EN
+X-WR-CALNAME:Multiple
+X-APPLE-CALENDAR-COLOR:#795AAB
+BEGIN:VEVENT
+CREATED:20220218T044833Z
+DTSTAMP:20220218T044837Z
+LAST-MODIFIED:20220218T044837Z
+SEQUENCE:2
+UID:dc343863-b57c-43a5-9ba4-19ae2740cd7e
+DTSTART;VALUE=DATE:20220607
+DTEND;VALUE=DATE:20220608
+STATUS:CONFIRMED
+SUMMARY:Event 4
+END:VEVENT
+BEGIN:VEVENT
+CREATED:20220218T044806Z
+DTSTAMP:20220218T044809Z
+LAST-MODIFIED:20220218T044809Z
+SEQUENCE:2
+UID:ae28b642-7e11-4e16-818a-06c89ae74f44
+DTSTART;VALUE=DATE:20220218
+DTEND;VALUE=DATE:20220219
+STATUS:CONFIRMED
+SUMMARY:Event 1
+END:VEVENT
+BEGIN:VEVENT
+CREATED:20220218T044820Z
+DTSTAMP:20220218T044827Z
+LAST-MODIFIED:20220218T044827Z
+SEQUENCE:2
+UID:5edfb90e-44b3-47c6-863b-f632327f46f0
+DTSTART;VALUE=DATE:20220518
+DTEND;VALUE=DATE:20220519
+STATUS:CONFIRMED
+SUMMARY:Event 3
+END:VEVENT
+BEGIN:VEVENT
+CREATED:20220218T044810Z
+DTSTAMP:20220218T044814Z
+LAST-MODIFIED:20220218T044814Z
+SEQUENCE:2
+UID:9789f684-1cf9-4ee7-90cb-54cdec6a6f03
+DTSTART;VALUE=DATE:20220223
+DTEND;VALUE=DATE:20220224
+STATUS:CONFIRMED
+SUMMARY:Event 2
+END:VEVENT
+BEGIN:VEVENT
+CREATED:20220218T044915Z
+DTSTAMP:20220218T044918Z
+LAST-MODIFIED:20220218T044918Z
+SEQUENCE:2
+UID:11c3d9fd-fb54-4384-ab68-40b5d6acef6f
+DTSTART;VALUE=DATE:20221010
+DTEND;VALUE=DATE:20221011
+STATUS:CONFIRMED
+SUMMARY:Event 5
+END:VEVENT
+END:VCALENDAR
--- /dev/null
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//SabreDAV//SabreDAV//EN
+CALSCALE:GREGORIAN
+X-WR-CALNAME:Recurring
+X-APPLE-CALENDAR-COLOR:#0082c9
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+DTSTART:19810329T020000
+TZNAME:GMT+2
+TZOFFSETTO:+0200
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+DTSTART:19961027T030000
+TZNAME:GMT+1
+TZOFFSETTO:+0100
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20160809T163629Z
+DTSTAMP:20160809T163629Z
+UID:0AD16F58-01B3-463B-A215-FD09FC729A02
+SUMMARY:TEST
+RRULE:FREQ=WEEKLY
+DTSTART;TZID=Europe/Berlin:20200301T150000
+DTEND;TZID=Europe/Berlin:20200301T160000
+END:VEVENT
+BEGIN:VEVENT
+CREATED:20160809T163629Z
+DTSTAMP:20160809T163629Z
+UID:0AD16F58-01B3-463B-A215-FD09FC729A02
+SUMMARY:TEST EX 1
+RECURRENCE-ID;TZID=Europe/Berlin:20200308T150000
+DTSTART;TZID=Europe/Berlin:20200401T150000
+DTEND;TZID=Europe/Berlin:20200401T160000
+END:VEVENT
+BEGIN:VEVENT
+CREATED:20160809T163629Z
+DTSTAMP:20160809T163629Z
+UID:0AD16F58-01B3-463B-A215-FD09FC729A02
+SUMMARY:TEST EX 2
+RECURRENCE-ID;TZID=Europe/Berlin:20200315T150000
+DTSTART;TZID=Europe/Berlin:20201101T150000
+DTEND;TZID=Europe/Berlin:20201101T160000
+END:VEVENT
+BEGIN:VEVENT
+CREATED:20160809T163629Z
+DTSTAMP:20160809T163629Z
+UID:0AD16F58-01B3-463B-A215-FD09FC729A02
+SUMMARY:TEST EX 3
+RECURRENCE-ID;TZID=Europe/Berlin:20200405T150000
+DTSTART;TZID=Europe/Berlin:20200406T150000
+DTEND;TZID=Europe/Berlin:20200406T160000
+END:VEVENT
+BEGIN:VEVENT
+CREATED:20160809T163629Z
+DTSTAMP:20160809T163629Z
+UID:0AD16F58-01B3-463B-A215-FD09FC729A02
+SUMMARY:TEST EX 4
+RECURRENCE-ID;TZID=Europe/Berlin:20200412T150000
+DTSTART;TZID=Europe/Berlin:20201201T150000
+DTEND;TZID=Europe/Berlin:20201201T160000
+END:VEVENT
+BEGIN:VEVENT
+CREATED:20160809T163629Z
+DTSTAMP:20160809T163629Z
+UID:0AD16F58-01B3-463B-A215-FD09FC729A02
+SUMMARY:TEST EX 5
+RECURRENCE-ID;TZID=Europe/Berlin:20200426T150000
+DTSTART;TZID=Europe/Berlin:20200410T150000
+DTEND;TZID=Europe/Berlin:20200410T160000
+END:VEVENT
+BEGIN:VEVENT
+CREATED:20160809T163629Z
+DTSTAMP:20160809T163629Z
+UID:0AD16F58-01B3-463B-A215-FD09FC729A02
+SUMMARY:INVALID RECURRENCE-ID
+RECURRENCE-ID;TZID=Europe/Berlin:20200427T150000
+DTSTART;TZID=Europe/Berlin:20200420T150000
+DTEND;TZID=Europe/Berlin:20200420T160000
+END:VEVENT
+END:VCALENDAR
--- /dev/null
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//SabreDAV//SabreDAV//EN
+X-WR-CALNAME:Timed
+X-APPLE-CALENDAR-COLOR:#0082c9
+CALSCALE:GREGORIAN
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+DTSTART:19810329T020000
+TZNAME:GMT+2
+TZOFFSETTO:+0200
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+DTSTART:19961027T030000
+TZNAME:GMT+1
+TZOFFSETTO:+0100
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20160809T163629Z
+UID:0AD16F58-01B3-463B-A215-FD09FC729A02
+DTEND;TZID=Europe/Berlin:20160816T100000
+TRANSP:OPAQUE
+SUMMARY:Test Europe Berlin
+DTSTART;TZID=Europe/Berlin:20160816T090000
+DTSTAMP:20160809T163632Z
+SEQUENCE:0
+END:VEVENT
+END:VCALENDAR
--- /dev/null
+BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//SabreDAV//SabreDAV//EN
+X-WR-CALNAME:Journal Todo Event
+X-APPLE-CALENDAR-COLOR:#0082c9
+BEGIN:VJOURNAL
+UID:19970901T130000Z-123405@example.com
+DTSTAMP:19970901T130000Z
+DTSTART;VALUE=DATE:19970317
+SUMMARY:Staff meeting minutes
+DESCRIPTION:1. Staff meeting: Participants include Joe\,
+ Lisa\, and Bob. Aurora project plans were reviewed.
+ There is currently no budget reserves for this project.
+ Lisa will escalate to management. Next meeting on Tuesday.\n
+ 2. Telephone Conference: ABC Corp. sales representative
+ called to discuss new printer. Promised to get us a demo by
+ Friday.\n3. Henry Miller (Handsoff Insurance): Car was
+ totaled by tree. Is looking into a loaner car. 555-2323
+ (tel).
+END:VJOURNAL
+BEGIN:VTODO
+UID:20070313T123432Z-456553@example.com
+DTSTAMP:20070313T123432Z
+DUE;VALUE=DATE:20070501
+SUMMARY:Submit Quebec Income Tax Return for 2006
+CLASS:CONFIDENTIAL
+CATEGORIES:FAMILY,FINANCE
+STATUS:NEEDS-ACTION
+END:VTODO
+BEGIN:VEVENT
+CREATED:20160809T163629Z
+UID:0AD16F58-01B3-463B-A215-FD09FC729A02
+DTEND:20160816T100000
+TRANSP:OPAQUE
+SUMMARY:Test Event
+DTSTART:20160816T090000
+DTSTAMP:20160809T163632Z
+SEQUENCE:0
+END:VEVENT
+END:VCALENDAR
--- /dev/null
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//SabreDAV//SabreDAV//EN
+CALSCALE:GREGORIAN
+X-WR-CALNAME:Journal
+X-APPLE-CALENDAR-COLOR:#0082c9
+BEGIN:VJOURNAL
+UID:19970901T130000Z-123405@example.com
+DTSTAMP:19970901T130000Z
+DTSTART;VALUE=DATE:19970317
+SUMMARY:Staff meeting minutes
+DESCRIPTION:1. Staff meeting: Participants include Joe\,
+ Lisa\, and Bob. Aurora project plans were reviewed.
+ There is currently no budget reserves for this project.
+ Lisa will escalate to management. Next meeting on Tuesday.\n
+ 2. Telephone Conference: ABC Corp. sales representative
+ called to discuss new printer. Promised to get us a demo by
+ Friday.\n3. Henry Miller (Handsoff Insurance): Car was
+ totaled by tree. Is looking into a loaner car. 555-2323
+ (tel).
+END:VJOURNAL
+END:VCALENDAR
--- /dev/null
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//SabreDAV//SabreDAV//EN
+CALSCALE:GREGORIAN
+X-WR-CALNAME:Todo
+X-APPLE-CALENDAR-COLOR:#0082c9
+BEGIN:VTODO
+UID:20070313T123432Z-456553@example.com
+DTSTAMP:20070313T123432Z
+DUE;VALUE=DATE:20070501
+SUMMARY:Submit Quebec Income Tax Return for 2006
+CLASS:CONFIDENTIAL
+CATEGORIES:FAMILY,FINANCE
+STATUS:NEEDS-ACTION
+END:VTODO
+END:VCALENDAR