]> source.dussan.org Git - nextcloud-server.git/commitdiff
fix(dav): don't schedule out-of-office jobs for dates in the past 41778/head
authorRichard Steinmetz <richard@steinmetz.cloud>
Mon, 27 Nov 2023 14:49:08 +0000 (15:49 +0100)
committerRichard Steinmetz <richard@steinmetz.cloud>
Tue, 28 Nov 2023 14:21:06 +0000 (15:21 +0100)
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
apps/dav/lib/Service/AbsenceService.php
apps/dav/tests/unit/Service/AbsenceServiceTest.php [new file with mode: 0644]

index 874e86f6e1cec4a43e69cf42366c850514d88646..7c0d6eec0824be22ffa17670ba75850e470d61c0 100644 (file)
@@ -34,9 +34,7 @@ use OCA\DAV\Db\AbsenceMapper;
 use OCP\AppFramework\Db\DoesNotExistException;
 use OCP\AppFramework\Utility\ITimeFactory;
 use OCP\BackgroundJob\IJobList;
-use OCP\Calendar\IManager;
 use OCP\EventDispatcher\IEventDispatcher;
-use OCP\IConfig;
 use OCP\IUser;
 use OCP\User\Events\OutOfOfficeChangedEvent;
 use OCP\User\Events\OutOfOfficeClearedEvent;
@@ -50,8 +48,6 @@ class AbsenceService {
                private IJobList $jobList,
                private TimezoneService $timezoneService,
                private ITimeFactory $timeFactory,
-               private IConfig $appConfig,
-               private IManager $calendarManager,
        ) {
        }
 
@@ -97,22 +93,27 @@ class AbsenceService {
                        $this->eventDispatcher->dispatchTyped(new OutOfOfficeChangedEvent($eventData));
                }
 
-               $this->jobList->scheduleAfter(
-                       OutOfOfficeEventDispatcherJob::class,
-                       $eventData->getStartDate(),
-                       [
-                               'id' => $absence->getId(),
-                               'event' => OutOfOfficeEventDispatcherJob::EVENT_START,
-                       ],
-               );
-               $this->jobList->scheduleAfter(
-                       OutOfOfficeEventDispatcherJob::class,
-                       $eventData->getEndDate(),
-                       [
-                               'id' => $absence->getId(),
-                               'event' => OutOfOfficeEventDispatcherJob::EVENT_END,
-                       ],
-               );
+               $now = $this->timeFactory->getTime();
+               if ($eventData->getStartDate() > $now) {
+                       $this->jobList->scheduleAfter(
+                               OutOfOfficeEventDispatcherJob::class,
+                               $eventData->getStartDate(),
+                               [
+                                       'id' => $absence->getId(),
+                                       'event' => OutOfOfficeEventDispatcherJob::EVENT_START,
+                               ],
+                       );
+               }
+               if ($eventData->getEndDate() > $now) {
+                       $this->jobList->scheduleAfter(
+                               OutOfOfficeEventDispatcherJob::class,
+                               $eventData->getEndDate(),
+                               [
+                                       'id' => $absence->getId(),
+                                       'event' => OutOfOfficeEventDispatcherJob::EVENT_END,
+                               ],
+                       );
+               }
 
                return $absence;
        }
