aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apps/dav/lib/ServerFactory.php5
-rw-r--r--lib/composer/composer/autoload_classmap.php2
-rw-r--r--lib/composer/composer/autoload_static.php2
-rw-r--r--lib/private/Calendar/AvailabilityResult.php28
-rw-r--r--lib/private/Calendar/Manager.php93
-rw-r--r--lib/public/Calendar/IAvailabilityResult.php32
-rw-r--r--lib/public/Calendar/IManager.php19
-rw-r--r--tests/data/ics/free-busy-request.ics14
-rw-r--r--tests/data/ics/free-busy-request.ics.license2
-rw-r--r--tests/lib/Calendar/ManagerTest.php260
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);
+ }
}