2020-02-10 10:04:38 -05:00
< ? php
2024-07-24 10:11:47 -04:00
declare ( strict_types = 1 );
2020-02-10 10:04:38 -05:00
/**
2024-05-28 06:34:11 -04:00
* SPDX - FileCopyrightText : 2020 Nextcloud GmbH and Nextcloud contributors
* SPDX - License - Identifier : AGPL - 3.0 - or - later
2020-02-10 10:04:38 -05:00
*/
2022-03-31 09:34:43 -04:00
namespace OCA\DAV\Tests\unit\CalDAV\WebcalCaching ;
2020-02-10 10:04:38 -05:00
use OCA\DAV\CalDAV\CalDavBackend ;
2025-11-27 09:56:01 -05:00
use OCA\DAV\CalDAV\Import\ImportService ;
2024-07-24 10:11:47 -04:00
use OCA\DAV\CalDAV\WebcalCaching\Connection ;
2020-02-10 10:04:38 -05:00
use OCA\DAV\CalDAV\WebcalCaching\RefreshWebcalService ;
2024-07-24 10:11:47 -04:00
use OCP\AppFramework\Utility\ITimeFactory ;
2020-02-10 10:04:38 -05:00
use PHPUnit\Framework\MockObject\MockObject ;
2022-03-31 09:34:57 -04:00
use Psr\Log\LoggerInterface ;
2020-09-20 01:47:30 -04:00
use Sabre\DAV\Exception\BadRequest ;
2020-02-10 10:04:38 -05:00
use Sabre\VObject ;
2020-09-20 01:47:30 -04:00
use Sabre\VObject\Recur\NoInstancesException ;
2020-02-10 10:04:38 -05:00
use Test\TestCase ;
class RefreshWebcalServiceTest extends TestCase {
2025-05-24 17:00:05 -04:00
private CalDavBackend & MockObject $caldavBackend ;
private Connection & MockObject $connection ;
private LoggerInterface & MockObject $logger ;
2025-11-27 09:56:01 -05:00
private ImportService & MockObject $importService ;
private ITimeFactory & MockObject $timeFactory ;
2020-02-10 10:04:38 -05:00
protected function setUp () : void {
parent :: setUp ();
$this -> caldavBackend = $this -> createMock ( CalDavBackend :: class );
2024-07-24 10:11:47 -04:00
$this -> connection = $this -> createMock ( Connection :: class );
2022-03-31 09:34:57 -04:00
$this -> logger = $this -> createMock ( LoggerInterface :: class );
2025-11-27 09:56:01 -05:00
$this -> importService = $this -> createMock ( ImportService :: class );
$this -> timeFactory = $this -> createMock ( ITimeFactory :: class );
// Default time factory behavior: current time is far in the future so refresh always happens
$this -> timeFactory -> method ( 'getTime' ) -> willReturn ( PHP_INT_MAX );
$this -> timeFactory -> method ( 'getDateTime' ) -> willReturn ( new \DateTime ());
2020-02-10 10:04:38 -05:00
}
2025-11-27 09:56:01 -05:00
/**
* Helper to create a resource stream from string content
*/
private function createStreamFromString ( string $content ) {
$stream = fopen ( 'php://temp' , 'r+' );
fwrite ( $stream , $content );
rewind ( $stream );
return $stream ;
}
2020-03-16 10:04:21 -04:00
2026-01-07 08:21:48 -05:00
#[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'runDataProvider')]
2025-11-27 09:56:01 -05:00
public function testRun ( string $body , string $format , string $result ) : void {
$refreshWebcalService = new RefreshWebcalService (
$this -> caldavBackend ,
$this -> logger ,
$this -> connection ,
$this -> timeFactory ,
$this -> importService
);
2020-02-10 10:04:38 -05:00
2024-07-24 10:11:47 -04:00
$this -> caldavBackend -> expects ( self :: once ())
2020-02-10 10:04:38 -05:00
-> method ( 'getSubscriptionsForUser' )
-> with ( 'principals/users/testuser' )
2020-03-25 17:21:27 -04:00
-> willReturn ([
2020-02-10 10:04:38 -05:00
[
'id' => '99' ,
'uri' => 'sub456' ,
2024-07-24 10:11:47 -04:00
RefreshWebcalService :: REFRESH_RATE => 'P1D' ,
RefreshWebcalService :: STRIP_TODOS => '1' ,
RefreshWebcalService :: STRIP_ALARMS => '1' ,
RefreshWebcalService :: STRIP_ATTACHMENTS => '1' ,
'source' => 'webcal://foo.bar/bla' ,
'lastmodified' => 0 ,
2020-02-10 10:04:38 -05:00
],
[
'id' => '42' ,
'uri' => 'sub123' ,
2024-07-24 10:11:47 -04:00
RefreshWebcalService :: REFRESH_RATE => 'PT1H' ,
RefreshWebcalService :: STRIP_TODOS => '1' ,
RefreshWebcalService :: STRIP_ALARMS => '1' ,
RefreshWebcalService :: STRIP_ATTACHMENTS => '1' ,
'source' => 'webcal://foo.bar/bla2' ,
'lastmodified' => 0 ,
2020-02-10 10:04:38 -05:00
],
2020-03-25 17:21:27 -04:00
]);
2020-02-10 10:04:38 -05:00
2025-11-27 09:56:01 -05:00
$stream = $this -> createStreamFromString ( $body );
2024-07-24 10:11:47 -04:00
$this -> connection -> expects ( self :: once ())
-> method ( 'queryWebcalFeed' )
2025-11-27 09:56:01 -05:00
-> willReturn ([ 'data' => $stream , 'format' => $format ]);
$this -> caldavBackend -> expects ( self :: once ())
-> method ( 'getLimitedCalendarObjects' )
-> willReturn ([]);
// Create a VCalendar object that will be yielded by the import service
$vCalendar = VObject\Reader :: read ( $result );
$generator = function () use ( $vCalendar ) {
yield $vCalendar ;
};
$this -> importService -> expects ( self :: once ())
-> method ( 'importText' )
-> willReturn ( $generator ());
2024-07-24 10:11:47 -04:00
$this -> caldavBackend -> expects ( self :: once ())
2020-02-10 10:04:38 -05:00
-> method ( 'createCalendarObject' )
2025-11-27 09:56:01 -05:00
-> with (
'42' ,
self :: matchesRegularExpression ( '/^[a-f0-9-]+\.ics$/' ),
$result ,
CalDavBackend :: CALENDAR_TYPE_SUBSCRIPTION
);
2020-02-10 10:04:38 -05:00
$refreshWebcalService -> refreshSubscription ( 'principals/users/testuser' , 'sub123' );
}
2022-03-31 09:34:57 -04:00
2026-01-07 08:21:48 -05:00
#[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'identicalDataProvider')]
2025-11-27 09:56:01 -05:00
public function testRunIdentical ( string $uid , array $calendarObject , string $body , string $format , string $result ) : void {
$refreshWebcalService = new RefreshWebcalService (
$this -> caldavBackend ,
$this -> logger ,
$this -> connection ,
$this -> timeFactory ,
$this -> importService
);
2024-07-24 10:11:47 -04:00
$this -> caldavBackend -> expects ( self :: once ())
-> method ( 'getSubscriptionsForUser' )
-> with ( 'principals/users/testuser' )
-> willReturn ([
[
'id' => '99' ,
'uri' => 'sub456' ,
RefreshWebcalService :: REFRESH_RATE => 'P1D' ,
RefreshWebcalService :: STRIP_TODOS => '1' ,
RefreshWebcalService :: STRIP_ALARMS => '1' ,
RefreshWebcalService :: STRIP_ATTACHMENTS => '1' ,
'source' => 'webcal://foo.bar/bla' ,
'lastmodified' => 0 ,
],
[
'id' => '42' ,
'uri' => 'sub123' ,
RefreshWebcalService :: REFRESH_RATE => 'PT1H' ,
RefreshWebcalService :: STRIP_TODOS => '1' ,
RefreshWebcalService :: STRIP_ALARMS => '1' ,
RefreshWebcalService :: STRIP_ATTACHMENTS => '1' ,
'source' => 'webcal://foo.bar/bla2' ,
'lastmodified' => 0 ,
],
]);
2025-11-27 09:56:01 -05:00
$stream = $this -> createStreamFromString ( $body );
2024-07-24 10:11:47 -04:00
$this -> connection -> expects ( self :: once ())
-> method ( 'queryWebcalFeed' )
2025-11-27 09:56:01 -05:00
-> willReturn ([ 'data' => $stream , 'format' => $format ]);
2024-07-24 10:11:47 -04:00
$this -> caldavBackend -> expects ( self :: once ())
-> method ( 'getLimitedCalendarObjects' )
-> willReturn ( $calendarObject );
2025-11-27 09:56:01 -05:00
// Create a VCalendar object that will be yielded by the import service
$vCalendar = VObject\Reader :: read ( $result );
2024-07-24 10:11:47 -04:00
2025-11-27 09:56:01 -05:00
$generator = function () use ( $vCalendar ) {
yield $vCalendar ;
};
$this -> importService -> expects ( self :: once ())
-> method ( 'importText' )
-> willReturn ( $generator ());
2024-07-24 10:11:47 -04:00
$this -> caldavBackend -> expects ( self :: never ())
-> method ( 'createCalendarObject' );
2025-11-27 09:56:01 -05:00
$refreshWebcalService -> refreshSubscription ( 'principals/users/testuser' , 'sub123' );
2024-07-24 10:11:47 -04:00
}
2025-11-27 09:56:01 -05:00
public function testSubscriptionNotFound () : void {
$refreshWebcalService = new RefreshWebcalService (
$this -> caldavBackend ,
$this -> logger ,
$this -> connection ,
$this -> timeFactory ,
$this -> importService
);
2024-07-24 10:11:47 -04:00
2025-11-27 09:56:01 -05:00
$this -> caldavBackend -> expects ( self :: once ())
-> method ( 'getSubscriptionsForUser' )
-> with ( 'principals/users/testuser' )
-> willReturn ([]);
$this -> connection -> expects ( self :: never ())
-> method ( 'queryWebcalFeed' );
$refreshWebcalService -> refreshSubscription ( 'principals/users/testuser' , 'sub123' );
}
public function testConnectionReturnsNull () : void {
$refreshWebcalService = new RefreshWebcalService (
$this -> caldavBackend ,
$this -> logger ,
$this -> connection ,
$this -> timeFactory ,
$this -> importService
);
2024-07-24 10:11:47 -04:00
$this -> caldavBackend -> expects ( self :: once ())
-> method ( 'getSubscriptionsForUser' )
-> with ( 'principals/users/testuser' )
-> willReturn ([
[
2025-11-27 09:56:01 -05:00
'id' => '42' ,
'uri' => 'sub123' ,
2024-07-24 10:11:47 -04:00
RefreshWebcalService :: STRIP_TODOS => '1' ,
RefreshWebcalService :: STRIP_ALARMS => '1' ,
RefreshWebcalService :: STRIP_ATTACHMENTS => '1' ,
2025-11-27 09:56:01 -05:00
'source' => 'webcal://foo.bar/bla2' ,
'lastmodified' => 0 ,
2024-07-24 10:11:47 -04:00
],
2025-11-27 09:56:01 -05:00
]);
$this -> connection -> expects ( self :: once ())
-> method ( 'queryWebcalFeed' )
-> willReturn ( null );
$this -> importService -> expects ( self :: never ())
-> method ( 'importText' );
$this -> caldavBackend -> expects ( self :: never ())
-> method ( 'createCalendarObject' );
$refreshWebcalService -> refreshSubscription ( 'principals/users/testuser' , 'sub123' );
}
public function testDeletedObjectsArePurged () : void {
$refreshWebcalService = new RefreshWebcalService (
$this -> caldavBackend ,
$this -> logger ,
$this -> connection ,
$this -> timeFactory ,
$this -> importService
);
$this -> caldavBackend -> expects ( self :: once ())
-> method ( 'getSubscriptionsForUser' )
-> with ( 'principals/users/testuser' )
-> willReturn ([
2024-07-24 10:11:47 -04:00
[
'id' => '42' ,
'uri' => 'sub123' ,
RefreshWebcalService :: STRIP_TODOS => '1' ,
RefreshWebcalService :: STRIP_ALARMS => '1' ,
RefreshWebcalService :: STRIP_ATTACHMENTS => '1' ,
'source' => 'webcal://foo.bar/bla2' ,
2025-11-27 09:56:01 -05:00
'lastmodified' => 0 ,
2024-07-24 10:11:47 -04:00
],
]);
2025-11-27 09:56:01 -05:00
$body = " BEGIN:VCALENDAR \r \n VERSION:2.0 \r \n PRODID:-//Test//Test//EN \r \n BEGIN:VEVENT \r \n UID:new-event \r \n DTSTAMP:20160218T133704Z \r \n DTSTART:20160218T133704Z \r \n SUMMARY:New Event \r \n END:VEVENT \r \n END:VCALENDAR \r \n " ;
$stream = $this -> createStreamFromString ( $body );
$this -> connection -> expects ( self :: once ())
-> method ( 'queryWebcalFeed' )
-> willReturn ([ 'data' => $stream , 'format' => 'ical' ]);
// Existing objects include one that won't be in the feed
$this -> caldavBackend -> expects ( self :: once ())
-> method ( 'getLimitedCalendarObjects' )
-> willReturn ([
'old-deleted-event' => [
'id' => 99 ,
'uid' => 'old-deleted-event' ,
'etag' => 'old-etag' ,
'uri' => 'old-event.ics' ,
],
]);
$vCalendar = VObject\Reader :: read ( $body );
$generator = function () use ( $vCalendar ) {
yield $vCalendar ;
};
$this -> importService -> expects ( self :: once ())
-> method ( 'importText' )
-> willReturn ( $generator ());
$this -> caldavBackend -> expects ( self :: once ())
-> method ( 'createCalendarObject' );
$this -> caldavBackend -> expects ( self :: once ())
-> method ( 'purgeCachedEventsForSubscription' )
-> with ( 42 , [ 99 ], [ 'old-event.ics' ]);
$refreshWebcalService -> refreshSubscription ( 'principals/users/testuser' , 'sub123' );
}
public function testLongUidIsSkipped () : void {
$refreshWebcalService = new RefreshWebcalService (
$this -> caldavBackend ,
$this -> logger ,
$this -> connection ,
$this -> timeFactory ,
$this -> importService
);
$this -> caldavBackend -> expects ( self :: once ())
-> method ( 'getSubscriptionsForUser' )
-> with ( 'principals/users/testuser' )
-> willReturn ([
[
'id' => '42' ,
'uri' => 'sub123' ,
RefreshWebcalService :: STRIP_TODOS => '1' ,
RefreshWebcalService :: STRIP_ALARMS => '1' ,
RefreshWebcalService :: STRIP_ATTACHMENTS => '1' ,
'source' => 'webcal://foo.bar/bla2' ,
'lastmodified' => 0 ,
],
]);
// Create a UID that is longer than 512 characters
$longUid = str_repeat ( 'a' , 513 );
$body = " BEGIN:VCALENDAR \r \n VERSION:2.0 \r \n PRODID:-//Test//Test//EN \r \n BEGIN:VEVENT \r \n UID: $longUid\r\nDTSTAMP :20160218T133704Z \r \n DTSTART:20160218T133704Z \r \n SUMMARY:Event with long UID \r \n END:VEVENT \r \n END:VCALENDAR \r \n " ;
$stream = $this -> createStreamFromString ( $body );
$this -> connection -> expects ( self :: once ())
-> method ( 'queryWebcalFeed' )
-> willReturn ([ 'data' => $stream , 'format' => 'ical' ]);
$this -> caldavBackend -> expects ( self :: once ())
-> method ( 'getLimitedCalendarObjects' )
-> willReturn ([]);
$vCalendar = VObject\Reader :: read ( $body );
$generator = function () use ( $vCalendar ) {
yield $vCalendar ;
};
$this -> importService -> expects ( self :: once ())
-> method ( 'importText' )
-> willReturn ( $generator ());
// Event with long UID should be skipped, so createCalendarObject should never be called
2024-07-24 10:11:47 -04:00
$this -> caldavBackend -> expects ( self :: never ())
-> method ( 'createCalendarObject' );
$refreshWebcalService -> refreshSubscription ( 'principals/users/testuser' , 'sub123' );
}
2026-01-07 08:21:48 -05:00
#[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'runDataProvider')]
2025-11-27 09:56:01 -05:00
public function testRunCreateCalendarNoException ( string $body , string $format , string $result ) : void {
2020-09-20 01:47:30 -04:00
$refreshWebcalService = $this -> getMockBuilder ( RefreshWebcalService :: class )
2025-11-27 09:56:01 -05:00
-> onlyMethods ([ 'getSubscription' ])
-> setConstructorArgs ([ $this -> caldavBackend , $this -> logger , $this -> connection , $this -> timeFactory , $this -> importService ])
2020-09-20 01:47:30 -04:00
-> getMock ();
$refreshWebcalService
-> method ( 'getSubscription' )
-> willReturn ([
'id' => '42' ,
'uri' => 'sub123' ,
2024-07-24 10:11:47 -04:00
RefreshWebcalService :: REFRESH_RATE => 'PT1H' ,
RefreshWebcalService :: STRIP_TODOS => '1' ,
RefreshWebcalService :: STRIP_ALARMS => '1' ,
RefreshWebcalService :: STRIP_ATTACHMENTS => '1' ,
'source' => 'webcal://foo.bar/bla2' ,
'lastmodified' => 0 ,
2020-09-20 01:47:30 -04:00
]);
2025-11-27 09:56:01 -05:00
$stream = $this -> createStreamFromString ( $body );
2024-07-24 10:11:47 -04:00
$this -> connection -> expects ( self :: once ())
-> method ( 'queryWebcalFeed' )
2025-11-27 09:56:01 -05:00
-> willReturn ([ 'data' => $stream , 'format' => $format ]);
2024-07-24 10:11:47 -04:00
$this -> caldavBackend -> expects ( self :: once ())
2025-11-27 09:56:01 -05:00
-> method ( 'getLimitedCalendarObjects' )
-> willReturn ([]);
// Create a VCalendar object that will be yielded by the import service
$vCalendar = VObject\Reader :: read ( $result );
$generator = function () use ( $vCalendar ) {
yield $vCalendar ;
};
$this -> importService -> expects ( self :: once ())
-> method ( 'importText' )
-> willReturn ( $generator ());
2022-03-31 09:34:57 -04:00
2020-09-20 01:47:30 -04:00
$noInstanceException = new NoInstancesException ( " can't add calendar object " );
2024-07-24 10:11:47 -04:00
$this -> caldavBackend -> expects ( self :: once ())
2020-09-20 01:47:30 -04:00
-> method ( 'createCalendarObject' )
-> willThrowException ( $noInstanceException );
2022-03-31 09:34:57 -04:00
2024-07-24 10:11:47 -04:00
$this -> logger -> expects ( self :: once ())
2024-09-25 06:29:12 -04:00
-> method ( 'warning' )
2022-03-18 13:07:24 -04:00
-> with ( 'Unable to create calendar object from subscription {subscriptionId}' , [ 'exception' => $noInstanceException , 'subscriptionId' => '42' , 'source' => 'webcal://foo.bar/bla2' ]);
2020-09-20 01:47:30 -04:00
$refreshWebcalService -> refreshSubscription ( 'principals/users/testuser' , 'sub123' );
}
2026-01-07 08:21:48 -05:00
#[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'runDataProvider')]
2025-11-27 09:56:01 -05:00
public function testRunCreateCalendarBadRequest ( string $body , string $format , string $result ) : void {
2020-09-20 01:47:30 -04:00
$refreshWebcalService = $this -> getMockBuilder ( RefreshWebcalService :: class )
2025-11-27 09:56:01 -05:00
-> onlyMethods ([ 'getSubscription' ])
-> setConstructorArgs ([ $this -> caldavBackend , $this -> logger , $this -> connection , $this -> timeFactory , $this -> importService ])
2020-09-20 01:47:30 -04:00
-> getMock ();
$refreshWebcalService
-> method ( 'getSubscription' )
-> willReturn ([
'id' => '42' ,
'uri' => 'sub123' ,
2024-07-24 10:11:47 -04:00
RefreshWebcalService :: REFRESH_RATE => 'PT1H' ,
RefreshWebcalService :: STRIP_TODOS => '1' ,
RefreshWebcalService :: STRIP_ALARMS => '1' ,
RefreshWebcalService :: STRIP_ATTACHMENTS => '1' ,
'source' => 'webcal://foo.bar/bla2' ,
'lastmodified' => 0 ,
2020-09-20 01:47:30 -04:00
]);
2025-11-27 09:56:01 -05:00
$stream = $this -> createStreamFromString ( $body );
2024-07-24 10:11:47 -04:00
$this -> connection -> expects ( self :: once ())
-> method ( 'queryWebcalFeed' )
2025-11-27 09:56:01 -05:00
-> willReturn ([ 'data' => $stream , 'format' => $format ]);
2024-07-24 10:11:47 -04:00
$this -> caldavBackend -> expects ( self :: once ())
2025-11-27 09:56:01 -05:00
-> method ( 'getLimitedCalendarObjects' )
-> willReturn ([]);
// Create a VCalendar object that will be yielded by the import service
$vCalendar = VObject\Reader :: read ( $result );
$generator = function () use ( $vCalendar ) {
yield $vCalendar ;
};
$this -> importService -> expects ( self :: once ())
-> method ( 'importText' )
-> willReturn ( $generator ());
2022-03-31 09:34:57 -04:00
2020-09-20 01:47:30 -04:00
$badRequestException = new BadRequest ( " can't add reach calendar url " );
2024-07-24 10:11:47 -04:00
$this -> caldavBackend -> expects ( self :: once ())
2020-09-20 01:47:30 -04:00
-> method ( 'createCalendarObject' )
-> willThrowException ( $badRequestException );
2022-03-31 09:34:57 -04:00
2024-07-24 10:11:47 -04:00
$this -> logger -> expects ( self :: once ())
2024-09-25 06:29:12 -04:00
-> method ( 'warning' )
2022-03-18 13:07:24 -04:00
-> with ( 'Unable to create calendar object from subscription {subscriptionId}' , [ 'exception' => $badRequestException , 'subscriptionId' => '42' , 'source' => 'webcal://foo.bar/bla2' ]);
2020-09-20 01:47:30 -04:00
$refreshWebcalService -> refreshSubscription ( 'principals/users/testuser' , 'sub123' );
}
2020-02-10 10:04:38 -05:00
2025-05-24 17:00:05 -04:00
public static function identicalDataProvider () : array {
2025-11-27 09:56:01 -05:00
$icalBody = " BEGIN:VCALENDAR \r \n VERSION:2.0 \r \n PRODID:-//Sabre//Sabre VObject " . VObject\Version :: VERSION . " //EN \r \n CALSCALE:GREGORIAN \r \n BEGIN:VEVENT \r \n UID:12345 \r \n DTSTAMP:20160218T133704Z \r \n DTSTART;VALUE=DATE:19000101 \r \n DTEND;VALUE=DATE:19000102 \r \n RRULE:FREQ=YEARLY \r \n SUMMARY:12345's Birthday (1900) \r \n TRANSP:TRANSPARENT \r \n END:VEVENT \r \n END:VCALENDAR \r \n " ;
$etag = md5 ( $icalBody );
2024-07-24 10:11:47 -04:00
return [
[
'12345' ,
[
'12345' => [
'id' => 42 ,
2025-11-27 09:56:01 -05:00
'etag' => $etag ,
'uri' => 'sub456.ics' ,
2024-07-24 10:11:47 -04:00
],
],
" BEGIN:VCALENDAR \r \n VERSION:2.0 \r \n PRODID:-//Sabre//Sabre VObject 4.1.1//EN \r \n CALSCALE:GREGORIAN \r \n BEGIN:VEVENT \r \n UID:12345 \r \n DTSTAMP:20160218T133704Z \r \n DTSTART;VALUE=DATE:19000101 \r \n DTEND;VALUE=DATE:19000102 \r \n RRULE:FREQ=YEARLY \r \n SUMMARY:12345's Birthday (1900) \r \n TRANSP:TRANSPARENT \r \n END:VEVENT \r \n END:VCALENDAR \r \n " ,
2025-11-27 09:56:01 -05:00
'ical' ,
$icalBody ,
2024-07-24 10:11:47 -04:00
],
];
}
2025-05-24 17:00:05 -04:00
public static function runDataProvider () : array {
2020-02-10 10:04:38 -05:00
return [
[
" BEGIN:VCALENDAR \r \n VERSION:2.0 \r \n PRODID:-//Sabre//Sabre VObject 4.1.1//EN \r \n CALSCALE:GREGORIAN \r \n BEGIN:VEVENT \r \n UID:12345 \r \n DTSTAMP:20160218T133704Z \r \n DTSTART;VALUE=DATE:19000101 \r \n DTEND;VALUE=DATE:19000102 \r \n RRULE:FREQ=YEARLY \r \n SUMMARY:12345's Birthday (1900) \r \n TRANSP:TRANSPARENT \r \n END:VEVENT \r \n END:VCALENDAR \r \n " ,
2025-11-27 09:56:01 -05:00
'ical' ,
2020-02-10 10:04:38 -05:00
" BEGIN:VCALENDAR \r \n VERSION:2.0 \r \n PRODID:-//Sabre//Sabre VObject " . VObject\Version :: VERSION . " //EN \r \n CALSCALE:GREGORIAN \r \n BEGIN:VEVENT \r \n UID:12345 \r \n DTSTAMP:20160218T133704Z \r \n DTSTART;VALUE=DATE:19000101 \r \n DTEND;VALUE=DATE:19000102 \r \n RRULE:FREQ=YEARLY \r \n SUMMARY:12345's Birthday (1900) \r \n TRANSP:TRANSPARENT \r \n END:VEVENT \r \n END:VCALENDAR \r \n " ,
],
];
}
}