diff --git a/apps/dav/lib/BackgroundJob/UserStatusAutomation.php b/apps/dav/lib/BackgroundJob/UserStatusAutomation.php index 8edc28c9a69a7..43fccbf233e22 100644 --- a/apps/dav/lib/BackgroundJob/UserStatusAutomation.php +++ b/apps/dav/lib/BackgroundJob/UserStatusAutomation.php @@ -92,7 +92,7 @@ protected function run($argument) { $isCurrentlyAvailable = false; $nextPotentialToggles = []; - $now = new \DateTime('now'); + $now = $this->time->getDateTime(); $lastMidnight = (clone $now)->setTime(0, 0); $vObject = Reader::read($property); @@ -105,9 +105,16 @@ protected function run($argument) { foreach ($availables as $available) { /** @var Available $available */ if ($available->name === 'AVAILABLE') { - /** @var \DateTimeInterface $effectiveStart */ - /** @var \DateTimeInterface $effectiveEnd */ - [$effectiveStart, $effectiveEnd] = $available->getEffectiveStartEnd(); + /** @var \DateTimeImmutable $originalStart */ + /** @var \DateTimeImmutable $originalEnd */ + [$originalStart, $originalEnd] = $available->getEffectiveStartEnd(); + + // Little shenanigans to fix the automation on the day the rules were adjusted + // Otherwise the $originalStart would match rules for Thursdays on a Friday, etc. + // So we simply wind back a week and then fastForward to the next occurrence + // since today's midnight, which then also accounts for the week days. + $effectiveStart = \DateTime::createFromImmutable($originalStart)->sub(new \DateInterval('P7D')); + $effectiveEnd = \DateTime::createFromImmutable($originalEnd)->sub(new \DateInterval('P7D')); try { $it = new RRuleIterator((string) $available->RRULE, $effectiveStart); @@ -150,8 +157,10 @@ protected function run($argument) { $this->setLastRunToNextToggleTime($userId, $nextAutomaticToggle - 1); if ($isCurrentlyAvailable) { + $this->logger->debug('User is currently available, reverting DND status if applicable'); $this->manager->revertUserStatus($userId, IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND); } else { + $this->logger->debug('User is currently NOT available, reverting call status if applicable and then setting DND'); // The DND status automation is more important than the "Away - In call" so we also restore that one if it exists. $this->manager->revertUserStatus($userId, IUserStatus::MESSAGE_CALL, IUserStatus::AWAY); $this->manager->setUserStatus($userId, IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND, true); diff --git a/apps/dav/tests/unit/BackgroundJob/UserStatusAutomationTest.php b/apps/dav/tests/unit/BackgroundJob/UserStatusAutomationTest.php new file mode 100644 index 0000000000000..59438c7cd28e4 --- /dev/null +++ b/apps/dav/tests/unit/BackgroundJob/UserStatusAutomationTest.php @@ -0,0 +1,204 @@ + + * + * @author Joas Schilling + * + * @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 . + * + */ + +namespace OCA\DAV\Tests\unit\BackgroundJob; + +use OCA\DAV\BackgroundJob\UserStatusAutomation; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJobList; +use OCP\IConfig; +use OCP\UserStatus\IManager; +use OCP\UserStatus\IUserStatus; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +/** + * @group DB + */ +class UserStatusAutomationTest extends TestCase { + + protected MockObject|ITimeFactory $time; + protected MockObject|IJobList $jobList; + protected MockObject|LoggerInterface $logger; + protected MockObject|IManager $statusManager; + protected MockObject|IConfig $config; + + protected function setUp(): void { + parent::setUp(); + + $this->time = $this->createMock(ITimeFactory::class); + $this->jobList = $this->createMock(IJobList::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->statusManager = $this->createMock(IManager::class); + $this->config = $this->createMock(IConfig::class); + + } + + protected function getAutomationMock(array $methods): MockObject|UserStatusAutomation { + if (empty($methods)) { + return new UserStatusAutomation( + $this->time, + \OC::$server->getDatabaseConnection(), + $this->jobList, + $this->logger, + $this->statusManager, + $this->config, + ); + } + + return $this->getMockBuilder(UserStatusAutomation::class) + ->setConstructorArgs([ + $this->time, + \OC::$server->getDatabaseConnection(), + $this->jobList, + $this->logger, + $this->statusManager, + $this->config, + ]) + ->setMethods($methods) + ->getMock(); + } + + public function dataRun(): array { + return [ + ['20230217', '2023-02-24 10:49:36.613834', true], + ['20230224', '2023-02-24 10:49:36.613834', true], + ['20230217', '2023-02-24 13:58:24.479357', false], + ['20230224', '2023-02-24 13:58:24.479357', false], + ]; + } + + /** + * @dataProvider dataRun + */ + public function testRun(string $ruleDay, string $currentTime, bool $isAvailable): void { + $this->config->method('getUserValue') + ->with('user', 'dav', 'user_status_automation', 'no') + ->willReturn('yes'); + + $this->time->method('getDateTime') + ->willReturn(new \DateTime($currentTime, new \DateTimeZone('UTC'))); + + $automation = $this->getAutomationMock(['getAvailabilityFromPropertiesTable']); + $automation->method('getAvailabilityFromPropertiesTable') + ->with('user') + ->willReturn('BEGIN:VCALENDAR +PRODID:Nextcloud DAV app +BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:STANDARD +TZNAME:CET +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:CEST +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VAVAILABILITY +BEGIN:AVAILABLE +DTSTART;TZID=Europe/Berlin:' . $ruleDay . 'T090000 +DTEND;TZID=Europe/Berlin:' . $ruleDay . 'T170000 +UID:3e6feeec-8e00-4265-b822-b73174e8b39f +RRULE:FREQ=WEEKLY;BYDAY=TH +END:AVAILABLE +BEGIN:AVAILABLE +DTSTART;TZID=Europe/Berlin:' . $ruleDay . 'T090000 +DTEND;TZID=Europe/Berlin:' . $ruleDay . 'T120000 +UID:8a634e99-07cf-443b-b480-005a0e1db323 +RRULE:FREQ=WEEKLY;BYDAY=FR +END:AVAILABLE +END:VAVAILABILITY +END:VCALENDAR'); + + if ($isAvailable) { + $this->statusManager->expects($this->once()) + ->method('revertUserStatus') + ->with('user', IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND); + } else { + $this->statusManager->expects($this->once()) + ->method('revertUserStatus') + ->with('user', IUserStatus::MESSAGE_CALL, IUserStatus::AWAY); + $this->statusManager->expects($this->once()) + ->method('setUserStatus') + ->with('user', IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND, true); + } + + self::invokePrivate($automation, 'run', [['userId' => 'user']]); + } + + public function testRunNoMoreAvailabilityDefined(): void { + $this->config->method('getUserValue') + ->with('user', 'dav', 'user_status_automation', 'no') + ->willReturn('yes'); + + $this->time->method('getDateTime') + ->willReturn(new \DateTime('2023-02-24 13:58:24.479357', new \DateTimeZone('UTC'))); + + $automation = $this->getAutomationMock(['getAvailabilityFromPropertiesTable']); + $automation->method('getAvailabilityFromPropertiesTable') + ->with('user') + ->willReturn('BEGIN:VCALENDAR +PRODID:Nextcloud DAV app +BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:STANDARD +TZNAME:CET +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +BEGIN:DAYLIGHT +TZNAME:CEST +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VAVAILABILITY +END:VAVAILABILITY +END:VCALENDAR'); + + $this->statusManager->expects($this->once()) + ->method('revertUserStatus') + ->with('user', IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND); + + $this->jobList->expects($this->once()) + ->method('remove') + ->with(UserStatusAutomation::class, ['userId' => 'user']); + + self::invokePrivate($automation, 'run', [['userId' => 'user']]); + } +}