diff --git a/apps/dav/tests/unit/Service/AbsenceServiceTest.php b/apps/dav/tests/unit/Service/AbsenceServiceTest.php
new file mode 100644 (file)
index 0000000..f6f16e2
--- /dev/null
@@ -0,0 +1,471 @@
+<?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\tests\unit\Service;
+
+use DateTimeImmutable;
+use DateTimeZone;
+use OCA\DAV\BackgroundJob\OutOfOfficeEventDispatcherJob;
+use OCA\DAV\CalDAV\TimezoneService;
+use OCA\DAV\Db\Absence;
+use OCA\DAV\Db\AbsenceMapper;
+use OCA\DAV\Service\AbsenceService;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\IJobList;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IUser;
+use OCP\User\Events\OutOfOfficeChangedEvent;
+use OCP\User\Events\OutOfOfficeScheduledEvent;
+use PHPUnit\Framework\TestCase;
+
+class AbsenceServiceTest extends TestCase {
+       private AbsenceService $absenceService;
+
+       /** @var MockObject|AbsenceMapper */
+       private $absenceMapper;
+
+       /** @var MockObject|IEventDispatcher */
+       private $eventDispatcher;
+
+       /** @var MockObject|IJobList */
+       private $jobList;
+
+       /** @var MockObject|TimezoneService */
+       private $timezoneService;
+
+       /** @var MockObject|ITimeFactory */
+       private $timeFactory;
+
+       protected function setUp(): void {
+               parent::setUp();
+
+               $this->absenceMapper = $this->createMock(AbsenceMapper::class);
+               $this->eventDispatcher = $this->createMock(IEventDispatcher::class);
+               $this->jobList = $this->createMock(IJobList::class);
+               $this->timezoneService = $this->createMock(TimezoneService::class);
+               $this->timeFactory = $this->createMock(ITimeFactory::class);
+
+               $this->absenceService = new AbsenceService(
+                       $this->absenceMapper,
+                       $this->eventDispatcher,
+                       $this->jobList,
+                       $this->timezoneService,
+                       $this->timeFactory,
+               );
+       }
+
+       public function testCreateAbsenceEmitsScheduledEvent() {
+               $tz = new DateTimeZone('Europe/Berlin');
+               $user = $this->createMock(IUser::class);
+               $user->method('getUID')
+                       ->willReturn('user');
+
+               $this->absenceMapper->expects(self::once())
+                       ->method('findByUserId')
+                       ->with('user')
+                       ->willThrowException(new DoesNotExistException('foo bar'));
+               $this->absenceMapper->expects(self::once())
+                       ->method('insert')
+                       ->willReturnCallback(function (Absence $absence): Absence {
+                               $absence->setId(1);
+                               return $absence;
+                       });
+               $this->timezoneService->expects(self::once())
+                       ->method('getUserTimezone')
+                       ->with('user')
+                       ->willReturn('Europe/Berlin');
+               $this->eventDispatcher->expects(self::once())
+                       ->method('dispatchTyped')
+                       ->with(self::callback(static function (Event $event) use ($user, $tz): bool {
+                               self::assertInstanceOf(OutOfOfficeScheduledEvent::class, $event);
+                               /** @var OutOfOfficeScheduledEvent $event */
+                               $data = $event->getData();
+                               self::assertEquals('1', $data->getId());
+                               self::assertEquals($user, $data->getUser());
+                               self::assertEquals(
+                                       (new DateTimeImmutable('2023-01-05', $tz))->getTimeStamp(),
+                                       $data->getStartDate(),
+                               );
+                               self::assertEquals(
+                                       (new DateTimeImmutable('2023-01-10', $tz))->getTimeStamp() + 3600 * 23 + 59 * 60,
+                                       $data->getEndDate(),
+                               );
+                               self::assertEquals('status', $data->getShortMessage());
+                               self::assertEquals('message', $data->getMessage());
+                               return true;
+                       }));
+               $this->timeFactory->expects(self::once())
+                       ->method('getTime')
+                       ->willReturn(PHP_INT_MAX);
+               $this->jobList->expects(self::never())
+                       ->method('scheduleAfter');
+
+               $this->absenceService->createOrUpdateAbsence(
+                       $user,
+                       '2023-01-05',
+                       '2023-01-10',
+                       'status',
+                       'message',
+               );
+       }
+
+       public function testUpdateAbsenceEmitsChangedEvent() {
+               $tz = new DateTimeZone('Europe/Berlin');
+               $user = $this->createMock(IUser::class);
+               $user->method('getUID')
+                       ->willReturn('user');
+               $absence = new Absence();
+               $absence->setId(1);
+               $absence->setFirstDay('1970-01-01');
+               $absence->setLastDay('1970-01-10');
+               $absence->setStatus('old status');
+               $absence->setMessage('old message');
+
+               $this->absenceMapper->expects(self::once())
+                       ->method('findByUserId')
+                       ->with('user')
+                       ->willReturn($absence);
+               $this->absenceMapper->expects(self::once())
+                       ->method('update')
+                       ->willReturnCallback(static function (Absence $absence): Absence {
+                               self::assertEquals('2023-01-05', $absence->getFirstDay());
+                               self::assertEquals('2023-01-10', $absence->getLastDay());
+                               self::assertEquals('status', $absence->getStatus());
+                               self::assertEquals('message', $absence->getMessage());
+                               return $absence;
+                       });
+               $this->timezoneService->expects(self::once())
+                       ->method('getUserTimezone')
+                       ->with('user')
+                       ->willReturn('Europe/Berlin');
+               $this->eventDispatcher->expects(self::once())
+                       ->method('dispatchTyped')
+                       ->with(self::callback(static function (Event $event) use ($user, $tz): bool {
+                               self::assertInstanceOf(OutOfOfficeChangedEvent::class, $event);
+                               /** @var OutOfOfficeChangedEvent $event */
+                               $data = $event->getData();
+                               self::assertEquals('1', $data->getId());
+                               self::assertEquals($user, $data->getUser());
+                               self::assertEquals(
+                                       (new DateTimeImmutable('2023-01-05', $tz))->getTimeStamp(),
+                                       $data->getStartDate(),
+                               );
+                               self::assertEquals(
+                                       (new DateTimeImmutable('2023-01-10', $tz))->getTimeStamp() + 3600 * 23 + 59 * 60,
+                                       $data->getEndDate(),
+                               );
+                               self::assertEquals('status', $data->getShortMessage());
+                               self::assertEquals('message', $data->getMessage());
+                               return true;
+                       }));
+               $this->timeFactory->expects(self::once())
+                       ->method('getTime')
+                       ->willReturn(PHP_INT_MAX);
+               $this->jobList->expects(self::never())
+                       ->method('scheduleAfter');
+
+               $this->absenceService->createOrUpdateAbsence(
+                       $user,
+                       '2023-01-05',
+                       '2023-01-10',
+                       'status',
+                       'message',
+               );
+       }
+
+       public function testCreateAbsenceSchedulesBothJobs() {
+               $tz = new DateTimeZone('Europe/Berlin');
+               $startDateString = '2023-01-05';
+               $startDate = new DateTimeImmutable($startDateString, $tz);
+               $endDateString = '2023-01-10';
+               $endDate = new DateTimeImmutable($endDateString, $tz);
+               $user = $this->createMock(IUser::class);
+               $user->method('getUID')
+                       ->willReturn('user');
+
+               $this->absenceMapper->expects(self::once())
+                       ->method('findByUserId')
+                       ->with('user')
+                       ->willThrowException(new DoesNotExistException('foo bar'));
+               $this->absenceMapper->expects(self::once())
+                       ->method('insert')
+                       ->willReturnCallback(function (Absence $absence): Absence {
+                               $absence->setId(1);
+                               return $absence;
+                       });
+               $this->timezoneService->expects(self::once())
+                       ->method('getUserTimezone')
+                       ->with('user')
+                       ->willReturn($tz->getName());
+               $this->timeFactory->expects(self::once())
+                       ->method('getTime')
+                       ->willReturn((new DateTimeImmutable('2023-01-01', $tz))->getTimestamp());
+               $this->jobList->expects(self::exactly(2))
+                       ->method('scheduleAfter')
+                       ->willReturnMap([
+                               [OutOfOfficeEventDispatcherJob::class, $startDate->getTimestamp(), [
+                                       'id' => '1',
+                                       'event' => OutOfOfficeEventDispatcherJob::EVENT_START,
+                               ]],
+                               [OutOfOfficeEventDispatcherJob::class, $endDate->getTimestamp() + 3600 * 23 + 59 * 60, [
+                                       'id' => '1',
+                                       'event' => OutOfOfficeEventDispatcherJob::EVENT_END,
+                               ]],
+                       ]);
+
+               $this->absenceService->createOrUpdateAbsence(
+                       $user,
+                       $startDateString,
+                       $endDateString,
+                       '',
+                       '',
+               );
+       }
+
+       public function testCreateAbsenceSchedulesOnlyEndJob() {
+               $tz = new DateTimeZone('Europe/Berlin');
+               $endDateString = '2023-01-10';
+               $endDate = new DateTimeImmutable($endDateString, $tz);
+               $user = $this->createMock(IUser::class);
+               $user->method('getUID')
+                       ->willReturn('user');
+
+               $this->absenceMapper->expects(self::once())
+                       ->method('findByUserId')
+                       ->with('user')
+                       ->willThrowException(new DoesNotExistException('foo bar'));
+               $this->absenceMapper->expects(self::once())
+                       ->method('insert')
+                       ->willReturnCallback(function (Absence $absence): Absence {
+                               $absence->setId(1);
+                               return $absence;
+                       });
+               $this->timezoneService->expects(self::once())
+                       ->method('getUserTimezone')
+                       ->with('user')
+                       ->willReturn($tz->getName());
+               $this->timeFactory->expects(self::once())
+                       ->method('getTime')
+                       ->willReturn((new DateTimeImmutable('2023-01-07', $tz))->getTimestamp());
+               $this->jobList->expects(self::once())
+                       ->method('scheduleAfter')
+                       ->with(OutOfOfficeEventDispatcherJob::class, $endDate->getTimestamp() + 3600 * 23 + 59 * 60, [
+                               'id' => '1',
+                               'event' => OutOfOfficeEventDispatcherJob::EVENT_END,
+                       ]);
+
+               $this->absenceService->createOrUpdateAbsence(
+                       $user,
+                       '2023-01-05',
+                       $endDateString,
+                       '',
+                       '',
+               );
+       }
+
+       public function testCreateAbsenceSchedulesNoJob() {
+               $tz = new DateTimeZone('Europe/Berlin');
+               $user = $this->createMock(IUser::class);
+               $user->method('getUID')
+                       ->willReturn('user');
+
+               $this->absenceMapper->expects(self::once())
+                       ->method('findByUserId')
+                       ->with('user')
+                       ->willThrowException(new DoesNotExistException('foo bar'));
+               $this->absenceMapper->expects(self::once())
+                       ->method('insert')
+                       ->willReturnCallback(function (Absence $absence): Absence {
+                               $absence->setId(1);
+                               return $absence;
+                       });
+               $this->timezoneService->expects(self::once())
+                       ->method('getUserTimezone')
+                       ->with('user')
+                       ->willReturn($tz->getName());
+               $this->timeFactory->expects(self::once())
+                       ->method('getTime')
+                       ->willReturn((new DateTimeImmutable('2023-01-12', $tz))->getTimestamp());
+               $this->jobList->expects(self::never())
+                       ->method('scheduleAfter');
+
+               $this->absenceService->createOrUpdateAbsence(
+                       $user,
+                       '2023-01-05',
+                       '2023-01-10',
+                       '',
+                       '',
+               );
+       }
+
+       public function testUpdateAbsenceSchedulesBothJobs() {
+               $tz = new DateTimeZone('Europe/Berlin');
+               $startDateString = '2023-01-05';
+               $startDate = new DateTimeImmutable($startDateString, $tz);
+               $endDateString = '2023-01-10';
+               $endDate = new DateTimeImmutable($endDateString, $tz);
+               $user = $this->createMock(IUser::class);
+               $user->method('getUID')
+                       ->willReturn('user');
+               $absence = new Absence();
+               $absence->setId(1);
+               $absence->setFirstDay('1970-01-01');
+               $absence->setLastDay('1970-01-10');
+               $absence->setStatus('old status');
+               $absence->setMessage('old message');
+
+               $this->absenceMapper->expects(self::once())
+                       ->method('findByUserId')
+                       ->with('user')
+                       ->willReturn($absence);
+               $this->absenceMapper->expects(self::once())
+                       ->method('update')
+                       ->willReturnCallback(static function (Absence $absence) use ($startDateString, $endDateString): Absence {
+                               self::assertEquals($startDateString, $absence->getFirstDay());
+                               self::assertEquals($endDateString, $absence->getLastDay());
+                               return $absence;
+                       });
+               $this->timezoneService->expects(self::once())
+                       ->method('getUserTimezone')
+                       ->with('user')
+                       ->willReturn($tz->getName());
+               $this->timeFactory->expects(self::once())
+                       ->method('getTime')
+                       ->willReturn((new DateTimeImmutable('2023-01-01', $tz))->getTimestamp());
+               $this->jobList->expects(self::exactly(2))
+                       ->method('scheduleAfter')
+                       ->willReturnMap([
+                               [OutOfOfficeEventDispatcherJob::class, $startDate->getTimestamp(), [
+                                       'id' => '1',
+                                       'event' => OutOfOfficeEventDispatcherJob::EVENT_START,
+                               ]],
+                               [OutOfOfficeEventDispatcherJob::class, $endDate->getTimestamp() + 3600 * 23 + 59 * 60, [
+                                       'id' => '1',
+                                       'event' => OutOfOfficeEventDispatcherJob::EVENT_END,
+                               ]],
+                       ]);
+
+               $this->absenceService->createOrUpdateAbsence(
+                       $user,
+                       $startDateString,
+                       $endDateString,
+                       '',
+                       '',
+               );
+       }
+
+       public function testUpdateSchedulesOnlyEndJob() {
+               $tz = new DateTimeZone('Europe/Berlin');
+               $endDateString = '2023-01-10';
+               $endDate = new DateTimeImmutable($endDateString, $tz);
+               $user = $this->createMock(IUser::class);
+               $user->method('getUID')
+                       ->willReturn('user');
+               $absence = new Absence();
+               $absence->setId(1);
+               $absence->setFirstDay('1970-01-01');
+               $absence->setLastDay('1970-01-10');
+               $absence->setStatus('old status');
+               $absence->setMessage('old message');
+
+               $this->absenceMapper->expects(self::once())
+                       ->method('findByUserId')
+                       ->with('user')
+                       ->willReturn($absence);
+               $this->absenceMapper->expects(self::once())
+                       ->method('update')
+                       ->willReturnCallback(static function (Absence $absence) use ($endDateString): Absence {
+                               self::assertEquals('2023-01-05', $absence->getFirstDay());
+                               self::assertEquals($endDateString, $absence->getLastDay());
+                               return $absence;
+                       });
+               $this->timezoneService->expects(self::once())
+                       ->method('getUserTimezone')
+                       ->with('user')
+                       ->willReturn($tz->getName());
+               $this->timeFactory->expects(self::once())
+                       ->method('getTime')
+                       ->willReturn((new DateTimeImmutable('2023-01-07', $tz))->getTimestamp());
+               $this->jobList->expects(self::once())
+                       ->method('scheduleAfter')
+                       ->with(OutOfOfficeEventDispatcherJob::class, $endDate->getTimestamp() + 23 * 3600 + 59 * 60, [
+                               'id' => '1',
+                               'event' => OutOfOfficeEventDispatcherJob::EVENT_END,
+                       ]);
+
+               $this->absenceService->createOrUpdateAbsence(
+                       $user,
+                       '2023-01-05',
+                       $endDateString,
+                       '',
+                       '',
+               );
+       }
+
+       public function testUpdateAbsenceSchedulesNoJob() {
+               $tz = new DateTimeZone('Europe/Berlin');
+               $user = $this->createMock(IUser::class);
+               $user->method('getUID')
+                       ->willReturn('user');
+               $absence = new Absence();
+               $absence->setId(1);
+               $absence->setFirstDay('1970-01-01');
+               $absence->setLastDay('1970-01-10');
+               $absence->setStatus('old status');
+               $absence->setMessage('old message');
+
+               $this->absenceMapper->expects(self::once())
+                       ->method('findByUserId')
+                       ->with('user')
+                       ->willReturn($absence);
+               $this->absenceMapper->expects(self::once())
+                       ->method('update')
+                       ->willReturnCallback(static function (Absence $absence): Absence {
+                               self::assertEquals('2023-01-05', $absence->getFirstDay());
+                               self::assertEquals('2023-01-10', $absence->getLastDay());
+                               return $absence;
+                       });
+               $this->timezoneService->expects(self::once())
+                       ->method('getUserTimezone')
+                       ->with('user')
+                       ->willReturn($tz->getName());
+               $this->timeFactory->expects(self::once())
+                       ->method('getTime')
+                       ->willReturn((new DateTimeImmutable('2023-01-12', $tz))->getTimestamp());
+               $this->jobList->expects(self::never())
+                       ->method('scheduleAfter');
+
+               $this->absenceService->createOrUpdateAbsence(
+                       $user,
+                       '2023-01-05',
+                       '2023-01-10',
+                       '',
+                       '',
+               );
+       }
+}