]> source.dussan.org Git - nextcloud-server.git/commitdiff
feat: Add out-of-office message API 40653/head
authorChristoph Wurst <christoph@winzerhof-wurst.at>
Wed, 27 Sep 2023 09:41:20 +0000 (11:41 +0200)
committerRichard Steinmetz <richard@steinmetz.cloud>
Thu, 9 Nov 2023 09:36:11 +0000 (10:36 +0100)
[skipci]

Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
18 files changed:
apps/dav/appinfo/routes.php
apps/dav/composer/composer/autoload_classmap.php
apps/dav/composer/composer/autoload_static.php
apps/dav/lib/Controller/OutOfOfficeController.php [new file with mode: 0644]
apps/dav/lib/Db/Absence.php
apps/dav/lib/ResponseDefinitions.php [new file with mode: 0644]
apps/dav/lib/Service/AbsenceService.php
apps/dav/openapi.json
lib/composer/composer/autoload_classmap.php
lib/composer/composer/autoload_static.php
lib/private/User/AvailabilityCoordinator.php [new file with mode: 0644]
lib/private/User/OutOfOfficeData.php [new file with mode: 0644]
lib/public/User/Events/OutOfOfficeChangedEvent.php [new file with mode: 0644]
lib/public/User/Events/OutOfOfficeClearedEvent.php [new file with mode: 0644]
lib/public/User/Events/OutOfOfficeScheduledEvent.php [new file with mode: 0644]
lib/public/User/IAvailabilityCoordinator.php [new file with mode: 0644]
lib/public/User/IOutOfOfficeData.php [new file with mode: 0644]
tests/lib/User/AvailabilityCoordinatorTest.php [new file with mode: 0644]

index 3236c0642af6e422077048698fc18c6ed63fd967..1b2fa0094bfd58c7c1b097b54d5f73fd762569c4 100644 (file)
@@ -35,5 +35,6 @@ return [
        ],
        'ocs' => [
                ['name' => 'direct#getUrl', 'url' => '/api/v1/direct', 'verb' => 'POST'],
+               ['name' => 'out_of_office#getCurrentOutOfOfficeData', 'url' => '/api/v1/outOfOffice/{userId}', 'verb' => 'GET'],
        ],
 ];
