]> source.dussan.org Git - nextcloud-server.git/commitdiff
feat: add iMip Request Handling 47826/head
authorSebastianKrupinski <krupinskis05@gmail.com>
Sat, 7 Sep 2024 22:28:50 +0000 (18:28 -0400)
committerSebastianKrupinski <krupinskis05@gmail.com>
Fri, 8 Nov 2024 02:12:37 +0000 (21:12 -0500)
Signed-off-by: SebastianKrupinski <krupinskis05@gmail.com>
apps/dav/lib/CalDAV/CachedSubscriptionImpl.php
apps/dav/lib/CalDAV/CalendarImpl.php
lib/composer/composer/autoload_classmap.php
lib/composer/composer/autoload_static.php
lib/private/Calendar/Manager.php
lib/public/Calendar/ICalendar.php
lib/public/Calendar/ICalendarIsShared.php [new file with mode: 0644]
lib/public/Calendar/ICalendarIsWritable.php [new file with mode: 0644]
lib/public/Calendar/IManager.php
tests/lib/Calendar/ManagerTest.php

index 00fa90f5d20e1673164d1dfcc132c286c248dc15..4d25f5bb501907c9ddc3bc9df3ccd8f28fb9e833 100644 (file)
@@ -9,9 +9,12 @@ declare(strict_types=1);
 namespace OCA\DAV\CalDAV;
 
 use OCP\Calendar\ICalendar;
+use OCP\Calendar\ICalendarIsShared;
+use OCP\Calendar\ICalendarIsWritable;
 use OCP\Constants;
 
