diff options
-rw-r--r-- | apps/dav/lib/ServerFactory.php | 5 | ||||
-rw-r--r-- | lib/composer/composer/autoload_classmap.php | 2 | ||||
-rw-r--r-- | lib/composer/composer/autoload_static.php | 2 | ||||
-rw-r--r-- | lib/private/Calendar/AvailabilityResult.php | 28 | ||||
-rw-r--r-- | lib/private/Calendar/Manager.php | 93 | ||||
-rw-r--r-- | lib/public/Calendar/IAvailabilityResult.php | 32 | ||||
-rw-r--r-- | lib/public/Calendar/IManager.php | 19 | ||||
-rw-r--r-- | tests/data/ics/free-busy-request.ics | 14 | ||||
-rw-r--r-- | tests/data/ics/free-busy-request.ics.license | 2 | ||||
-rw-r--r-- | tests/lib/Calendar/ManagerTest.php | 260 |
10 files changed, 457 insertions, 0 deletions
diff --git a/apps/dav/lib/ServerFactory.php b/apps/dav/lib/ServerFactory.php index 7a3f0b27747..f632ee6015d 100644 --- a/apps/dav/lib/ServerFactory.php +++ b/apps/dav/lib/ServerFactory.php @@ -10,10 +10,15 @@ declare(strict_types=1); namespace OCA\DAV; use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer; +use OCA\DAV\Connector\Sabre\Server; class ServerFactory { public function createInviationResponseServer(bool $public): InvitationResponseServer { return new InvitationResponseServer(false); } + + public function createAttendeeAvailabilityServer(): Server { + return (new InvitationResponseServer(false))->getServer(); + } } diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 200e2e75612..ffa8da43873 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -191,6 +191,7 @@ return array( 'OCP\\Cache\\CappedMemoryCache' => $baseDir . '/lib/public/Cache/CappedMemoryCache.php', 'OCP\\Calendar\\BackendTemporarilyUnavailableException' => $baseDir . '/lib/public/Calendar/BackendTemporarilyUnavailableException.php', 'OCP\\Calendar\\Exceptions\\CalendarException' => $baseDir . '/lib/public/Calendar/Exceptions/CalendarException.php', + 'OCP\\Calendar\\IAvailabilityResult' => $baseDir . '/lib/public/Calendar/IAvailabilityResult.php', 'OCP\\Calendar\\ICalendar' => $baseDir . '/lib/public/Calendar/ICalendar.php', 'OCP\\Calendar\\ICalendarEventBuilder' => $baseDir . '/lib/public/Calendar/ICalendarEventBuilder.php', 'OCP\\Calendar\\ICalendarIsShared' => $baseDir . '/lib/public/Calendar/ICalendarIsShared.php', @@ -1117,6 +1118,7 @@ return array( 'OC\\Broadcast\\Events\\BroadcastEvent' => $baseDir . '/lib/private/Broadcast/Events/BroadcastEvent.php', 'OC\\Cache\\CappedMemoryCache' => $baseDir . '/lib/private/Cache/CappedMemoryCache.php', 'OC\\Cache\\File' => $baseDir . '/lib/private/Cache/File.php', + 'OC\\Calendar\\AvailabilityResult' => $baseDir . '/lib/private/Calendar/AvailabilityResult.php', 'OC\\Calendar\\CalendarEventBuilder' => $baseDir . '/lib/private/Calendar/CalendarEventBuilder.php', 'OC\\Calendar\\CalendarQuery' => $baseDir . '/lib/private/Calendar/CalendarQuery.php', 'OC\\Calendar\\Manager' => $baseDir . '/lib/private/Calendar/Manager.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index bf9385c1741..89c0cd4395b 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -232,6 +232,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\Cache\\CappedMemoryCache' => __DIR__ . '/../../..' . '/lib/public/Cache/CappedMemoryCache.php', 'OCP\\Calendar\\BackendTemporarilyUnavailableException' => __DIR__ . '/../../..' . '/lib/public/Calendar/BackendTemporarilyUnavailableException.php', 'OCP\\Calendar\\Exceptions\\CalendarException' => __DIR__ . '/../../..' . '/lib/public/Calendar/Exceptions/CalendarException.php', + 'OCP\\Calendar\\IAvailabilityResult' => __DIR__ . '/../../..' . '/lib/public/Calendar/IAvailabilityResult.php', 'OCP\\Calendar\\ICalendar' => __DIR__ . '/../../..' . '/lib/public/Calendar/ICalendar.php', 'OCP\\Calendar\\ICalendarEventBuilder' => __DIR__ . '/../../..' . '/lib/public/Calendar/ICalendarEventBuilder.php', 'OCP\\Calendar\\ICalendarIsShared' => __DIR__ . '/../../..' . '/lib/public/Calendar/ICalendarIsShared.php', @@ -1158,6 +1159,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Broadcast\\Events\\BroadcastEvent' => __DIR__ . '/../../..' . '/lib/private/Broadcast/Events/BroadcastEvent.php', 'OC\\Cache\\CappedMemoryCache' => __DIR__ . '/../../..' . '/lib/private/Cache/CappedMemoryCache.php', 'OC\\Cache\\File' => __DIR__ . '/../../..' . '/lib/private/Cache/File.php', + 'OC\\Calendar\\AvailabilityResult' => __DIR__ . '/../../..' . '/lib/private/Calendar/AvailabilityResult.php', 'OC\\Calendar\\CalendarEventBuilder' => __DIR__ . '/../../..' . '/lib/private/Calendar/CalendarEventBuilder.php', 'OC\\Calendar\\CalendarQuery' => __DIR__ . '/../../..' . '/lib/private/Calendar/CalendarQuery.php', 'OC\\Calendar\\Manager' => __DIR__ . '/../../..' . '/lib/private/Calendar/Manager.php', diff --git a/lib/private/Calendar/AvailabilityResult.php b/lib/private/Calendar/AvailabilityResult.php new file mode 100644 index 00000000000..8031758f64e --- /dev/null +++ b/lib/private/Calendar/AvailabilityResult.php @@ -0,0 +1,28 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Calendar; + +use OCP\Calendar\IAvailabilityResult; + +class AvailabilityResult implements IAvailabilityResult { + public function __construct( + private readonly string $attendee, + private readonly bool $available, + ) { + } + + public function getAttendeeEmail(): string { + return $this->attendee; + } + + public function isAvailable(): bool { + return $this->available; + } +} diff --git a/lib/private/Calendar/Manager.php b/lib/private/Calendar/Manager.php index 3469193a364..e86e0e1d410 100644 --- a/lib/private/Calendar/Manager.php +++ b/lib/private/Calendar/Manager.php @@ -8,7 +8,10 @@ declare(strict_types=1); */ namespace OC\Calendar; +use DateTimeInterface; use OC\AppFramework\Bootstrap\Coordinator; +use OCA\DAV\CalDAV\Auth\CustomPrincipalPlugin; +use OCA\DAV\ServerFactory; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Calendar\Exceptions\CalendarException; use OCP\Calendar\ICalendar; @@ -20,11 +23,16 @@ use OCP\Calendar\ICalendarQuery; use OCP\Calendar\ICreateFromString; use OCP\Calendar\IHandleImipMessage; use OCP\Calendar\IManager; +use OCP\IUser; +use OCP\IUserManager; use OCP\Security\ISecureRandom; use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; +use Sabre\HTTP\Request; +use Sabre\HTTP\Response; use Sabre\VObject\Component\VCalendar; use Sabre\VObject\Component\VEvent; +use Sabre\VObject\Component\VFreeBusy; use Sabre\VObject\Property\VCard\DateTime; use Sabre\VObject\Reader; use Throwable; @@ -48,6 +56,8 @@ class Manager implements IManager { private LoggerInterface $logger, private ITimeFactory $timeFactory, private ISecureRandom $random, + private IUserManager $userManager, + private ServerFactory $serverFactory, ) { } @@ -472,4 +482,87 @@ class Manager implements IManager { $uid = $this->random->generate(32, ISecureRandom::CHAR_ALPHANUMERIC); return new CalendarEventBuilder($uid, $this->timeFactory); } + + public function checkAvailability( + DateTimeInterface $start, + DateTimeInterface $end, + IUser $organizer, + array $attendees, + ): array { + $organizerMailto = 'mailto:' . $organizer->getEMailAddress(); + $request = new VCalendar(); + $request->METHOD = 'REQUEST'; + $request->add('VFREEBUSY', [ + 'DTSTART' => $start, + 'DTEND' => $end, + 'ORGANIZER' => $organizerMailto, + 'ATTENDEE' => $organizerMailto, + ]); + + $mailtoLen = strlen('mailto:'); + foreach ($attendees as $attendee) { + if (str_starts_with($attendee, 'mailto:')) { + $attendee = substr($attendee, $mailtoLen); + } + + $attendeeUsers = $this->userManager->getByEmail($attendee); + if ($attendeeUsers === []) { + continue; + } + + $request->VFREEBUSY->add('ATTENDEE', "mailto:$attendee"); + } + + $organizerUid = $organizer->getUID(); + $server = $this->serverFactory->createAttendeeAvailabilityServer(); + /** @var CustomPrincipalPlugin $plugin */ + $plugin = $server->getPlugin('auth'); + $plugin->setCurrentPrincipal("principals/users/$organizerUid"); + + $request = new Request( + 'POST', + "/calendars/$organizerUid/outbox/", + [ + 'Content-Type' => 'text/calendar', + 'Depth' => 0, + ], + $request->serialize(), + ); + $response = new Response(); + $server->invokeMethod($request, $response, false); + + $xmlService = new \Sabre\Xml\Service(); + $xmlService->elementMap = [ + '{urn:ietf:params:xml:ns:caldav}response' => 'Sabre\Xml\Deserializer\keyValue', + '{urn:ietf:params:xml:ns:caldav}recipient' => 'Sabre\Xml\Deserializer\keyValue', + ]; + $parsedResponse = $xmlService->parse($response->getBodyAsString()); + + $result = []; + foreach ($parsedResponse as $freeBusyResponse) { + $freeBusyResponse = $freeBusyResponse['value']; + if ($freeBusyResponse['{urn:ietf:params:xml:ns:caldav}request-status'] !== '2.0;Success') { + continue; + } + + $freeBusyResponseData = \Sabre\VObject\Reader::read( + $freeBusyResponse['{urn:ietf:params:xml:ns:caldav}calendar-data'] + ); + + $attendee = substr( + $freeBusyResponse['{urn:ietf:params:xml:ns:caldav}recipient']['{DAV:}href'], + $mailtoLen, + ); + + $vFreeBusy = $freeBusyResponseData->VFREEBUSY; + if (!($vFreeBusy instanceof VFreeBusy)) { + continue; + } + + // TODO: actually check values of FREEBUSY properties to find a free slot + $result[] = new AvailabilityResult($attendee, $vFreeBusy->isFree($start, $end)); + } + + return $result; + } } diff --git a/lib/public/Calendar/IAvailabilityResult.php b/lib/public/Calendar/IAvailabilityResult.php new file mode 100644 index 00000000000..d437a5da047 --- /dev/null +++ b/lib/public/Calendar/IAvailabilityResult.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCP\Calendar; + +/** + * DTO for the availability check results. + * Holds information about whether an attendee is available or not during the request time slot. + * + * @since 31.0.0 + */ +interface IAvailabilityResult { + /** + * Get the attendee's email address. + * + * @since 31.0.0 + */ + public function getAttendeeEmail(): string; + + /** + * Whether the attendee is available during the requested time slot. + * + * @since 31.0.0 + */ + public function isAvailable(): bool; +} diff --git a/lib/public/Calendar/IManager.php b/lib/public/Calendar/IManager.php index 8056d57d859..124dc65f5f6 100644 --- a/lib/public/Calendar/IManager.php +++ b/lib/public/Calendar/IManager.php @@ -8,6 +8,9 @@ declare(strict_types=1); */ namespace OCP\Calendar; +use DateTimeInterface; +use OCP\IUser; + /** * This class provides access to the Nextcloud CalDAV backend. * Use this class exclusively if you want to access calendars. @@ -165,4 +168,20 @@ interface IManager { * @since 31.0.0 */ public function createEventBuilder(): ICalendarEventBuilder; + + /** + * Check the availability of the given organizer and attendees in the given time range. + * + * @since 31.0.0 + * + * @param IUser $organizer The organizing user from whose perspective to do the availability check. + * @param string[] $attendees Email addresses of attendees to check for (with or without a "mailto:" prefix). Only users on this instance can be checked. The rest will be silently ignored. + * @return IAvailabilityResult[] Availabilities of the organizer and all attendees which are also users on this instance. As such, the array might not contain an entry for each given attendee. + */ + public function checkAvailability( + DateTimeInterface $start, + DateTimeInterface $end, + IUser $organizer, + array $attendees, + ): array; } diff --git a/tests/data/ics/free-busy-request.ics b/tests/data/ics/free-busy-request.ics new file mode 100644 index 00000000000..dd01d35b671 --- /dev/null +++ b/tests/data/ics/free-busy-request.ics @@ -0,0 +1,14 @@ +BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Sabre//Sabre VObject 4.5.6//EN
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VFREEBUSY
+DTSTART:20250116T060000Z
+DTEND:20250117T060000Z
+ORGANIZER:mailto:admin@imap.localhost
+ATTENDEE:mailto:admin@imap.localhost
+ATTENDEE:mailto:user@imap.localhost
+ATTENDEE:mailto:empty@imap.localhost
+END:VFREEBUSY
+END:VCALENDAR
diff --git a/tests/data/ics/free-busy-request.ics.license b/tests/data/ics/free-busy-request.ics.license new file mode 100644 index 00000000000..f7f52efa96f --- /dev/null +++ b/tests/data/ics/free-busy-request.ics.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/tests/lib/Calendar/ManagerTest.php b/tests/lib/Calendar/ManagerTest.php index a7aeed98046..6c01cd90811 100644 --- a/tests/lib/Calendar/ManagerTest.php +++ b/tests/lib/Calendar/ManagerTest.php @@ -6,18 +6,26 @@ namespace Test\Calendar; +use DateTimeImmutable; use OC\AppFramework\Bootstrap\Coordinator; +use OC\Calendar\AvailabilityResult; use OC\Calendar\Manager; +use OCA\DAV\CalDAV\Auth\CustomPrincipalPlugin; +use OCA\DAV\ServerFactory; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Calendar\ICalendar; use OCP\Calendar\ICalendarIsShared; use OCP\Calendar\ICalendarIsWritable; use OCP\Calendar\ICreateFromString; use OCP\Calendar\IHandleImipMessage; +use OCP\IUser; +use OCP\IUserManager; use OCP\Security\ISecureRandom; use PHPUnit\Framework\MockObject\MockObject; use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; use Sabre\VObject\Component\VCalendar; use Sabre\VObject\Document; use Sabre\VObject\Reader; @@ -48,6 +56,9 @@ class ManagerTest extends TestCase { /** @var ISecureRandom&MockObject */ private ISecureRandom $secureRandom; + private IUserManager&MockObject $userManager; + private ServerFactory&MockObject $serverFactory; + private VCalendar $vCalendar1a; protected function setUp(): void { @@ -58,6 +69,8 @@ class ManagerTest extends TestCase { $this->logger = $this->createMock(LoggerInterface::class); $this->time = $this->createMock(ITimeFactory::class); $this->secureRandom = $this->createMock(ISecureRandom::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->serverFactory = $this->createMock(ServerFactory::class); $this->manager = new Manager( $this->coordinator, @@ -65,6 +78,8 @@ class ManagerTest extends TestCase { $this->logger, $this->time, $this->secureRandom, + $this->userManager, + $this->serverFactory, ); // construct calendar with a 1 hour event and same start/end time zones @@ -268,6 +283,8 @@ class ManagerTest extends TestCase { $this->logger, $this->time, $this->secureRandom, + $this->userManager, + $this->serverFactory, ]) ->onlyMethods(['getCalendarsForPrincipal']) ->getMock(); @@ -300,6 +317,8 @@ class ManagerTest extends TestCase { $this->logger, $this->time, $this->secureRandom, + $this->userManager, + $this->serverFactory, ]) ->onlyMethods(['getCalendarsForPrincipal']) ->getMock(); @@ -331,6 +350,8 @@ class ManagerTest extends TestCase { $this->logger, $this->time, $this->secureRandom, + $this->userManager, + $this->serverFactory, ]) ->onlyMethods(['getCalendarsForPrincipal']) ->getMock(); @@ -363,6 +384,8 @@ class ManagerTest extends TestCase { $this->logger, $this->time, $this->secureRandom, + $this->userManager, + $this->serverFactory, ]) ->onlyMethods(['getCalendarsForPrincipal']) ->getMock(); @@ -396,6 +419,8 @@ class ManagerTest extends TestCase { $this->logger, $this->time, $this->secureRandom, + $this->userManager, + $this->serverFactory, ]) ->onlyMethods(['getCalendarsForPrincipal']) ->getMock(); @@ -429,6 +454,8 @@ class ManagerTest extends TestCase { $this->logger, $this->time, $this->secureRandom, + $this->userManager, + $this->serverFactory, ]) ->onlyMethods(['getCalendarsForPrincipal']) ->getMock(); @@ -462,6 +489,8 @@ class ManagerTest extends TestCase { $this->logger, $this->time, $this->secureRandom, + $this->userManager, + $this->serverFactory, ]) ->onlyMethods(['getCalendarsForPrincipal']) ->getMock(); @@ -506,6 +535,8 @@ class ManagerTest extends TestCase { $this->logger, $this->time, $this->secureRandom, + $this->userManager, + $this->serverFactory, ]) ->onlyMethods(['getCalendarsForPrincipal']) ->getMock(); @@ -550,6 +581,8 @@ class ManagerTest extends TestCase { $this->logger, $this->time, $this->secureRandom, + $this->userManager, + $this->serverFactory, ]) ->onlyMethods(['getCalendarsForPrincipal']) ->getMock(); @@ -629,6 +662,8 @@ class ManagerTest extends TestCase { $this->logger, $this->time, $this->secureRandom, + $this->userManager, + $this->serverFactory, ]) ->setMethods([ 'getCalendarsForPrincipal' @@ -661,6 +696,8 @@ class ManagerTest extends TestCase { $this->logger, $this->time, $this->secureRandom, + $this->userManager, + $this->serverFactory, ]) ->setMethods([ 'getCalendarsForPrincipal' @@ -699,6 +736,8 @@ class ManagerTest extends TestCase { $this->logger, $this->time, $this->secureRandom, + $this->userManager, + $this->serverFactory, ]) ->setMethods([ 'getCalendarsForPrincipal' @@ -787,6 +826,8 @@ class ManagerTest extends TestCase { $this->logger, $this->time, $this->secureRandom, + $this->userManager, + $this->serverFactory, ]) ->setMethods([ 'getCalendarsForPrincipal' @@ -821,6 +862,8 @@ class ManagerTest extends TestCase { $this->logger, $this->time, $this->secureRandom, + $this->userManager, + $this->serverFactory, ]) ->setMethods([ 'getCalendarsForPrincipal' @@ -859,6 +902,8 @@ class ManagerTest extends TestCase { $this->logger, $this->time, $this->secureRandom, + $this->userManager, + $this->serverFactory, ]) ->setMethods([ 'getCalendarsForPrincipal' @@ -945,4 +990,219 @@ END:VCALENDAR EOF; return Reader::read($data); } + + private function getFreeBusyResponse(): string { + return <<<EOF +<?xml version="1.0" encoding="utf-8"?> +<cal:schedule-response xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns"> + <cal:response> + <cal:recipient> + <d:href>mailto:admin@imap.localhost</d:href> + </cal:recipient> + <cal:request-status>2.0;Success</cal:request-status> + <cal:calendar-data>BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject 4.5.6//EN +CALSCALE:GREGORIAN +METHOD:REPLY +BEGIN:VFREEBUSY +DTSTART:20250116T060000Z +DTEND:20250117T060000Z +DTSTAMP:20250111T125634Z +FREEBUSY:20250116T060000Z/20250116T230000Z +FREEBUSY;FBTYPE=BUSY-UNAVAILABLE:20250116T230000Z/20250117T060000Z +ATTENDEE:mailto:admin@imap.localhost +UID:6099eab3-9bf1-4c7a-809e-4d46957cc372 +ORGANIZER;CN=admin:mailto:admin@imap.localhost +END:VFREEBUSY +END:VCALENDAR +</cal:calendar-data> + </cal:response> + <cal:response> + <cal:recipient> + <d:href>mailto:empty@imap.localhost</d:href> + </cal:recipient> + <cal:request-status>2.0;Success</cal:request-status> + <cal:calendar-data>BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject 4.5.6//EN +CALSCALE:GREGORIAN +METHOD:REPLY +BEGIN:VFREEBUSY +DTSTART:20250116T060000Z +DTEND:20250117T060000Z +DTSTAMP:20250111T125634Z +ATTENDEE:mailto:empty@imap.localhost +UID:6099eab3-9bf1-4c7a-809e-4d46957cc372 +ORGANIZER;CN=admin:mailto:admin@imap.localhost +END:VFREEBUSY +END:VCALENDAR +</cal:calendar-data> + </cal:response> + <cal:response> + <cal:recipient> + <d:href>mailto:user@imap.localhost</d:href> + </cal:recipient> + <cal:request-status>2.0;Success</cal:request-status> + <cal:calendar-data>BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject 4.5.6//EN +CALSCALE:GREGORIAN +METHOD:REPLY +BEGIN:VFREEBUSY +DTSTART:20250116T060000Z +DTEND:20250117T060000Z +DTSTAMP:20250111T125634Z +FREEBUSY:20250116T060000Z/20250116T230000Z +FREEBUSY;FBTYPE=BUSY-UNAVAILABLE:20250116T230000Z/20250117T060000Z +ATTENDEE:mailto:user@imap.localhost +UID:6099eab3-9bf1-4c7a-809e-4d46957cc372 +ORGANIZER;CN=admin:mailto:admin@imap.localhost +END:VFREEBUSY +END:VCALENDAR +</cal:calendar-data> + </cal:response> + <cal:response> + <cal:recipient> + <d:href>mailto:nouser@domain.tld</d:href> + </cal:recipient> + <cal:request-status>3.7;Could not find principal</cal:request-status> + </cal:response> +</cal:schedule-response> +EOF; + } + + public function testCheckAvailability(): void { + $organizer = $this->createMock(IUser::class); + $organizer->expects(self::once()) + ->method('getUID') + ->willReturn('admin'); + $organizer->expects(self::once()) + ->method('getEMailAddress') + ->willReturn('admin@imap.localhost'); + + $user1 = $this->createMock(IUser::class); + $user2 = $this->createMock(IUser::class); + + $this->userManager->expects(self::exactly(3)) + ->method('getByEmail') + ->willReturnMap([ + ['user@imap.localhost', [$user1]], + ['empty@imap.localhost', [$user2]], + ['nouser@domain.tld', []], + ]); + + $authPlugin = $this->createMock(CustomPrincipalPlugin::class); + $authPlugin->expects(self::once()) + ->method('setCurrentPrincipal') + ->with('principals/users/admin'); + + $server = $this->createMock(\OCA\DAV\Connector\Sabre\Server::class); + $server->expects(self::once()) + ->method('getPlugin') + ->with('auth') + ->willReturn($authPlugin); + $server->expects(self::once()) + ->method('invokeMethod') + ->willReturnCallback(function ( + RequestInterface $request, + ResponseInterface $response, + bool $sendResponse, + ) { + $requestBody = file_get_contents(__DIR__ . '/../../data/ics/free-busy-request.ics'); + $this->assertEquals('POST', $request->getMethod()); + $this->assertEquals('calendars/admin/outbox', $request->getPath()); + $this->assertEquals('text/calendar', $request->getHeader('Content-Type')); + $this->assertEquals('0', $request->getHeader('Depth')); + $this->assertEquals($requestBody, $request->getBodyAsString()); + $this->assertFalse($sendResponse); + $response->setStatus(200); + $response->setBody($this->getFreeBusyResponse()); + }); + + $this->serverFactory->expects(self::once()) + ->method('createAttendeeAvailabilityServer') + ->willReturn($server); + + $start = new DateTimeImmutable('2025-01-16T06:00:00Z'); + $end = new DateTimeImmutable('2025-01-17T06:00:00Z'); + $actual = $this->manager->checkAvailability($start, $end, $organizer, [ + 'user@imap.localhost', + 'empty@imap.localhost', + 'nouser@domain.tld', + ]); + $expected = [ + new AvailabilityResult('admin@imap.localhost', false), + new AvailabilityResult('empty@imap.localhost', true), + new AvailabilityResult('user@imap.localhost', false), + ]; + $this->assertEquals($expected, $actual); + } + + public function testCheckAvailabilityWithMailtoPrefix(): void { + $organizer = $this->createMock(IUser::class); + $organizer->expects(self::once()) + ->method('getUID') + ->willReturn('admin'); + $organizer->expects(self::once()) + ->method('getEMailAddress') + ->willReturn('admin@imap.localhost'); + + $user1 = $this->createMock(IUser::class); + $user2 = $this->createMock(IUser::class); + + $this->userManager->expects(self::exactly(3)) + ->method('getByEmail') + ->willReturnMap([ + ['user@imap.localhost', [$user1]], + ['empty@imap.localhost', [$user2]], + ['nouser@domain.tld', []], + ]); + + $authPlugin = $this->createMock(CustomPrincipalPlugin::class); + $authPlugin->expects(self::once()) + ->method('setCurrentPrincipal') + ->with('principals/users/admin'); + + $server = $this->createMock(\OCA\DAV\Connector\Sabre\Server::class); + $server->expects(self::once()) + ->method('getPlugin') + ->with('auth') + ->willReturn($authPlugin); + $server->expects(self::once()) + ->method('invokeMethod') + ->willReturnCallback(function ( + RequestInterface $request, + ResponseInterface $response, + bool $sendResponse, + ) { + $requestBody = file_get_contents(__DIR__ . '/../../data/ics/free-busy-request.ics'); + $this->assertEquals('POST', $request->getMethod()); + $this->assertEquals('calendars/admin/outbox', $request->getPath()); + $this->assertEquals('text/calendar', $request->getHeader('Content-Type')); + $this->assertEquals('0', $request->getHeader('Depth')); + $this->assertEquals($requestBody, $request->getBodyAsString()); + $this->assertFalse($sendResponse); + $response->setStatus(200); + $response->setBody($this->getFreeBusyResponse()); + }); + + $this->serverFactory->expects(self::once()) + ->method('createAttendeeAvailabilityServer') + ->willReturn($server); + + $start = new DateTimeImmutable('2025-01-16T06:00:00Z'); + $end = new DateTimeImmutable('2025-01-17T06:00:00Z'); + $actual = $this->manager->checkAvailability($start, $end, $organizer, [ + 'mailto:user@imap.localhost', + 'mailto:empty@imap.localhost', + 'mailto:nouser@domain.tld', + ]); + $expected = [ + new AvailabilityResult('admin@imap.localhost', false), + new AvailabilityResult('empty@imap.localhost', true), + new AvailabilityResult('user@imap.localhost', false), + ]; + $this->assertEquals($expected, $actual); + } } |