index e0c3e20dc6bd072846528327eb91d71a952dd93b..bb424e47787ac8387be12b17b6002ad90682bd28 100644 (file)
@@ -192,6 +192,7 @@ return array(
     'OCA\\DAV\\Controller\\BirthdayCalendarController' => $baseDir . '/../lib/Controller/BirthdayCalendarController.php',
     'OCA\\DAV\\Controller\\DirectController' => $baseDir . '/../lib/Controller/DirectController.php',
     'OCA\\DAV\\Controller\\InvitationResponseController' => $baseDir . '/../lib/Controller/InvitationResponseController.php',
+    'OCA\\DAV\\Controller\\OutOfOfficeController' => $baseDir . '/../lib/Controller/OutOfOfficeController.php',
     'OCA\\DAV\\DAV\\CustomPropertiesBackend' => $baseDir . '/../lib/DAV/CustomPropertiesBackend.php',
     'OCA\\DAV\\DAV\\GroupPrincipalBackend' => $baseDir . '/../lib/DAV/GroupPrincipalBackend.php',
     'OCA\\DAV\\DAV\\PublicAuth' => $baseDir . '/../lib/DAV/PublicAuth.php',
@@ -305,6 +306,7 @@ return array(
     'OCA\\DAV\\Profiler\\ProfilerPlugin' => $baseDir . '/../lib/Profiler/ProfilerPlugin.php',
     'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningNode.php',
     'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php',
+    'OCA\\DAV\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php',
     'OCA\\DAV\\RootCollection' => $baseDir . '/../lib/RootCollection.php',
     'OCA\\DAV\\Search\\ACalendarSearchProvider' => $baseDir . '/../lib/Search/ACalendarSearchProvider.php',
     'OCA\\DAV\\Search\\ContactsSearchProvider' => $baseDir . '/../lib/Search/ContactsSearchProvider.php',
index 9292731af985dc19fb3653a1807fa40f2d08606b..ef040e79674e324288a183592e43afe09f5ab284 100644 (file)
@@ -207,6 +207,7 @@ class ComposerStaticInitDAV
         'OCA\\DAV\\Controller\\BirthdayCalendarController' => __DIR__ . '/..' . '/../lib/Controller/BirthdayCalendarController.php',
         'OCA\\DAV\\Controller\\DirectController' => __DIR__ . '/..' . '/../lib/Controller/DirectController.php',
         'OCA\\DAV\\Controller\\InvitationResponseController' => __DIR__ . '/..' . '/../lib/Controller/InvitationResponseController.php',
+        'OCA\\DAV\\Controller\\OutOfOfficeController' => __DIR__ . '/..' . '/../lib/Controller/OutOfOfficeController.php',
         'OCA\\DAV\\DAV\\CustomPropertiesBackend' => __DIR__ . '/..' . '/../lib/DAV/CustomPropertiesBackend.php',
         'OCA\\DAV\\DAV\\GroupPrincipalBackend' => __DIR__ . '/..' . '/../lib/DAV/GroupPrincipalBackend.php',
         'OCA\\DAV\\DAV\\PublicAuth' => __DIR__ . '/..' . '/../lib/DAV/PublicAuth.php',
@@ -320,6 +321,7 @@ class ComposerStaticInitDAV
         'OCA\\DAV\\Profiler\\ProfilerPlugin' => __DIR__ . '/..' . '/../lib/Profiler/ProfilerPlugin.php',
         'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningNode.php',
         'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php',
+        'OCA\\DAV\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php',
         'OCA\\DAV\\RootCollection' => __DIR__ . '/..' . '/../lib/RootCollection.php',
         'OCA\\DAV\\Search\\ACalendarSearchProvider' => __DIR__ . '/..' . '/../lib/Search/ACalendarSearchProvider.php',
         'OCA\\DAV\\Search\\ContactsSearchProvider' => __DIR__ . '/..' . '/../lib/Search/ContactsSearchProvider.php',
diff --git a/apps/dav/lib/Controller/OutOfOfficeController.php b/apps/dav/lib/Controller/OutOfOfficeController.php
new file mode 100644 (file)
index 0000000..fe4200e
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\DAV\Controller;
+
+use OCA\DAV\Db\AbsenceMapper;
+use OCA\DAV\ResponseDefinitions;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\AppFramework\OCSController;
+use OCP\IRequest;
+
+/**
+ * @psalm-import-type DAVOutOfOfficeData from ResponseDefinitions
+ */
+class OutOfOfficeController extends OCSController {
+
+       public function __construct(
+               string $appName,
+               IRequest $request,
+               private AbsenceMapper $absenceMapper,
+       ) {
+               parent::__construct($appName, $request);
+       }
+
+       /**
+        * Get the currently configured out-of-office data of a user.
+        *
+        * @NoAdminRequired
+        * @NoCSRFRequired
+        *
+        * @param string $userId The user id to get out-of-office data for.
+        * @return DataResponse<Http::STATUS_OK|Http::STATUS_NOT_FOUND, ?DAVOutOfOfficeData, array{}>
+        *
+        * 200: Out-of-office data
+        * 404: No out-of-office data was found
+        */
+       public function getCurrentOutOfOfficeData(string $userId): DataResponse {
+               try {
+                       $data = $this->absenceMapper->findByUserId($userId);
+               } catch (DoesNotExistException) {
+                       return new DataResponse(null, Http::STATUS_NOT_FOUND);
+               }
+
+               return new DataResponse([
+                       'id' => $data->getId(),
+                       'userId' => $data->getUserId(),
+                       'firstDay' => $data->getFirstDay(),
+                       'lastDay' => $data->getLastDay(),
+                       'status' => $data->getStatus(),
+                       'message' => $data->getMessage(),
+               ]);
+       }
+}
index f705b99ef30582eea046751a14319ddb0e327e51..e9ce1d2ea64968431bc822dccba3147810e4e33c 100644 (file)
@@ -26,8 +26,13 @@ declare(strict_types=1);
 
 namespace OCA\DAV\Db;
 
+use DateTimeImmutable;
+use InvalidArgumentException;
 use JsonSerializable;
+use OC\User\OutOfOfficeData;
 use OCP\AppFramework\Db\Entity;
+use OCP\IUser;
+use OCP\User\IOutOfOfficeData;
 
 /**
  * @method string getUserId()
@@ -43,8 +48,13 @@ use OCP\AppFramework\Db\Entity;
  */
 class Absence extends Entity implements JsonSerializable {
        protected string $userId = '';
+
+       /** Inclusive, formatted as YYYY-MM-DD */
        protected string $firstDay = '';
+
+       /** Inclusive, formatted as YYYY-MM-DD */
        protected string $lastDay = '';
+
        protected string $status = '';
        protected string $message = '';
 
@@ -56,6 +66,24 @@ class Absence extends Entity implements JsonSerializable {
                $this->addType('message', 'string');
        }
 
+       public function toOutOufOfficeData(IUser $user): IOutOfOfficeData {
+               if ($user->getUID() !== $this->getUserId()) {
+                       throw new InvalidArgumentException("The user doesn't match the user id of this absence! Expected " . $this->getUserId() . ", got " . $user->getUID());
+               }
+
+               //$user = $userManager->get($this->getUserId());
+               $startDate = new DateTimeImmutable($this->getFirstDay());
+               $endDate = new DateTimeImmutable($this->getLastDay());
+               return new OutOfOfficeData(
+                       (string)$this->getId(),
+                       $user,
+                       $startDate->getTimestamp(),
+                       $endDate->getTimestamp(),
+                       $this->getStatus(),
+                       $this->getMessage(),
+               );
+       }
+
        public function jsonSerialize(): array {
                return [
                        'userId' => $this->userId,
diff --git a/apps/dav/lib/ResponseDefinitions.php b/apps/dav/lib/ResponseDefinitions.php
new file mode 100644 (file)
index 0000000..97bd8e9
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\DAV;
+
+/**
+ * @psalm-type DAVOutOfOfficeData = array{
+ *     id: int,
+ *     userId: string,
+ *     firstDay: string,
+ *     lastDay: string,
+ *     status: string,
+ *     message: string,
+ * }
+ */
+class ResponseDefinitions {
+}
index 228007b3af1141f82e977c495fc5ebce6fb8fcd5..69dee1bd8cc36e9ee98e75df4cb1f85f2009d894 100644 (file)
@@ -26,18 +26,30 @@ declare(strict_types=1);
 
 namespace OCA\DAV\Service;
 
+use InvalidArgumentException;
 use OCA\DAV\Db\Absence;
 use OCA\DAV\Db\AbsenceMapper;
 use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IUserManager;
+use OCP\User\Events\OutOfOfficeChangedEvent;
+use OCP\User\Events\OutOfOfficeClearedEvent;
+use OCP\User\Events\OutOfOfficeScheduledEvent;
 
 class AbsenceService {
        public function __construct(
                private AbsenceMapper $absenceMapper,
+               private IEventDispatcher $eventDispatcher,
+               private IUserManager $userManager,
        ) {
        }
 
        /**
+        * @param string $firstDay The first day (inclusive) of the absence formatted as YYYY-MM-DD.
+        * @param string $lastDay The last day (inclusive) of the absence formatted as YYYY-MM-DD.
+        *
         * @throws \OCP\DB\Exception
+        * @throws InvalidArgumentException If no user with the given user id exists.
         */
        public function createOrUpdateAbsence(
                string $userId,
@@ -58,9 +70,19 @@ class AbsenceService {
                $absence->setStatus($status);
                $absence->setMessage($message);
 
+               // TODO: this method should probably just take a IUser instance
+               $user = $this->userManager->get($userId);
+               if ($user === null) {
+                       throw new InvalidArgumentException("User $userId does not exist");
+               }
+               $eventData = $absence->toOutOufOfficeData($user);
+
                if ($absence->getId() === null) {
+                       $this->eventDispatcher->dispatchTyped(new OutOfOfficeScheduledEvent($eventData));
                        return $this->absenceMapper->insert($absence);
                }
+
+               $this->eventDispatcher->dispatchTyped(new OutOfOfficeChangedEvent($eventData));
                return $this->absenceMapper->update($absence);
        }
 
@@ -68,7 +90,20 @@ class AbsenceService {
         * @throws \OCP\DB\Exception
         */
        public function clearAbsence(string $userId): void {
-               $this->absenceMapper->deleteByUserId($userId);
+               try {
+                       $absence = $this->absenceMapper->findByUserId($userId);
+               } catch (DoesNotExistException $e) {
+                       // Nothing to clear
+                       return;
+               }
+               $this->absenceMapper->delete($absence);
+               // TODO: this method should probably just take a IUser instance
+               $user = $this->userManager->get($userId);
+               if ($user === null) {
+                       throw new InvalidArgumentException("User $userId does not exist");
+               }
+               $eventData = $absence->toOutOufOfficeData($user);
+               $this->eventDispatcher->dispatchTyped(new OutOfOfficeClearedEvent($eventData));
        }
 }
 
index 09da0c19ff3eded1b98ec85b4ed530e7f894cbdf..994d0cde11ddcdd4543651cc1419be08cce34f49 100644 (file)
                         "type": "string"
                     }
                 }
+            },
+            "OutOfOfficeData": {
+                "type": "object",
+                "required": [
+                    "id",
+                    "userId",
+                    "firstDay",
+                    "lastDay",
+                    "status",
+                    "message"
+                ],
+                "properties": {
+                    "id": {
+                        "type": "integer",
+                        "format": "int64"
+                    },
+                    "userId": {
+                        "type": "string"
+                    },
+                    "firstDay": {
+                        "type": "string"
+                    },
+                    "lastDay": {
+                        "type": "string"
+                    },
+                    "status": {
+                        "type": "string"
+                    },
+                    "message": {
+                        "type": "string"
+                    }
+                }
             }
         }
     },
                     }
                 }
             }