-class CachedSubscriptionImpl implements ICalendar {
+class CachedSubscriptionImpl implements ICalendar, ICalendarIsShared, ICalendarIsWritable {
+
        public function __construct(
                private CachedSubscription $calendar,
                /** @var array<string, mixed> */
@@ -83,10 +86,18 @@ class CachedSubscriptionImpl implements ICalendar {
                return $result;
        }
 
+       public function isWritable(): bool {
+               return false;
+       }
+
        public function isDeleted(): bool {
                return false;
        }
 
+       public function isShared(): bool {
+               return true;
+       }
+
        public function getSource(): string {
                return $this->calendarInfo['source'];
        }
index 85ca7f78ca4e366599387abe95e8c5e51c986968..919b08eefce2bb11913a0da716e1af5fd600fefc 100644 (file)
@@ -127,6 +127,13 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage {
                return $result;
        }
 
+       /**
+        * @since 31.0.0
+        */
+       public function isWritable(): bool {
+               return $this->calendar->canWrite();
+       }
+       
        /**
         * @since 26.0.0
         */
@@ -134,6 +141,13 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage {
                return $this->calendar->isDeleted();
        }
 
+       /**
+        * @since 31.0.0
+        */
+       public function isShared(): bool {
+               return $this->calendar->isShared();
+       }
+
        /**
         * Create a new calendar event for this calendar
         * by way of an ICS string
@@ -215,7 +229,10 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage {
                $attendee = $vEvent->{'ATTENDEE'}->getValue();
 
                $iTipMessage->method = $vObject->{'METHOD'}->getValue();
-               if ($iTipMessage->method === 'REPLY') {
+               if ($iTipMessage->method === 'REQUEST') {
+                       $iTipMessage->sender = $organizer;
+                       $iTipMessage->recipient = $attendee;
+               } elseif ($iTipMessage->method === 'REPLY') {
                        if ($server->isExternalAttendee($vEvent->{'ATTENDEE'}->getValue())) {
                                $iTipMessage->recipient = $organizer;
                        } else {
index 7265edfe95f981c9ff53b7d2c7395fb57afb1c7b..352c3c11a9ed0c38811efcb5ec2be46d4268003a 100644 (file)
@@ -163,6 +163,8 @@ return array(
     'OCP\\Calendar\\BackendTemporarilyUnavailableException' => $baseDir . '/lib/public/Calendar/BackendTemporarilyUnavailableException.php',
     'OCP\\Calendar\\Exceptions\\CalendarException' => $baseDir . '/lib/public/Calendar/Exceptions/CalendarException.php',
     'OCP\\Calendar\\ICalendar' => $baseDir . '/lib/public/Calendar/ICalendar.php',
+    'OCP\\Calendar\\ICalendarIsShared' => $baseDir . '/lib/public/Calendar/ICalendarIsShared.php',
+    'OCP\\Calendar\\ICalendarIsWritable' => $baseDir . '/lib/public/Calendar/ICalendarIsWritable.php',
     'OCP\\Calendar\\ICalendarProvider' => $baseDir . '/lib/public/Calendar/ICalendarProvider.php',
     'OCP\\Calendar\\ICalendarQuery' => $baseDir . '/lib/public/Calendar/ICalendarQuery.php',
     'OCP\\Calendar\\ICreateFromString' => $baseDir . '/lib/public/Calendar/ICreateFromString.php',
index 4bac34425ac16f33f500482b4678b3846ea8a072..890c778da1d4d23a72b41e9b74ca1093fbb2ca2a 100644 (file)
@@ -196,6 +196,8 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
         'OCP\\Calendar\\BackendTemporarilyUnavailableException' => __DIR__ . '/../../..' . '/lib/public/Calendar/BackendTemporarilyUnavailableException.php',
         'OCP\\Calendar\\Exceptions\\CalendarException' => __DIR__ . '/../../..' . '/lib/public/Calendar/Exceptions/CalendarException.php',
         'OCP\\Calendar\\ICalendar' => __DIR__ . '/../../..' . '/lib/public/Calendar/ICalendar.php',
+        'OCP\\Calendar\\ICalendarIsShared' => __DIR__ . '/../../..' . '/lib/public/Calendar/ICalendarIsShared.php',
+        'OCP\\Calendar\\ICalendarIsWritable' => __DIR__ . '/../../..' . '/lib/public/Calendar/ICalendarIsWritable.php',
         'OCP\\Calendar\\ICalendarProvider' => __DIR__ . '/../../..' . '/lib/public/Calendar/ICalendarProvider.php',
         'OCP\\Calendar\\ICalendarQuery' => __DIR__ . '/../../..' . '/lib/public/Calendar/ICalendarQuery.php',
         'OCP\\Calendar\\ICreateFromString' => __DIR__ . '/../../..' . '/lib/public/Calendar/ICreateFromString.php',
index fa324273f5cb898f66e84f2040612b18fdb5ccf8..ba2124a5c239a3d819e7690eb4c1638af00ff6f5 100644 (file)
@@ -12,6 +12,8 @@ use OC\AppFramework\Bootstrap\Coordinator;
 use OCP\AppFramework\Utility\ITimeFactory;
 use OCP\Calendar\Exceptions\CalendarException;
 use OCP\Calendar\ICalendar;
+use OCP\Calendar\ICalendarIsShared;
+use OCP\Calendar\ICalendarIsWritable;
 use OCP\Calendar\ICalendarProvider;
 use OCP\Calendar\ICalendarQuery;
 use OCP\Calendar\ICreateFromString;
@@ -204,6 +206,87 @@ class Manager implements IManager {
                return new CalendarQuery($principalUri);
        }
 
+       /**
+        * @since 31.0.0
+        * @throws \OCP\DB\Exception
+        */
+       public function handleIMipRequest(
+               string $principalUri,
+               string $sender,
+               string $recipient,
+               string $calendarData,
+       ): bool {
+               
+               $userCalendars = $this->getCalendarsForPrincipal($principalUri);
+               if (empty($userCalendars)) {
+                       $this->logger->warning('iMip message could not be processed because user has no calendars');
+                       return false;
+               }
+               
+               /** @var VCalendar $vObject|null */
+               $calendarObject = Reader::read($calendarData);
+               
+               if (!isset($calendarObject->METHOD) || $calendarObject->METHOD->getValue() !== 'REQUEST') {
+                       $this->logger->warning('iMip message contains an incorrect or invalid method');
+                       return false;
+               }
+               
+               if (!isset($calendarObject->VEVENT)) {
+                       $this->logger->warning('iMip message contains no event');
+                       return false;
+               }
+
+               $eventObject = $calendarObject->VEVENT;
+
+               if (!isset($eventObject->UID)) {
+                       $this->logger->warning('iMip message event dose not contains a UID');
+                       return false;
+               }
+               
+               if (!isset($eventObject->ATTENDEE)) {
+                       $this->logger->warning('iMip message event dose not contains any attendees');
+                       return false;
+               }
+               
+               foreach ($eventObject->ATTENDEE as $entry) {
+                       $address = trim(str_replace('mailto:', '', $entry->getValue()));
+                       if ($address === $recipient) {
+                               $attendee = $address;
+                               break;
+                       }
+               }
+               if (!isset($attendee)) {
+                       $this->logger->warning('iMip message event does not contain a attendee that matches the recipient');
+                       return false;
+               }
+               
+               foreach ($userCalendars as $calendar) {
+                       
+                       if (!$calendar instanceof ICalendarIsWritable && !$calendar instanceof ICalendarIsShared) {
+                               continue;
+                       }
+                       
+                       if ($calendar->isDeleted() || !$calendar->isWritable() || $calendar->isShared()) {
+                               continue;
+                       }
+                       
+                       if (!empty($calendar->search($recipient, ['ATTENDEE'], ['uid' => $eventObject->UID->getValue()]))) {
+                               try {
+                                       if ($calendar instanceof IHandleImipMessage) {
+                                               $calendar->handleIMipMessage('', $calendarData);
+                                       }
+                                       return true;
+                               } catch (CalendarException $e) {
+                                       $this->logger->error('An error occurred while processing the iMip message event', ['exception' => $e]);
+                                       return false;
+                               }
+                       }
+               }
+               
+               $this->logger->warning('iMip message event could not be processed because the no corresponding event was found in any calendar');
+               return false;
+       }
+
        /**
         * @throws \OCP\DB\Exception
         */
index 2f74d3291194614df14671c9490dd9414c7abe3a..f29d6f301763652352353add7019f3df1f5e26c6 100644 (file)
@@ -59,7 +59,8 @@ interface ICalendar {
        public function getPermissions(): int;
 
        /**
-        * Whether the calendar is deleted
+        * Indicates whether the calendar is in the trash bin
+        *
         * @since 26.0.0
         */
        public function isDeleted(): bool;
diff --git a/lib/public/Calendar/ICalendarIsShared.php b/lib/public/Calendar/ICalendarIsShared.php
new file mode 100644 (file)
index 0000000..8121c82
--- /dev/null
@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCP\Calendar;
+
+/**
+ * ICalendar Interface Extension
+ *
+ * @since 31.0.0
+ */
+interface ICalendarIsShared {
+       
+       /**
+        * Indicates whether the calendar is shared with the current user
+        *
+        * @since 31.0.0
+        */
+       public function isShared(): bool;
+
+}
diff --git a/lib/public/Calendar/ICalendarIsWritable.php b/lib/public/Calendar/ICalendarIsWritable.php
new file mode 100644 (file)
index 0000000..f80769e
--- /dev/null
@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCP\Calendar;
+
+/**
+ * ICalendar Interface Extension
+ *
+ * @since 31.0.0
+ */
+interface ICalendarIsWritable {
+       
+       /**
+        * Indicates whether the calendar can be modified
+        *
+        * @since 31.0.0
+        */
+       public function isWritable(): bool;
+
+}
index 8a9fe48587143522bb028ecbc55e022c5814563b..bb3808f133c8d41ffb58c03befcfec0a72e27af9 100644 (file)
@@ -137,6 +137,13 @@ interface IManager {
         */
        public function newQuery(string $principalUri) : ICalendarQuery;
 
+       /**
+        * Handle a iMip REQUEST message
+        *
+        * @since 31.0.0
+        */
+       public function handleIMipRequest(string $principalUri, string $sender, string $recipient, string $calendarData): bool;
+
        /**
         * Handle a iMip REPLY message
         *
index 3d1a46d3f2ab5c62327ae9621aa416f24ab4f4b2..f0ca278f352ffc1dca8cfa4762ca02dab39d91f6 100644 (file)
@@ -10,11 +10,14 @@ use OC\AppFramework\Bootstrap\Coordinator;
 use OC\Calendar\Manager;
 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 PHPUnit\Framework\MockObject\MockObject;
 use Psr\Container\ContainerInterface;
 use Psr\Log\LoggerInterface;
+use Sabre\VObject\Component\VCalendar;
 use Sabre\VObject\Document;
 use Sabre\VObject\Reader;
 use Test\TestCase;
@@ -22,25 +25,27 @@ use Test\TestCase;
 /*
  * This allows us to create Mock object supporting both interfaces
  */
-interface ICreateFromStringAndHandleImipMessage extends ICreateFromString, IHandleImipMessage {
+interface ITestCalendar extends ICreateFromString, IHandleImipMessage, ICalendarIsShared, ICalendarIsWritable {
 }
 
 class ManagerTest extends TestCase {
-       /** @var Coordinator|MockObject */
+       /** @var Coordinator&MockObject */
        private $coordinator;
 
-       /** @var MockObject|ContainerInterface */
+       /** @var ContainerInterface&MockObject */
        private $container;
 
-       /** @var MockObject|LoggerInterface */
+       /** @var LoggerInterface&MockObject */
        private $logger;
 
        /** @var Manager */
        private $manager;
 
-       /** @var ITimeFactory|ITimeFactory&MockObject|MockObject */
+       /** @var ITimeFactory&MockObject */
        private $time;
 
+       private VCalendar $vCalendar1a;
+
        protected function setUp(): void {
                parent::setUp();
 
@@ -55,6 +60,23 @@ class ManagerTest extends TestCase {
                        $this->logger,
                        $this->time,
                );
+
+               // construct calendar with a 1 hour event and same start/end time zones
+               $this->vCalendar1a = new VCalendar();
+               /** @var VEvent $vEvent */
+               $vEvent = $this->vCalendar1a->add('VEVENT', []);
+               $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+               $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+               $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
+               $vEvent->add('SUMMARY', 'Test Event');
+               $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+               $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+                       'CN' => 'Attendee One',
+                       'CUTYPE' => 'INDIVIDUAL',
+                       'PARTSTAT' => 'NEEDS-ACTION',
+                       'ROLE' => 'REQ-PARTICIPANT',
+                       'RSVP' => 'TRUE'
+               ]);
        }
 
        /**
@@ -230,6 +252,310 @@ class ManagerTest extends TestCase {
                $this->assertTrue($isEnabled);
        }
 
+       public function testHandleImipRequestWithNoCalendars(): void {
+               // construct calendar manager returns
+               /** @var Manager&MockObject $manager */
+               $manager = $this->getMockBuilder(Manager::class)
+                       ->setConstructorArgs([
+                               $this->coordinator,
+                               $this->container,
+                               $this->logger,
+                               $this->time
+                       ])
+                       ->onlyMethods(['getCalendarsForPrincipal'])
+                       ->getMock();
+               $manager->expects(self::once())
+                       ->method('getCalendarsForPrincipal')
+                       ->willReturn([]);
+               // construct logger returns
+               $this->logger->expects(self::once())->method('warning')
+                       ->with('iMip message could not be processed because user has no calendars');
+               // construct parameters
+               $principalUri = 'principals/user/attendee1';
+               $sender = 'organizer@testing.com';
+               $recipient = 'attendee1@testing.com';
+               $calendar = $this->vCalendar1a;
+               $calendar->add('METHOD', 'REQUEST');
+               // test method
+               $result = $manager->handleIMipRequest($principalUri, $sender, $recipient, $calendar->serialize());
+               $this->assertFalse($result);
+       }
+
+       public function testHandleImipRequestWithNoMethod(): void {
+               // construct mock user calendar
+               $userCalendar = $this->createMock(ITestCalendar::class);
+               // construct mock calendar manager and returns
+               /** @var Manager&MockObject $manager */
+               $manager = $this->getMockBuilder(Manager::class)
+                       ->setConstructorArgs([
+                               $this->coordinator,
+                               $this->container,
+                               $this->logger,
+                               $this->time
+                       ])
+                       ->onlyMethods(['getCalendarsForPrincipal'])
+                       ->getMock();
+               $manager->expects(self::once())
+                       ->method('getCalendarsForPrincipal')
+                       ->willReturn([$userCalendar]);
+               // construct logger returns
+               $this->logger->expects(self::once())->method('warning')
+                       ->with('iMip message contains an incorrect or invalid method');
+               // construct parameters
+               $principalUri = 'principals/user/attendee1';
+               $sender = 'organizer@testing.com';
+               $recipient = 'attendee1@testing.com';
+               $calendar = $this->vCalendar1a;
+               // test method
+               $result = $manager->handleIMipRequest($principalUri, $sender, $recipient, $calendar->serialize());
+               $this->assertFalse($result);
+       }
+
+       public function testHandleImipRequestWithInvalidMethod(): void {
+               // construct mock user calendar
+               $userCalendar = $this->createMock(ITestCalendar::class);
+               // construct mock calendar manager and returns
+               /** @var Manager&MockObject $manager */
+               $manager = $this->getMockBuilder(Manager::class)
+                       ->setConstructorArgs([
+                               $this->coordinator,
+                               $this->container,
+                               $this->logger,
+                               $this->time
+                       ])
+                       ->onlyMethods(['getCalendarsForPrincipal'])
+                       ->getMock();
+               $manager->expects(self::once())
+                       ->method('getCalendarsForPrincipal')
+                       ->willReturn([$userCalendar]);
+               // construct logger returns
+               $this->logger->expects(self::once())->method('warning')
+                       ->with('iMip message contains an incorrect or invalid method');
+               // construct parameters
+               $principalUri = 'principals/user/attendee1';
+               $sender = 'organizer@testing.com';
+               $recipient = 'attendee1@testing.com';
+               $calendar = $this->vCalendar1a;
+               $calendar->add('METHOD', 'CANCEL');
+               // test method
+               $result = $manager->handleIMipRequest($principalUri, $sender, $recipient, $calendar->serialize());
+               $this->assertFalse($result);
+       }
+
+       public function testHandleImipRequestWithNoEvent(): void {
+               // construct mock user calendar
+               $userCalendar = $this->createMock(ITestCalendar::class);
+               // construct mock calendar manager and returns
+               /** @var Manager&MockObject $manager */
+               $manager = $this->getMockBuilder(Manager::class)
+                       ->setConstructorArgs([
+                               $this->coordinator,
+                               $this->container,
+                               $this->logger,
+                               $this->time
+                       ])
+                       ->onlyMethods(['getCalendarsForPrincipal'])
+                       ->getMock();
+               $manager->expects(self::once())
+                       ->method('getCalendarsForPrincipal')
+                       ->willReturn([$userCalendar]);
+               // construct logger returns
+               $this->logger->expects(self::once())->method('warning')
+                       ->with('iMip message contains no event');
+               // construct parameters
+               $principalUri = 'principals/user/attendee1';
+               $sender = 'organizer@testing.com';
+               $recipient = 'attendee1@testing.com';
+               $calendar = $this->vCalendar1a;
+               $calendar->add('METHOD', 'REQUEST');
+               $calendar->remove('VEVENT');
+               // test method
+               $result = $manager->handleIMipRequest($principalUri, $sender, $recipient, $calendar->serialize());
+               $this->assertFalse($result);
+       }
+
+       public function testHandleImipRequestWithNoUid(): void {
+               // construct mock user calendar
+               $userCalendar = $this->createMock(ITestCalendar::class);
+               // construct mock calendar manager and returns
+               /** @var Manager&MockObject $manager */
+               $manager = $this->getMockBuilder(Manager::class)
+                       ->setConstructorArgs([
+                               $this->coordinator,
+                               $this->container,
+                               $this->logger,
+                               $this->time
+                       ])
+                       ->onlyMethods(['getCalendarsForPrincipal'])
+                       ->getMock();
+               $manager->expects(self::once())
+                       ->method('getCalendarsForPrincipal')
+                       ->willReturn([$userCalendar]);
+               // construct logger returns
+               $this->logger->expects(self::once())->method('warning')
+                       ->with('iMip message event dose not contains a UID');
+               // construct parameters
+               $principalUri = 'principals/user/attendee1';
+               $sender = 'organizer@testing.com';
+               $recipient = 'attendee1@testing.com';
+               $calendar = $this->vCalendar1a;
+               $calendar->add('METHOD', 'REQUEST');
+               $calendar->VEVENT->remove('UID');
+               // test method
+               $result = $manager->handleIMipRequest($principalUri, $sender, $recipient, $calendar->serialize());
+               $this->assertFalse($result);
+       }
+
+       public function testHandleImipRequestWithNoAttendee(): void {
+               // construct mock user calendar
+               $userCalendar = $this->createMock(ITestCalendar::class);
+               // construct mock calendar manager and returns
+               /** @var Manager&MockObject $manager */
+               $manager = $this->getMockBuilder(Manager::class)
+                       ->setConstructorArgs([
+                               $this->coordinator,
+                               $this->container,
+                               $this->logger,
+                               $this->time
+                       ])
+                       ->onlyMethods(['getCalendarsForPrincipal'])
+                       ->getMock();
+               $manager->expects(self::once())
+                       ->method('getCalendarsForPrincipal')
+                       ->willReturn([$userCalendar]);
+               // construct logger returns
+               $this->logger->expects(self::once())->method('warning')
+                       ->with('iMip message event dose not contains any attendees');
+               // construct parameters
+               $principalUri = 'principals/user/attendee1';
+               $sender = 'organizer@testing.com';
+               $recipient = 'attendee1@testing.com';
+               $calendar = $this->vCalendar1a;
+               $calendar->add('METHOD', 'REQUEST');
+               $calendar->VEVENT->remove('ATTENDEE');
+               // test method
+               $result = $manager->handleIMipRequest($principalUri, $sender, $recipient, $calendar->serialize());
+               $this->assertFalse($result);
+       }
+
+       public function testHandleImipRequestWithInvalidAttendee(): void {
+               // construct mock user calendar
+               $userCalendar = $this->createMock(ITestCalendar::class);
+               // construct mock calendar manager and returns
+               /** @var Manager&MockObject $manager */
+               $manager = $this->getMockBuilder(Manager::class)
+                       ->setConstructorArgs([
+                               $this->coordinator,
+                               $this->container,
+                               $this->logger,
+                               $this->time
+                       ])
+                       ->onlyMethods(['getCalendarsForPrincipal'])
+                       ->getMock();
+               $manager->expects(self::once())
+                       ->method('getCalendarsForPrincipal')
+                       ->willReturn([$userCalendar]);
+               // construct logger returns
+               $this->logger->expects(self::once())->method('warning')
+                       ->with('iMip message event does not contain a attendee that matches the recipient');
+               // construct parameters
+               $principalUri = 'principals/user/attendee1';
+               $sender = 'organizer@testing.com';
+               $recipient = 'attendee2@testing.com';
+               $calendar = $this->vCalendar1a;
+               $calendar->add('METHOD', 'REQUEST');
+               // test method
+               $result = $manager->handleIMipRequest($principalUri, $sender, $recipient, $calendar->serialize());
+               $this->assertFalse($result);
+       }
+
+       public function testHandleImipRequestWithNoMatch(): void {
+               // construct mock user calendar
+               $userCalendar = $this->createMock(ITestCalendar::class);
+               $userCalendar->expects(self::once())
+                       ->method('isDeleted')
+                       ->willReturn(false);
+               $userCalendar->expects(self::once())
+                       ->method('isWritable')
+                       ->willReturn(true);
+               $userCalendar->expects(self::once())
+                       ->method('isShared')
+                       ->willReturn(false);
+               $userCalendar->expects(self::once())
+                       ->method('search')
+                       ->willReturn([]);
+               // construct mock calendar manager and returns
+               /** @var Manager&MockObject $manager */
+               $manager = $this->getMockBuilder(Manager::class)
+                       ->setConstructorArgs([
+                               $this->coordinator,
+                               $this->container,
+                               $this->logger,
+                               $this->time
+                       ])
+                       ->onlyMethods(['getCalendarsForPrincipal'])
+                       ->getMock();
+               $manager->expects(self::once())
+                       ->method('getCalendarsForPrincipal')
+                       ->willReturn([$userCalendar]);
+               // construct logger returns
+               $this->logger->expects(self::once())->method('warning')
+                       ->with('iMip message event could not be processed because the no corresponding event was found in any calendar');
+               // construct parameters
+               $principalUri = 'principals/user/attendee1';
+               $sender = 'organizer@testing.com';
+               $recipient = 'attendee1@testing.com';
+               $calendar = $this->vCalendar1a;
+               $calendar->add('METHOD', 'REQUEST');
+               // test method
+               $result = $manager->handleIMipRequest($principalUri, $sender, $recipient, $calendar->serialize());
+               $this->assertFalse($result);
+       }
+
+       public function testHandleImipRequest(): void {
+               // construct mock user calendar
+               $userCalendar = $this->createMock(ITestCalendar::class);
+               $userCalendar->expects(self::once())
+                       ->method('isDeleted')
+                       ->willReturn(false);
+               $userCalendar->expects(self::once())
+                       ->method('isWritable')
+                       ->willReturn(true);
+               $userCalendar->expects(self::once())
+                       ->method('isShared')
+                       ->willReturn(false);
+               $userCalendar->expects(self::once())
+                       ->method('search')
+                       ->willReturn([['uri' => 'principals/user/attendee1/personal']]);
+               // construct mock calendar manager and returns
+               /** @var Manager&MockObject $manager */
+               $manager = $this->getMockBuilder(Manager::class)
+                       ->setConstructorArgs([
+                               $this->coordinator,
+                               $this->container,
+                               $this->logger,
+                               $this->time
+                       ])
+                       ->onlyMethods(['getCalendarsForPrincipal'])
+                       ->getMock();
+               $manager->expects(self::once())
+                       ->method('getCalendarsForPrincipal')
+                       ->willReturn([$userCalendar]);
+               // construct parameters
+               $principalUri = 'principals/user/attendee1';
+               $sender = 'organizer@testing.com';
+               $recipient = 'attendee1@testing.com';
+               $calendar = $this->vCalendar1a;
+               $calendar->add('METHOD', 'REQUEST');
+               // construct user calendar returns
+               $userCalendar->expects(self::once())
+                       ->method('handleIMipMessage')
+                       ->with('', $calendar->serialize());
+               // test method
+               $result = $manager->handleIMipRequest($principalUri, $sender, $recipient, $calendar->serialize());
+               $this->assertTrue($result);
+       }
+
        public function testHandleImipReplyWrongMethod(): void {
                $principalUri = 'principals/user/linus';
                $sender = 'pierre@general-store.com';
@@ -323,7 +649,7 @@ class ManagerTest extends TestCase {
                                'getCalendarsForPrincipal'
                        ])
                        ->getMock();
-               $calendar = $this->createMock(ICreateFromStringAndHandleImipMessage::class);
+               $calendar = $this->createMock(ITestCalendar::class);
                $principalUri = 'principals/user/linus';
                $sender = 'pierre@general-store.com';
                $recipient = 'linus@stardew-tent-living.com';
@@ -360,7 +686,7 @@ class ManagerTest extends TestCase {
                                'getCalendarsForPrincipal'
                        ])
                        ->getMock();
-               $calendar = $this->createMock(ICreateFromStringAndHandleImipMessage::class);
+               $calendar = $this->createMock(ITestCalendar::class);
                $principalUri = 'principals/user/linus';
                $sender = 'pierre@general-store.com';
                $recipient = 'linus@stardew-tent-living.com';
@@ -484,7 +810,7 @@ class ManagerTest extends TestCase {
                $sender = 'clint@stardew-blacksmiths.com';
                $recipient = 'pierre@general-store.com';
                $replyTo = 'linus@stardew-tent-living.com';
-               $calendar = $this->createMock(ICreateFromStringAndHandleImipMessage::class);
+               $calendar = $this->createMock(ITestCalendar::class);
                $calendarData = $this->getVCalendarCancel();
 
                $this->time->expects(self::once())
@@ -521,7 +847,7 @@ class ManagerTest extends TestCase {
                $sender = 'linus@stardew-tent-living.com';
                $recipient = 'pierre@general-store.com';
                $replyTo = null;
-               $calendar = $this->createMock(ICreateFromStringAndHandleImipMessage::class);
+               $calendar = $this->createMock(ITestCalendar::class);
                $calendarData = $this->getVCalendarCancel();
 
                $this->time->expects(self::once())
@@ -540,7 +866,7 @@ class ManagerTest extends TestCase {
                $result = $manager->handleIMipCancel($principalUri, $sender, $replyTo, $recipient, $calendarData->serialize());
                $this->assertTrue($result);
        }
-
+       
        private function getVCalendarReply(): Document {
                $data = <<<EOF
 BEGIN:VCALENDAR