From: Richard Steinmetz Date: Mon, 27 Nov 2023 14:49:08 +0000 (+0100) Subject: fix(dav): don't schedule out-of-office jobs for dates in the past X-Git-Tag: v29.0.0beta1~766^2 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=26248d0fedf32fd3c2bb7daa33bb5c364d7b40c3;p=nextcloud-server.git fix(dav): don't schedule out-of-office jobs for dates in the past Signed-off-by: Richard Steinmetz --- diff --git a/apps/dav/lib/Service/AbsenceService.php b/apps/dav/lib/Service/AbsenceService.php index 874e86f6e1c..7c0d6eec082 100644 --- a/apps/dav/lib/Service/AbsenceService.php +++ b/apps/dav/lib/Service/AbsenceService.php @@ -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 index 00000000000..f6f16e28a23 --- /dev/null +++ b/apps/dav/tests/unit/Service/AbsenceServiceTest.php @@ -0,0 +1,471 @@ + + * + * @author Richard Steinmetz + * + * @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 . + * + */ + +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', + '', + '', + ); + } +}