+        },
+        "/ocs/v2.php/apps/dav/api/v1/outOfOffice/{userId}": {
+            "get": {
+                "operationId": "out_of_office-get-current-out-of-office-data",
+                "summary": "Get the currently configured out-of-office data of a user.",
+                "tags": [
+                    "out_of_office"
+                ],
+                "security": [
+                    {
+                        "bearer_auth": []
+                    },
+                    {
+                        "basic_auth": []
+                    }
+                ],
+                "parameters": [
+                    {
+                        "name": "userId",
+                        "in": "path",
+                        "description": "The user id to get out-of-office data for.",
+                        "required": true,
+                        "schema": {
+                            "type": "string"
+                        }
+                    },
+                    {
+                        "name": "OCS-APIRequest",
+                        "in": "header",
+                        "description": "Required to be true for the API request to pass",
+                        "required": true,
+                        "schema": {
+                            "type": "boolean",
+                            "default": true
+                        }
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "Out-of-office data",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "type": "object",
+                                    "required": [
+                                        "ocs"
+                                    ],
+                                    "properties": {
+                                        "ocs": {
+                                            "type": "object",
+                                            "required": [
+                                                "meta",
+                                                "data"
+                                            ],
+                                            "properties": {
+                                                "meta": {
+                                                    "$ref": "#/components/schemas/OCSMeta"
+                                                },
+                                                "data": {
+                                                    "$ref": "#/components/schemas/OutOfOfficeData",
+                                                    "nullable": true
+                                                }
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    },
+                    "404": {
+                        "description": "No out-of-office data was found",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "type": "object",
+                                    "required": [
+                                        "ocs"
+                                    ],
+                                    "properties": {
+                                        "ocs": {
+                                            "type": "object",
+                                            "required": [
+                                                "meta",
+                                                "data"
+                                            ],
+                                            "properties": {
+                                                "meta": {
+                                                    "$ref": "#/components/schemas/OCSMeta"
+                                                },
+                                                "data": {
+                                                    "$ref": "#/components/schemas/OutOfOfficeData",
+                                                    "nullable": true
+                                                }
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
         }
     },
     "tags": []
index 1a55209c28635d777623a96344bae1276dd7e15b..9757095dc04a75ff006989a13a84df72604ab2e3 100644 (file)
@@ -731,6 +731,9 @@ return array(
     'OCP\\User\\Events\\BeforeUserLoggedInEvent' => $baseDir . '/lib/public/User/Events/BeforeUserLoggedInEvent.php',
     'OCP\\User\\Events\\BeforeUserLoggedInWithCookieEvent' => $baseDir . '/lib/public/User/Events/BeforeUserLoggedInWithCookieEvent.php',
     'OCP\\User\\Events\\BeforeUserLoggedOutEvent' => $baseDir . '/lib/public/User/Events/BeforeUserLoggedOutEvent.php',
+    'OCP\\User\\Events\\OutOfOfficeChangedEvent' => $baseDir . '/lib/public/User/Events/OutOfOfficeChangedEvent.php',
+    'OCP\\User\\Events\\OutOfOfficeClearedEvent' => $baseDir . '/lib/public/User/Events/OutOfOfficeClearedEvent.php',
+    'OCP\\User\\Events\\OutOfOfficeScheduledEvent' => $baseDir . '/lib/public/User/Events/OutOfOfficeScheduledEvent.php',
     'OCP\\User\\Events\\PasswordUpdatedEvent' => $baseDir . '/lib/public/User/Events/PasswordUpdatedEvent.php',
     'OCP\\User\\Events\\PostLoginEvent' => $baseDir . '/lib/public/User/Events/PostLoginEvent.php',
     'OCP\\User\\Events\\UserChangedEvent' => $baseDir . '/lib/public/User/Events/UserChangedEvent.php',
@@ -742,6 +745,8 @@ return array(
     'OCP\\User\\Events\\UserLoggedInWithCookieEvent' => $baseDir . '/lib/public/User/Events/UserLoggedInWithCookieEvent.php',
     'OCP\\User\\Events\\UserLoggedOutEvent' => $baseDir . '/lib/public/User/Events/UserLoggedOutEvent.php',
     'OCP\\User\\GetQuotaEvent' => $baseDir . '/lib/public/User/GetQuotaEvent.php',
+    'OCP\\User\\IAvailabilityCoordinator' => $baseDir . '/lib/public/User/IAvailabilityCoordinator.php',
+    'OCP\\User\\IOutOfOfficeData' => $baseDir . '/lib/public/User/IOutOfOfficeData.php',
     'OCP\\Util' => $baseDir . '/lib/public/Util.php',
     'OCP\\WorkflowEngine\\EntityContext\\IContextPortation' => $baseDir . '/lib/public/WorkflowEngine/EntityContext/IContextPortation.php',
     'OCP\\WorkflowEngine\\EntityContext\\IDisplayName' => $baseDir . '/lib/public/WorkflowEngine/EntityContext/IDisplayName.php',
@@ -1762,6 +1767,7 @@ return array(
     'OC\\Updater\\VersionCheck' => $baseDir . '/lib/private/Updater/VersionCheck.php',
     'OC\\UserStatus\\ISettableProvider' => $baseDir . '/lib/private/UserStatus/ISettableProvider.php',
     'OC\\UserStatus\\Manager' => $baseDir . '/lib/private/UserStatus/Manager.php',
+    'OC\\User\\AvailabilityCoordinator' => $baseDir . '/lib/private/User/AvailabilityCoordinator.php',
     'OC\\User\\Backend' => $baseDir . '/lib/private/User/Backend.php',
     'OC\\User\\Database' => $baseDir . '/lib/private/User/Database.php',
     'OC\\User\\DisplayNameCache' => $baseDir . '/lib/private/User/DisplayNameCache.php',
@@ -1771,6 +1777,7 @@ return array(
     'OC\\User\\LoginException' => $baseDir . '/lib/private/User/LoginException.php',
     'OC\\User\\Manager' => $baseDir . '/lib/private/User/Manager.php',
     'OC\\User\\NoUserException' => $baseDir . '/lib/private/User/NoUserException.php',
+    'OC\\User\\OutOfOfficeData' => $baseDir . '/lib/private/User/OutOfOfficeData.php',
     'OC\\User\\Session' => $baseDir . '/lib/private/User/Session.php',
     'OC\\User\\User' => $baseDir . '/lib/private/User/User.php',
     'OC_API' => $baseDir . '/lib/private/legacy/OC_API.php',
index bebf5be91bdd280029618dfea2871b5127bff0f2..a0492513f8ccef83f04fe88af73dae3f7481c778 100644 (file)
@@ -764,6 +764,9 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
         'OCP\\User\\Events\\BeforeUserLoggedInEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/BeforeUserLoggedInEvent.php',
         'OCP\\User\\Events\\BeforeUserLoggedInWithCookieEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/BeforeUserLoggedInWithCookieEvent.php',
         'OCP\\User\\Events\\BeforeUserLoggedOutEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/BeforeUserLoggedOutEvent.php',
+        'OCP\\User\\Events\\OutOfOfficeChangedEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/OutOfOfficeChangedEvent.php',
+        'OCP\\User\\Events\\OutOfOfficeClearedEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/OutOfOfficeClearedEvent.php',
+        'OCP\\User\\Events\\OutOfOfficeScheduledEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/OutOfOfficeScheduledEvent.php',
         'OCP\\User\\Events\\PasswordUpdatedEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/PasswordUpdatedEvent.php',
         'OCP\\User\\Events\\PostLoginEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/PostLoginEvent.php',
         'OCP\\User\\Events\\UserChangedEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/UserChangedEvent.php',
@@ -775,6 +778,8 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
         'OCP\\User\\Events\\UserLoggedInWithCookieEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/UserLoggedInWithCookieEvent.php',
         'OCP\\User\\Events\\UserLoggedOutEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/UserLoggedOutEvent.php',
         'OCP\\User\\GetQuotaEvent' => __DIR__ . '/../../..' . '/lib/public/User/GetQuotaEvent.php',
+        'OCP\\User\\IAvailabilityCoordinator' => __DIR__ . '/../../..' . '/lib/public/User/IAvailabilityCoordinator.php',
+        'OCP\\User\\IOutOfOfficeData' => __DIR__ . '/../../..' . '/lib/public/User/IOutOfOfficeData.php',
         'OCP\\Util' => __DIR__ . '/../../..' . '/lib/public/Util.php',
         'OCP\\WorkflowEngine\\EntityContext\\IContextPortation' => __DIR__ . '/../../..' . '/lib/public/WorkflowEngine/EntityContext/IContextPortation.php',
         'OCP\\WorkflowEngine\\EntityContext\\IDisplayName' => __DIR__ . '/../../..' . '/lib/public/WorkflowEngine/EntityContext/IDisplayName.php',
@@ -1795,6 +1800,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
         'OC\\Updater\\VersionCheck' => __DIR__ . '/../../..' . '/lib/private/Updater/VersionCheck.php',
         'OC\\UserStatus\\ISettableProvider' => __DIR__ . '/../../..' . '/lib/private/UserStatus/ISettableProvider.php',
         'OC\\UserStatus\\Manager' => __DIR__ . '/../../..' . '/lib/private/UserStatus/Manager.php',
+        'OC\\User\\AvailabilityCoordinator' => __DIR__ . '/../../..' . '/lib/private/User/AvailabilityCoordinator.php',
         'OC\\User\\Backend' => __DIR__ . '/../../..' . '/lib/private/User/Backend.php',
         'OC\\User\\Database' => __DIR__ . '/../../..' . '/lib/private/User/Database.php',
         'OC\\User\\DisplayNameCache' => __DIR__ . '/../../..' . '/lib/private/User/DisplayNameCache.php',
@@ -1804,6 +1810,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
         'OC\\User\\LoginException' => __DIR__ . '/../../..' . '/lib/private/User/LoginException.php',
         'OC\\User\\Manager' => __DIR__ . '/../../..' . '/lib/private/User/Manager.php',
         'OC\\User\\NoUserException' => __DIR__ . '/../../..' . '/lib/private/User/NoUserException.php',
+        'OC\\User\\OutOfOfficeData' => __DIR__ . '/../../..' . '/lib/private/User/OutOfOfficeData.php',
         'OC\\User\\Session' => __DIR__ . '/../../..' . '/lib/private/User/Session.php',
         'OC\\User\\User' => __DIR__ . '/../../..' . '/lib/private/User/User.php',
         'OC_API' => __DIR__ . '/../../..' . '/lib/private/legacy/OC_API.php',
diff --git a/lib/private/User/AvailabilityCoordinator.php b/lib/private/User/AvailabilityCoordinator.php
new file mode 100644 (file)
index 0000000..fe0db92
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OC\User;
+
+use JsonException;
+use OCA\DAV\Db\AbsenceMapper;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\ICache;
+use OCP\ICacheFactory;
+use OCP\IUser;
+use OCP\User\IAvailabilityCoordinator;
+use OCP\User\IOutOfOfficeData;
+use Psr\Log\LoggerInterface;
+
+class AvailabilityCoordinator implements IAvailabilityCoordinator {
+       private ICache $cache;
+
+       public function __construct(
+               ICacheFactory $cacheFactory,
+               private AbsenceMapper $absenceMapper,
+               private LoggerInterface $logger,
+       ) {
+               $this->cache = $cacheFactory->createLocal('OutOfOfficeData');
+       }
+
+       private function getCachedOutOfOfficeData(IUser $user): ?OutOfOfficeData {
+               $cachedString = $this->cache->get($user->getUID());
+               if ($cachedString === null) {
+                       return null;
+               }
+
+               try {
+                       $cachedData = json_decode($cachedString, true, 10, JSON_THROW_ON_ERROR);
+               } catch (JsonException $e) {
+                       $this->logger->error('Failed to deserialize cached out-of-office data: ' . $e->getMessage(), [
+                               'exception' => $e,
+                               'json' => $cachedString,
+                       ]);
+                       return null;
+               }
+
+               return new OutOfOfficeData(
+                       $cachedData['id'],
+                       $user,
+                       $cachedData['startDate'],
+                       $cachedData['endDate'],
+                       $cachedData['shortMessage'],
+                       $cachedData['message'],
+               );
+       }
+
+       private function setCachedOutOfOfficeData(IOutOfOfficeData $data): void {
+               try {
+                       $cachedString = json_encode([
+                               'id' => $data->getId(),
+                               'startDate' => $data->getStartDate(),
+                               'endDate' => $data->getEndDate(),
+                               'shortMessage' => $data->getShortMessage(),
+                               'message' => $data->getMessage(),
+                       ], JSON_THROW_ON_ERROR);
+               } catch (JsonException $e) {
+                       $this->logger->error('Failed to serialize out-of-office data: ' . $e->getMessage(), [
+                               'exception' => $e,
+                       ]);
+                       return;
+               }
+
+               $this->cache->set($data->getUser()->getUID(), $cachedString, 300);
+       }
+
+       public function getCurrentOutOfOfficeData(IUser $user): ?IOutOfOfficeData {
+               $cachedData = $this->getCachedOutOfOfficeData($user);
+               if ($cachedData !== null) {
+                       return $cachedData;
+               }
+
+               try {
+                       $absenceData = $this->absenceMapper->findByUserId($user->getUID());
+               } catch (DoesNotExistException $e) {
+                       return null;
+               }
+
+               $data = $absenceData->toOutOufOfficeData($user);
+               $this->setCachedOutOfOfficeData($data);
+               return $data;
+       }
+}
diff --git a/lib/private/User/OutOfOfficeData.php b/lib/private/User/OutOfOfficeData.php
new file mode 100644 (file)
index 0000000..12b7e03
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OC\User;
+
+use OCP\IUser;
+use OCP\User\IOutOfOfficeData;
+
+class OutOfOfficeData implements IOutOfOfficeData {
+       public function __construct(private string $id,
+               private IUser $user,
+               private int $startDate,
+               private int $endDate,
+               private string $shortMessage,
+               private string $message) {
+       }
+
+       public function getId(): string {
+               return $this->id;
+       }
+
+       public function getUser(): IUser {
+               return $this->user;
+       }
+
+       public function getStartDate(): int {
+               return $this->startDate;
+       }
+
+       public function getEndDate(): int {
+               return $this->endDate;
+       }
+
+       public function getShortMessage(): string {
+               return $this->shortMessage;
+       }
+
+       public function getMessage(): string {
+               return $this->message;
+       }
+}
diff --git a/lib/public/User/Events/OutOfOfficeChangedEvent.php b/lib/public/User/Events/OutOfOfficeChangedEvent.php
new file mode 100644 (file)
index 0000000..5e5753b
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCP\User\Events;
+
+use OCP\EventDispatcher\Event;
+use OCP\User\IOutOfOfficeData;
+
+/**
+ * Emitted when a user's out-of-office period has changed
+ *
+ * @since 28.0.0
+ */
+class OutOfOfficeChangedEvent extends Event {
+       /**
+        * @since 28.0.0
+        */
+       public function __construct(private IOutOfOfficeData $data) {
+               parent::__construct();
+       }
+
+       /**
+        * @since 28.0.0
+        */
+       public function getData(): IOutOfOfficeData {
+               return $this->data;
+       }
+}
diff --git a/lib/public/User/Events/OutOfOfficeClearedEvent.php b/lib/public/User/Events/OutOfOfficeClearedEvent.php
new file mode 100644 (file)
index 0000000..48a77c7
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCP\User\Events;
+
+use OCP\EventDispatcher\Event;
+use OCP\User\IOutOfOfficeData;
+
+/**
+ * Emitted when a user's out-of-office period is cleared
+ *
+ * @since 28.0.0
+ */
+class OutOfOfficeClearedEvent extends Event {
+       /**
+        * @since 28.0.0
+        */
+       public function __construct(private IOutOfOfficeData $data) {
+               parent::__construct();
+       }
+
+       /**
+        * @since 28.0.0
+        */
+       public function getData(): IOutOfOfficeData {
+               return $this->data;
+       }
+}
diff --git a/lib/public/User/Events/OutOfOfficeScheduledEvent.php b/lib/public/User/Events/OutOfOfficeScheduledEvent.php
new file mode 100644 (file)
index 0000000..2bcbec6
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCP\User\Events;
+
+use OCP\EventDispatcher\Event;
+use OCP\User\IOutOfOfficeData;
+
+/**
+ * Emitted when a user's out-of-office period is scheduled
+ *
+ * @since 28.0.0
+ */
+class OutOfOfficeScheduledEvent extends Event {
+       /**
+        * @since 28.0.0
+        */
+       public function __construct(private IOutOfOfficeData $data) {
+               parent::__construct();
+       }
+
+       /**
+        * @since 28.0.0
+        */
+       public function getData(): IOutOfOfficeData {
+               return $this->data;
+       }
+}
diff --git a/lib/public/User/IAvailabilityCoordinator.php b/lib/public/User/IAvailabilityCoordinator.php
new file mode 100644 (file)
index 0000000..113e349
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCP\User;
+
+use OCP\IUser;
+
+/**
+ * Coordinator for availability and out-of-office messages
+ *
+ * @since 28.0.0
+ */
+interface IAvailabilityCoordinator {
+       /**
+        * Get the user's out-of-office message, if any
+        *
+        * @since 28.0.0
+        */
+       public function getCurrentOutOfOfficeData(IUser $user): ?IOutOfOfficeData;
+}
diff --git a/lib/public/User/IOutOfOfficeData.php b/lib/public/User/IOutOfOfficeData.php
new file mode 100644 (file)
index 0000000..0344444
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCP\User;
+
+use OCP\IUser;
+
+/**
+ * DTO to hold out-of-office information of a user
+ *
+ * @since 28.0.0
+ */
+interface IOutOfOfficeData {
+       /**
+        * Get the unique token assigned to the current out-of-office event
+        *
+        * @since 28.0.0
+        */
+       public function getId(): string;
+
+       /**
+        * @since 28.0.0
+        */
+       public function getUser(): IUser;
+
+       /**
+        * Get the accurate out-of-office start date
+        *
+        * This event is not guaranteed to be emitted exactly at start date
+        *
+        * @since 28.0.0
+        */
+       public function getStartDate(): int;
+
+       /**
+        * Get the (preliminary) out-of-office end date
+        *
+        * @since 28.0.0
+        */
+       public function getEndDate(): int;
+
+       /**
+        * Get the short summary text displayed in the user status and similar
+        *
+        * @since 28.0.0
+        */
+       public function getShortMessage(): string;
+
+       /**
+        * Get the long out-of-office message for auto responders and similar
+        *
+        * @since 28.0.0
+        */
+       public function getMessage(): string;
+}
diff --git a/tests/lib/User/AvailabilityCoordinatorTest.php b/tests/lib/User/AvailabilityCoordinatorTest.php
new file mode 100644 (file)
index 0000000..8e847f7
--- /dev/null
@@ -0,0 +1,164 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace Test\User;
+
+use OC\User\AvailabilityCoordinator;
+use OC\User\OutOfOfficeData;
+use OCA\DAV\Db\Absence;
+use OCA\DAV\Db\AbsenceMapper;
+use OCP\ICache;
+use OCP\ICacheFactory;
+use OCP\IUser;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+class AvailabilityCoordinatorTest extends TestCase {
+       private AvailabilityCoordinator $availabilityCoordinator;
+       private ICacheFactory $cacheFactory;
+       private ICache $cache;
+       private AbsenceMapper $absenceMapper;
+       private LoggerInterface $logger;
+
+       protected function setUp(): void {
+               parent::setUp();
+
+               $this->cacheFactory = $this->createMock(ICacheFactory::class);
+               $this->cache = $this->createMock(ICache::class);
+               $this->absenceMapper = $this->createMock(AbsenceMapper::class);
+               $this->logger = $this->createMock(LoggerInterface::class);
+
+               $this->cacheFactory->expects(self::once())
+                       ->method('createLocal')
+                       ->willReturn($this->cache);
+
+               $this->availabilityCoordinator = new AvailabilityCoordinator(
+                       $this->cacheFactory,
+                       $this->absenceMapper,
+                       $this->logger,
+               );
+       }
+
+       public function testGetOutOfOfficeData(): void {
+               $absence = new Absence();
+               $absence->setId(420);
+               $absence->setUserId('user');
+               $absence->setFirstDay('2023-10-01');
+               $absence->setLastDay('2023-10-08');
+               $absence->setStatus('Vacation');
+               $absence->setMessage('On vacation');
+
+               $user = $this->createMock(IUser::class);
+               $user->method('getUID')
+                       ->willReturn('user');
+
+               $this->cache->expects(self::once())
+                       ->method('get')
+                       ->with('user')
+                       ->willReturn(null);
+               $this->absenceMapper->expects(self::once())
+                       ->method('findByUserId')
+                       ->with('user')
+                       ->willReturn($absence);
+               $this->cache->expects(self::once())
+                       ->method('set')
+                       ->with('user', '{"id":"420","startDate":1696118400,"endDate":1696723200,"shortMessage":"Vacation","message":"On vacation"}', 300);
+
+               $expected = new OutOfOfficeData(
+                       '420',
+                       $user,
+                       1696118400,
+                       1696723200,
+                       'Vacation',
+                       'On vacation',
+               );
+               $actual = $this->availabilityCoordinator->getCurrentOutOfOfficeData($user);
+               self::assertEquals($expected, $actual);
+       }
+
+       public function testGetOutOfOfficeDataWithCachedData(): void {
+               $user = $this->createMock(IUser::class);
+               $user->method('getUID')
+                       ->willReturn('user');
+
+               $this->cache->expects(self::once())
+                       ->method('get')
+                       ->with('user')
+                       ->willReturn('{"id":"420","startDate":1696118400,"endDate":1696723200,"shortMessage":"Vacation","message":"On vacation"}');
+               $this->absenceMapper->expects(self::never())
+                       ->method('findByUserId');
+               $this->cache->expects(self::never())
+                       ->method('set');
+
+               $expected = new OutOfOfficeData(
+                       '420',
+                       $user,
+                       1696118400,
+                       1696723200,
+                       'Vacation',
+                       'On vacation',
+               );
+               $actual = $this->availabilityCoordinator->getCurrentOutOfOfficeData($user);
+               self::assertEquals($expected, $actual);
+       }
+
+       public function testGetOutOfOfficeDataWithInvalidCachedData(): void {
+               $absence = new Absence();
+               $absence->setId(420);
+               $absence->setUserId('user');
+               $absence->setFirstDay('2023-10-01');
+               $absence->setLastDay('2023-10-08');
+               $absence->setStatus('Vacation');
+               $absence->setMessage('On vacation');
+
+               $user = $this->createMock(IUser::class);
+               $user->method('getUID')
+                       ->willReturn('user');
+
+               $this->cache->expects(self::once())
+                       ->method('get')
+                       ->with('user')
+                       ->willReturn('{"id":"420",}');
+               $this->absenceMapper->expects(self::once())
+                       ->method('findByUserId')
+                       ->with('user')
+                       ->willReturn($absence);
+               $this->cache->expects(self::once())
+                       ->method('set')
+                       ->with('user', '{"id":"420","startDate":1696118400,"endDate":1696723200,"shortMessage":"Vacation","message":"On vacation"}', 300);
+
+               $expected = new OutOfOfficeData(
+                       '420',
+                       $user,
+                       1696118400,
+                       1696723200,
+                       'Vacation',
+                       'On vacation',
+               );
+               $actual = $this->availabilityCoordinator->getCurrentOutOfOfficeData($user);
+               self::assertEquals($expected, $actual);
+       }
+}