diff options
Diffstat (limited to 'apps/user_status/tests')
15 files changed, 2508 insertions, 0 deletions
diff --git a/apps/user_status/tests/Integration/Service/StatusServiceIntegrationTest.php b/apps/user_status/tests/Integration/Service/StatusServiceIntegrationTest.php new file mode 100644 index 00000000000..8a21052b09f --- /dev/null +++ b/apps/user_status/tests/Integration/Service/StatusServiceIntegrationTest.php @@ -0,0 +1,196 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\UserStatus\Tests\Integration\Service; + +use OCA\UserStatus\Service\StatusService; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\IDBConnection; +use OCP\Server; +use OCP\UserStatus\IUserStatus; +use Test\TestCase; +use function sleep; +use function time; + +/** + * @group DB + */ +class StatusServiceIntegrationTest extends TestCase { + + private StatusService $service; + + protected function setUp(): void { + parent::setUp(); + + $this->service = Server::get(StatusService::class); + + $db = Server::get(IDBConnection::class); + $qb = $db->getQueryBuilder(); + $qb->delete('user_status')->executeStatement(); + } + + public function testNoStatusYet(): void { + $this->expectException(DoesNotExistException::class); + + $this->service->findByUserId('test123'); + } + + public function testCustomStatusMessageTimestamp(): void { + $before = time(); + $this->service->setCustomMessage( + 'test123', + '🍕', + 'Lunch', + null, + ); + $after = time(); + + $status = $this->service->findByUserId('test123'); + + self::assertSame('Lunch', $status->getCustomMessage()); + self::assertGreaterThanOrEqual($before, $status->getStatusMessageTimestamp()); + self::assertLessThanOrEqual($after, $status->getStatusMessageTimestamp()); + } + + public function testOnlineStatusKeepsMessageTimestamp(): void { + $this->service->setStatus( + 'test123', + IUserStatus::OFFLINE, + time() + 1000, + false, + ); + $this->service->setCustomMessage( + 'test123', + '🍕', + 'Lunch', + null, + ); + $timeAfterInsert = time(); + sleep(1); + $this->service->setStatus( + 'test123', + IUserStatus::ONLINE, + time() + 2000, + false, + ); + $status = $this->service->findByUserId('test123'); + + self::assertSame('Lunch', $status->getCustomMessage()); + self::assertLessThanOrEqual($timeAfterInsert, $status->getStatusMessageTimestamp()); + } + + public function testCreateRestoreBackupAutomatically(): void { + $this->service->setStatus( + 'test123', + IUserStatus::ONLINE, + null, + false, + ); + $this->service->setUserStatus( + 'test123', + IUserStatus::DND, + 'meeting', + true, + ); + + self::assertSame( + 'meeting', + $this->service->findByUserId('test123')->getMessageId(), + ); + self::assertSame( + IUserStatus::ONLINE, + $this->service->findByUserId('_test123')->getStatus(), + ); + + $revertedStatus = $this->service->revertUserStatus( + 'test123', + 'meeting', + ); + + self::assertNotNull($revertedStatus, 'Status should have been reverted'); + + try { + $this->service->findByUserId('_test123'); + $this->fail('Expected DoesNotExistException() to be thrown when finding backup status after reverting'); + } catch (DoesNotExistException) { + } + + self::assertSame( + IUserStatus::ONLINE, + $this->service->findByUserId('test123')->getStatus(), + ); + } + + public function testCallOverwritesMeetingStatus(): void { + $this->service->setStatus( + 'test123', + IUserStatus::ONLINE, + null, + false, + ); + $this->service->setUserStatus( + 'test123', + IUserStatus::BUSY, + IUserStatus::MESSAGE_CALENDAR_BUSY, + true, + ); + self::assertSame( + 'meeting', + $this->service->findByUserId('test123')->getMessageId(), + ); + + $this->service->setUserStatus( + 'test123', + IUserStatus::BUSY, + IUserStatus::MESSAGE_CALL, + true, + ); + self::assertSame( + IUserStatus::BUSY, + $this->service->findByUserId('test123')->getStatus(), + ); + + self::assertSame( + IUserStatus::MESSAGE_CALL, + $this->service->findByUserId('test123')->getMessageId(), + ); + } + + public function testOtherAutomationsDoNotOverwriteEachOther(): void { + $this->service->setStatus( + 'test123', + IUserStatus::ONLINE, + null, + false, + ); + $this->service->setUserStatus( + 'test123', + IUserStatus::DND, + IUserStatus::MESSAGE_AVAILABILITY, + true, + ); + self::assertSame( + 'availability', + $this->service->findByUserId('test123')->getMessageId(), + ); + + $nostatus = $this->service->setUserStatus( + 'test123', + IUserStatus::BUSY, + IUserStatus::MESSAGE_CALENDAR_BUSY, + true, + ); + + self::assertNull($nostatus); + self::assertSame( + IUserStatus::MESSAGE_AVAILABILITY, + $this->service->findByUserId('test123')->getMessageId(), + ); + } +} diff --git a/apps/user_status/tests/Unit/BackgroundJob/ClearOldStatusesBackgroundJobTest.php b/apps/user_status/tests/Unit/BackgroundJob/ClearOldStatusesBackgroundJobTest.php new file mode 100644 index 00000000000..66142082343 --- /dev/null +++ b/apps/user_status/tests/Unit/BackgroundJob/ClearOldStatusesBackgroundJobTest.php @@ -0,0 +1,44 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Tests\BackgroundJob; + +use OCA\UserStatus\BackgroundJob\ClearOldStatusesBackgroundJob; +use OCA\UserStatus\Db\UserStatusMapper; +use OCP\AppFramework\Utility\ITimeFactory; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class ClearOldStatusesBackgroundJobTest extends TestCase { + private ITimeFactory&MockObject $time; + private UserStatusMapper&MockObject $mapper; + private ClearOldStatusesBackgroundJob $job; + + protected function setUp(): void { + parent::setUp(); + + $this->time = $this->createMock(ITimeFactory::class); + $this->mapper = $this->createMock(UserStatusMapper::class); + + $this->job = new ClearOldStatusesBackgroundJob($this->time, $this->mapper); + } + + public function testRun(): void { + $this->mapper->expects($this->once()) + ->method('clearOlderThanClearAt') + ->with(1337); + $this->mapper->expects($this->once()) + ->method('clearStatusesOlderThan') + ->with(437, 1337); + + $this->time->method('getTime') + ->willReturn(1337); + + self::invokePrivate($this->job, 'run', [[]]); + } +} diff --git a/apps/user_status/tests/Unit/CapabilitiesTest.php b/apps/user_status/tests/Unit/CapabilitiesTest.php new file mode 100644 index 00000000000..601fb207df4 --- /dev/null +++ b/apps/user_status/tests/Unit/CapabilitiesTest.php @@ -0,0 +1,49 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Tests; + +use OCA\UserStatus\Capabilities; +use OCP\IEmojiHelper; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class CapabilitiesTest extends TestCase { + private IEmojiHelper&MockObject $emojiHelper; + private Capabilities $capabilities; + + protected function setUp(): void { + parent::setUp(); + + $this->emojiHelper = $this->createMock(IEmojiHelper::class); + $this->capabilities = new Capabilities($this->emojiHelper); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('getCapabilitiesDataProvider')] + public function testGetCapabilities(bool $supportsEmojis): void { + $this->emojiHelper->expects($this->once()) + ->method('doesPlatformSupportEmoji') + ->willReturn($supportsEmojis); + + $this->assertEquals([ + 'user_status' => [ + 'enabled' => true, + 'restore' => true, + 'supports_emoji' => $supportsEmojis, + 'supports_busy' => true, + ] + ], $this->capabilities->getCapabilities()); + } + + public static function getCapabilitiesDataProvider(): array { + return [ + [true], + [false], + ]; + } +} diff --git a/apps/user_status/tests/Unit/Connector/UserStatusProviderTest.php b/apps/user_status/tests/Unit/Connector/UserStatusProviderTest.php new file mode 100644 index 00000000000..df6c55488d5 --- /dev/null +++ b/apps/user_status/tests/Unit/Connector/UserStatusProviderTest.php @@ -0,0 +1,73 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Tests\Connector; + +use OCA\UserStatus\Connector\UserStatusProvider; +use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\Service\StatusService; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class UserStatusProviderTest extends TestCase { + private StatusService&MockObject $service; + private UserStatusProvider $provider; + + protected function setUp(): void { + parent::setUp(); + + $this->service = $this->createMock(StatusService::class); + $this->provider = new UserStatusProvider($this->service); + } + + public function testGetUserStatuses(): void { + $userStatus2 = new UserStatus(); + $userStatus2->setUserId('userId2'); + $userStatus2->setStatus('dnd'); + $userStatus2->setStatusTimestamp(5000); + $userStatus2->setIsUserDefined(true); + $userStatus2->setCustomIcon('💩'); + $userStatus2->setCustomMessage('Do not disturb'); + $userStatus2->setClearAt(50000); + + $userStatus3 = new UserStatus(); + $userStatus3->setUserId('userId3'); + $userStatus3->setStatus('away'); + $userStatus3->setStatusTimestamp(5000); + $userStatus3->setIsUserDefined(false); + $userStatus3->setCustomIcon('🏝'); + $userStatus3->setCustomMessage('On vacation'); + $userStatus3->setClearAt(60000); + + $this->service->expects($this->once()) + ->method('findByUserIds') + ->with(['userId1', 'userId2', 'userId3']) + ->willReturn([$userStatus2, $userStatus3]); + + $actual = $this->provider->getUserStatuses(['userId1', 'userId2', 'userId3']); + + $this->assertCount(2, $actual); + $status2 = $actual['userId2']; + $this->assertEquals('userId2', $status2->getUserId()); + $this->assertEquals('dnd', $status2->getStatus()); + $this->assertEquals('Do not disturb', $status2->getMessage()); + $this->assertEquals('💩', $status2->getIcon()); + $dateTime2 = $status2->getClearAt(); + $this->assertInstanceOf(\DateTimeImmutable::class, $dateTime2); + $this->assertEquals('50000', $dateTime2->format('U')); + + $status3 = $actual['userId3']; + $this->assertEquals('userId3', $status3->getUserId()); + $this->assertEquals('away', $status3->getStatus()); + $this->assertEquals('On vacation', $status3->getMessage()); + $this->assertEquals('🏝', $status3->getIcon()); + $dateTime3 = $status3->getClearAt(); + $this->assertInstanceOf(\DateTimeImmutable::class, $dateTime3); + $this->assertEquals('60000', $dateTime3->format('U')); + } +} diff --git a/apps/user_status/tests/Unit/Connector/UserStatusTest.php b/apps/user_status/tests/Unit/Connector/UserStatusTest.php new file mode 100644 index 00000000000..fee9b4e4b89 --- /dev/null +++ b/apps/user_status/tests/Unit/Connector/UserStatusTest.php @@ -0,0 +1,53 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Tests\Connector; + +use OCA\UserStatus\Connector\UserStatus; +use OCA\UserStatus\Db; +use Test\TestCase; + +class UserStatusTest extends TestCase { + public function testUserStatus(): void { + $status = new Db\UserStatus(); + $status->setUserId('user2'); + $status->setStatus('away'); + $status->setStatusTimestamp(5000); + $status->setIsUserDefined(false); + $status->setCustomIcon('🏝'); + $status->setCustomMessage('On vacation'); + $status->setClearAt(60000); + + $userStatus = new UserStatus($status); + $this->assertEquals('user2', $userStatus->getUserId()); + $this->assertEquals('away', $userStatus->getStatus()); + $this->assertEquals('On vacation', $userStatus->getMessage()); + $this->assertEquals('🏝', $userStatus->getIcon()); + + $dateTime = $userStatus->getClearAt(); + $this->assertInstanceOf(\DateTimeImmutable::class, $dateTime); + $this->assertEquals('60000', $dateTime->format('U')); + } + + public function testUserStatusInvisible(): void { + $status = new Db\UserStatus(); + $status->setUserId('user2'); + $status->setStatus('invisible'); + $status->setStatusTimestamp(5000); + $status->setIsUserDefined(false); + $status->setCustomIcon('🏝'); + $status->setCustomMessage('On vacation'); + $status->setClearAt(60000); + + $userStatus = new UserStatus($status); + $this->assertEquals('user2', $userStatus->getUserId()); + $this->assertEquals('offline', $userStatus->getStatus()); + $this->assertEquals('On vacation', $userStatus->getMessage()); + $this->assertEquals('🏝', $userStatus->getIcon()); + } +} diff --git a/apps/user_status/tests/Unit/Controller/PredefinedStatusControllerTest.php b/apps/user_status/tests/Unit/Controller/PredefinedStatusControllerTest.php new file mode 100644 index 00000000000..0f96f41a524 --- /dev/null +++ b/apps/user_status/tests/Unit/Controller/PredefinedStatusControllerTest.php @@ -0,0 +1,53 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Tests\Controller; + +use OCA\UserStatus\Controller\PredefinedStatusController; +use OCA\UserStatus\Service\PredefinedStatusService; +use OCP\IRequest; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class PredefinedStatusControllerTest extends TestCase { + private PredefinedStatusService&MockObject $service; + private PredefinedStatusController $controller; + + protected function setUp(): void { + parent::setUp(); + + $request = $this->createMock(IRequest::class); + $this->service = $this->createMock(PredefinedStatusService::class); + + $this->controller = new PredefinedStatusController('user_status', $request, $this->service); + } + + public function testFindAll(): void { + $this->service->expects($this->once()) + ->method('getDefaultStatuses') + ->with() + ->willReturn([ + [ + 'id' => 'predefined-status-one', + ], + [ + 'id' => 'predefined-status-two', + ], + ]); + + $actual = $this->controller->findAll(); + $this->assertEquals([ + [ + 'id' => 'predefined-status-one', + ], + [ + 'id' => 'predefined-status-two', + ], + ], $actual->getData()); + } +} diff --git a/apps/user_status/tests/Unit/Controller/StatusesControllerTest.php b/apps/user_status/tests/Unit/Controller/StatusesControllerTest.php new file mode 100644 index 00000000000..76d337879c3 --- /dev/null +++ b/apps/user_status/tests/Unit/Controller/StatusesControllerTest.php @@ -0,0 +1,94 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Tests\Controller; + +use OCA\UserStatus\Controller\StatusesController; +use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\Service\StatusService; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\IRequest; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class StatusesControllerTest extends TestCase { + private StatusService&MockObject $service; + private StatusesController $controller; + + protected function setUp(): void { + parent::setUp(); + + $request = $this->createMock(IRequest::class); + $this->service = $this->createMock(StatusService::class); + + $this->controller = new StatusesController('user_status', $request, $this->service); + } + + public function testFindAll(): void { + $userStatus = $this->getUserStatus(); + + $this->service->expects($this->once()) + ->method('findAll') + ->with(20, 40) + ->willReturn([$userStatus]); + + $response = $this->controller->findAll(20, 40); + $this->assertEquals([[ + 'userId' => 'john.doe', + 'status' => 'offline', + 'icon' => '🏝', + 'message' => 'On vacation', + 'clearAt' => 60000, + ]], $response->getData()); + } + + public function testFind(): void { + $userStatus = $this->getUserStatus(); + + $this->service->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willReturn($userStatus); + + $response = $this->controller->find('john.doe'); + $this->assertEquals([ + 'userId' => 'john.doe', + 'status' => 'offline', + 'icon' => '🏝', + 'message' => 'On vacation', + 'clearAt' => 60000, + ], $response->getData()); + } + + public function testFindDoesNotExist(): void { + $this->service->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willThrowException(new DoesNotExistException('')); + + $this->expectException(OCSNotFoundException::class); + $this->expectExceptionMessage('No status for the requested userId'); + + $this->controller->find('john.doe'); + } + + private function getUserStatus(): UserStatus { + $userStatus = new UserStatus(); + $userStatus->setId(1337); + $userStatus->setUserId('john.doe'); + $userStatus->setStatus('invisible'); + $userStatus->setStatusTimestamp(5000); + $userStatus->setIsUserDefined(true); + $userStatus->setCustomIcon('🏝'); + $userStatus->setCustomMessage('On vacation'); + $userStatus->setClearAt(60000); + + return $userStatus; + } +} diff --git a/apps/user_status/tests/Unit/Controller/UserStatusControllerTest.php b/apps/user_status/tests/Unit/Controller/UserStatusControllerTest.php new file mode 100644 index 00000000000..e99290319ed --- /dev/null +++ b/apps/user_status/tests/Unit/Controller/UserStatusControllerTest.php @@ -0,0 +1,313 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Tests\Controller; + +use OCA\DAV\CalDAV\Status\StatusService as CalendarStatusService; +use OCA\UserStatus\Controller\UserStatusController; +use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\Exception\InvalidClearAtException; +use OCA\UserStatus\Exception\InvalidMessageIdException; +use OCA\UserStatus\Exception\InvalidStatusIconException; +use OCA\UserStatus\Exception\InvalidStatusTypeException; +use OCA\UserStatus\Exception\StatusMessageTooLongException; +use OCA\UserStatus\Service\StatusService; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\OCS\OCSBadRequestException; +use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\IRequest; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; +use Throwable; + +class UserStatusControllerTest extends TestCase { + private LoggerInterface&MockObject $logger; + private StatusService&MockObject $statusService; + private CalendarStatusService&MockObject $calendarStatusService; + private UserStatusController $controller; + + protected function setUp(): void { + parent::setUp(); + + $request = $this->createMock(IRequest::class); + $userId = 'john.doe'; + $this->logger = $this->createMock(LoggerInterface::class); + $this->statusService = $this->createMock(StatusService::class); + $this->calendarStatusService = $this->createMock(CalendarStatusService::class); + + $this->controller = new UserStatusController( + 'user_status', + $request, + $userId, + $this->logger, + $this->statusService, + $this->calendarStatusService, + ); + } + + public function testGetStatus(): void { + $userStatus = $this->getUserStatus(); + + $this->statusService->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willReturn($userStatus); + + $response = $this->controller->getStatus(); + $this->assertEquals([ + 'userId' => 'john.doe', + 'status' => 'invisible', + 'icon' => '🏝', + 'message' => 'On vacation', + 'clearAt' => 60000, + 'statusIsUserDefined' => true, + 'messageIsPredefined' => false, + 'messageId' => null, + ], $response->getData()); + } + + public function testGetStatusDoesNotExist(): void { + $this->calendarStatusService->expects(self::once()) + ->method('processCalendarStatus') + ->with('john.doe'); + $this->statusService->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willThrowException(new DoesNotExistException('')); + + $this->expectException(OCSNotFoundException::class); + $this->expectExceptionMessage('No status for the current user'); + + $this->controller->getStatus(); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('setStatusDataProvider')] + public function testSetStatus( + string $statusType, + ?string $statusIcon, + ?string $message, + ?int $clearAt, + bool $expectSuccess, + bool $expectException, + ?Throwable $exception, + bool $expectLogger, + ?string $expectedLogMessage, + ): void { + $userStatus = $this->getUserStatus(); + + if ($expectException) { + $this->statusService->expects($this->once()) + ->method('setStatus') + ->with('john.doe', $statusType, null, true) + ->willThrowException($exception); + } else { + $this->statusService->expects($this->once()) + ->method('setStatus') + ->with('john.doe', $statusType, null, true) + ->willReturn($userStatus); + } + + if ($expectLogger) { + $this->logger->expects($this->once()) + ->method('debug') + ->with($expectedLogMessage); + } + if ($expectException) { + $this->expectException(OCSBadRequestException::class); + $this->expectExceptionMessage('Original exception message'); + } + + $response = $this->controller->setStatus($statusType); + + if ($expectSuccess) { + $this->assertEquals([ + 'userId' => 'john.doe', + 'status' => 'invisible', + 'icon' => '🏝', + 'message' => 'On vacation', + 'clearAt' => 60000, + 'statusIsUserDefined' => true, + 'messageIsPredefined' => false, + 'messageId' => null, + ], $response->getData()); + } + } + + public static function setStatusDataProvider(): array { + return [ + ['busy', '👨🏽💻', 'Busy developing the status feature', 500, true, false, null, false, null], + ['busy', '👨🏽💻', 'Busy developing the status feature', 500, false, true, new InvalidStatusTypeException('Original exception message'), true, + 'New user-status for "john.doe" was rejected due to an invalid status type "busy"'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('setPredefinedMessageDataProvider')] + public function testSetPredefinedMessage( + string $messageId, + ?int $clearAt, + bool $expectSuccess, + bool $expectException, + ?Throwable $exception, + bool $expectLogger, + ?string $expectedLogMessage, + ): void { + $userStatus = $this->getUserStatus(); + + if ($expectException) { + $this->statusService->expects($this->once()) + ->method('setPredefinedMessage') + ->with('john.doe', $messageId, $clearAt) + ->willThrowException($exception); + } else { + $this->statusService->expects($this->once()) + ->method('setPredefinedMessage') + ->with('john.doe', $messageId, $clearAt) + ->willReturn($userStatus); + } + + if ($expectLogger) { + $this->logger->expects($this->once()) + ->method('debug') + ->with($expectedLogMessage); + } + if ($expectException) { + $this->expectException(OCSBadRequestException::class); + $this->expectExceptionMessage('Original exception message'); + } + + $response = $this->controller->setPredefinedMessage($messageId, $clearAt); + + if ($expectSuccess) { + $this->assertEquals([ + 'userId' => 'john.doe', + 'status' => 'invisible', + 'icon' => '🏝', + 'message' => 'On vacation', + 'clearAt' => 60000, + 'statusIsUserDefined' => true, + 'messageIsPredefined' => false, + 'messageId' => null, + ], $response->getData()); + } + } + + public static function setPredefinedMessageDataProvider(): array { + return [ + ['messageId-42', 500, true, false, null, false, null], + ['messageId-42', 500, false, true, new InvalidClearAtException('Original exception message'), true, + 'New user-status for "john.doe" was rejected due to an invalid clearAt value "500"'], + ['messageId-42', 500, false, true, new InvalidMessageIdException('Original exception message'), true, + 'New user-status for "john.doe" was rejected due to an invalid message-id "messageId-42"'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('setCustomMessageDataProvider')] + public function testSetCustomMessage( + ?string $statusIcon, + string $message, + ?int $clearAt, + bool $expectSuccess, + bool $expectException, + ?Throwable $exception, + bool $expectLogger, + ?string $expectedLogMessage, + bool $expectSuccessAsReset = false, + ): void { + $userStatus = $this->getUserStatus(); + + if ($expectException) { + $this->statusService->expects($this->once()) + ->method('setCustomMessage') + ->with('john.doe', $statusIcon, $message, $clearAt) + ->willThrowException($exception); + } else { + if ($expectSuccessAsReset) { + $this->statusService->expects($this->never()) + ->method('setCustomMessage'); + $this->statusService->expects($this->once()) + ->method('clearMessage') + ->with('john.doe'); + $this->statusService->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willReturn($userStatus); + } else { + $this->statusService->expects($this->once()) + ->method('setCustomMessage') + ->with('john.doe', $statusIcon, $message, $clearAt) + ->willReturn($userStatus); + + $this->statusService->expects($this->never()) + ->method('clearMessage'); + } + } + + if ($expectLogger) { + $this->logger->expects($this->once()) + ->method('debug') + ->with($expectedLogMessage); + } + if ($expectException) { + $this->expectException(OCSBadRequestException::class); + $this->expectExceptionMessage('Original exception message'); + } + + $response = $this->controller->setCustomMessage($statusIcon, $message, $clearAt); + + if ($expectSuccess) { + $this->assertEquals([ + 'userId' => 'john.doe', + 'status' => 'invisible', + 'icon' => '🏝', + 'message' => 'On vacation', + 'clearAt' => 60000, + 'statusIsUserDefined' => true, + 'messageIsPredefined' => false, + 'messageId' => null, + ], $response->getData()); + } + } + + public static function setCustomMessageDataProvider(): array { + return [ + ['👨🏽💻', 'Busy developing the status feature', 500, true, false, null, false, null], + ['👨🏽💻', '', 500, true, false, null, false, null, false], + ['👨🏽💻', '', 0, true, false, null, false, null, false], + ['👨🏽💻', 'Busy developing the status feature', 500, false, true, new InvalidClearAtException('Original exception message'), true, + 'New user-status for "john.doe" was rejected due to an invalid clearAt value "500"'], + ['👨🏽💻', 'Busy developing the status feature', 500, false, true, new InvalidStatusIconException('Original exception message'), true, + 'New user-status for "john.doe" was rejected due to an invalid icon value "👨🏽💻"'], + ['👨🏽💻', 'Busy developing the status feature', 500, false, true, new StatusMessageTooLongException('Original exception message'), true, + 'New user-status for "john.doe" was rejected due to a too long status message.'], + ]; + } + + public function testClearMessage(): void { + $this->statusService->expects($this->once()) + ->method('clearMessage') + ->with('john.doe'); + + $response = $this->controller->clearMessage(); + $this->assertEquals([], $response->getData()); + } + + private function getUserStatus(): UserStatus { + $userStatus = new UserStatus(); + $userStatus->setId(1337); + $userStatus->setUserId('john.doe'); + $userStatus->setStatus('invisible'); + $userStatus->setStatusTimestamp(5000); + $userStatus->setIsUserDefined(true); + $userStatus->setCustomIcon('🏝'); + $userStatus->setCustomMessage('On vacation'); + $userStatus->setClearAt(60000); + + return $userStatus; + } +} diff --git a/apps/user_status/tests/Unit/Dashboard/UserStatusWidgetTest.php b/apps/user_status/tests/Unit/Dashboard/UserStatusWidgetTest.php new file mode 100644 index 00000000000..8773b04c95f --- /dev/null +++ b/apps/user_status/tests/Unit/Dashboard/UserStatusWidgetTest.php @@ -0,0 +1,69 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Tests\Dashboard; + +use OCA\UserStatus\Dashboard\UserStatusWidget; +use OCA\UserStatus\Service\StatusService; +use OCP\AppFramework\Services\IInitialState; +use OCP\IDateTimeFormatter; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\IUserManager; +use OCP\IUserSession; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class UserStatusWidgetTest extends TestCase { + private IL10N&MockObject $l10n; + private IDateTimeFormatter&MockObject $dateTimeFormatter; + private IURLGenerator&MockObject $urlGenerator; + private IInitialState&MockObject $initialState; + private IUserManager&MockObject $userManager; + private IUserSession&MockObject $userSession; + private StatusService&MockObject $service; + private UserStatusWidget $widget; + + protected function setUp(): void { + parent::setUp(); + + $this->l10n = $this->createMock(IL10N::class); + $this->dateTimeFormatter = $this->createMock(IDateTimeFormatter::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + $this->initialState = $this->createMock(IInitialState::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->userSession = $this->createMock(IUserSession::class); + $this->service = $this->createMock(StatusService::class); + + $this->widget = new UserStatusWidget($this->l10n, $this->dateTimeFormatter, $this->urlGenerator, $this->initialState, $this->userManager, $this->userSession, $this->service); + } + + public function testGetId(): void { + $this->assertEquals('user_status', $this->widget->getId()); + } + + public function testGetTitle(): void { + $this->l10n->expects($this->exactly(1)) + ->method('t') + ->willReturnArgument(0); + + $this->assertEquals('Recent statuses', $this->widget->getTitle()); + } + + public function testGetOrder(): void { + $this->assertEquals(5, $this->widget->getOrder()); + } + + public function testGetIconClass(): void { + $this->assertEquals('icon-user-status-dark', $this->widget->getIconClass()); + } + + public function testGetUrl(): void { + $this->assertNull($this->widget->getUrl()); + } +} diff --git a/apps/user_status/tests/Unit/Db/UserStatusMapperTest.php b/apps/user_status/tests/Unit/Db/UserStatusMapperTest.php new file mode 100644 index 00000000000..ea4480489c7 --- /dev/null +++ b/apps/user_status/tests/Unit/Db/UserStatusMapperTest.php @@ -0,0 +1,332 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Tests\Db; + +use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\Db\UserStatusMapper; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\DB\Exception; +use Test\TestCase; + +class UserStatusMapperTest extends TestCase { + private UserStatusMapper $mapper; + + protected function setUp(): void { + parent::setUp(); + + // make sure that DB is empty + $qb = self::$realDatabase->getQueryBuilder(); + $qb->delete('user_status')->execute(); + + $this->mapper = new UserStatusMapper(self::$realDatabase); + } + + public function testGetTableName(): void { + $this->assertEquals('user_status', $this->mapper->getTableName()); + } + + public function testGetFindAll(): void { + $this->insertSampleStatuses(); + + $allResults = $this->mapper->findAll(); + $this->assertCount(3, $allResults); + + $limitedResults = $this->mapper->findAll(2); + $this->assertCount(2, $limitedResults); + $this->assertEquals('admin', $limitedResults[0]->getUserId()); + $this->assertEquals('user1', $limitedResults[1]->getUserId()); + + $offsetResults = $this->mapper->findAll(null, 2); + $this->assertCount(1, $offsetResults); + $this->assertEquals('user2', $offsetResults[0]->getUserId()); + } + + public function testFindAllRecent(): void { + $this->insertSampleStatuses(); + + $allResults = $this->mapper->findAllRecent(2, 0); + $this->assertCount(2, $allResults); + $this->assertEquals('user2', $allResults[0]->getUserId()); + $this->assertEquals('user1', $allResults[1]->getUserId()); + } + + public function testGetFind(): void { + $this->insertSampleStatuses(); + + $adminStatus = $this->mapper->findByUserId('admin'); + $this->assertEquals('admin', $adminStatus->getUserId()); + $this->assertEquals('offline', $adminStatus->getStatus()); + $this->assertEquals(0, $adminStatus->getStatusTimestamp()); + $this->assertEquals(false, $adminStatus->getIsUserDefined()); + $this->assertEquals(null, $adminStatus->getCustomIcon()); + $this->assertEquals(null, $adminStatus->getCustomMessage()); + $this->assertEquals(null, $adminStatus->getClearAt()); + + $user1Status = $this->mapper->findByUserId('user1'); + $this->assertEquals('user1', $user1Status->getUserId()); + $this->assertEquals('dnd', $user1Status->getStatus()); + $this->assertEquals(5000, $user1Status->getStatusTimestamp()); + $this->assertEquals(true, $user1Status->getIsUserDefined()); + $this->assertEquals('💩', $user1Status->getCustomIcon()); + $this->assertEquals('Do not disturb', $user1Status->getCustomMessage()); + $this->assertEquals(50000, $user1Status->getClearAt()); + + $user2Status = $this->mapper->findByUserId('user2'); + $this->assertEquals('user2', $user2Status->getUserId()); + $this->assertEquals('away', $user2Status->getStatus()); + $this->assertEquals(6000, $user2Status->getStatusTimestamp()); + $this->assertEquals(false, $user2Status->getIsUserDefined()); + $this->assertEquals('🏝', $user2Status->getCustomIcon()); + $this->assertEquals('On vacation', $user2Status->getCustomMessage()); + $this->assertEquals(60000, $user2Status->getClearAt()); + } + + public function testFindByUserIds(): void { + $this->insertSampleStatuses(); + + $statuses = $this->mapper->findByUserIds(['admin', 'user2']); + $this->assertCount(2, $statuses); + + $adminStatus = $statuses[0]; + $this->assertEquals('admin', $adminStatus->getUserId()); + $this->assertEquals('offline', $adminStatus->getStatus()); + $this->assertEquals(0, $adminStatus->getStatusTimestamp()); + $this->assertEquals(false, $adminStatus->getIsUserDefined()); + $this->assertEquals(null, $adminStatus->getCustomIcon()); + $this->assertEquals(null, $adminStatus->getCustomMessage()); + $this->assertEquals(null, $adminStatus->getClearAt()); + + $user2Status = $statuses[1]; + $this->assertEquals('user2', $user2Status->getUserId()); + $this->assertEquals('away', $user2Status->getStatus()); + $this->assertEquals(6000, $user2Status->getStatusTimestamp()); + $this->assertEquals(false, $user2Status->getIsUserDefined()); + $this->assertEquals('🏝', $user2Status->getCustomIcon()); + $this->assertEquals('On vacation', $user2Status->getCustomMessage()); + $this->assertEquals(60000, $user2Status->getClearAt()); + } + + public function testUserIdUnique(): void { + // Test that inserting a second status for a user is throwing an exception + + $userStatus1 = new UserStatus(); + $userStatus1->setUserId('admin'); + $userStatus1->setStatus('dnd'); + $userStatus1->setStatusTimestamp(5000); + $userStatus1->setIsUserDefined(true); + + $this->mapper->insert($userStatus1); + + $userStatus2 = new UserStatus(); + $userStatus2->setUserId('admin'); + $userStatus2->setStatus('away'); + $userStatus2->setStatusTimestamp(6000); + $userStatus2->setIsUserDefined(false); + + $this->expectException(Exception::class); + + $this->mapper->insert($userStatus2); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('clearStatusesOlderThanDataProvider')] + public function testClearStatusesOlderThan(string $status, bool $isUserDefined, int $timestamp, bool $expectsClean): void { + $oldStatus = UserStatus::fromParams([ + 'userId' => 'john.doe', + 'status' => $status, + 'isUserDefined' => $isUserDefined, + 'statusTimestamp' => $timestamp, + ]); + + $this->mapper->insert($oldStatus); + + $this->mapper->clearStatusesOlderThan(5000, 8000); + + $updatedStatus = $this->mapper->findAll()[0]; + + if ($expectsClean) { + $this->assertEquals('offline', $updatedStatus->getStatus()); + $this->assertFalse($updatedStatus->getIsUserDefined()); + $this->assertEquals(8000, $updatedStatus->getStatusTimestamp()); + } else { + $this->assertEquals($status, $updatedStatus->getStatus()); + $this->assertEquals($isUserDefined, $updatedStatus->getIsUserDefined()); + $this->assertEquals($timestamp, $updatedStatus->getStatusTimestamp()); + } + } + + public static function clearStatusesOlderThanDataProvider(): array { + return [ + ['offline', false, 6000, false], + ['online', true, 6000, false], + ['online', true, 4000, true], + ['online', false, 6000, false], + ['online', false, 4000, true], + ['away', true, 6000, false], + ['away', true, 4000, false], + ['away', false, 6000, false], + ['away', false, 4000, true], + ['dnd', true, 6000, false], + ['dnd', true, 4000, false], + ['invisible', true, 6000, false], + ['invisible', true, 4000, false], + ]; + } + + public function testClearOlderThanClearAt(): void { + $this->insertSampleStatuses(); + + $this->mapper->clearOlderThanClearAt(55000); + + $allStatuses = $this->mapper->findAll(); + $this->assertCount(2, $allStatuses); + + $this->expectException(DoesNotExistException::class); + $this->mapper->findByUserId('user1'); + } + + private function insertSampleStatuses(): void { + $userStatus1 = new UserStatus(); + $userStatus1->setUserId('admin'); + $userStatus1->setStatus('offline'); + $userStatus1->setStatusTimestamp(0); + $userStatus1->setIsUserDefined(false); + + $userStatus2 = new UserStatus(); + $userStatus2->setUserId('user1'); + $userStatus2->setStatus('dnd'); + $userStatus2->setStatusTimestamp(5000); + $userStatus2->setStatusMessageTimestamp(5000); + $userStatus2->setIsUserDefined(true); + $userStatus2->setCustomIcon('💩'); + $userStatus2->setCustomMessage('Do not disturb'); + $userStatus2->setClearAt(50000); + + $userStatus3 = new UserStatus(); + $userStatus3->setUserId('user2'); + $userStatus3->setStatus('away'); + $userStatus3->setStatusTimestamp(6000); + $userStatus3->setStatusMessageTimestamp(6000); + $userStatus3->setIsUserDefined(false); + $userStatus3->setCustomIcon('🏝'); + $userStatus3->setCustomMessage('On vacation'); + $userStatus3->setClearAt(60000); + + $this->mapper->insert($userStatus1); + $this->mapper->insert($userStatus2); + $this->mapper->insert($userStatus3); + } + + public static function dataCreateBackupStatus(): array { + return [ + [false, false, false], + [true, false, true], + [false, true, false], + [true, true, false], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataCreateBackupStatus')] + public function testCreateBackupStatus(bool $hasStatus, bool $hasBackup, bool $backupCreated): void { + if ($hasStatus) { + $userStatus1 = new UserStatus(); + $userStatus1->setUserId('user1'); + $userStatus1->setStatus('online'); + $userStatus1->setStatusTimestamp(5000); + $userStatus1->setIsUserDefined(true); + $userStatus1->setIsBackup(false); + $userStatus1->setCustomIcon('🚀'); + $userStatus1->setCustomMessage('Current'); + $userStatus1->setClearAt(50000); + $this->mapper->insert($userStatus1); + } + + if ($hasBackup) { + $userStatus1 = new UserStatus(); + $userStatus1->setUserId('_user1'); + $userStatus1->setStatus('online'); + $userStatus1->setStatusTimestamp(5000); + $userStatus1->setIsUserDefined(true); + $userStatus1->setIsBackup(true); + $userStatus1->setCustomIcon('🚀'); + $userStatus1->setCustomMessage('Backup'); + $userStatus1->setClearAt(50000); + $this->mapper->insert($userStatus1); + } + + if ($hasStatus && $hasBackup) { + $this->expectException(Exception::class); + } + + self::assertSame($backupCreated, $this->mapper->createBackupStatus('user1')); + + if ($backupCreated) { + $user1Status = $this->mapper->findByUserId('user1', true); + $this->assertEquals('_user1', $user1Status->getUserId()); + $this->assertEquals(true, $user1Status->getIsBackup()); + $this->assertEquals('Current', $user1Status->getCustomMessage()); + } elseif ($hasBackup) { + $user1Status = $this->mapper->findByUserId('user1', true); + $this->assertEquals('_user1', $user1Status->getUserId()); + $this->assertEquals(true, $user1Status->getIsBackup()); + $this->assertEquals('Backup', $user1Status->getCustomMessage()); + } + } + + public function testRestoreBackupStatuses(): void { + $userStatus1 = new UserStatus(); + $userStatus1->setUserId('_user1'); + $userStatus1->setStatus('online'); + $userStatus1->setStatusTimestamp(5000); + $userStatus1->setIsUserDefined(true); + $userStatus1->setIsBackup(true); + $userStatus1->setCustomIcon('🚀'); + $userStatus1->setCustomMessage('Releasing'); + $userStatus1->setClearAt(50000); + $userStatus1 = $this->mapper->insert($userStatus1); + + $userStatus2 = new UserStatus(); + $userStatus2->setUserId('_user2'); + $userStatus2->setStatus('away'); + $userStatus2->setStatusTimestamp(5000); + $userStatus2->setIsUserDefined(true); + $userStatus2->setIsBackup(true); + $userStatus2->setCustomIcon('💩'); + $userStatus2->setCustomMessage('Do not disturb'); + $userStatus2->setClearAt(50000); + $userStatus2 = $this->mapper->insert($userStatus2); + + $userStatus3 = new UserStatus(); + $userStatus3->setUserId('_user3'); + $userStatus3->setStatus('away'); + $userStatus3->setStatusTimestamp(5000); + $userStatus3->setIsUserDefined(true); + $userStatus3->setIsBackup(true); + $userStatus3->setCustomIcon('🏝️'); + $userStatus3->setCustomMessage('Vacationing'); + $userStatus3->setClearAt(50000); + $this->mapper->insert($userStatus3); + + $this->mapper->restoreBackupStatuses([$userStatus1->getId(), $userStatus2->getId()]); + + $user1Status = $this->mapper->findByUserId('user1', false); + $this->assertEquals('user1', $user1Status->getUserId()); + $this->assertEquals(false, $user1Status->getIsBackup()); + $this->assertEquals('Releasing', $user1Status->getCustomMessage()); + + $user2Status = $this->mapper->findByUserId('user2', false); + $this->assertEquals('user2', $user2Status->getUserId()); + $this->assertEquals(false, $user2Status->getIsBackup()); + $this->assertEquals('Do not disturb', $user2Status->getCustomMessage()); + + $user3Status = $this->mapper->findByUserId('user3', true); + $this->assertEquals('_user3', $user3Status->getUserId()); + $this->assertEquals(true, $user3Status->getIsBackup()); + $this->assertEquals('Vacationing', $user3Status->getCustomMessage()); + } +} diff --git a/apps/user_status/tests/Unit/Listener/UserDeletedListenerTest.php b/apps/user_status/tests/Unit/Listener/UserDeletedListenerTest.php new file mode 100644 index 00000000000..fbcea23338d --- /dev/null +++ b/apps/user_status/tests/Unit/Listener/UserDeletedListenerTest.php @@ -0,0 +1,51 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Tests\Listener; + +use OCA\UserStatus\Listener\UserDeletedListener; +use OCA\UserStatus\Service\StatusService; +use OCP\EventDispatcher\GenericEvent; +use OCP\IUser; +use OCP\User\Events\UserDeletedEvent; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class UserDeletedListenerTest extends TestCase { + private StatusService&MockObject $service; + private UserDeletedListener $listener; + + protected function setUp(): void { + parent::setUp(); + + $this->service = $this->createMock(StatusService::class); + $this->listener = new UserDeletedListener($this->service); + } + + public function testHandleWithCorrectEvent(): void { + $user = $this->createMock(IUser::class); + $user->expects($this->once()) + ->method('getUID') + ->willReturn('john.doe'); + + $this->service->expects($this->once()) + ->method('removeUserStatus') + ->with('john.doe'); + + $event = new UserDeletedEvent($user); + $this->listener->handle($event); + } + + public function testHandleWithWrongEvent(): void { + $this->service->expects($this->never()) + ->method('removeUserStatus'); + + $event = new GenericEvent(); + $this->listener->handle($event); + } +} diff --git a/apps/user_status/tests/Unit/Listener/UserLiveStatusListenerTest.php b/apps/user_status/tests/Unit/Listener/UserLiveStatusListenerTest.php new file mode 100644 index 00000000000..c03eed0089e --- /dev/null +++ b/apps/user_status/tests/Unit/Listener/UserLiveStatusListenerTest.php @@ -0,0 +1,149 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Tests\Listener; + +use OCA\DAV\CalDAV\Status\StatusService as CalendarStatusService; +use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\Db\UserStatusMapper; +use OCA\UserStatus\Listener\UserLiveStatusListener; +use OCA\UserStatus\Service\StatusService; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\EventDispatcher\GenericEvent; +use OCP\IUser; +use OCP\User\Events\UserLiveStatusEvent; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class UserLiveStatusListenerTest extends TestCase { + private UserStatusMapper&MockObject $mapper; + private StatusService&MockObject $statusService; + private ITimeFactory&MockObject $timeFactory; + private CalendarStatusService&MockObject $calendarStatusService; + + private LoggerInterface&MockObject $logger; + private UserLiveStatusListener $listener; + + protected function setUp(): void { + parent::setUp(); + + $this->mapper = $this->createMock(UserStatusMapper::class); + $this->statusService = $this->createMock(StatusService::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->calendarStatusService = $this->createMock(CalendarStatusService::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->listener = new UserLiveStatusListener( + $this->mapper, + $this->statusService, + $this->timeFactory, + $this->calendarStatusService, + $this->logger, + ); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('handleEventWithCorrectEventDataProvider')] + public function testHandleWithCorrectEvent( + string $userId, + string $previousStatus, + int $previousTimestamp, + bool $previousIsUserDefined, + string $eventStatus, + int $eventTimestamp, + bool $expectExisting, + bool $expectUpdate, + ): void { + $userStatus = new UserStatus(); + + if ($expectExisting) { + $userStatus->setId(42); + $userStatus->setUserId($userId); + $userStatus->setStatus($previousStatus); + $userStatus->setStatusTimestamp($previousTimestamp); + $userStatus->setIsUserDefined($previousIsUserDefined); + + $this->statusService->expects($this->once()) + ->method('findByUserId') + ->with($userId) + ->willReturn($userStatus); + } else { + $this->statusService->expects($this->once()) + ->method('findByUserId') + ->with($userId) + ->willThrowException(new DoesNotExistException('')); + } + + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn($userId); + $event = new UserLiveStatusEvent($user, $eventStatus, $eventTimestamp); + + $this->timeFactory->expects($this->atMost(1)) + ->method('getTime') + ->willReturn(5000); + + if ($expectUpdate) { + if ($expectExisting) { + $this->mapper->expects($this->never()) + ->method('insert'); + $this->mapper->expects($this->once()) + ->method('update') + ->with($this->callback(function ($userStatus) use ($eventStatus, $eventTimestamp) { + $this->assertEquals($eventStatus, $userStatus->getStatus()); + $this->assertEquals($eventTimestamp, $userStatus->getStatusTimestamp()); + $this->assertFalse($userStatus->getIsUserDefined()); + + return true; + })); + } else { + $this->mapper->expects($this->once()) + ->method('insert') + ->with($this->callback(function ($userStatus) use ($eventStatus, $eventTimestamp) { + $this->assertEquals($eventStatus, $userStatus->getStatus()); + $this->assertEquals($eventTimestamp, $userStatus->getStatusTimestamp()); + $this->assertFalse($userStatus->getIsUserDefined()); + + return true; + })); + $this->mapper->expects($this->never()) + ->method('update'); + } + + $this->listener->handle($event); + } else { + $this->mapper->expects($this->never()) + ->method('insert'); + $this->mapper->expects($this->never()) + ->method('update'); + + $this->listener->handle($event); + } + } + + public static function handleEventWithCorrectEventDataProvider(): array { + return [ + ['john.doe', 'offline', 0, false, 'online', 5000, true, true], + ['john.doe', 'offline', 0, false, 'online', 5000, false, true], + ['john.doe', 'online', 5000, false, 'online', 5000, true, false], + ['john.doe', 'online', 5000, false, 'online', 5000, false, true], + ['john.doe', 'away', 5000, false, 'online', 5000, true, true], + ['john.doe', 'online', 5000, false, 'away', 5000, true, false], + ['john.doe', 'away', 5000, true, 'online', 5000, true, false], + ['john.doe', 'online', 5000, true, 'away', 5000, true, false], + ]; + } + + public function testHandleWithWrongEvent(): void { + $this->mapper->expects($this->never()) + ->method('insertOrUpdate'); + + $event = new GenericEvent(); + $this->listener->handle($event); + } +} diff --git a/apps/user_status/tests/Unit/Service/PredefinedStatusServiceTest.php b/apps/user_status/tests/Unit/Service/PredefinedStatusServiceTest.php new file mode 100644 index 00000000000..78e4a18d9f1 --- /dev/null +++ b/apps/user_status/tests/Unit/Service/PredefinedStatusServiceTest.php @@ -0,0 +1,184 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Tests\Service; + +use OCA\UserStatus\Service\PredefinedStatusService; +use OCP\IL10N; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class PredefinedStatusServiceTest extends TestCase { + protected IL10N&MockObject $l10n; + protected PredefinedStatusService $service; + + protected function setUp(): void { + parent::setUp(); + + $this->l10n = $this->createMock(IL10N::class); + + $this->service = new PredefinedStatusService($this->l10n); + } + + public function testGetDefaultStatuses(): void { + $this->l10n->expects($this->exactly(8)) + ->method('t') + ->willReturnCallback(function ($text, $parameters = []) { + return vsprintf($text, $parameters); + }); + + $actual = $this->service->getDefaultStatuses(); + $this->assertEquals([ + [ + 'id' => 'meeting', + 'icon' => '📅', + 'message' => 'In a meeting', + 'clearAt' => [ + 'type' => 'period', + 'time' => 3600, + ], + ], + [ + 'id' => 'commuting', + 'icon' => '🚌', + 'message' => 'Commuting', + 'clearAt' => [ + 'type' => 'period', + 'time' => 1800, + ], + ], + [ + 'id' => 'be-right-back', + 'icon' => '⏳', + 'message' => 'Be right back', + 'clearAt' => [ + 'type' => 'period', + 'time' => 900, + ], + ], + [ + 'id' => 'remote-work', + 'icon' => '🏡', + 'message' => 'Working remotely', + 'clearAt' => [ + 'type' => 'end-of', + 'time' => 'day', + ], + ], + [ + 'id' => 'sick-leave', + 'icon' => '🤒', + 'message' => 'Out sick', + 'clearAt' => [ + 'type' => 'end-of', + 'time' => 'day', + ], + ], + [ + 'id' => 'vacationing', + 'icon' => '🌴', + 'message' => 'Vacationing', + 'clearAt' => null, + ], + [ + 'id' => 'call', + 'icon' => '💬', + 'message' => 'In a call', + 'clearAt' => null, + 'visible' => false, + ], + [ + 'id' => 'out-of-office', + 'icon' => '🛑', + 'message' => 'Out of office', + 'clearAt' => null, + 'visible' => false, + ], + ], $actual); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('getIconForIdDataProvider')] + public function testGetIconForId(string $id, ?string $expectedIcon): void { + $actual = $this->service->getIconForId($id); + $this->assertEquals($expectedIcon, $actual); + } + + public static function getIconForIdDataProvider(): array { + return [ + ['meeting', '📅'], + ['commuting', '🚌'], + ['sick-leave', '🤒'], + ['vacationing', '🌴'], + ['remote-work', '🏡'], + ['be-right-back', '⏳'], + ['call', '💬'], + ['unknown-id', null], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('getTranslatedStatusForIdDataProvider')] + public function testGetTranslatedStatusForId(string $id, ?string $expected): void { + $this->l10n->method('t') + ->willReturnArgument(0); + + $actual = $this->service->getTranslatedStatusForId($id); + $this->assertEquals($expected, $actual); + } + + public static function getTranslatedStatusForIdDataProvider(): array { + return [ + ['meeting', 'In a meeting'], + ['commuting', 'Commuting'], + ['sick-leave', 'Out sick'], + ['vacationing', 'Vacationing'], + ['remote-work', 'Working remotely'], + ['be-right-back', 'Be right back'], + ['call', 'In a call'], + ['unknown-id', null], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('isValidIdDataProvider')] + public function testIsValidId(string $id, bool $expected): void { + $actual = $this->service->isValidId($id); + $this->assertEquals($expected, $actual); + } + + public static function isValidIdDataProvider(): array { + return [ + ['meeting', true], + ['commuting', true], + ['sick-leave', true], + ['vacationing', true], + ['remote-work', true], + ['be-right-back', true], + ['call', true], + ['unknown-id', false], + ]; + } + + public function testGetDefaultStatusById(): void { + $this->l10n->expects($this->exactly(8)) + ->method('t') + ->willReturnCallback(function ($text, $parameters = []) { + return vsprintf($text, $parameters); + }); + + $this->assertEquals([ + 'id' => 'call', + 'icon' => '💬', + 'message' => 'In a call', + 'clearAt' => null, + 'visible' => false, + ], $this->service->getDefaultStatusById('call')); + } + + public function testGetDefaultStatusByUnknownId(): void { + $this->assertNull($this->service->getDefaultStatusById('unknown')); + } +} diff --git a/apps/user_status/tests/Unit/Service/StatusServiceTest.php b/apps/user_status/tests/Unit/Service/StatusServiceTest.php new file mode 100644 index 00000000000..7dfa5b0d064 --- /dev/null +++ b/apps/user_status/tests/Unit/Service/StatusServiceTest.php @@ -0,0 +1,828 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Tests\Service; + +use Doctrine\DBAL\Exception\UniqueConstraintViolationException; +use OC\DB\Exceptions\DbalException; +use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\Db\UserStatusMapper; +use OCA\UserStatus\Exception\InvalidClearAtException; +use OCA\UserStatus\Exception\InvalidMessageIdException; +use OCA\UserStatus\Exception\InvalidStatusIconException; +use OCA\UserStatus\Exception\InvalidStatusTypeException; +use OCA\UserStatus\Exception\StatusMessageTooLongException; +use OCA\UserStatus\Service\PredefinedStatusService; +use OCA\UserStatus\Service\StatusService; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\DB\Exception; +use OCP\IConfig; +use OCP\IEmojiHelper; +use OCP\IUserManager; +use OCP\UserStatus\IUserStatus; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class StatusServiceTest extends TestCase { + private UserStatusMapper&MockObject $mapper; + private ITimeFactory&MockObject $timeFactory; + private PredefinedStatusService&MockObject $predefinedStatusService; + private IEmojiHelper&MockObject $emojiHelper; + private IConfig&MockObject $config; + private IUserManager&MockObject $userManager; + private LoggerInterface&MockObject $logger; + + private StatusService $service; + + protected function setUp(): void { + parent::setUp(); + + $this->mapper = $this->createMock(UserStatusMapper::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->predefinedStatusService = $this->createMock(PredefinedStatusService::class); + $this->emojiHelper = $this->createMock(IEmojiHelper::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->config = $this->createMock(IConfig::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->config->method('getAppValue') + ->willReturnMap([ + ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'], + ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'] + ]); + + $this->service = new StatusService($this->mapper, + $this->timeFactory, + $this->predefinedStatusService, + $this->emojiHelper, + $this->config, + $this->userManager, + $this->logger, + ); + } + + public function testFindAll(): void { + $status1 = $this->createMock(UserStatus::class); + $status2 = $this->createMock(UserStatus::class); + + $this->mapper->expects($this->once()) + ->method('findAll') + ->with(20, 50) + ->willReturn([$status1, $status2]); + + $this->assertEquals([ + $status1, + $status2, + ], $this->service->findAll(20, 50)); + } + + public function testFindAllRecentStatusChanges(): void { + $status1 = $this->createMock(UserStatus::class); + $status2 = $this->createMock(UserStatus::class); + + $this->mapper->expects($this->once()) + ->method('findAllRecent') + ->with(20, 50) + ->willReturn([$status1, $status2]); + + $this->assertEquals([ + $status1, + $status2, + ], $this->service->findAllRecentStatusChanges(20, 50)); + } + + public function testFindAllRecentStatusChangesNoEnumeration(): void { + $status1 = $this->createMock(UserStatus::class); + $status2 = $this->createMock(UserStatus::class); + + $this->mapper->method('findAllRecent') + ->with(20, 50) + ->willReturn([$status1, $status2]); + + // Rebuild $this->service with user enumeration turned off + $this->config = $this->createMock(IConfig::class); + + $this->config->method('getAppValue') + ->willReturnMap([ + ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'no'], + ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'] + ]); + + $this->service = new StatusService($this->mapper, + $this->timeFactory, + $this->predefinedStatusService, + $this->emojiHelper, + $this->config, + $this->userManager, + $this->logger, + ); + + $this->assertEquals([], $this->service->findAllRecentStatusChanges(20, 50)); + + // Rebuild $this->service with user enumeration limited to common groups + $this->config = $this->createMock(IConfig::class); + + $this->config->method('getAppValue') + ->willReturnMap([ + ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'], + ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'yes'] + ]); + + $this->service = new StatusService($this->mapper, + $this->timeFactory, + $this->predefinedStatusService, + $this->emojiHelper, + $this->config, + $this->userManager, + $this->logger, + ); + + $this->assertEquals([], $this->service->findAllRecentStatusChanges(20, 50)); + } + + public function testFindByUserIdDoesNotExist(): void { + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willThrowException(new DoesNotExistException('')); + + $this->expectException(DoesNotExistException::class); + $this->service->findByUserId('john.doe'); + } + + public function testFindAllAddDefaultMessage(): void { + $status = new UserStatus(); + $status->setMessageId('commuting'); + + $this->predefinedStatusService->expects($this->once()) + ->method('getDefaultStatusById') + ->with('commuting') + ->willReturn([ + 'id' => 'commuting', + 'icon' => '🚌', + 'message' => 'Commuting', + 'clearAt' => [ + 'type' => 'period', + 'time' => 1800, + ], + ]); + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willReturn($status); + + $this->assertEquals($status, $this->service->findByUserId('john.doe')); + $this->assertEquals('🚌', $status->getCustomIcon()); + $this->assertEquals('Commuting', $status->getCustomMessage()); + } + + public function testFindAllClearStatus(): void { + $status = new UserStatus(); + $status->setStatus('online'); + $status->setStatusTimestamp(1000); + $status->setIsUserDefined(true); + + $this->timeFactory->method('getTime') + ->willReturn(2600); + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willReturn($status); + + $this->assertEquals($status, $this->service->findByUserId('john.doe')); + $this->assertEquals('offline', $status->getStatus()); + $this->assertEquals(2600, $status->getStatusTimestamp()); + $this->assertFalse($status->getIsUserDefined()); + } + + public function testFindAllClearMessage(): void { + $status = new UserStatus(); + $status->setClearAt(50); + $status->setMessageId('commuting'); + $status->setStatusTimestamp(60); + + $this->timeFactory->method('getTime') + ->willReturn(60); + $this->predefinedStatusService->expects($this->never()) + ->method('getDefaultStatusById'); + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willReturn($status); + $this->assertEquals($status, $this->service->findByUserId('john.doe')); + $this->assertNull($status->getClearAt()); + $this->assertNull($status->getMessageId()); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('setStatusDataProvider')] + public function testSetStatus( + string $userId, + string $status, + ?int $statusTimestamp, + bool $isUserDefined, + bool $expectExisting, + bool $expectSuccess, + bool $expectTimeFactory, + bool $expectException, + ?string $expectedExceptionClass, + ?string $expectedExceptionMessage, + ): void { + $userStatus = new UserStatus(); + + if ($expectExisting) { + $userStatus->setId(42); + $userStatus->setUserId($userId); + + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with($userId) + ->willReturn($userStatus); + } else { + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with($userId) + ->willThrowException(new DoesNotExistException('')); + } + + if ($expectTimeFactory) { + $this->timeFactory + ->method('getTime') + ->willReturn(40); + } + + if ($expectException) { + $this->expectException($expectedExceptionClass); + $this->expectExceptionMessage($expectedExceptionMessage); + + $this->service->setStatus($userId, $status, $statusTimestamp, $isUserDefined); + } + + if ($expectSuccess) { + if ($expectExisting) { + $this->mapper->expects($this->once()) + ->method('update') + ->willReturnArgument(0); + } else { + $this->mapper->expects($this->once()) + ->method('insert') + ->willReturnArgument(0); + } + + $actual = $this->service->setStatus($userId, $status, $statusTimestamp, $isUserDefined); + + $this->assertEquals('john.doe', $actual->getUserId()); + $this->assertEquals($status, $actual->getStatus()); + $this->assertEquals($statusTimestamp ?? 40, $actual->getStatusTimestamp()); + $this->assertEquals($isUserDefined, $actual->getIsUserDefined()); + } + } + + public static function setStatusDataProvider(): array { + return [ + ['john.doe', 'online', 50, true, true, true, false, false, null, null], + ['john.doe', 'online', 50, true, false, true, false, false, null, null], + ['john.doe', 'online', 50, false, true, true, false, false, null, null], + ['john.doe', 'online', 50, false, false, true, false, false, null, null], + ['john.doe', 'online', null, true, true, true, true, false, null, null], + ['john.doe', 'online', null, true, false, true, true, false, null, null], + ['john.doe', 'online', null, false, true, true, true, false, null, null], + ['john.doe', 'online', null, false, false, true, true, false, null, null], + + ['john.doe', 'away', 50, true, true, true, false, false, null, null], + ['john.doe', 'away', 50, true, false, true, false, false, null, null], + ['john.doe', 'away', 50, false, true, true, false, false, null, null], + ['john.doe', 'away', 50, false, false, true, false, false, null, null], + ['john.doe', 'away', null, true, true, true, true, false, null, null], + ['john.doe', 'away', null, true, false, true, true, false, null, null], + ['john.doe', 'away', null, false, true, true, true, false, null, null], + ['john.doe', 'away', null, false, false, true, true, false, null, null], + + ['john.doe', 'dnd', 50, true, true, true, false, false, null, null], + ['john.doe', 'dnd', 50, true, false, true, false, false, null, null], + ['john.doe', 'dnd', 50, false, true, true, false, false, null, null], + ['john.doe', 'dnd', 50, false, false, true, false, false, null, null], + ['john.doe', 'dnd', null, true, true, true, true, false, null, null], + ['john.doe', 'dnd', null, true, false, true, true, false, null, null], + ['john.doe', 'dnd', null, false, true, true, true, false, null, null], + ['john.doe', 'dnd', null, false, false, true, true, false, null, null], + + ['john.doe', 'invisible', 50, true, true, true, false, false, null, null], + ['john.doe', 'invisible', 50, true, false, true, false, false, null, null], + ['john.doe', 'invisible', 50, false, true, true, false, false, null, null], + ['john.doe', 'invisible', 50, false, false, true, false, false, null, null], + ['john.doe', 'invisible', null, true, true, true, true, false, null, null], + ['john.doe', 'invisible', null, true, false, true, true, false, null, null], + ['john.doe', 'invisible', null, false, true, true, true, false, null, null], + ['john.doe', 'invisible', null, false, false, true, true, false, null, null], + + ['john.doe', 'offline', 50, true, true, true, false, false, null, null], + ['john.doe', 'offline', 50, true, false, true, false, false, null, null], + ['john.doe', 'offline', 50, false, true, true, false, false, null, null], + ['john.doe', 'offline', 50, false, false, true, false, false, null, null], + ['john.doe', 'offline', null, true, true, true, true, false, null, null], + ['john.doe', 'offline', null, true, false, true, true, false, null, null], + ['john.doe', 'offline', null, false, true, true, true, false, null, null], + ['john.doe', 'offline', null, false, false, true, true, false, null, null], + + ['john.doe', 'illegal-status', 50, true, true, false, false, true, InvalidStatusTypeException::class, 'Status-type "illegal-status" is not supported'], + ['john.doe', 'illegal-status', 50, true, false, false, false, true, InvalidStatusTypeException::class, 'Status-type "illegal-status" is not supported'], + ['john.doe', 'illegal-status', 50, false, true, false, false, true, InvalidStatusTypeException::class, 'Status-type "illegal-status" is not supported'], + ['john.doe', 'illegal-status', 50, false, false, false, false, true, InvalidStatusTypeException::class, 'Status-type "illegal-status" is not supported'], + ['john.doe', 'illegal-status', null, true, true, false, true, true, InvalidStatusTypeException::class, 'Status-type "illegal-status" is not supported'], + ['john.doe', 'illegal-status', null, true, false, false, true, true, InvalidStatusTypeException::class, 'Status-type "illegal-status" is not supported'], + ['john.doe', 'illegal-status', null, false, true, false, true, true, InvalidStatusTypeException::class, 'Status-type "illegal-status" is not supported'], + ['john.doe', 'illegal-status', null, false, false, false, true, true, InvalidStatusTypeException::class, 'Status-type "illegal-status" is not supported'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('setPredefinedMessageDataProvider')] + public function testSetPredefinedMessage( + string $userId, + string $messageId, + bool $isValidMessageId, + ?int $clearAt, + bool $expectExisting, + bool $expectSuccess, + bool $expectException, + ?string $expectedExceptionClass, + ?string $expectedExceptionMessage, + ): void { + $userStatus = new UserStatus(); + + if ($expectExisting) { + $userStatus->setId(42); + $userStatus->setUserId($userId); + $userStatus->setStatus('offline'); + $userStatus->setStatusTimestamp(0); + $userStatus->setIsUserDefined(false); + $userStatus->setCustomIcon('😀'); + $userStatus->setCustomMessage('Foo'); + + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with($userId) + ->willReturn($userStatus); + } else { + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with($userId) + ->willThrowException(new DoesNotExistException('')); + } + + $this->predefinedStatusService->expects($this->once()) + ->method('isValidId') + ->with($messageId) + ->willReturn($isValidMessageId); + + $this->timeFactory + ->method('getTime') + ->willReturn(40); + + if ($expectException) { + $this->expectException($expectedExceptionClass); + $this->expectExceptionMessage($expectedExceptionMessage); + + $this->service->setPredefinedMessage($userId, $messageId, $clearAt); + } + + if ($expectSuccess) { + if ($expectExisting) { + $this->mapper->expects($this->once()) + ->method('update') + ->willReturnArgument(0); + } else { + $this->mapper->expects($this->once()) + ->method('insert') + ->willReturnArgument(0); + } + + $actual = $this->service->setPredefinedMessage($userId, $messageId, $clearAt); + + $this->assertEquals('john.doe', $actual->getUserId()); + $this->assertEquals('offline', $actual->getStatus()); + $this->assertEquals(0, $actual->getStatusTimestamp()); + $this->assertEquals(false, $actual->getIsUserDefined()); + $this->assertEquals($messageId, $actual->getMessageId()); + $this->assertNull($actual->getCustomIcon()); + $this->assertNull($actual->getCustomMessage()); + $this->assertEquals($clearAt, $actual->getClearAt()); + } + } + + public static function setPredefinedMessageDataProvider(): array { + return [ + ['john.doe', 'sick-leave', true, null, true, true, false, null, null], + ['john.doe', 'sick-leave', true, null, false, true, false, null, null], + ['john.doe', 'sick-leave', true, 20, true, false, true, InvalidClearAtException::class, 'ClearAt is in the past'], + ['john.doe', 'sick-leave', true, 20, false, false, true, InvalidClearAtException::class, 'ClearAt is in the past'], + ['john.doe', 'sick-leave', true, 60, true, true, false, null, null], + ['john.doe', 'sick-leave', true, 60, false, true, false, null, null], + ['john.doe', 'illegal-message-id', false, null, true, false, true, InvalidMessageIdException::class, 'Message-Id "illegal-message-id" is not supported'], + ['john.doe', 'illegal-message-id', false, null, false, false, true, InvalidMessageIdException::class, 'Message-Id "illegal-message-id" is not supported'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('setCustomMessageDataProvider')] + public function testSetCustomMessage( + string $userId, + ?string $statusIcon, + bool $supportsEmoji, + string $message, + ?int $clearAt, + bool $expectExisting, + bool $expectSuccess, + bool $expectException, + ?string $expectedExceptionClass, + ?string $expectedExceptionMessage, + ): void { + $userStatus = new UserStatus(); + + if ($expectExisting) { + $userStatus->setId(42); + $userStatus->setUserId($userId); + $userStatus->setStatus('offline'); + $userStatus->setStatusTimestamp(0); + $userStatus->setIsUserDefined(false); + $userStatus->setMessageId('messageId-42'); + + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with($userId) + ->willReturn($userStatus); + } else { + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with($userId) + ->willThrowException(new DoesNotExistException('')); + } + + $this->emojiHelper->method('isValidSingleEmoji') + ->with($statusIcon) + ->willReturn($supportsEmoji); + + $this->timeFactory + ->method('getTime') + ->willReturn(40); + + if ($expectException) { + $this->expectException($expectedExceptionClass); + $this->expectExceptionMessage($expectedExceptionMessage); + + $this->service->setCustomMessage($userId, $statusIcon, $message, $clearAt); + } + + if ($expectSuccess) { + if ($expectExisting) { + $this->mapper->expects($this->once()) + ->method('update') + ->willReturnArgument(0); + } else { + $this->mapper->expects($this->once()) + ->method('insert') + ->willReturnArgument(0); + } + + $actual = $this->service->setCustomMessage($userId, $statusIcon, $message, $clearAt); + + $this->assertEquals('john.doe', $actual->getUserId()); + $this->assertEquals('offline', $actual->getStatus()); + $this->assertEquals(0, $actual->getStatusTimestamp()); + $this->assertEquals(false, $actual->getIsUserDefined()); + $this->assertNull($actual->getMessageId()); + $this->assertEquals($statusIcon, $actual->getCustomIcon()); + $this->assertEquals($message, $actual->getCustomMessage()); + $this->assertEquals($clearAt, $actual->getClearAt()); + } + } + + public static function setCustomMessageDataProvider(): array { + return [ + ['john.doe', '😁', true, 'Custom message', null, true, true, false, null, null], + ['john.doe', '😁', true, 'Custom message', null, false, true, false, null, null], + ['john.doe', null, false, 'Custom message', null, true, true, false, null, null], + ['john.doe', null, false, 'Custom message', null, false, true, false, null, null], + ['john.doe', '😁', false, 'Custom message', null, true, false, true, InvalidStatusIconException::class, 'Status-Icon is longer than one character'], + ['john.doe', '😁', false, 'Custom message', null, false, false, true, InvalidStatusIconException::class, 'Status-Icon is longer than one character'], + ['john.doe', null, false, 'Custom message that is way too long and violates the maximum length and hence should be rejected', null, true, false, true, StatusMessageTooLongException::class, 'Message is longer than supported length of 80 characters'], + ['john.doe', null, false, 'Custom message that is way too long and violates the maximum length and hence should be rejected', null, false, false, true, StatusMessageTooLongException::class, 'Message is longer than supported length of 80 characters'], + ['john.doe', '😁', true, 'Custom message', 80, true, true, false, null, null], + ['john.doe', '😁', true, 'Custom message', 80, false, true, false, null, null], + ['john.doe', '😁', true, 'Custom message', 20, true, false, true, InvalidClearAtException::class, 'ClearAt is in the past'], + ['john.doe', '😁', true, 'Custom message', 20, false, false, true, InvalidClearAtException::class, 'ClearAt is in the past'], + ]; + } + + public function testClearStatus(): void { + $status = new UserStatus(); + $status->setId(1); + $status->setUserId('john.doe'); + $status->setStatus('dnd'); + $status->setStatusTimestamp(1337); + $status->setIsUserDefined(true); + $status->setMessageId('messageId-42'); + $status->setCustomIcon('🙊'); + $status->setCustomMessage('My custom status message'); + $status->setClearAt(42); + + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willReturn($status); + + $this->mapper->expects($this->once()) + ->method('update') + ->with($status); + + $actual = $this->service->clearStatus('john.doe'); + $this->assertTrue($actual); + $this->assertEquals('offline', $status->getStatus()); + $this->assertEquals(0, $status->getStatusTimestamp()); + $this->assertFalse($status->getIsUserDefined()); + } + + public function testClearStatusDoesNotExist(): void { + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willThrowException(new DoesNotExistException('')); + + $this->mapper->expects($this->never()) + ->method('update'); + + $actual = $this->service->clearStatus('john.doe'); + $this->assertFalse($actual); + } + + public function testClearMessage(): void { + $status = new UserStatus(); + $status->setId(1); + $status->setUserId('john.doe'); + $status->setStatus('dnd'); + $status->setStatusTimestamp(1337); + $status->setIsUserDefined(true); + $status->setMessageId('messageId-42'); + $status->setCustomIcon('🙊'); + $status->setCustomMessage('My custom status message'); + $status->setClearAt(42); + + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willReturn($status); + + $this->mapper->expects($this->once()) + ->method('update') + ->with($status); + + $actual = $this->service->clearMessage('john.doe'); + $this->assertTrue($actual); + $this->assertNull($status->getMessageId()); + $this->assertNull($status->getCustomMessage()); + $this->assertNull($status->getCustomIcon()); + $this->assertNull($status->getClearAt()); + } + + public function testClearMessageDoesNotExist(): void { + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willThrowException(new DoesNotExistException('')); + + $this->mapper->expects($this->never()) + ->method('update'); + + $actual = $this->service->clearMessage('john.doe'); + $this->assertFalse($actual); + } + + public function testRemoveUserStatus(): void { + $status = $this->createMock(UserStatus::class); + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willReturn($status); + + $this->mapper->expects($this->once()) + ->method('delete') + ->with($status); + + $actual = $this->service->removeUserStatus('john.doe'); + $this->assertTrue($actual); + } + + public function testRemoveUserStatusDoesNotExist(): void { + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with('john.doe') + ->willThrowException(new DoesNotExistException('')); + + $this->mapper->expects($this->never()) + ->method('delete'); + + $actual = $this->service->removeUserStatus('john.doe'); + $this->assertFalse($actual); + } + + public function testCleanStatusAutomaticOnline(): void { + $status = new UserStatus(); + $status->setStatus(IUserStatus::ONLINE); + $status->setStatusTimestamp(1337); + $status->setIsUserDefined(false); + + $this->mapper->expects(self::once()) + ->method('update') + ->with($status); + + parent::invokePrivate($this->service, 'cleanStatus', [$status]); + } + + public function testCleanStatusCustomOffline(): void { + $status = new UserStatus(); + $status->setStatus(IUserStatus::OFFLINE); + $status->setStatusTimestamp(1337); + $status->setIsUserDefined(true); + + $this->mapper->expects(self::once()) + ->method('update') + ->with($status); + + parent::invokePrivate($this->service, 'cleanStatus', [$status]); + } + + public function testCleanStatusCleanedAlready(): void { + $status = new UserStatus(); + $status->setStatus(IUserStatus::OFFLINE); + $status->setStatusTimestamp(1337); + $status->setIsUserDefined(false); + + // Don't update the status again and again when no value changed + $this->mapper->expects(self::never()) + ->method('update') + ->with($status); + + parent::invokePrivate($this->service, 'cleanStatus', [$status]); + } + + public function testBackupWorkingHasBackupAlready(): void { + $p = $this->createMock(UniqueConstraintViolationException::class); + $e = DbalException::wrap($p); + $this->mapper->expects($this->once()) + ->method('createBackupStatus') + ->with('john') + ->willThrowException($e); + + $this->assertFalse($this->service->backupCurrentStatus('john')); + } + + public function testBackupThrowsOther(): void { + $e = new Exception('', Exception::REASON_CONNECTION_LOST); + $this->mapper->expects($this->once()) + ->method('createBackupStatus') + ->with('john') + ->willThrowException($e); + + $this->expectException(Exception::class); + $this->service->backupCurrentStatus('john'); + } + + public function testBackup(): void { + $this->mapper->expects($this->once()) + ->method('createBackupStatus') + ->with('john') + ->willReturn(true); + + $this->assertTrue($this->service->backupCurrentStatus('john')); + } + + public function testRevertMultipleUserStatus(): void { + $john = new UserStatus(); + $john->setId(1); + $john->setStatus(IUserStatus::AWAY); + $john->setStatusTimestamp(1337); + $john->setIsUserDefined(false); + $john->setMessageId('call'); + $john->setUserId('john'); + $john->setIsBackup(false); + + $johnBackup = new UserStatus(); + $johnBackup->setId(2); + $johnBackup->setStatus(IUserStatus::ONLINE); + $johnBackup->setStatusTimestamp(1337); + $johnBackup->setIsUserDefined(true); + $johnBackup->setMessageId('hello'); + $johnBackup->setUserId('_john'); + $johnBackup->setIsBackup(true); + + $noBackup = new UserStatus(); + $noBackup->setId(3); + $noBackup->setStatus(IUserStatus::AWAY); + $noBackup->setStatusTimestamp(1337); + $noBackup->setIsUserDefined(false); + $noBackup->setMessageId('call'); + $noBackup->setUserId('nobackup'); + $noBackup->setIsBackup(false); + + $backupOnly = new UserStatus(); + $backupOnly->setId(4); + $backupOnly->setStatus(IUserStatus::ONLINE); + $backupOnly->setStatusTimestamp(1337); + $backupOnly->setIsUserDefined(true); + $backupOnly->setMessageId('hello'); + $backupOnly->setUserId('_backuponly'); + $backupOnly->setIsBackup(true); + + $noBackupDND = new UserStatus(); + $noBackupDND->setId(5); + $noBackupDND->setStatus(IUserStatus::DND); + $noBackupDND->setStatusTimestamp(1337); + $noBackupDND->setIsUserDefined(false); + $noBackupDND->setMessageId('call'); + $noBackupDND->setUserId('nobackupanddnd'); + $noBackupDND->setIsBackup(false); + + $this->mapper->expects($this->once()) + ->method('findByUserIds') + ->with(['john', 'nobackup', 'backuponly', 'nobackupanddnd', '_john', '_nobackup', '_backuponly', '_nobackupanddnd']) + ->willReturn([ + $john, + $johnBackup, + $noBackup, + $backupOnly, + $noBackupDND, + ]); + + $this->mapper->expects($this->once()) + ->method('deleteByIds') + ->with([1, 3, 5]); + + $this->mapper->expects($this->once()) + ->method('restoreBackupStatuses') + ->with([2]); + + $this->service->revertMultipleUserStatus(['john', 'nobackup', 'backuponly', 'nobackupanddnd'], 'call'); + } + + public static function dataSetUserStatus(): array { + return [ + [IUserStatus::MESSAGE_CALENDAR_BUSY, '', false], + + // Call > Meeting + [IUserStatus::MESSAGE_CALENDAR_BUSY, IUserStatus::MESSAGE_CALL, false], + [IUserStatus::MESSAGE_CALL, IUserStatus::MESSAGE_CALENDAR_BUSY, true], + + // Availability > Call&Meeting + [IUserStatus::MESSAGE_CALENDAR_BUSY, IUserStatus::MESSAGE_AVAILABILITY, false], + [IUserStatus::MESSAGE_CALL, IUserStatus::MESSAGE_AVAILABILITY, false], + [IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::MESSAGE_CALENDAR_BUSY, true], + [IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::MESSAGE_CALL, true], + + // Out-of-office > Availability&Call&Meeting + [IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::MESSAGE_OUT_OF_OFFICE, false], + [IUserStatus::MESSAGE_CALENDAR_BUSY, IUserStatus::MESSAGE_OUT_OF_OFFICE, false], + [IUserStatus::MESSAGE_CALL, IUserStatus::MESSAGE_OUT_OF_OFFICE, false], + [IUserStatus::MESSAGE_OUT_OF_OFFICE, IUserStatus::MESSAGE_AVAILABILITY, true], + [IUserStatus::MESSAGE_OUT_OF_OFFICE, IUserStatus::MESSAGE_CALENDAR_BUSY, true], + [IUserStatus::MESSAGE_OUT_OF_OFFICE, IUserStatus::MESSAGE_CALL, true], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataSetUserStatus')] + public function testSetUserStatus(string $messageId, string $oldMessageId, bool $expectedUpdateShortcut): void { + $previous = new UserStatus(); + $previous->setId(1); + $previous->setStatus(IUserStatus::AWAY); + $previous->setStatusTimestamp(1337); + $previous->setIsUserDefined(false); + $previous->setMessageId($oldMessageId); + $previous->setUserId('john'); + $previous->setIsBackup(false); + + $this->mapper->expects($this->once()) + ->method('findByUserId') + ->with('john') + ->willReturn($previous); + + $e = DbalException::wrap($this->createMock(UniqueConstraintViolationException::class)); + $this->mapper->expects($expectedUpdateShortcut ? $this->never() : $this->once()) + ->method('createBackupStatus') + ->willThrowException($e); + + $this->mapper->expects($this->any()) + ->method('update') + ->willReturnArgument(0); + + $this->predefinedStatusService->expects($this->once()) + ->method('isValidId') + ->with($messageId) + ->willReturn(true); + + $this->service->setUserStatus('john', IUserStatus::DND, $messageId, true); + } +} diff --git a/apps/user_status/tests/bootstrap.php b/apps/user_status/tests/bootstrap.php new file mode 100644 index 00000000000..c98daca1dfc --- /dev/null +++ b/apps/user_status/tests/bootstrap.php @@ -0,0 +1,20 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +use OCP\App\IAppManager; +use OCP\Server; + +if (!defined('PHPUNIT_RUN')) { + define('PHPUNIT_RUN', 1); +} + +require_once __DIR__ . '/../../../lib/base.php'; +require_once __DIR__ . '/../../../tests/autoload.php'; + +Server::get(IAppManager::class)->loadApp('user_status'); |