diff options
Diffstat (limited to 'apps/dav/tests/unit')
259 files changed, 43386 insertions, 12440 deletions
diff --git a/apps/dav/tests/unit/AppInfo/ApplicationTest.php b/apps/dav/tests/unit/AppInfo/ApplicationTest.php new file mode 100644 index 00000000000..336f487e0b8 --- /dev/null +++ b/apps/dav/tests/unit/AppInfo/ApplicationTest.php @@ -0,0 +1,34 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\AppInfo; + +use OCA\DAV\AppInfo\Application; +use OCA\DAV\CardDAV\CardDavBackend; +use OCA\DAV\CardDAV\ContactsManager; +use Test\TestCase; + +/** + * Class ApplicationTest + * + * @group DB + * + * @package OCA\DAV\Tests\Unit\AppInfo + */ +class ApplicationTest extends TestCase { + public function test(): void { + $app = new Application(); + $c = $app->getContainer(); + + // assert service instances in the container are properly setup + $s = $c->query(ContactsManager::class); + $this->assertInstanceOf(ContactsManager::class, $s); + $s = $c->query(CardDavBackend::class); + $this->assertInstanceOf(CardDavBackend::class, $s); + } +} diff --git a/apps/dav/tests/unit/AppInfo/PluginManagerTest.php b/apps/dav/tests/unit/AppInfo/PluginManagerTest.php new file mode 100644 index 00000000000..0082aa45286 --- /dev/null +++ b/apps/dav/tests/unit/AppInfo/PluginManagerTest.php @@ -0,0 +1,126 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016 ownCloud GmbH. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\AppInfo; + +use OC\App\AppManager; +use OC\ServerContainer; +use OCA\DAV\AppInfo\PluginManager; +use OCA\DAV\CalDAV\AppCalendar\AppCalendarPlugin; +use OCA\DAV\CalDAV\Integration\ICalendarProvider; +use Sabre\DAV\Collection; +use Sabre\DAV\ServerPlugin; +use Test\TestCase; + +/** + * Class PluginManagerTest + * + * @package OCA\DAV\Tests\Unit\AppInfo + */ +class PluginManagerTest extends TestCase { + public function test(): void { + $server = $this->createMock(ServerContainer::class); + + $appManager = $this->createMock(AppManager::class); + $appManager->method('getEnabledApps') + ->willReturn(['adavapp', 'adavapp2']); + + $appInfo1 = [ + 'types' => ['dav'], + 'sabre' => [ + 'plugins' => [ + 'plugin' => [ + '\OCA\DAV\ADavApp\PluginOne', + '\OCA\DAV\ADavApp\PluginTwo', + ], + ], + 'calendar-plugins' => [ + 'plugin' => [ + '\OCA\DAV\ADavApp\CalendarPluginOne', + '\OCA\DAV\ADavApp\CalendarPluginTwo', + ], + ], + 'collections' => [ + 'collection' => [ + '\OCA\DAV\ADavApp\CollectionOne', + '\OCA\DAV\ADavApp\CollectionTwo', + ] + ], + ], + ]; + $appInfo2 = [ + 'types' => ['logging', 'dav'], + 'sabre' => [ + 'plugins' => [ + 'plugin' => '\OCA\DAV\ADavApp2\PluginOne', + ], + 'calendar-plugins' => [ + 'plugin' => '\OCA\DAV\ADavApp2\CalendarPluginOne', + ], + 'collections' => [ + 'collection' => '\OCA\DAV\ADavApp2\CollectionOne', + ], + ], + ]; + + $appManager->method('getAppInfo') + ->willReturnMap([ + ['adavapp', false, null, $appInfo1], + ['adavapp2', false, null, $appInfo2], + ]); + + $pluginManager = new PluginManager($server, $appManager); + + $appCalendarPlugin = $this->createMock(AppCalendarPlugin::class); + $calendarPlugin1 = $this->createMock(ICalendarProvider::class); + $calendarPlugin2 = $this->createMock(ICalendarProvider::class); + $calendarPlugin3 = $this->createMock(ICalendarProvider::class); + + $dummyPlugin1 = $this->createMock(ServerPlugin::class); + $dummyPlugin2 = $this->createMock(ServerPlugin::class); + $dummy2Plugin1 = $this->createMock(ServerPlugin::class); + + $dummyCollection1 = $this->createMock(Collection::class); + $dummyCollection2 = $this->createMock(Collection::class); + $dummy2Collection1 = $this->createMock(Collection::class); + + $server->method('get') + ->willReturnMap([ + [AppCalendarPlugin::class, $appCalendarPlugin], + ['\OCA\DAV\ADavApp\PluginOne', $dummyPlugin1], + ['\OCA\DAV\ADavApp\PluginTwo', $dummyPlugin2], + ['\OCA\DAV\ADavApp\CalendarPluginOne', $calendarPlugin1], + ['\OCA\DAV\ADavApp\CalendarPluginTwo', $calendarPlugin2], + ['\OCA\DAV\ADavApp\CollectionOne', $dummyCollection1], + ['\OCA\DAV\ADavApp\CollectionTwo', $dummyCollection2], + ['\OCA\DAV\ADavApp2\PluginOne', $dummy2Plugin1], + ['\OCA\DAV\ADavApp2\CalendarPluginOne', $calendarPlugin3], + ['\OCA\DAV\ADavApp2\CollectionOne', $dummy2Collection1], + ]); + + $expectedPlugins = [ + $dummyPlugin1, + $dummyPlugin2, + $dummy2Plugin1, + ]; + $expectedCalendarPlugins = [ + $appCalendarPlugin, + $calendarPlugin1, + $calendarPlugin2, + $calendarPlugin3, + ]; + $expectedCollections = [ + $dummyCollection1, + $dummyCollection2, + $dummy2Collection1, + ]; + + $this->assertEquals($expectedPlugins, $pluginManager->getAppPlugins()); + $this->assertEquals($expectedCalendarPlugins, $pluginManager->getCalendarPlugins()); + $this->assertEquals($expectedCollections, $pluginManager->getAppCollections()); + } +} diff --git a/apps/dav/tests/unit/Avatars/AvatarHomeTest.php b/apps/dav/tests/unit/Avatars/AvatarHomeTest.php new file mode 100644 index 00000000000..7117637a000 --- /dev/null +++ b/apps/dav/tests/unit/Avatars/AvatarHomeTest.php @@ -0,0 +1,99 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2017 ownCloud GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Avatars; + +use OCA\DAV\Avatars\AvatarHome; +use OCA\DAV\Avatars\AvatarNode; +use OCP\IAvatar; +use OCP\IAvatarManager; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\Exception\MethodNotAllowed; +use Sabre\DAV\Exception\NotFound; +use Test\TestCase; + +class AvatarHomeTest extends TestCase { + private AvatarHome $home; + private IAvatarManager&MockObject $avatarManager; + + protected function setUp(): void { + parent::setUp(); + $this->avatarManager = $this->createMock(IAvatarManager::class); + $this->home = new AvatarHome(['uri' => 'principals/users/admin'], $this->avatarManager); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providesForbiddenMethods')] + public function testForbiddenMethods($method): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + $this->home->$method(''); + } + + public static function providesForbiddenMethods(): array { + return [ + ['createFile'], + ['createDirectory'], + ['delete'], + ['setName'] + ]; + } + + public function testGetName(): void { + $n = $this->home->getName(); + self::assertEquals('admin', $n); + } + + public static function providesTestGetChild(): array { + return [ + [MethodNotAllowed::class, false, ''], + [MethodNotAllowed::class, false, 'bla.foo'], + [MethodNotAllowed::class, false, 'bla.png'], + [NotFound::class, false, '512.png'], + [null, true, '512.png'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providesTestGetChild')] + public function testGetChild(?string $expectedException, bool $hasAvatar, string $path): void { + if ($expectedException !== null) { + $this->expectException($expectedException); + } + + $avatar = $this->createMock(IAvatar::class); + $avatar->method('exists')->willReturn($hasAvatar); + + $this->avatarManager->expects($this->any())->method('getAvatar')->with('admin')->willReturn($avatar); + $avatarNode = $this->home->getChild($path); + $this->assertInstanceOf(AvatarNode::class, $avatarNode); + } + + public function testGetChildren(): void { + $avatarNodes = $this->home->getChildren(); + self::assertEquals(0, count($avatarNodes)); + + $avatar = $this->createMock(IAvatar::class); + $avatar->expects($this->once())->method('exists')->willReturn(true); + $this->avatarManager->expects($this->any())->method('getAvatar')->with('admin')->willReturn($avatar); + $avatarNodes = $this->home->getChildren(); + self::assertEquals(1, count($avatarNodes)); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providesTestGetChild')] + public function testChildExists(?string $expectedException, bool $hasAvatar, string $path): void { + $avatar = $this->createMock(IAvatar::class); + $avatar->method('exists')->willReturn($hasAvatar); + + $this->avatarManager->expects($this->any())->method('getAvatar')->with('admin')->willReturn($avatar); + $childExists = $this->home->childExists($path); + $this->assertEquals($hasAvatar, $childExists); + } + + public function testGetLastModified(): void { + self::assertNull($this->home->getLastModified()); + } +} diff --git a/apps/dav/tests/unit/Avatars/AvatarNodeTest.php b/apps/dav/tests/unit/Avatars/AvatarNodeTest.php new file mode 100644 index 00000000000..0ca147a1f3b --- /dev/null +++ b/apps/dav/tests/unit/Avatars/AvatarNodeTest.php @@ -0,0 +1,33 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2017 ownCloud GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Avatars; + +use OCA\DAV\Avatars\AvatarNode; +use OCP\IAvatar; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class AvatarNodeTest extends TestCase { + public function testGetName(): void { + /** @var IAvatar&MockObject $a */ + $a = $this->createMock(IAvatar::class); + $n = new AvatarNode(1024, 'png', $a); + $this->assertEquals('1024.png', $n->getName()); + } + + public function testGetContentType(): void { + /** @var IAvatar&MockObject $a */ + $a = $this->createMock(IAvatar::class); + $n = new AvatarNode(1024, 'png', $a); + $this->assertEquals('image/png', $n->getContentType()); + + $n = new AvatarNode(1024, 'jpeg', $a); + $this->assertEquals('image/jpeg', $n->getContentType()); + } +} diff --git a/apps/dav/tests/unit/BackgroundJob/CleanupInvitationTokenJobTest.php b/apps/dav/tests/unit/BackgroundJob/CleanupInvitationTokenJobTest.php new file mode 100644 index 00000000000..b2199e3e657 --- /dev/null +++ b/apps/dav/tests/unit/BackgroundJob/CleanupInvitationTokenJobTest.php @@ -0,0 +1,81 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\BackgroundJob; + +use OCA\DAV\BackgroundJob\CleanupInvitationTokenJob; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\DB\QueryBuilder\IExpressionBuilder; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class CleanupInvitationTokenJobTest extends TestCase { + private IDBConnection&MockObject $dbConnection; + private ITimeFactory&MockObject $timeFactory; + private CleanupInvitationTokenJob $backgroundJob; + + protected function setUp(): void { + parent::setUp(); + + $this->dbConnection = $this->createMock(IDBConnection::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + + $this->backgroundJob = new CleanupInvitationTokenJob( + $this->dbConnection, $this->timeFactory); + } + + public function testRun(): void { + $this->timeFactory->expects($this->once()) + ->method('getTime') + ->with() + ->willReturn(1337); + + $queryBuilder = $this->createMock(IQueryBuilder::class); + $expr = $this->createMock(IExpressionBuilder::class); + $stmt = $this->createMock(\Doctrine\DBAL\Driver\Statement::class); + + $this->dbConnection->expects($this->once()) + ->method('getQueryBuilder') + ->with() + ->willReturn($queryBuilder); + $queryBuilder->method('expr') + ->willReturn($expr); + $queryBuilder->method('createNamedParameter') + ->willReturnMap([ + [1337, \PDO::PARAM_STR, null, 'namedParameter1337'] + ]); + + $function = 'function1337'; + $expr->expects($this->once()) + ->method('lt') + ->with('expiration', 'namedParameter1337') + ->willReturn($function); + + $this->dbConnection->expects($this->once()) + ->method('getQueryBuilder') + ->with() + ->willReturn($queryBuilder); + + $queryBuilder->expects($this->once()) + ->method('delete') + ->with('calendar_invitations') + ->willReturn($queryBuilder); + $queryBuilder->expects($this->once()) + ->method('where') + ->with($function) + ->willReturn($queryBuilder); + $queryBuilder->expects($this->once()) + ->method('execute') + ->with() + ->willReturn($stmt); + + $this->backgroundJob->run([]); + } +} diff --git a/apps/dav/tests/unit/BackgroundJob/CleanupOrphanedChildrenJobTest.php b/apps/dav/tests/unit/BackgroundJob/CleanupOrphanedChildrenJobTest.php new file mode 100644 index 00000000000..2065b8fe946 --- /dev/null +++ b/apps/dav/tests/unit/BackgroundJob/CleanupOrphanedChildrenJobTest.php @@ -0,0 +1,170 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Tests\unit\BackgroundJob; + +use OCA\DAV\BackgroundJob\CleanupOrphanedChildrenJob; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJobList; +use OCP\DB\IResult; +use OCP\DB\QueryBuilder\IExpressionBuilder; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class CleanupOrphanedChildrenJobTest extends TestCase { + private CleanupOrphanedChildrenJob $job; + + private ITimeFactory&MockObject $timeFactory; + private IDBConnection&MockObject $connection; + private LoggerInterface&MockObject $logger; + private IJobList&MockObject $jobList; + + protected function setUp(): void { + parent::setUp(); + + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->connection = $this->createMock(IDBConnection::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->jobList = $this->createMock(IJobList::class); + + $this->job = new CleanupOrphanedChildrenJob( + $this->timeFactory, + $this->connection, + $this->logger, + $this->jobList, + ); + } + + private function getArgument(): array { + return [ + 'childTable' => 'childTable', + 'parentTable' => 'parentTable', + 'parentId' => 'parentId', + 'logMessage' => 'logMessage', + ]; + } + + private function getMockQueryBuilder(): IQueryBuilder&MockObject { + $expr = $this->createMock(IExpressionBuilder::class); + $qb = $this->createMock(IQueryBuilder::class); + $qb->method('select') + ->willReturnSelf(); + $qb->method('from') + ->willReturnSelf(); + $qb->method('leftJoin') + ->willReturnSelf(); + $qb->method('where') + ->willReturnSelf(); + $qb->method('setMaxResults') + ->willReturnSelf(); + $qb->method('andWhere') + ->willReturnSelf(); + $qb->method('expr') + ->willReturn($expr); + $qb->method('delete') + ->willReturnSelf(); + return $qb; + } + + public function testRunWithoutOrphans(): void { + $argument = $this->getArgument(); + $selectQb = $this->getMockQueryBuilder(); + $result = $this->createMock(IResult::class); + + $this->connection->expects(self::once()) + ->method('getQueryBuilder') + ->willReturn($selectQb); + $selectQb->expects(self::once()) + ->method('executeQuery') + ->willReturn($result); + $result->expects(self::once()) + ->method('fetchAll') + ->willReturn([]); + $result->expects(self::once()) + ->method('closeCursor'); + $this->jobList->expects(self::never()) + ->method('add'); + + self::invokePrivate($this->job, 'run', [$argument]); + } + + public function testRunWithPartialBatch(): void { + $argument = $this->getArgument(); + $selectQb = $this->getMockQueryBuilder(); + $deleteQb = $this->getMockQueryBuilder(); + $result = $this->createMock(IResult::class); + + $calls = [ + $selectQb, + $deleteQb, + ]; + $this->connection->method('getQueryBuilder') + ->willReturnCallback(function () use (&$calls) { + return array_shift($calls); + }); + $selectQb->expects(self::once()) + ->method('executeQuery') + ->willReturn($result); + $result->expects(self::once()) + ->method('fetchAll') + ->willReturn([ + ['id' => 42], + ['id' => 43], + ]); + $result->expects(self::once()) + ->method('closeCursor'); + $deleteQb->expects(self::once()) + ->method('delete') + ->willReturnSelf(); + $deleteQb->expects(self::once()) + ->method('executeStatement'); + $this->jobList->expects(self::never()) + ->method('add'); + + self::invokePrivate($this->job, 'run', [$argument]); + } + + public function testRunWithFullBatch(): void { + $argument = $this->getArgument(); + $selectQb = $this->getMockQueryBuilder(); + $deleteQb = $this->getMockQueryBuilder(); + $result = $this->createMock(IResult::class); + + $calls = [ + $selectQb, + $deleteQb, + ]; + $this->connection->method('getQueryBuilder') + ->willReturnCallback(function () use (&$calls) { + return array_shift($calls); + }); + + $selectQb->expects(self::once()) + ->method('executeQuery') + ->willReturn($result); + $result->expects(self::once()) + ->method('fetchAll') + ->willReturn(array_map(static fn ($i) => ['id' => 42 + $i], range(0, 999))); + $result->expects(self::once()) + ->method('closeCursor'); + $deleteQb->expects(self::once()) + ->method('delete') + ->willReturnSelf(); + $deleteQb->expects(self::once()) + ->method('executeStatement'); + $this->jobList->expects(self::once()) + ->method('add') + ->with(CleanupOrphanedChildrenJob::class, $argument); + + self::invokePrivate($this->job, 'run', [$argument]); + } +} diff --git a/apps/dav/tests/unit/BackgroundJob/EventReminderJobTest.php b/apps/dav/tests/unit/BackgroundJob/EventReminderJobTest.php new file mode 100644 index 00000000000..a46a1e5e5b0 --- /dev/null +++ b/apps/dav/tests/unit/BackgroundJob/EventReminderJobTest.php @@ -0,0 +1,72 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\BackgroundJob; + +use OCA\DAV\BackgroundJob\EventReminderJob; +use OCA\DAV\CalDAV\Reminder\ReminderService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class EventReminderJobTest extends TestCase { + private ITimeFactory&MockObject $time; + private ReminderService&MockObject $reminderService; + private IConfig&MockObject $config; + private EventReminderJob $backgroundJob; + + protected function setUp(): void { + parent::setUp(); + + $this->time = $this->createMock(ITimeFactory::class); + $this->reminderService = $this->createMock(ReminderService::class); + $this->config = $this->createMock(IConfig::class); + + $this->backgroundJob = new EventReminderJob( + $this->time, + $this->reminderService, + $this->config, + ); + } + + public static function data(): array { + return [ + [true, true, true], + [true, false, false], + [false, true, false], + [false, false, false], + ]; + } + + /** + * + * @param bool $sendEventReminders + * @param bool $sendEventRemindersMode + * @param bool $expectCall + */ + #[\PHPUnit\Framework\Attributes\DataProvider('data')] + public function testRun(bool $sendEventReminders, bool $sendEventRemindersMode, bool $expectCall): void { + $this->config->expects($this->exactly($sendEventReminders ? 2 : 1)) + ->method('getAppValue') + ->willReturnMap([ + ['dav', 'sendEventReminders', 'yes', ($sendEventReminders ? 'yes' : 'no')], + ['dav', 'sendEventRemindersMode', 'backgroundjob', ($sendEventRemindersMode ? 'backgroundjob' : 'cron')], + ]); + + if ($expectCall) { + $this->reminderService->expects($this->once()) + ->method('processReminders'); + } else { + $this->reminderService->expects($this->never()) + ->method('processReminders'); + } + + $this->backgroundJob->run([]); + } +} diff --git a/apps/dav/tests/unit/BackgroundJob/GenerateBirthdayCalendarBackgroundJobTest.php b/apps/dav/tests/unit/BackgroundJob/GenerateBirthdayCalendarBackgroundJobTest.php new file mode 100644 index 00000000000..88a76ae1332 --- /dev/null +++ b/apps/dav/tests/unit/BackgroundJob/GenerateBirthdayCalendarBackgroundJobTest.php @@ -0,0 +1,113 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\BackgroundJob; + +use OCA\DAV\BackgroundJob\GenerateBirthdayCalendarBackgroundJob; +use OCA\DAV\CalDAV\BirthdayService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class GenerateBirthdayCalendarBackgroundJobTest extends TestCase { + private ITimeFactory&MockObject $time; + private BirthdayService&MockObject $birthdayService; + private IConfig&MockObject $config; + private GenerateBirthdayCalendarBackgroundJob $backgroundJob; + + protected function setUp(): void { + parent::setUp(); + + $this->time = $this->createMock(ITimeFactory::class); + $this->birthdayService = $this->createMock(BirthdayService::class); + $this->config = $this->createMock(IConfig::class); + + $this->backgroundJob = new GenerateBirthdayCalendarBackgroundJob( + $this->time, + $this->birthdayService, + $this->config, + ); + } + + public function testRun(): void { + $this->config->expects($this->once()) + ->method('getAppValue') + ->with('dav', 'generateBirthdayCalendar', 'yes') + ->willReturn('yes'); + + $this->config->expects($this->once()) + ->method('getUserValue') + ->with('user123', 'dav', 'generateBirthdayCalendar', 'yes') + ->willReturn('yes'); + + $this->birthdayService->expects($this->never()) + ->method('resetForUser') + ->with('user123'); + + $this->birthdayService->expects($this->once()) + ->method('syncUser') + ->with('user123'); + + $this->backgroundJob->run(['userId' => 'user123']); + } + + public function testRunAndReset(): void { + $this->config->expects($this->once()) + ->method('getAppValue') + ->with('dav', 'generateBirthdayCalendar', 'yes') + ->willReturn('yes'); + + $this->config->expects($this->once()) + ->method('getUserValue') + ->with('user123', 'dav', 'generateBirthdayCalendar', 'yes') + ->willReturn('yes'); + + $this->birthdayService->expects($this->once()) + ->method('resetForUser') + ->with('user123'); + + $this->birthdayService->expects($this->once()) + ->method('syncUser') + ->with('user123'); + + $this->backgroundJob->run(['userId' => 'user123', 'purgeBeforeGenerating' => true]); + } + + public function testRunGloballyDisabled(): void { + $this->config->expects($this->once()) + ->method('getAppValue') + ->with('dav', 'generateBirthdayCalendar', 'yes') + ->willReturn('no'); + + $this->config->expects($this->never()) + ->method('getUserValue'); + + $this->birthdayService->expects($this->never()) + ->method('syncUser'); + + $this->backgroundJob->run(['userId' => 'user123']); + } + + public function testRunUserDisabled(): void { + $this->config->expects($this->once()) + ->method('getAppValue') + ->with('dav', 'generateBirthdayCalendar', 'yes') + ->willReturn('yes'); + + $this->config->expects($this->once()) + ->method('getUserValue') + ->with('user123', 'dav', 'generateBirthdayCalendar', 'yes') + ->willReturn('no'); + + $this->birthdayService->expects($this->never()) + ->method('syncUser'); + + $this->backgroundJob->run(['userId' => 'user123']); + } +} diff --git a/apps/dav/tests/unit/BackgroundJob/OutOfOfficeEventDispatcherJobTest.php b/apps/dav/tests/unit/BackgroundJob/OutOfOfficeEventDispatcherJobTest.php new file mode 100644 index 00000000000..6135fd00fdc --- /dev/null +++ b/apps/dav/tests/unit/BackgroundJob/OutOfOfficeEventDispatcherJobTest.php @@ -0,0 +1,148 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Tests\unit\BackgroundJob; + +use OCA\DAV\BackgroundJob\OutOfOfficeEventDispatcherJob; +use OCA\DAV\CalDAV\TimezoneService; +use OCA\DAV\Db\Absence; +use OCA\DAV\Db\AbsenceMapper; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IUser; +use OCP\IUserManager; +use OCP\User\Events\OutOfOfficeEndedEvent; +use OCP\User\Events\OutOfOfficeStartedEvent; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class OutOfOfficeEventDispatcherJobTest extends TestCase { + private OutOfOfficeEventDispatcherJob $job; + private ITimeFactory&MockObject $timeFactory; + private AbsenceMapper&MockObject $absenceMapper; + private LoggerInterface&MockObject $logger; + private IEventDispatcher&MockObject $eventDispatcher; + private IUserManager&MockObject $userManager; + private MockObject|TimezoneService $timezoneService; + + protected function setUp(): void { + parent::setUp(); + + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->absenceMapper = $this->createMock(AbsenceMapper::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->eventDispatcher = $this->createMock(IEventDispatcher::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->timezoneService = $this->createMock(TimezoneService::class); + + $this->job = new OutOfOfficeEventDispatcherJob( + $this->timeFactory, + $this->absenceMapper, + $this->logger, + $this->eventDispatcher, + $this->userManager, + $this->timezoneService, + ); + } + + public function testDispatchStartEvent(): void { + $this->timezoneService->method('getUserTimezone')->with('user')->willReturn('Europe/Berlin'); + + $absence = new Absence(); + $absence->setId(200); + $absence->setUserId('user'); + + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('user'); + + $this->absenceMapper->expects(self::once()) + ->method('findById') + ->with(1) + ->willReturn($absence); + $this->userManager->expects(self::once()) + ->method('get') + ->with('user') + ->willReturn($user); + $this->eventDispatcher->expects(self::once()) + ->method('dispatchTyped') + ->with(self::callback(static function ($event): bool { + self::assertInstanceOf(OutOfOfficeStartedEvent::class, $event); + return true; + })); + + $this->job->run([ + 'id' => 1, + 'event' => OutOfOfficeEventDispatcherJob::EVENT_START, + ]); + } + + public function testDispatchStopEvent(): void { + $this->timezoneService->method('getUserTimezone')->with('user')->willReturn('Europe/Berlin'); + + $absence = new Absence(); + $absence->setId(200); + $absence->setUserId('user'); + + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('user'); + + $this->absenceMapper->expects(self::once()) + ->method('findById') + ->with(1) + ->willReturn($absence); + $this->userManager->expects(self::once()) + ->method('get') + ->with('user') + ->willReturn($user); + $this->eventDispatcher->expects(self::once()) + ->method('dispatchTyped') + ->with(self::callback(static function ($event): bool { + self::assertInstanceOf(OutOfOfficeEndedEvent::class, $event); + return true; + })); + + $this->job->run([ + 'id' => 1, + 'event' => OutOfOfficeEventDispatcherJob::EVENT_END, + ]); + } + + public function testDoesntDispatchUnknownEvent(): void { + $this->timezoneService->method('getUserTimezone')->with('user')->willReturn('Europe/Berlin'); + + $absence = new Absence(); + $absence->setId(100); + $absence->setUserId('user'); + + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('user'); + + $this->absenceMapper->expects(self::once()) + ->method('findById') + ->with(1) + ->willReturn($absence); + $this->userManager->expects(self::once()) + ->method('get') + ->with('user') + ->willReturn($user); + $this->eventDispatcher->expects(self::never()) + ->method('dispatchTyped'); + $this->logger->expects(self::once()) + ->method('error'); + + $this->job->run([ + 'id' => 1, + 'event' => 'foobar', + ]); + } +} diff --git a/apps/dav/tests/unit/BackgroundJob/PruneOutdatedSyncTokensJobTest.php b/apps/dav/tests/unit/BackgroundJob/PruneOutdatedSyncTokensJobTest.php new file mode 100644 index 00000000000..1838fb2537d --- /dev/null +++ b/apps/dav/tests/unit/BackgroundJob/PruneOutdatedSyncTokensJobTest.php @@ -0,0 +1,82 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\BackgroundJob; + +use InvalidArgumentException; +use OCA\DAV\AppInfo\Application; +use OCA\DAV\BackgroundJob\PruneOutdatedSyncTokensJob; +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CardDAV\CardDavBackend; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class PruneOutdatedSyncTokensJobTest extends TestCase { + private ITimeFactory&MockObject $timeFactory; + private CalDavBackend&MockObject $calDavBackend; + private CardDavBackend&MockObject $cardDavBackend; + private IConfig&MockObject $config; + private LoggerInterface&MockObject $logger; + private PruneOutdatedSyncTokensJob $backgroundJob; + + protected function setUp(): void { + parent::setUp(); + + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->calDavBackend = $this->createMock(CalDavBackend::class); + $this->cardDavBackend = $this->createMock(CardDavBackend::class); + $this->config = $this->createMock(IConfig::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->backgroundJob = new PruneOutdatedSyncTokensJob($this->timeFactory, $this->calDavBackend, $this->cardDavBackend, $this->config, $this->logger); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataForTestRun')] + public function testRun(string $configToKeep, string $configRetentionDays, int $actualLimit, int $retentionDays, int $deletedCalendarSyncTokens, int $deletedAddressBookSyncTokens): void { + $this->config->expects($this->exactly(2)) + ->method('getAppValue') + ->with(Application::APP_ID, self::anything(), self::anything()) + ->willReturnCallback(function ($app, $key) use ($configToKeep, $configRetentionDays) { + switch ($key) { + case 'totalNumberOfSyncTokensToKeep': + return $configToKeep; + case 'syncTokensRetentionDays': + return $configRetentionDays; + default: + throw new InvalidArgumentException(); + } + }); + $this->calDavBackend->expects($this->once()) + ->method('pruneOutdatedSyncTokens') + ->with($actualLimit) + ->willReturn($deletedCalendarSyncTokens); + $this->cardDavBackend->expects($this->once()) + ->method('pruneOutdatedSyncTokens') + ->with($actualLimit, $retentionDays) + ->willReturn($deletedAddressBookSyncTokens); + $this->logger->expects($this->once()) + ->method('info') + ->with('Pruned {calendarSyncTokensNumber} calendar sync tokens and {addressBooksSyncTokensNumber} address book sync tokens', [ + 'calendarSyncTokensNumber' => $deletedCalendarSyncTokens, + 'addressBooksSyncTokensNumber' => $deletedAddressBookSyncTokens + ]); + + $this->backgroundJob->run(null); + } + + public static function dataForTestRun(): array { + return [ + ['100', '2', 100, 7 * 24 * 3600, 2, 3], + ['100', '14', 100, 14 * 24 * 3600, 2, 3], + ['0', '60', 1, 60 * 24 * 3600, 0, 0] + ]; + } +} diff --git a/apps/dav/tests/unit/BackgroundJob/RefreshWebcalJobTest.php b/apps/dav/tests/unit/BackgroundJob/RefreshWebcalJobTest.php new file mode 100644 index 00000000000..7713ef2945a --- /dev/null +++ b/apps/dav/tests/unit/BackgroundJob/RefreshWebcalJobTest.php @@ -0,0 +1,96 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\BackgroundJob; + +use OCA\DAV\BackgroundJob\RefreshWebcalJob; +use OCA\DAV\CalDAV\WebcalCaching\RefreshWebcalService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJobList; +use OCP\IConfig; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; + +use Test\TestCase; + +class RefreshWebcalJobTest extends TestCase { + private RefreshWebcalService&MockObject $refreshWebcalService; + private IConfig&MockObject $config; + private LoggerInterface $logger; + private ITimeFactory&MockObject $timeFactory; + private IJobList&MockObject $jobList; + + protected function setUp(): void { + parent::setUp(); + + $this->refreshWebcalService = $this->createMock(RefreshWebcalService::class); + $this->config = $this->createMock(IConfig::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + + $this->jobList = $this->createMock(IJobList::class); + } + + /** + * + * @param int $lastRun + * @param int $time + * @param bool $process + */ + #[\PHPUnit\Framework\Attributes\DataProvider('runDataProvider')] + public function testRun(int $lastRun, int $time, bool $process): void { + $backgroundJob = new RefreshWebcalJob($this->refreshWebcalService, $this->config, $this->logger, $this->timeFactory); + $backgroundJob->setId(42); + + $backgroundJob->setArgument([ + 'principaluri' => 'principals/users/testuser', + 'uri' => 'sub123', + ]); + $backgroundJob->setLastRun($lastRun); + + $this->refreshWebcalService->expects($this->once()) + ->method('getSubscription') + ->with('principals/users/testuser', 'sub123') + ->willReturn([ + 'id' => '99', + 'uri' => 'sub456', + '{http://apple.com/ns/ical/}refreshrate' => 'P1D', + '{http://calendarserver.org/ns/}subscribed-strip-todos' => '1', + '{http://calendarserver.org/ns/}subscribed-strip-alarms' => '1', + '{http://calendarserver.org/ns/}subscribed-strip-attachments' => '1', + 'source' => 'webcal://foo.bar/bla' + ]); + + $this->config->expects($this->once()) + ->method('getAppValue') + ->with('dav', 'calendarSubscriptionRefreshRate', 'P1D') + ->willReturn('P1W'); + + $this->timeFactory->method('getTime') + ->willReturn($time); + + if ($process) { + $this->refreshWebcalService->expects($this->once()) + ->method('refreshSubscription') + ->with('principals/users/testuser', 'sub123'); + } else { + $this->refreshWebcalService->expects($this->never()) + ->method('refreshSubscription') + ->with('principals/users/testuser', 'sub123'); + } + + $backgroundJob->start($this->jobList); + } + + public static function runDataProvider():array { + return [ + [0, 100000, true], + [100000, 100000, false] + ]; + } +} diff --git a/apps/dav/tests/unit/BackgroundJob/RegisterRegenerateBirthdayCalendarsTest.php b/apps/dav/tests/unit/BackgroundJob/RegisterRegenerateBirthdayCalendarsTest.php new file mode 100644 index 00000000000..6c9214d0268 --- /dev/null +++ b/apps/dav/tests/unit/BackgroundJob/RegisterRegenerateBirthdayCalendarsTest.php @@ -0,0 +1,79 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\BackgroundJob; + +use OCA\DAV\BackgroundJob\GenerateBirthdayCalendarBackgroundJob; +use OCA\DAV\BackgroundJob\RegisterRegenerateBirthdayCalendars; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJobList; +use OCP\IUser; +use OCP\IUserManager; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class RegisterRegenerateBirthdayCalendarsTest extends TestCase { + private ITimeFactory&MockObject $time; + private IUserManager&MockObject $userManager; + private IJobList&MockObject $jobList; + private RegisterRegenerateBirthdayCalendars $backgroundJob; + + protected function setUp(): void { + parent::setUp(); + + $this->time = $this->createMock(ITimeFactory::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->jobList = $this->createMock(IJobList::class); + + $this->backgroundJob = new RegisterRegenerateBirthdayCalendars( + $this->time, + $this->userManager, + $this->jobList + ); + } + + public function testRun(): void { + $this->userManager->expects($this->once()) + ->method('callForSeenUsers') + ->willReturnCallback(function ($closure): void { + $user1 = $this->createMock(IUser::class); + $user1->method('getUID')->willReturn('uid1'); + $user2 = $this->createMock(IUser::class); + $user2->method('getUID')->willReturn('uid2'); + $user3 = $this->createMock(IUser::class); + $user3->method('getUID')->willReturn('uid3'); + + $closure($user1); + $closure($user2); + $closure($user3); + }); + + $calls = [ + 'uid1', + 'uid2', + 'uid3', + ]; + $this->jobList->expects($this->exactly(3)) + ->method('add') + ->willReturnCallback(function () use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals( + [ + GenerateBirthdayCalendarBackgroundJob::class, + [ + 'userId' => $expected, + 'purgeBeforeGenerating' => true + ] + ], + func_get_args() + ); + }); + + $this->backgroundJob->run([]); + } +} diff --git a/apps/dav/tests/unit/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJobTest.php b/apps/dav/tests/unit/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJobTest.php new file mode 100644 index 00000000000..38a981787cd --- /dev/null +++ b/apps/dav/tests/unit/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJobTest.php @@ -0,0 +1,47 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\BackgroundJob; + +use OCA\DAV\BackgroundJob\UpdateCalendarResourcesRoomsBackgroundJob; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Calendar\Resource\IManager as IResourceManager; +use OCP\Calendar\Room\IManager as IRoomManager; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class UpdateCalendarResourcesRoomsBackgroundJobTest extends TestCase { + private UpdateCalendarResourcesRoomsBackgroundJob $backgroundJob; + private ITimeFactory&MockObject $time; + private IResourceManager&MockObject $resourceManager; + private IRoomManager&MockObject $roomManager; + + protected function setUp(): void { + parent::setUp(); + + $this->time = $this->createMock(ITimeFactory::class); + $this->resourceManager = $this->createMock(IResourceManager::class); + $this->roomManager = $this->createMock(IRoomManager::class); + + $this->backgroundJob = new UpdateCalendarResourcesRoomsBackgroundJob( + $this->time, + $this->resourceManager, + $this->roomManager, + ); + } + + public function testRun(): void { + $this->resourceManager->expects(self::once()) + ->method('update'); + $this->roomManager->expects(self::once()) + ->method('update'); + + $this->backgroundJob->run([]); + } +} diff --git a/apps/dav/tests/unit/BackgroundJob/UserStatusAutomationTest.php b/apps/dav/tests/unit/BackgroundJob/UserStatusAutomationTest.php new file mode 100644 index 00000000000..d49d20180d9 --- /dev/null +++ b/apps/dav/tests/unit/BackgroundJob/UserStatusAutomationTest.php @@ -0,0 +1,220 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Tests\unit\BackgroundJob; + +use OC\User\OutOfOfficeData; +use OCA\DAV\BackgroundJob\UserStatusAutomation; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJobList; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IUser; +use OCP\IUserManager; +use OCP\Server; +use OCP\User\IAvailabilityCoordinator; +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 ITimeFactory&MockObject $time; + protected IJobList&MockObject $jobList; + protected LoggerInterface&MockObject $logger; + protected IManager&MockObject $statusManager; + protected IConfig&MockObject $config; + private IAvailabilityCoordinator&MockObject $coordinator; + private IUserManager&MockObject $userManager; + + 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); + $this->coordinator = $this->createMock(IAvailabilityCoordinator::class); + $this->userManager = $this->createMock(IUserManager::class); + + } + + protected function getAutomationMock(array $methods): MockObject|UserStatusAutomation { + if (empty($methods)) { + return new UserStatusAutomation( + $this->time, + Server::get(IDBConnection::class), + $this->jobList, + $this->logger, + $this->statusManager, + $this->config, + $this->coordinator, + $this->userManager, + ); + } + + return $this->getMockBuilder(UserStatusAutomation::class) + ->setConstructorArgs([ + $this->time, + Server::get(IDBConnection::class), + $this->jobList, + $this->logger, + $this->statusManager, + $this->config, + $this->coordinator, + $this->userManager, + ]) + ->onlyMethods($methods) + ->getMock(); + } + + public static 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], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataRun')] + public function testRunNoOOO(string $ruleDay, string $currentTime, bool $isAvailable): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'user' + ]); + + $this->userManager->expects(self::once()) + ->method('get') + ->willReturn($user); + $this->coordinator->expects(self::once()) + ->method('getCurrentOutOfOfficeData') + ->willReturn(null); + $this->config->method('getUserValue') + ->with('user', 'dav', 'user_status_automation', 'no') + ->willReturn('yes'); + $this->time->method('getDateTime') + ->willReturn(new \DateTime($currentTime, new \DateTimeZone('UTC'))); + $this->logger->expects(self::exactly(4)) + ->method('debug'); + if (!$isAvailable) { + $this->statusManager->expects(self::once()) + ->method('setUserStatus') + ->with('user', IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND, true); + } + $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'); + + self::invokePrivate($automation, 'run', [['userId' => 'user']]); + } + + public function testRunNoAvailabilityNoOOO(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'user' + ]); + + $this->userManager->expects(self::once()) + ->method('get') + ->willReturn($user); + $this->coordinator->expects(self::once()) + ->method('getCurrentOutOfOfficeData') + ->willReturn(null); + $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'))); + $this->jobList->expects($this->once()) + ->method('remove') + ->with(UserStatusAutomation::class, ['userId' => 'user']); + $this->logger->expects(self::once()) + ->method('debug'); + $this->logger->expects(self::once()) + ->method('info'); + $automation = $this->getAutomationMock(['getAvailabilityFromPropertiesTable']); + $automation->method('getAvailabilityFromPropertiesTable') + ->with('user') + ->willReturn(false); + + self::invokePrivate($automation, 'run', [['userId' => 'user']]); + } + + public function testRunNoAvailabilityWithOOO(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'user' + ]); + $ooo = $this->createConfiguredMock(OutOfOfficeData::class, [ + 'getShortMessage' => 'On Vacation', + 'getEndDate' => 123456, + ]); + + $this->userManager->expects(self::once()) + ->method('get') + ->willReturn($user); + $this->coordinator->expects(self::once()) + ->method('getCurrentOutOfOfficeData') + ->willReturn($ooo); + $this->coordinator->expects(self::once()) + ->method('isInEffect') + ->willReturn(true); + $this->statusManager->expects(self::once()) + ->method('setUserStatus') + ->with('user', IUserStatus::MESSAGE_OUT_OF_OFFICE, IUserStatus::DND, true, $ooo->getShortMessage()); + $this->config->expects(self::never()) + ->method('getUserValue'); + $this->time->method('getDateTime') + ->willReturn(new \DateTime('2023-02-24 13:58:24.479357', new \DateTimeZone('UTC'))); + $this->jobList->expects($this->never()) + ->method('remove'); + $this->logger->expects(self::exactly(2)) + ->method('debug'); + $automation = $this->getAutomationMock([]); + + self::invokePrivate($automation, 'run', [['userId' => 'user']]); + } +} diff --git a/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php b/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php new file mode 100644 index 00000000000..45937d86873 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php @@ -0,0 +1,263 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\CalDAV; + +use OC\KnownUser\KnownUserService; +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CalDAV\Proxy\ProxyMapper; +use OCA\DAV\CalDAV\Sharing\Backend as SharingBackend; +use OCA\DAV\CalDAV\Sharing\Service; +use OCA\DAV\Connector\Sabre\Principal; +use OCA\DAV\DAV\Sharing\SharingMapper; +use OCP\Accounts\IAccountManager; +use OCP\App\IAppManager; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\ICacheFactory; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IGroupManager; +use OCP\IUserManager; +use OCP\IUserSession; +use OCP\L10N\IFactory; +use OCP\Security\ISecureRandom; +use OCP\Server; +use OCP\Share\IManager as ShareManager; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet; +use Sabre\DAV\Xml\Property\Href; +use Test\TestCase; + +/** + * Class CalDavBackendTest + * + * @group DB + * + * @package OCA\DAV\Tests\unit\CalDAV + */ +abstract class AbstractCalDavBackend extends TestCase { + + + protected CalDavBackend $backend; + protected Principal&MockObject $principal; + protected IUserManager&MockObject $userManager; + protected IGroupManager&MockObject $groupManager; + protected IEventDispatcher&MockObject $dispatcher; + private LoggerInterface&MockObject $logger; + private IConfig&MockObject $config; + private ISecureRandom $random; + protected SharingBackend $sharingBackend; + protected IDBConnection $db; + public const UNIT_TEST_USER = 'principals/users/caldav-unit-test'; + public const UNIT_TEST_USER1 = 'principals/users/caldav-unit-test1'; + public const UNIT_TEST_GROUP = 'principals/groups/caldav-unit-test-group'; + public const UNIT_TEST_GROUP2 = 'principals/groups/caldav-unit-test-group2'; + + protected function setUp(): void { + parent::setUp(); + + $this->userManager = $this->createMock(IUserManager::class); + $this->groupManager = $this->createMock(IGroupManager::class); + $this->dispatcher = $this->createMock(IEventDispatcher::class); + $this->principal = $this->getMockBuilder(Principal::class) + ->setConstructorArgs([ + $this->userManager, + $this->groupManager, + $this->createMock(IAccountManager::class), + $this->createMock(ShareManager::class), + $this->createMock(IUserSession::class), + $this->createMock(IAppManager::class), + $this->createMock(ProxyMapper::class), + $this->createMock(KnownUserService::class), + $this->createMock(IConfig::class), + $this->createMock(IFactory::class) + ]) + ->onlyMethods(['getPrincipalByPath', 'getGroupMembership', 'findByUri']) + ->getMock(); + $this->principal->expects($this->any())->method('getPrincipalByPath') + ->willReturn([ + 'uri' => 'principals/best-friend', + '{DAV:}displayname' => 'User\'s displayname', + ]); + $this->principal->expects($this->any())->method('getGroupMembership') + ->withAnyParameters() + ->willReturn([self::UNIT_TEST_GROUP, self::UNIT_TEST_GROUP2]); + + $this->db = Server::get(IDBConnection::class); + $this->random = Server::get(ISecureRandom::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->config = $this->createMock(IConfig::class); + $this->sharingBackend = new SharingBackend( + $this->userManager, + $this->groupManager, + $this->principal, + $this->createMock(ICacheFactory::class), + new Service(new SharingMapper($this->db)), + $this->logger); + $this->backend = new CalDavBackend( + $this->db, + $this->principal, + $this->userManager, + $this->random, + $this->logger, + $this->dispatcher, + $this->config, + $this->sharingBackend, + false, + ); + + $this->cleanUpBackend(); + } + + protected function tearDown(): void { + $this->cleanUpBackend(); + parent::tearDown(); + } + + public function cleanUpBackend(): void { + if (is_null($this->backend)) { + return; + } + $this->principal->expects($this->any())->method('getGroupMembership') + ->withAnyParameters() + ->willReturn([self::UNIT_TEST_GROUP, self::UNIT_TEST_GROUP2]); + $this->cleanupForPrincipal(self::UNIT_TEST_USER); + $this->cleanupForPrincipal(self::UNIT_TEST_USER1); + } + + private function cleanupForPrincipal($principal): void { + $calendars = $this->backend->getCalendarsForUser($principal); + $this->dispatcher->expects(self::any()) + ->method('dispatchTyped'); + foreach ($calendars as $calendar) { + $this->backend->deleteCalendar($calendar['id'], true); + } + $subscriptions = $this->backend->getSubscriptionsForUser($principal); + foreach ($subscriptions as $subscription) { + $this->backend->deleteSubscription($subscription['id']); + } + } + + protected function createTestCalendar(): int { + $this->dispatcher->expects(self::any()) + ->method('dispatchTyped'); + + $this->backend->createCalendar(self::UNIT_TEST_USER, 'Example', [ + '{http://apple.com/ns/ical/}calendar-color' => '#1C4587FF' + ]); + $calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER); + $this->assertEquals(1, count($calendars)); + $this->assertEquals(self::UNIT_TEST_USER, $calendars[0]['principaluri']); + /** @var SupportedCalendarComponentSet $components */ + $components = $calendars[0]['{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set']; + $this->assertEquals(['VEVENT','VTODO','VJOURNAL'], $components->getValue()); + $color = $calendars[0]['{http://apple.com/ns/ical/}calendar-color']; + $this->assertEquals('#1C4587FF', $color); + $this->assertEquals('Example', $calendars[0]['uri']); + $this->assertEquals('Example', $calendars[0]['{DAV:}displayname']); + return (int)$calendars[0]['id']; + } + + protected function createTestSubscription() { + $this->backend->createSubscription(self::UNIT_TEST_USER, 'Example', [ + '{http://apple.com/ns/ical/}calendar-color' => '#1C4587FF', + '{http://calendarserver.org/ns/}source' => new Href(['foo']), + ]); + $calendars = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER); + $this->assertEquals(1, count($calendars)); + $this->assertEquals(self::UNIT_TEST_USER, $calendars[0]['principaluri']); + $this->assertEquals('Example', $calendars[0]['uri']); + $calendarId = $calendars[0]['id']; + + return $calendarId; + } + + protected function createEvent($calendarId, $start = '20130912T130000Z', $end = '20130912T140000Z') { + $randomPart = self::getUniqueID(); + + $calData = <<<EOD +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:ownCloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3ec8-$randomPart +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:Test Event +DTSTART;VALUE=DATE-TIME:$start +DTEND;VALUE=DATE-TIME:$end +CLASS:PUBLIC +END:VEVENT +END:VCALENDAR +EOD; + $uri0 = $this->getUniqueID('event'); + + $this->dispatcher->expects(self::atLeastOnce()) + ->method('dispatchTyped'); + + $this->backend->createCalendarObject($calendarId, $uri0, $calData); + + return $uri0; + } + + protected function modifyEvent($calendarId, $objectId, $start = '20130912T130000Z', $end = '20130912T140000Z') { + $randomPart = self::getUniqueID(); + + $calData = <<<EOD +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:ownCloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3ec8-$randomPart +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:Test Event +DTSTART;VALUE=DATE-TIME:$start +DTEND;VALUE=DATE-TIME:$end +CLASS:PUBLIC +END:VEVENT +END:VCALENDAR +EOD; + + $this->backend->updateCalendarObject($calendarId, $objectId, $calData); + } + + protected function deleteEvent($calendarId, $objectId) { + $this->backend->deleteCalendarObject($calendarId, $objectId); + } + + protected function assertAcl($principal, $privilege, $acl) { + foreach ($acl as $a) { + if ($a['principal'] === $principal && $a['privilege'] === $privilege) { + $this->addToAssertionCount(1); + return; + } + } + $this->fail("ACL does not contain $principal / $privilege"); + } + + protected function assertNotAcl($principal, $privilege, $acl) { + foreach ($acl as $a) { + if ($a['principal'] === $principal && $a['privilege'] === $privilege) { + $this->fail("ACL contains $principal / $privilege"); + return; + } + } + $this->addToAssertionCount(1); + } + + protected function assertAccess($shouldHaveAcl, $principal, $privilege, $acl) { + if ($shouldHaveAcl) { + $this->assertAcl($principal, $privilege, $acl); + } else { + $this->assertNotAcl($principal, $privilege, $acl); + } + } +} diff --git a/apps/dav/tests/unit/CalDAV/Activity/BackendTest.php b/apps/dav/tests/unit/CalDAV/Activity/BackendTest.php new file mode 100644 index 00000000000..4848a01f6b9 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Activity/BackendTest.php @@ -0,0 +1,349 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\Activity; + +use OCA\DAV\CalDAV\Activity\Backend; +use OCA\DAV\CalDAV\Activity\Provider\Calendar; +use OCP\Activity\IEvent; +use OCP\Activity\IManager; +use OCP\App\IAppManager; +use OCP\IGroup; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\IUserManager; +use OCP\IUserSession; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class BackendTest extends TestCase { + protected IManager&MockObject $activityManager; + protected IGroupManager&MockObject $groupManager; + protected IUserSession&MockObject $userSession; + protected IAppManager&MockObject $appManager; + protected IUserManager&MockObject $userManager; + + protected function setUp(): void { + parent::setUp(); + $this->activityManager = $this->createMock(IManager::class); + $this->groupManager = $this->createMock(IGroupManager::class); + $this->userSession = $this->createMock(IUserSession::class); + $this->appManager = $this->createMock(IAppManager::class); + $this->userManager = $this->createMock(IUserManager::class); + } + + /** + * @return Backend|(Backend&MockObject) + */ + protected function getBackend(array $methods = []): Backend { + if (empty($methods)) { + return new Backend( + $this->activityManager, + $this->groupManager, + $this->userSession, + $this->appManager, + $this->userManager + ); + } else { + return $this->getMockBuilder(Backend::class) + ->setConstructorArgs([ + $this->activityManager, + $this->groupManager, + $this->userSession, + $this->appManager, + $this->userManager + ]) + ->onlyMethods($methods) + ->getMock(); + } + } + + public static function dataCallTriggerCalendarActivity(): array { + return [ + ['onCalendarAdd', [['data']], Calendar::SUBJECT_ADD, [['data'], [], []]], + ['onCalendarUpdate', [['data'], ['shares'], ['changed-properties']], Calendar::SUBJECT_UPDATE, [['data'], ['shares'], ['changed-properties']]], + ['onCalendarDelete', [['data'], ['shares']], Calendar::SUBJECT_DELETE, [['data'], ['shares'], []]], + ['onCalendarPublication', [['data'], true], Calendar::SUBJECT_PUBLISH, [['data'], [], []]], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataCallTriggerCalendarActivity')] + public function testCallTriggerCalendarActivity(string $method, array $payload, string $expectedSubject, array $expectedPayload): void { + $backend = $this->getBackend(['triggerCalendarActivity']); + $backend->expects($this->once()) + ->method('triggerCalendarActivity') + ->willReturnCallback(function () use ($expectedPayload, $expectedSubject): void { + $arguments = func_get_args(); + $this->assertSame($expectedSubject, array_shift($arguments)); + $this->assertEquals($expectedPayload, $arguments); + }); + + call_user_func_array([$backend, $method], $payload); + } + + public static function dataTriggerCalendarActivity(): array { + return [ + // Add calendar + [Calendar::SUBJECT_ADD, [], [], [], '', '', null, []], + [Calendar::SUBJECT_ADD, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + 'uri' => 'this-uri', + '{DAV:}displayname' => 'Name of calendar', + ], [], [], '', 'admin', null, ['admin']], + [Calendar::SUBJECT_ADD, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + 'uri' => 'this-uri', + '{DAV:}displayname' => 'Name of calendar', + ], [], [], 'test2', 'test2', null, ['admin']], + + // Update calendar + [Calendar::SUBJECT_UPDATE, [], [], [], '', '', null, []], + // No visible change - owner only + [Calendar::SUBJECT_UPDATE, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + 'uri' => 'this-uri', + '{DAV:}displayname' => 'Name of calendar', + ], ['shares'], [], '', 'admin', null, ['admin']], + // Visible change + [Calendar::SUBJECT_UPDATE, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + 'uri' => 'this-uri', + '{DAV:}displayname' => 'Name of calendar', + ], ['shares'], ['{DAV:}displayname' => 'Name'], '', 'admin', ['user1'], ['user1', 'admin']], + [Calendar::SUBJECT_UPDATE, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + 'uri' => 'this-uri', + '{DAV:}displayname' => 'Name of calendar', + ], ['shares'], ['{DAV:}displayname' => 'Name'], 'test2', 'test2', ['user1'], ['user1', 'admin']], + + // Delete calendar + [Calendar::SUBJECT_DELETE, [], [], [], '', '', null, []], + [Calendar::SUBJECT_DELETE, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + 'uri' => 'this-uri', + '{DAV:}displayname' => 'Name of calendar', + ], ['shares'], [], '', 'admin', [], ['admin']], + [Calendar::SUBJECT_DELETE, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + 'uri' => 'this-uri', + '{DAV:}displayname' => 'Name of calendar', + ], ['shares'], [], '', 'admin', ['user1'], ['user1', 'admin']], + [Calendar::SUBJECT_DELETE, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + 'uri' => 'this-uri', + '{DAV:}displayname' => 'Name of calendar', + ], ['shares'], [], 'test2', 'test2', ['user1'], ['user1', 'admin']], + + // Publish calendar + [Calendar::SUBJECT_PUBLISH, [], [], [], '', '', null, []], + [Calendar::SUBJECT_PUBLISH, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + 'uri' => 'this-uri', + '{DAV:}displayname' => 'Name of calendar', + ], ['shares'], [], '', 'admin', [], ['admin']], + + // Unpublish calendar + [Calendar::SUBJECT_UNPUBLISH, [], [], [], '', '', null, []], + [Calendar::SUBJECT_UNPUBLISH, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + 'uri' => 'this-uri', + '{DAV:}displayname' => 'Name of calendar', + ], ['shares'], [], '', 'admin', [], ['admin']], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataTriggerCalendarActivity')] + public function testTriggerCalendarActivity(string $action, array $data, array $shares, array $changedProperties, string $currentUser, string $author, ?array $shareUsers, array $users): void { + $backend = $this->getBackend(['getUsersForShares']); + + if ($shareUsers === null) { + $backend->expects($this->never()) + ->method('getUsersForShares'); + } else { + $backend->expects($this->once()) + ->method('getUsersForShares') + ->with($shares) + ->willReturn($shareUsers); + } + + if ($author !== '') { + if ($currentUser !== '') { + $this->userSession->expects($this->once()) + ->method('getUser') + ->willReturn($this->getUserMock($currentUser)); + } else { + $this->userSession->expects($this->once()) + ->method('getUser') + ->willReturn(null); + } + + $event = $this->createMock(IEvent::class); + $this->activityManager->expects($this->once()) + ->method('generateEvent') + ->willReturn($event); + + $event->expects($this->once()) + ->method('setApp') + ->with('dav') + ->willReturnSelf(); + $event->expects($this->once()) + ->method('setObject') + ->with('calendar', $data['id']) + ->willReturnSelf(); + $event->expects($this->once()) + ->method('setType') + ->with('calendar') + ->willReturnSelf(); + $event->expects($this->once()) + ->method('setAuthor') + ->with($author) + ->willReturnSelf(); + + $this->userManager->expects($action === Calendar::SUBJECT_DELETE ? $this->exactly(sizeof($users)) : $this->never()) + ->method('userExists') + ->willReturn(true); + + $event->expects($this->exactly(sizeof($users))) + ->method('setAffectedUser') + ->willReturnSelf(); + $event->expects($this->exactly(sizeof($users))) + ->method('setSubject') + ->willReturnSelf(); + $this->activityManager->expects($this->exactly(sizeof($users))) + ->method('publish') + ->with($event); + } else { + $this->activityManager->expects($this->never()) + ->method('generateEvent'); + } + + $this->invokePrivate($backend, 'triggerCalendarActivity', [$action, $data, $shares, $changedProperties]); + } + + public function testUserDeletionDoesNotCreateActivity(): void { + $backend = $this->getBackend(); + + $this->userManager->expects($this->once()) + ->method('userExists') + ->willReturn(false); + + $this->activityManager->expects($this->never()) + ->method('publish'); + + $this->invokePrivate($backend, 'triggerCalendarActivity', [Calendar::SUBJECT_DELETE, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + 'uri' => 'this-uri', + '{DAV:}displayname' => 'Name of calendar', + ], [], []]); + } + + public static function dataGetUsersForShares(): array { + return [ + [ + [], + [], + [], + ], + [ + [ + ['{http://owncloud.org/ns}principal' => 'principal/users/user1'], + ['{http://owncloud.org/ns}principal' => 'principal/users/user2'], + ['{http://owncloud.org/ns}principal' => 'principal/users/user2'], + ['{http://owncloud.org/ns}principal' => 'principal/users/user2'], + ['{http://owncloud.org/ns}principal' => 'principal/users/user3'], + ], + [], + ['user1', 'user2', 'user3'], + ], + [ + [ + ['{http://owncloud.org/ns}principal' => 'principal/users/user1'], + ['{http://owncloud.org/ns}principal' => 'principal/users/user2'], + ['{http://owncloud.org/ns}principal' => 'principal/users/user2'], + ['{http://owncloud.org/ns}principal' => 'principal/groups/group2'], + ['{http://owncloud.org/ns}principal' => 'principal/groups/group3'], + ], + ['group2' => null, 'group3' => null], + ['user1', 'user2'], + ], + [ + [ + ['{http://owncloud.org/ns}principal' => 'principal/users/user1'], + ['{http://owncloud.org/ns}principal' => 'principal/users/user2'], + ['{http://owncloud.org/ns}principal' => 'principal/users/user2'], + ['{http://owncloud.org/ns}principal' => 'principal/groups/group2'], + ['{http://owncloud.org/ns}principal' => 'principal/groups/group3'], + ], + ['group2' => ['user1', 'user2', 'user3'], 'group3' => ['user2', 'user3', 'user4']], + ['user1', 'user2', 'user3', 'user4'], + ], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataGetUsersForShares')] + public function testGetUsersForShares(array $shares, array $groups, array $expected): void { + $backend = $this->getBackend(); + + $getGroups = []; + foreach ($groups as $gid => $members) { + if ($members === null) { + $getGroups[] = [$gid, null]; + continue; + } + + $group = $this->createMock(IGroup::class); + $group->expects($this->once()) + ->method('getUsers') + ->willReturn($this->getUsers($members)); + + $getGroups[] = [$gid, $group]; + } + + $this->groupManager->expects($this->exactly(sizeof($getGroups))) + ->method('get') + ->willReturnMap($getGroups); + + $users = $this->invokePrivate($backend, 'getUsersForShares', [$shares]); + sort($users); + $this->assertEquals($expected, $users); + } + + /** + * @param string[] $users + * @return IUser[]&MockObject[] + */ + protected function getUsers(array $users) { + $list = []; + foreach ($users as $user) { + $list[] = $this->getUserMock($user); + } + return $list; + } + + /** + * @param string $uid + * @return IUser&MockObject + */ + protected function getUserMock($uid) { + $user = $this->createMock(IUser::class); + $user->expects($this->once()) + ->method('getUID') + ->willReturn($uid); + return $user; + } +} diff --git a/apps/dav/tests/unit/CalDAV/Activity/Filter/CalendarTest.php b/apps/dav/tests/unit/CalDAV/Activity/Filter/CalendarTest.php new file mode 100644 index 00000000000..b4c4e14fe7d --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Activity/Filter/CalendarTest.php @@ -0,0 +1,67 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\Activity\Filter; + +use OCA\DAV\CalDAV\Activity\Filter\Calendar; +use OCP\Activity\IFilter; +use OCP\IL10N; +use OCP\IURLGenerator; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class CalendarTest extends TestCase { + protected IURLGenerator&MockObject $url; + protected IFilter $filter; + + protected function setUp(): void { + parent::setUp(); + $this->url = $this->createMock(IURLGenerator::class); + $l = $this->createMock(IL10N::class); + $l->expects($this->any()) + ->method('t') + ->willReturnCallback(function ($string, $args) { + return vsprintf($string, $args); + }); + + $this->filter = new Calendar( + $l, $this->url + ); + } + + public function testGetIcon(): void { + $this->url->expects($this->once()) + ->method('imagePath') + ->with('core', 'places/calendar.svg') + ->willReturn('path-to-icon'); + + $this->url->expects($this->once()) + ->method('getAbsoluteURL') + ->with('path-to-icon') + ->willReturn('absolute-path-to-icon'); + + $this->assertEquals('absolute-path-to-icon', $this->filter->getIcon()); + } + + public static function dataFilterTypes(): array { + return [ + [[], []], + [['calendar', 'calendar_event'], ['calendar', 'calendar_event']], + [['calendar', 'calendar_event', 'calendar_todo'], ['calendar', 'calendar_event']], + [['calendar', 'calendar_event', 'files'], ['calendar', 'calendar_event']], + ]; + } + + /** + * @param string[] $types + * @param string[] $expected + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataFilterTypes')] + public function testFilterTypes(array $types, array $expected): void { + $this->assertEquals($expected, $this->filter->filterTypes($types)); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Activity/Filter/GenericTest.php b/apps/dav/tests/unit/CalDAV/Activity/Filter/GenericTest.php new file mode 100644 index 00000000000..87b55f14bcc --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Activity/Filter/GenericTest.php @@ -0,0 +1,78 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\Activity\Filter; + +use OCA\DAV\CalDAV\Activity\Filter\Calendar; +use OCA\DAV\CalDAV\Activity\Filter\Todo; +use OCP\Activity\IFilter; +use OCP\Server; +use Test\TestCase; + +/** + * @group DB + */ +class GenericTest extends TestCase { + public static function dataFilters(): array { + return [ + [Calendar::class], + [Todo::class], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataFilters')] + public function testImplementsInterface(string $filterClass): void { + $filter = Server::get($filterClass); + $this->assertInstanceOf(IFilter::class, $filter); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataFilters')] + public function testGetIdentifier(string $filterClass): void { + /** @var IFilter $filter */ + $filter = Server::get($filterClass); + $this->assertIsString($filter->getIdentifier()); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataFilters')] + public function testGetName(string $filterClass): void { + /** @var IFilter $filter */ + $filter = Server::get($filterClass); + $this->assertIsString($filter->getName()); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataFilters')] + public function testGetPriority(string $filterClass): void { + /** @var IFilter $filter */ + $filter = Server::get($filterClass); + $priority = $filter->getPriority(); + $this->assertIsInt($filter->getPriority()); + $this->assertGreaterThanOrEqual(0, $priority); + $this->assertLessThanOrEqual(100, $priority); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataFilters')] + public function testGetIcon(string $filterClass): void { + /** @var IFilter $filter */ + $filter = Server::get($filterClass); + $this->assertIsString($filter->getIcon()); + $this->assertStringStartsWith('http', $filter->getIcon()); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataFilters')] + public function testFilterTypes(string $filterClass): void { + /** @var IFilter $filter */ + $filter = Server::get($filterClass); + $this->assertIsArray($filter->filterTypes([])); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataFilters')] + public function testAllowedApps(string $filterClass): void { + /** @var IFilter $filter */ + $filter = Server::get($filterClass); + $this->assertIsArray($filter->allowedApps()); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Activity/Filter/TodoTest.php b/apps/dav/tests/unit/CalDAV/Activity/Filter/TodoTest.php new file mode 100644 index 00000000000..f18d66b9774 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Activity/Filter/TodoTest.php @@ -0,0 +1,67 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\Activity\Filter; + +use OCA\DAV\CalDAV\Activity\Filter\Todo; +use OCP\Activity\IFilter; +use OCP\IL10N; +use OCP\IURLGenerator; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class TodoTest extends TestCase { + protected IURLGenerator&MockObject $url; + protected IFilter $filter; + + protected function setUp(): void { + parent::setUp(); + $this->url = $this->createMock(IURLGenerator::class); + $l = $this->createMock(IL10N::class); + $l->expects($this->any()) + ->method('t') + ->willReturnCallback(function ($string, $args) { + return vsprintf($string, $args); + }); + + $this->filter = new Todo( + $l, $this->url + ); + } + + public function testGetIcon(): void { + $this->url->expects($this->once()) + ->method('imagePath') + ->with('core', 'actions/checkmark.svg') + ->willReturn('path-to-icon'); + + $this->url->expects($this->once()) + ->method('getAbsoluteURL') + ->with('path-to-icon') + ->willReturn('absolute-path-to-icon'); + + $this->assertEquals('absolute-path-to-icon', $this->filter->getIcon()); + } + + public static function dataFilterTypes(): array { + return [ + [[], []], + [['calendar_todo'], ['calendar_todo']], + [['calendar', 'calendar_event', 'calendar_todo'], ['calendar_todo']], + [['calendar', 'calendar_todo', 'files'], ['calendar_todo']], + ]; + } + + /** + * @param string[] $types + * @param string[] $expected + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataFilterTypes')] + public function testFilterTypes(array $types, array $expected): void { + $this->assertEquals($expected, $this->filter->filterTypes($types)); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Activity/Provider/BaseTest.php b/apps/dav/tests/unit/CalDAV/Activity/Provider/BaseTest.php new file mode 100644 index 00000000000..3e6219beef8 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Activity/Provider/BaseTest.php @@ -0,0 +1,116 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\Activity\Provider; + +use OCA\DAV\CalDAV\Activity\Provider\Base; +use OCP\Activity\IEvent; +use OCP\IGroupManager; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\IUserManager; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class BaseTest extends TestCase { + protected IUserManager&MockObject $userManager; + protected IGroupManager&MockObject $groupManager; + protected IURLGenerator&MockObject $url; + protected Base&MockObject $provider; + + protected function setUp(): void { + parent::setUp(); + $this->userManager = $this->createMock(IUserManager::class); + $this->groupManager = $this->createMock(IGroupManager::class); + $this->url = $this->createMock(IURLGenerator::class); + $this->provider = $this->getMockBuilder(Base::class) + ->setConstructorArgs([ + $this->userManager, + $this->groupManager, + $this->url, + ]) + ->onlyMethods(['parse']) + ->getMock(); + } + + public static function dataSetSubjects(): array { + return [ + ['abc', []], + ['{actor} created {calendar}', ['actor' => ['name' => 'abc'], 'calendar' => ['name' => 'xyz']]], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataSetSubjects')] + public function testSetSubjects(string $subject, array $parameters): void { + $event = $this->createMock(IEvent::class); + $event->expects($this->once()) + ->method('setRichSubject') + ->with($subject, $parameters) + ->willReturnSelf(); + $event->expects($this->never()) + ->method('setParsedSubject'); + + $this->invokePrivate($this->provider, 'setSubjects', [$event, $subject, $parameters]); + } + + public static function dataGenerateCalendarParameter(): array { + return [ + [['id' => 23, 'uri' => 'foo', 'name' => 'bar'], 'bar'], + [['id' => 42, 'uri' => 'foo', 'name' => 'Personal'], 'Personal'], + [['id' => 42, 'uri' => 'personal', 'name' => 'bar'], 'bar'], + [['id' => 42, 'uri' => 'personal', 'name' => 'Personal'], 't(Personal)'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataGenerateCalendarParameter')] + public function testGenerateCalendarParameter(array $data, string $name): void { + $l = $this->createMock(IL10N::class); + $l->expects($this->any()) + ->method('t') + ->willReturnCallback(function ($string, $args) { + return 't(' . vsprintf($string, $args) . ')'; + }); + + $this->assertEquals([ + 'type' => 'calendar', + 'id' => $data['id'], + 'name' => $name, + ], $this->invokePrivate($this->provider, 'generateCalendarParameter', [$data, $l])); + } + + public static function dataGenerateLegacyCalendarParameter(): array { + return [ + [23, 'c1'], + [42, 'c2'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataGenerateLegacyCalendarParameter')] + public function testGenerateLegacyCalendarParameter(int $id, string $name): void { + $this->assertEquals([ + 'type' => 'calendar', + 'id' => $id, + 'name' => $name, + ], $this->invokePrivate($this->provider, 'generateLegacyCalendarParameter', [$id, $name])); + } + + public static function dataGenerateGroupParameter(): array { + return [ + ['g1'], + ['g2'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataGenerateGroupParameter')] + public function testGenerateGroupParameter(string $gid): void { + $this->assertEquals([ + 'type' => 'user-group', + 'id' => $gid, + 'name' => $gid, + ], $this->invokePrivate($this->provider, 'generateGroupParameter', [$gid])); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Activity/Provider/EventTest.php b/apps/dav/tests/unit/CalDAV/Activity/Provider/EventTest.php new file mode 100644 index 00000000000..4fd38c1aed2 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Activity/Provider/EventTest.php @@ -0,0 +1,190 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\Activity\Provider; + +use InvalidArgumentException; +use OCA\DAV\CalDAV\Activity\Provider\Event; +use OCP\Activity\IEventMerger; +use OCP\Activity\IManager; +use OCP\App\IAppManager; +use OCP\IGroupManager; +use OCP\IURLGenerator; +use OCP\IUserManager; +use OCP\L10N\IFactory; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; +use TypeError; + +class EventTest extends TestCase { + protected IUserManager&MockObject $userManager; + protected IGroupManager&MockObject $groupManager; + protected IURLGenerator&MockObject $url; + protected IAppManager&MockObject $appManager; + protected IFactory&MockObject $i10nFactory; + protected IManager&MockObject $activityManager; + protected IEventMerger&MockObject $eventMerger; + protected Event&MockObject $provider; + + protected function setUp(): void { + parent::setUp(); + $this->i10nFactory = $this->createMock(IFactory::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->groupManager = $this->createMock(IGroupManager::class); + $this->activityManager = $this->createMock(IManager::class); + $this->url = $this->createMock(IURLGenerator::class); + $this->appManager = $this->createMock(IAppManager::class); + $this->eventMerger = $this->createMock(IEventMerger::class); + $this->provider = $this->getMockBuilder(Event::class) + ->setConstructorArgs([ + $this->i10nFactory, + $this->url, + $this->activityManager, + $this->userManager, + $this->groupManager, + $this->eventMerger, + $this->appManager + ]) + ->onlyMethods(['parse']) + ->getMock(); + } + + public static function dataGenerateObjectParameter(): array { + $link = [ + 'object_uri' => 'someuuid.ics', + 'calendar_uri' => 'personal', + 'owner' => 'someuser' + ]; + + return [ + [23, 'c1', $link, true], + [23, 'c1', $link, false], + [42, 'c2', null], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataGenerateObjectParameter')] + public function testGenerateObjectParameter(int $id, string $name, ?array $link, bool $calendarAppEnabled = true): void { + $affectedUser = 'otheruser'; + if ($link) { + $affectedUser = $link['owner']; + $generatedLink = [ + 'objectId' => base64_encode('/remote.php/dav/calendars/' . $link['owner'] . '/' . $link['calendar_uri'] . '/' . $link['object_uri']), + ]; + $this->appManager->expects($this->once()) + ->method('isEnabledForUser') + ->with('calendar') + ->willReturn($calendarAppEnabled); + if ($calendarAppEnabled) { + $this->url->expects($this->once()) + ->method('getWebroot'); + $this->url->expects($this->once()) + ->method('linkToRouteAbsolute') + ->with('calendar.view.indexdirect.edit', $generatedLink) + ->willReturn('fullLink'); + } + } + $objectParameter = ['id' => $id, 'name' => $name]; + if ($link) { + $objectParameter['link'] = $link; + } + $result = [ + 'type' => 'calendar-event', + 'id' => $id, + 'name' => $name, + ]; + if ($link && $calendarAppEnabled) { + $result['link'] = 'fullLink'; + } + $this->assertEquals($result, $this->invokePrivate($this->provider, 'generateObjectParameter', [$objectParameter, $affectedUser])); + } + + public static function generateObjectParameterLinkEncodingDataProvider(): array { + return [ + [ // Shared calendar + [ + 'object_uri' => 'someuuid.ics', + 'calendar_uri' => 'personal', + 'owner' => 'sharer' + ], + base64_encode('/remote.php/dav/calendars/sharee/personal_shared_by_sharer/someuuid.ics'), + ], + [ // Shared calendar with umlauts + [ + 'object_uri' => 'someuuid.ics', + 'calendar_uri' => 'umlaut_äüöß', + 'owner' => 'sharer' + ], + base64_encode('/remote.php/dav/calendars/sharee/umlaut_%c3%a4%c3%bc%c3%b6%c3%9f_shared_by_sharer/someuuid.ics'), + ], + [ // Shared calendar with umlauts and mixed casing + [ + 'object_uri' => 'someuuid.ics', + 'calendar_uri' => 'Umlaut_äüöß', + 'owner' => 'sharer' + ], + base64_encode('/remote.php/dav/calendars/sharee/Umlaut_%c3%a4%c3%bc%c3%b6%c3%9f_shared_by_sharer/someuuid.ics'), + ], + [ // Owned calendar with umlauts + [ + 'object_uri' => 'someuuid.ics', + 'calendar_uri' => 'umlaut_äüöß', + 'owner' => 'sharee' + ], + base64_encode('/remote.php/dav/calendars/sharee/umlaut_%c3%a4%c3%bc%c3%b6%c3%9f/someuuid.ics'), + ], + [ // Owned calendar with umlauts and mixed casing + [ + 'object_uri' => 'someuuid.ics', + 'calendar_uri' => 'Umlaut_äüöß', + 'owner' => 'sharee' + ], + base64_encode('/remote.php/dav/calendars/sharee/Umlaut_%c3%a4%c3%bc%c3%b6%c3%9f/someuuid.ics'), + ], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('generateObjectParameterLinkEncodingDataProvider')] + public function testGenerateObjectParameterLinkEncoding(array $link, string $objectId): void { + $generatedLink = [ + 'objectId' => $objectId, + ]; + $this->appManager->expects($this->once()) + ->method('isEnabledForUser') + ->with('calendar') + ->willReturn(true); + $this->url->expects($this->once()) + ->method('getWebroot'); + $this->url->expects($this->once()) + ->method('linkToRouteAbsolute') + ->with('calendar.view.indexdirect.edit', $generatedLink) + ->willReturn('fullLink'); + $objectParameter = ['id' => 42, 'name' => 'calendar', 'link' => $link]; + $result = [ + 'type' => 'calendar-event', + 'id' => 42, + 'name' => 'calendar', + 'link' => 'fullLink', + ]; + $this->assertEquals($result, $this->invokePrivate($this->provider, 'generateObjectParameter', [$objectParameter, 'sharee'])); + } + + public static function dataGenerateObjectParameterThrows(): array { + return [ + ['event', TypeError::class], + [['name' => 'event']], + [['id' => 42]], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataGenerateObjectParameterThrows')] + public function testGenerateObjectParameterThrows(string|array $eventData, string $exception = InvalidArgumentException::class): void { + $this->expectException($exception); + + $this->invokePrivate($this->provider, 'generateObjectParameter', [$eventData, 'no_user']); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Activity/Setting/GenericTest.php b/apps/dav/tests/unit/CalDAV/Activity/Setting/GenericTest.php new file mode 100644 index 00000000000..23126b6bbcf --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Activity/Setting/GenericTest.php @@ -0,0 +1,83 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\Activity\Setting; + +use OCA\DAV\CalDAV\Activity\Setting\Calendar; +use OCA\DAV\CalDAV\Activity\Setting\Event; +use OCA\DAV\CalDAV\Activity\Setting\Todo; +use OCP\Activity\ISetting; +use OCP\Server; +use Test\TestCase; + +class GenericTest extends TestCase { + public static function dataSettings(): array { + return [ + [Calendar::class], + [Event::class], + [Todo::class], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataSettings')] + public function testImplementsInterface(string $settingClass): void { + $setting = Server::get($settingClass); + $this->assertInstanceOf(ISetting::class, $setting); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataSettings')] + public function testGetIdentifier(string $settingClass): void { + /** @var ISetting $setting */ + $setting = Server::get($settingClass); + $this->assertIsString($setting->getIdentifier()); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataSettings')] + public function testGetName(string $settingClass): void { + /** @var ISetting $setting */ + $setting = Server::get($settingClass); + $this->assertIsString($setting->getName()); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataSettings')] + public function testGetPriority(string $settingClass): void { + /** @var ISetting $setting */ + $setting = Server::get($settingClass); + $priority = $setting->getPriority(); + $this->assertIsInt($setting->getPriority()); + $this->assertGreaterThanOrEqual(0, $priority); + $this->assertLessThanOrEqual(100, $priority); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataSettings')] + public function testCanChangeStream(string $settingClass): void { + /** @var ISetting $setting */ + $setting = Server::get($settingClass); + $this->assertIsBool($setting->canChangeStream()); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataSettings')] + public function testIsDefaultEnabledStream(string $settingClass): void { + /** @var ISetting $setting */ + $setting = Server::get($settingClass); + $this->assertIsBool($setting->isDefaultEnabledStream()); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataSettings')] + public function testCanChangeMail(string $settingClass): void { + /** @var ISetting $setting */ + $setting = Server::get($settingClass); + $this->assertIsBool($setting->canChangeMail()); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataSettings')] + public function testIsDefaultEnabledMail(string $settingClass): void { + /** @var ISetting $setting */ + $setting = Server::get($settingClass); + $this->assertIsBool($setting->isDefaultEnabledMail()); + } +} diff --git a/apps/dav/tests/unit/CalDAV/AppCalendar/AppCalendarTest.php b/apps/dav/tests/unit/CalDAV/AppCalendar/AppCalendarTest.php new file mode 100644 index 00000000000..84879e87238 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/AppCalendar/AppCalendarTest.php @@ -0,0 +1,120 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\AppCalendar; + +use OCA\DAV\CalDAV\AppCalendar\AppCalendar; +use OCP\Calendar\ICalendar; +use OCP\Calendar\ICreateFromString; +use OCP\Constants; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +use function rewind; + +class AppCalendarTest extends TestCase { + private string $principal = 'principals/users/foo'; + + private AppCalendar $appCalendar; + private AppCalendar $writeableAppCalendar; + + private ICalendar&MockObject $calendar; + private ICalendar&MockObject $writeableCalendar; + + protected function setUp(): void { + parent::setUp(); + + $this->calendar = $this->getMockBuilder(ICalendar::class)->getMock(); + $this->calendar->method('getPermissions') + ->willReturn(Constants::PERMISSION_READ); + + $this->writeableCalendar = $this->getMockBuilder(ICreateFromString::class)->getMock(); + $this->writeableCalendar->method('getPermissions') + ->willReturn(Constants::PERMISSION_READ | Constants::PERMISSION_CREATE); + + $this->appCalendar = new AppCalendar('dav-wrapper', $this->calendar, $this->principal); + $this->writeableAppCalendar = new AppCalendar('dav-wrapper', $this->writeableCalendar, $this->principal); + } + + public function testGetPrincipal():void { + // Check that the correct name is returned + $this->assertEquals($this->principal, $this->appCalendar->getOwner()); + $this->assertEquals($this->principal, $this->writeableAppCalendar->getOwner()); + } + + public function testDelete(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + $this->expectExceptionMessage('Deleting an entry is not implemented'); + + $this->appCalendar->delete(); + } + + public function testCreateFile(): void { + $calls = [ + ['some-name', 'data'], + ['other-name', ''], + ['name', 'some data'], + ]; + $this->writeableCalendar->expects($this->exactly(3)) + ->method('createFromString') + ->willReturnCallback(function () use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + }); + + // pass data + $this->assertNull($this->writeableAppCalendar->createFile('some-name', 'data')); + // null is empty string + $this->assertNull($this->writeableAppCalendar->createFile('other-name', null)); + // resource to data + $fp = fopen('php://memory', 'r+'); + fwrite($fp, 'some data'); + rewind($fp); + $this->assertNull($this->writeableAppCalendar->createFile('name', $fp)); + fclose($fp); + } + + public function testCreateFile_readOnly(): void { + // If writing is not supported + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + $this->expectExceptionMessage('Creating a new entry is not allowed'); + + $this->appCalendar->createFile('some-name', 'data'); + } + + public function testSetACL(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + $this->expectExceptionMessage('Setting ACL is not supported on this node'); + + $this->appCalendar->setACL([]); + } + + public function testGetACL():void { + $expectedRO = [ + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->principal, + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write-properties', + 'principal' => $this->principal, + 'protected' => true, + ] + ]; + $expectedRW = $expectedRO; + $expectedRW[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->principal, + 'protected' => true, + ]; + + // Check that the correct ACL is returned (default be only readable) + $this->assertEquals($expectedRO, $this->appCalendar->getACL()); + $this->assertEquals($expectedRW, $this->writeableAppCalendar->getACL()); + } +} diff --git a/apps/dav/tests/unit/CalDAV/AppCalendar/CalendarObjectTest.php b/apps/dav/tests/unit/CalDAV/AppCalendar/CalendarObjectTest.php new file mode 100644 index 00000000000..3d72d5c97b8 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/AppCalendar/CalendarObjectTest.php @@ -0,0 +1,170 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\AppCalendar; + +use OCA\DAV\CalDAV\AppCalendar\AppCalendar; +use OCA\DAV\CalDAV\AppCalendar\CalendarObject; +use OCP\Calendar\ICalendar; +use OCP\Calendar\ICreateFromString; +use OCP\Constants; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\VObject\Component\VCalendar; +use Test\TestCase; + +class CalendarObjectTest extends TestCase { + private CalendarObject $calendarObject; + private AppCalendar&MockObject $calendar; + private ICalendar&MockObject $backend; + private VCalendar&MockObject $vobject; + + protected function setUp(): void { + parent::setUp(); + + $this->calendar = $this->createMock(AppCalendar::class); + $this->calendar->method('getOwner')->willReturn('owner'); + $this->calendar->method('getGroup')->willReturn('group'); + + $this->backend = $this->createMock(ICalendar::class); + $this->vobject = $this->createMock(VCalendar::class); + $this->calendarObject = new CalendarObject($this->calendar, $this->backend, $this->vobject); + } + + public function testGetOwner(): void { + $this->assertEquals($this->calendarObject->getOwner(), 'owner'); + } + + public function testGetGroup(): void { + $this->assertEquals($this->calendarObject->getGroup(), 'group'); + } + + public function testGetACL(): void { + $this->calendar->expects($this->exactly(2)) + ->method('getPermissions') + ->willReturnOnConsecutiveCalls(Constants::PERMISSION_READ, Constants::PERMISSION_ALL); + + // read only + $this->assertEquals($this->calendarObject->getACL(), [ + [ + 'privilege' => '{DAV:}read', + 'principal' => 'owner', + 'protected' => true, + ] + ]); + + // write permissions + $this->assertEquals($this->calendarObject->getACL(), [ + [ + 'privilege' => '{DAV:}read', + 'principal' => 'owner', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write-content', + 'principal' => 'owner', + 'protected' => true, + ] + ]); + } + + public function testSetACL(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + $this->calendarObject->setACL([]); + } + + public function testPut_readOnlyBackend(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + $this->calendarObject->put('foo'); + } + + public function testPut_noPermissions(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + $backend = $this->createMock(ICreateFromString::class); + $calendarObject = new CalendarObject($this->calendar, $backend, $this->vobject); + + $this->calendar->expects($this->once()) + ->method('getPermissions') + ->willReturn(Constants::PERMISSION_READ); + + $calendarObject->put('foo'); + } + + public function testPut(): void { + $backend = $this->createMock(ICreateFromString::class); + $calendarObject = new CalendarObject($this->calendar, $backend, $this->vobject); + + $this->vobject->expects($this->once()) + ->method('getBaseComponent') + ->willReturn((object)['UID' => 'someid']); + $this->calendar->expects($this->once()) + ->method('getPermissions') + ->willReturn(Constants::PERMISSION_ALL); + + $backend->expects($this->once()) + ->method('createFromString') + ->with('someid.ics', 'foo'); + $calendarObject->put('foo'); + } + + public function testGet(): void { + $this->vobject->expects($this->once()) + ->method('serialize') + ->willReturn('foo'); + $this->assertEquals($this->calendarObject->get(), 'foo'); + } + + public function testDelete_notWriteable(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + $this->calendarObject->delete(); + } + + public function testDelete_noPermission(): void { + $backend = $this->createMock(ICreateFromString::class); + $calendarObject = new CalendarObject($this->calendar, $backend, $this->vobject); + + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + $calendarObject->delete(); + } + + public function testDelete(): void { + $backend = $this->createMock(ICreateFromString::class); + $calendarObject = new CalendarObject($this->calendar, $backend, $this->vobject); + + $components = [(new VCalendar(['VEVENT' => ['UID' => 'someid']]))->getBaseComponent()]; + + $this->calendar->expects($this->once()) + ->method('getPermissions') + ->willReturn(Constants::PERMISSION_DELETE); + $this->vobject->expects($this->once()) + ->method('getBaseComponents') + ->willReturn($components); + $this->vobject->expects($this->once()) + ->method('getBaseComponent') + ->willReturn($components[0]); + + $backend->expects($this->once()) + ->method('createFromString') + ->with('someid.ics', self::callback(fn ($data): bool => preg_match('/BEGIN:VEVENT(.|\r\n)+STATUS:CANCELLED/', $data) === 1)); + + $calendarObject->delete(); + } + + public function testGetName(): void { + $this->vobject->expects($this->exactly(2)) + ->method('getBaseComponent') + ->willReturnOnConsecutiveCalls((object)['UID' => 'someid'], (object)['UID' => 'someid', 'X-FILENAME' => 'real-filename.ics']); + + $this->assertEquals($this->calendarObject->getName(), 'someid.ics'); + $this->assertEquals($this->calendarObject->getName(), 'real-filename.ics'); + } + + public function testSetName(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + $this->calendarObject->setName('Some name'); + } +} diff --git a/apps/dav/tests/unit/CalDAV/BirthdayCalendar/EnablePluginTest.php b/apps/dav/tests/unit/CalDAV/BirthdayCalendar/EnablePluginTest.php new file mode 100644 index 00000000000..a5811271ce2 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/BirthdayCalendar/EnablePluginTest.php @@ -0,0 +1,221 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\BirthdayCalendar; + +use OCA\DAV\CalDAV\BirthdayCalendar\EnablePlugin; +use OCA\DAV\CalDAV\BirthdayService; +use OCA\DAV\CalDAV\Calendar; +use OCA\DAV\CalDAV\CalendarHome; +use OCP\IConfig; +use OCP\IUser; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class EnablePluginTest extends TestCase { + protected \Sabre\DAV\Server&MockObject $server; + protected IConfig&MockObject $config; + protected BirthdayService&MockObject $birthdayService; + protected IUser&MockObject $user; + protected EnablePlugin $plugin; + + protected $request; + + protected $response; + + protected function setUp(): void { + parent::setUp(); + + $this->server = $this->createMock(\Sabre\DAV\Server::class); + $this->server->tree = $this->createMock(\Sabre\DAV\Tree::class); + $this->server->httpResponse = $this->createMock(\Sabre\HTTP\Response::class); + $this->server->xml = $this->createMock(\Sabre\DAV\Xml\Service::class); + + $this->config = $this->createMock(IConfig::class); + $this->birthdayService = $this->createMock(BirthdayService::class); + $this->user = $this->createMock(IUser::class); + + $this->plugin = new EnablePlugin($this->config, $this->birthdayService, $this->user); + $this->plugin->initialize($this->server); + + $this->request = $this->createMock(\Sabre\HTTP\RequestInterface::class); + $this->response = $this->createMock(\Sabre\HTTP\ResponseInterface::class); + } + + public function testGetFeatures(): void { + $this->assertEquals(['nc-enable-birthday-calendar'], $this->plugin->getFeatures()); + } + + public function testGetName(): void { + $this->assertEquals('nc-enable-birthday-calendar', $this->plugin->getPluginName()); + } + + public function testInitialize(): void { + $server = $this->createMock(\Sabre\DAV\Server::class); + + $plugin = new EnablePlugin($this->config, $this->birthdayService, $this->user); + + $server->expects($this->once()) + ->method('on') + ->with('method:POST', [$plugin, 'httpPost']); + + $plugin->initialize($server); + } + + public function testHttpPostNoCalendarHome(): void { + $calendar = $this->createMock(Calendar::class); + + $this->server->expects($this->once()) + ->method('getRequestUri') + ->willReturn('/bar/foo'); + $this->server->tree->expects($this->once()) + ->method('getNodeForPath') + ->with('/bar/foo') + ->willReturn($calendar); + + $this->config->expects($this->never()) + ->method('setUserValue'); + + $this->birthdayService->expects($this->never()) + ->method('syncUser'); + + $this->plugin->httpPost($this->request, $this->response); + } + + public function testHttpPostWrongRequest(): void { + $calendarHome = $this->createMock(CalendarHome::class); + + $this->server->expects($this->once()) + ->method('getRequestUri') + ->willReturn('/bar/foo'); + $this->server->tree->expects($this->once()) + ->method('getNodeForPath') + ->with('/bar/foo') + ->willReturn($calendarHome); + + $this->request->expects($this->once()) + ->method('getBodyAsString') + ->willReturn('<nc:disable-birthday-calendar xmlns:nc="http://nextcloud.com/ns"/>'); + + $this->request->expects($this->once()) + ->method('getUrl') + ->willReturn('url_abc'); + + $this->server->xml->expects($this->once()) + ->method('parse') + ->willReturnCallback(function ($requestBody, $url, &$documentType): void { + $documentType = '{http://nextcloud.com/ns}disable-birthday-calendar'; + }); + + $this->config->expects($this->never()) + ->method('setUserValue'); + + $this->birthdayService->expects($this->never()) + ->method('syncUser'); + + $this->plugin->httpPost($this->request, $this->response); + } + + public function testHttpPostNotAuthorized(): void { + $calendarHome = $this->createMock(CalendarHome::class); + + $this->server->expects($this->once()) + ->method('getRequestUri') + ->willReturn('/bar/foo'); + $this->server->tree->expects($this->once()) + ->method('getNodeForPath') + ->with('/bar/foo') + ->willReturn($calendarHome); + + $calendarHome->expects($this->once()) + ->method('getOwner') + ->willReturn('principals/users/BlaBlub'); + + $this->request->expects($this->once()) + ->method('getBodyAsString') + ->willReturn('<nc:enable-birthday-calendar xmlns:nc="http://nextcloud.com/ns"/>'); + + $this->request->expects($this->once()) + ->method('getUrl') + ->willReturn('url_abc'); + + $this->server->xml->expects($this->once()) + ->method('parse') + ->willReturnCallback(function ($requestBody, $url, &$documentType): void { + $documentType = '{http://nextcloud.com/ns}enable-birthday-calendar'; + }); + + $this->user->expects(self::once()) + ->method('getUID') + ->willReturn('admin'); + + $this->server->httpResponse->expects($this->once()) + ->method('setStatus') + ->with(403); + + $this->config->expects($this->never()) + ->method('setUserValue'); + + $this->birthdayService->expects($this->never()) + ->method('syncUser'); + + + $result = $this->plugin->httpPost($this->request, $this->response); + + $this->assertEquals(false, $result); + } + + public function testHttpPost(): void { + $calendarHome = $this->createMock(CalendarHome::class); + + $this->server->expects($this->once()) + ->method('getRequestUri') + ->willReturn('/bar/foo'); + $this->server->tree->expects($this->once()) + ->method('getNodeForPath') + ->with('/bar/foo') + ->willReturn($calendarHome); + + $calendarHome->expects($this->once()) + ->method('getOwner') + ->willReturn('principals/users/BlaBlub'); + + $this->request->expects($this->once()) + ->method('getBodyAsString') + ->willReturn('<nc:enable-birthday-calendar xmlns:nc="http://nextcloud.com/ns"/>'); + + $this->request->expects($this->once()) + ->method('getUrl') + ->willReturn('url_abc'); + + $this->server->xml->expects($this->once()) + ->method('parse') + ->willReturnCallback(function ($requestBody, $url, &$documentType): void { + $documentType = '{http://nextcloud.com/ns}enable-birthday-calendar'; + }); + + $this->user->expects(self::exactly(3)) + ->method('getUID') + ->willReturn('BlaBlub'); + + $this->config->expects($this->once()) + ->method('setUserValue') + ->with('BlaBlub', 'dav', 'generateBirthdayCalendar', 'yes'); + + $this->birthdayService->expects($this->once()) + ->method('syncUser') + ->with('BlaBlub'); + + $this->server->httpResponse->expects($this->once()) + ->method('setStatus') + ->with(204); + + $result = $this->plugin->httpPost($this->request, $this->response); + + $this->assertEquals(false, $result); + } +} diff --git a/apps/dav/tests/unit/CalDAV/CachedSubscriptionImplTest.php b/apps/dav/tests/unit/CalDAV/CachedSubscriptionImplTest.php new file mode 100644 index 00000000000..935d8314f29 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/CachedSubscriptionImplTest.php @@ -0,0 +1,80 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Tests\unit\CalDAV; + +use OCA\DAV\CalDAV\CachedSubscription; +use OCA\DAV\CalDAV\CachedSubscriptionImpl; +use OCA\DAV\CalDAV\CalDavBackend; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class CachedSubscriptionImplTest extends TestCase { + private CachedSubscription&MockObject $cachedSubscription; + private array $cachedSubscriptionInfo; + private CalDavBackend&MockObject $backend; + private CachedSubscriptionImpl $cachedSubscriptionImpl; + + protected function setUp(): void { + parent::setUp(); + + $this->cachedSubscription = $this->createMock(CachedSubscription::class); + $this->cachedSubscriptionInfo = [ + 'id' => 'fancy_id_123', + '{DAV:}displayname' => 'user readable name 123', + '{http://apple.com/ns/ical/}calendar-color' => '#AABBCC', + 'uri' => '/this/is/a/uri', + 'source' => 'https://test.localhost/calendar1', + ]; + $this->backend = $this->createMock(CalDavBackend::class); + + $this->cachedSubscriptionImpl = new CachedSubscriptionImpl( + $this->cachedSubscription, + $this->cachedSubscriptionInfo, + $this->backend + ); + } + + public function testGetKey(): void { + $this->assertEquals($this->cachedSubscriptionImpl->getKey(), 'fancy_id_123'); + } + + public function testGetDisplayname(): void { + $this->assertEquals($this->cachedSubscriptionImpl->getDisplayName(), 'user readable name 123'); + } + + public function testGetDisplayColor(): void { + $this->assertEquals($this->cachedSubscriptionImpl->getDisplayColor(), '#AABBCC'); + } + + public function testGetSource(): void { + $this->assertEquals($this->cachedSubscriptionImpl->getSource(), 'https://test.localhost/calendar1'); + } + + public function testSearch(): void { + $this->backend->expects($this->once()) + ->method('search') + ->with($this->cachedSubscriptionInfo, 'abc', ['def'], ['ghi'], 42, 1337) + ->willReturn(['SEARCHRESULTS']); + + $result = $this->cachedSubscriptionImpl->search('abc', ['def'], ['ghi'], 42, 1337); + $this->assertEquals($result, ['SEARCHRESULTS']); + } + + public function testGetPermissionRead(): void { + $this->cachedSubscription->expects($this->once()) + ->method('getACL') + ->with() + ->willReturn([ + ['privilege' => '{DAV:}read'] + ]); + + $this->assertEquals(1, $this->cachedSubscriptionImpl->getPermissions()); + } +} diff --git a/apps/dav/tests/unit/CalDAV/CachedSubscriptionObjectTest.php b/apps/dav/tests/unit/CalDAV/CachedSubscriptionObjectTest.php new file mode 100644 index 00000000000..03a2c9f20ee --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/CachedSubscriptionObjectTest.php @@ -0,0 +1,76 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV; + +use OCA\DAV\CalDAV\CachedSubscriptionObject; +use OCA\DAV\CalDAV\CalDavBackend; + +class CachedSubscriptionObjectTest extends \Test\TestCase { + public function testGet(): void { + $backend = $this->createMock(CalDavBackend::class); + $calendarInfo = [ + '{http://owncloud.org/ns}owner-principal' => 'user1', + 'principaluri' => 'user2', + 'id' => 666, + 'uri' => 'cal', + ]; + $objectData = [ + 'uri' => 'foo123' + ]; + + $backend->expects($this->once()) + ->method('getCalendarObject') + ->with(666, 'foo123', 1) + ->willReturn([ + 'calendardata' => 'BEGIN...', + ]); + + $calendarObject = new CachedSubscriptionObject($backend, $calendarInfo, $objectData); + $this->assertEquals('BEGIN...', $calendarObject->get()); + } + + + public function testPut(): void { + $this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class); + $this->expectExceptionMessage('Creating objects in a cached subscription is not allowed'); + + $backend = $this->createMock(CalDavBackend::class); + $calendarInfo = [ + '{http://owncloud.org/ns}owner-principal' => 'user1', + 'principaluri' => 'user2', + 'id' => 666, + 'uri' => 'cal', + ]; + $objectData = [ + 'uri' => 'foo123' + ]; + + $calendarObject = new CachedSubscriptionObject($backend, $calendarInfo, $objectData); + $calendarObject->put(''); + } + + + public function testDelete(): void { + $this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class); + $this->expectExceptionMessage('Deleting objects in a cached subscription is not allowed'); + + $backend = $this->createMock(CalDavBackend::class); + $calendarInfo = [ + '{http://owncloud.org/ns}owner-principal' => 'user1', + 'principaluri' => 'user2', + 'id' => 666, + 'uri' => 'cal', + ]; + $objectData = [ + 'uri' => 'foo123' + ]; + + $calendarObject = new CachedSubscriptionObject($backend, $calendarInfo, $objectData); + $calendarObject->delete(); + } +} diff --git a/apps/dav/tests/unit/CalDAV/CachedSubscriptionProviderTest.php b/apps/dav/tests/unit/CalDAV/CachedSubscriptionProviderTest.php new file mode 100644 index 00000000000..58d5ca7835c --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/CachedSubscriptionProviderTest.php @@ -0,0 +1,72 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Tests\unit\CalDAV; + +use OCA\DAV\CalDAV\CachedSubscriptionImpl; +use OCA\DAV\CalDAV\CachedSubscriptionProvider; +use OCA\DAV\CalDAV\CalDavBackend; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class CachedSubscriptionProviderTest extends TestCase { + + private CalDavBackend&MockObject $backend; + private CachedSubscriptionProvider $provider; + + protected function setUp(): void { + parent::setUp(); + + $this->backend = $this->createMock(CalDavBackend::class); + $this->backend + ->expects(self::once()) + ->method('getSubscriptionsForUser') + ->with('user-principal-123') + ->willReturn([ + [ + 'id' => 'subscription-1', + 'uri' => 'subscription-1', + 'principaluris' => 'user-principal-123', + 'source' => 'https://localhost/subscription-1', + // A subscription array has actually more properties. + ], + [ + 'id' => 'subscription-2', + 'uri' => 'subscription-2', + 'principaluri' => 'user-principal-123', + 'source' => 'https://localhost/subscription-2', + // A subscription array has actually more properties. + ] + ]); + + $this->provider = new CachedSubscriptionProvider($this->backend); + } + + public function testGetCalendars(): void { + $calendars = $this->provider->getCalendars( + 'user-principal-123', + [] + ); + + $this->assertCount(2, $calendars); + $this->assertInstanceOf(CachedSubscriptionImpl::class, $calendars[0]); + $this->assertInstanceOf(CachedSubscriptionImpl::class, $calendars[1]); + } + + public function testGetCalendarsFilterByUri(): void { + $calendars = $this->provider->getCalendars( + 'user-principal-123', + ['subscription-1'] + ); + + $this->assertCount(1, $calendars); + $this->assertInstanceOf(CachedSubscriptionImpl::class, $calendars[0]); + $this->assertEquals('subscription-1', $calendars[0]->getUri()); + } +} diff --git a/apps/dav/tests/unit/CalDAV/CachedSubscriptionTest.php b/apps/dav/tests/unit/CalDAV/CachedSubscriptionTest.php new file mode 100644 index 00000000000..ba0da422290 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/CachedSubscriptionTest.php @@ -0,0 +1,296 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV; + +use OCA\DAV\CalDAV\CachedSubscription; +use OCA\DAV\CalDAV\CachedSubscriptionObject; +use OCA\DAV\CalDAV\CalDavBackend; +use Sabre\DAV\PropPatch; + +class CachedSubscriptionTest extends \Test\TestCase { + public function testGetACL(): void { + $backend = $this->createMock(CalDavBackend::class); + $calendarInfo = [ + '{http://owncloud.org/ns}owner-principal' => 'user1', + 'principaluri' => 'user2', + 'id' => 666, + 'uri' => 'cal', + ]; + + $calendar = new CachedSubscription($backend, $calendarInfo); + $this->assertEquals([ + [ + 'privilege' => '{DAV:}read', + 'principal' => 'user1', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => 'user1/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => 'user1/calendar-proxy-read', + 'protected' => true, + ], + [ + 'privilege' => '{urn:ietf:params:xml:ns:caldav}read-free-busy', + 'principal' => '{DAV:}authenticated', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write-properties', + 'principal' => 'user1', + 'protected' => 'true' + ] + ], $calendar->getACL()); + } + + public function testGetChildACL(): void { + $backend = $this->createMock(CalDavBackend::class); + $calendarInfo = [ + '{http://owncloud.org/ns}owner-principal' => 'user1', + 'principaluri' => 'user2', + 'id' => 666, + 'uri' => 'cal', + ]; + + $calendar = new CachedSubscription($backend, $calendarInfo); + $this->assertEquals([ + [ + 'privilege' => '{DAV:}read', + 'principal' => 'user1', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => 'user1/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => 'user1/calendar-proxy-read', + 'protected' => true, + ] + ], $calendar->getChildACL()); + } + + public function testGetOwner(): void { + $backend = $this->createMock(CalDavBackend::class); + $calendarInfo = [ + '{http://owncloud.org/ns}owner-principal' => 'user1', + 'principaluri' => 'user2', + 'id' => 666, + 'uri' => 'cal', + ]; + + $calendar = new CachedSubscription($backend, $calendarInfo); + $this->assertEquals('user1', $calendar->getOwner()); + } + + public function testDelete(): void { + $backend = $this->createMock(CalDavBackend::class); + $calendarInfo = [ + '{http://owncloud.org/ns}owner-principal' => 'user1', + 'principaluri' => 'user2', + 'id' => 666, + 'uri' => 'cal', + ]; + + $backend->expects($this->once()) + ->method('deleteSubscription') + ->with(666); + + $calendar = new CachedSubscription($backend, $calendarInfo); + $calendar->delete(); + } + + public function testPropPatch(): void { + $backend = $this->createMock(CalDavBackend::class); + $calendarInfo = [ + '{http://owncloud.org/ns}owner-principal' => 'user1', + 'principaluri' => 'user2', + 'id' => 666, + 'uri' => 'cal', + ]; + $propPatch = $this->createMock(PropPatch::class); + + $backend->expects($this->once()) + ->method('updateSubscription') + ->with(666, $propPatch); + + $calendar = new CachedSubscription($backend, $calendarInfo); + $calendar->propPatch($propPatch); + } + + + public function testGetChild(): void { + $this->expectException(\Sabre\DAV\Exception\NotFound::class); + $this->expectExceptionMessage('Calendar object not found'); + + $backend = $this->createMock(CalDavBackend::class); + $calendarInfo = [ + '{http://owncloud.org/ns}owner-principal' => 'user1', + 'principaluri' => 'user2', + 'id' => 666, + 'uri' => 'cal', + ]; + + $calls = [ + [666, 'foo1', 1, [ + 'id' => 99, + 'uri' => 'foo1' + ]], + [666, 'foo2', 1, null], + ]; + $backend->expects($this->exactly(2)) + ->method('getCalendarObject') + ->willReturnCallback(function () use (&$calls) { + $expected = array_shift($calls); + $return = array_pop($expected); + $this->assertEquals($expected, func_get_args()); + return $return; + }); + + $calendar = new CachedSubscription($backend, $calendarInfo); + + $first = $calendar->getChild('foo1'); + $this->assertInstanceOf(CachedSubscriptionObject::class, $first); + + $calendar->getChild('foo2'); + } + + public function testGetChildren(): void { + $backend = $this->createMock(CalDavBackend::class); + $calendarInfo = [ + '{http://owncloud.org/ns}owner-principal' => 'user1', + 'principaluri' => 'user2', + 'id' => 666, + 'uri' => 'cal', + ]; + + $backend->expects($this->once()) + ->method('getCalendarObjects') + ->with(666, 1) + ->willReturn([ + [ + 'id' => 99, + 'uri' => 'foo1' + ], + [ + 'id' => 100, + 'uri' => 'foo2' + ], + ]); + + $calendar = new CachedSubscription($backend, $calendarInfo); + + $res = $calendar->getChildren(); + $this->assertCount(2, $res); + $this->assertInstanceOf(CachedSubscriptionObject::class, $res[0]); + $this->assertInstanceOf(CachedSubscriptionObject::class, $res[1]); + } + + public function testGetMultipleChildren(): void { + $backend = $this->createMock(CalDavBackend::class); + $calendarInfo = [ + '{http://owncloud.org/ns}owner-principal' => 'user1', + 'principaluri' => 'user2', + 'id' => 666, + 'uri' => 'cal', + ]; + + $backend->expects($this->once()) + ->method('getMultipleCalendarObjects') + ->with(666, ['foo1', 'foo2'], 1) + ->willReturn([ + [ + 'id' => 99, + 'uri' => 'foo1' + ], + [ + 'id' => 100, + 'uri' => 'foo2' + ], + ]); + + $calendar = new CachedSubscription($backend, $calendarInfo); + + $res = $calendar->getMultipleChildren(['foo1', 'foo2']); + $this->assertCount(2, $res); + $this->assertInstanceOf(CachedSubscriptionObject::class, $res[0]); + $this->assertInstanceOf(CachedSubscriptionObject::class, $res[1]); + } + + + public function testCreateFile(): void { + $this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class); + $this->expectExceptionMessage('Creating objects in cached subscription is not allowed'); + + $backend = $this->createMock(CalDavBackend::class); + $calendarInfo = [ + '{http://owncloud.org/ns}owner-principal' => 'user1', + 'principaluri' => 'user2', + 'id' => 666, + 'uri' => 'cal', + ]; + + $calendar = new CachedSubscription($backend, $calendarInfo); + $calendar->createFile('foo', []); + } + + public function testChildExists(): void { + $backend = $this->createMock(CalDavBackend::class); + $calendarInfo = [ + '{http://owncloud.org/ns}owner-principal' => 'user1', + 'principaluri' => 'user2', + 'id' => 666, + 'uri' => 'cal', + ]; + + $calls = [ + [666, 'foo1', 1, [ + 'id' => 99, + 'uri' => 'foo1' + ]], + [666, 'foo2', 1, null], + ]; + $backend->expects($this->exactly(2)) + ->method('getCalendarObject') + ->willReturnCallback(function () use (&$calls) { + $expected = array_shift($calls); + $return = array_pop($expected); + $this->assertEquals($expected, func_get_args()); + return $return; + }); + + $calendar = new CachedSubscription($backend, $calendarInfo); + + $this->assertEquals(true, $calendar->childExists('foo1')); + $this->assertEquals(false, $calendar->childExists('foo2')); + } + + public function testCalendarQuery(): void { + $backend = $this->createMock(CalDavBackend::class); + $calendarInfo = [ + '{http://owncloud.org/ns}owner-principal' => 'user1', + 'principaluri' => 'user2', + 'id' => 666, + 'uri' => 'cal', + ]; + + $backend->expects($this->once()) + ->method('calendarQuery') + ->with(666, ['foo'], 1) + ->willReturn([99]); + + $calendar = new CachedSubscription($backend, $calendarInfo); + + $this->assertEquals([99], $calendar->calendarQuery(['foo'])); + } +} diff --git a/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php b/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php new file mode 100644 index 00000000000..f9205d5d322 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php @@ -0,0 +1,1885 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace OCA\DAV\Tests\unit\CalDAV; + +use DateInterval; +use DateTime; +use DateTimeImmutable; +use DateTimeZone; +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CalDAV\Calendar; +use OCA\DAV\DAV\Sharing\Plugin as SharingPlugin; +use OCA\DAV\Events\CalendarDeletedEvent; +use OCP\IConfig; +use OCP\IL10N; +use Psr\Log\NullLogger; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\PropPatch; +use Sabre\DAV\Xml\Property\Href; +use Sabre\DAVACL\IACL; +use function time; + +/** + * Class CalDavBackendTest + * + * @group DB + */ +class CalDavBackendTest extends AbstractCalDavBackend { + public function testCalendarOperations(): void { + $calendarId = $this->createTestCalendar(); + + // update its display name + $patch = new PropPatch([ + '{DAV:}displayname' => 'Unit test', + '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'Calendar used for unit testing' + ]); + $this->dispatcher->expects(self::atLeastOnce()) + ->method('dispatchTyped'); + $this->backend->updateCalendar($calendarId, $patch); + $patch->commit(); + $this->assertEquals(1, $this->backend->getCalendarsForUserCount(self::UNIT_TEST_USER)); + $calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER); + $this->assertCount(1, $calendars); + $this->assertEquals('Unit test', $calendars[0]['{DAV:}displayname']); + $this->assertEquals('Calendar used for unit testing', $calendars[0]['{urn:ietf:params:xml:ns:caldav}calendar-description']); + $this->assertEquals('User\'s displayname', $calendars[0]['{http://nextcloud.com/ns}owner-displayname']); + + // delete the address book + $this->dispatcher->expects(self::atLeastOnce()) + ->method('dispatchTyped'); + $this->backend->deleteCalendar($calendars[0]['id'], true); + $calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER); + self::assertEmpty($calendars); + } + + public static function providesSharingData(): array { + return [ + [true, true, true, false, [ + [ + 'href' => 'principal:' . self::UNIT_TEST_USER1, + 'readOnly' => false + ], + [ + 'href' => 'principal:' . self::UNIT_TEST_GROUP, + 'readOnly' => true + ] + ], [ + self::UNIT_TEST_USER1, + self::UNIT_TEST_GROUP, + ]], + [true, true, true, false, [ + [ + 'href' => 'principal:' . self::UNIT_TEST_GROUP, + 'readOnly' => true, + ], + [ + 'href' => 'principal:' . self::UNIT_TEST_GROUP2, + 'readOnly' => false, + ], + ], [ + self::UNIT_TEST_GROUP, + self::UNIT_TEST_GROUP2, + ]], + [true, true, true, true, [ + [ + 'href' => 'principal:' . self::UNIT_TEST_GROUP, + 'readOnly' => false, + ], + [ + 'href' => 'principal:' . self::UNIT_TEST_GROUP2, + 'readOnly' => true, + ], + ], [ + self::UNIT_TEST_GROUP, + self::UNIT_TEST_GROUP2, + ]], + [true, false, false, false, [ + [ + 'href' => 'principal:' . self::UNIT_TEST_USER1, + 'readOnly' => true + ], + ], [ + self::UNIT_TEST_USER1, + ]], + + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providesSharingData')] + public function testCalendarSharing($userCanRead, $userCanWrite, $groupCanRead, $groupCanWrite, $add, $principals): void { + $logger = $this->createMock(\Psr\Log\LoggerInterface::class); + $config = $this->createMock(IConfig::class); + + $l10n = $this->createMock(IL10N::class); + $l10n->expects($this->any()) + ->method('t') + ->willReturnCallback(function ($text, $parameters = []) { + return vsprintf($text, $parameters); + }); + + $this->userManager->expects($this->any()) + ->method('userExists') + ->willReturn(true); + $this->groupManager->expects($this->any()) + ->method('groupExists') + ->willReturn(true); + $this->principal->expects(self::atLeastOnce()) + ->method('findByUri') + ->willReturnOnConsecutiveCalls(...$principals); + + $calendarId = $this->createTestCalendar(); + $calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER); + $this->assertCount(1, $calendars); + $calendar = new Calendar($this->backend, $calendars[0], $l10n, $config, $logger); + $this->backend->updateShares($calendar, $add, []); + $calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER1); + $this->assertCount(1, $calendars); + $calendar = new Calendar($this->backend, $calendars[0], $l10n, $config, $logger); + $acl = $calendar->getACL(); + $this->assertAcl(self::UNIT_TEST_USER, '{DAV:}read', $acl); + $this->assertAcl(self::UNIT_TEST_USER, '{DAV:}write', $acl); + $this->assertAccess($userCanRead, self::UNIT_TEST_USER1, '{DAV:}read', $acl); + $this->assertAccess($userCanWrite, self::UNIT_TEST_USER1, '{DAV:}write', $acl); + $this->assertEquals(self::UNIT_TEST_USER, $calendar->getOwner()); + + // test acls on the child + $uri = static::getUniqueID('calobj'); + $calData = <<<'EOD' +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:ownCloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3ec8 +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:Test Event +DTSTART;VALUE=DATE-TIME:20130912T130000Z +DTEND;VALUE=DATE-TIME:20130912T140000Z +CLASS:PUBLIC +END:VEVENT +END:VCALENDAR +EOD; + + $this->dispatcher->expects(self::atLeastOnce()) + ->method('dispatchTyped'); + $this->backend->createCalendarObject($calendarId, $uri, $calData); + + /** @var IACL $child */ + $child = $calendar->getChild($uri); + $acl = $child->getACL(); + $this->assertAcl(self::UNIT_TEST_USER, '{DAV:}read', $acl); + $this->assertAcl(self::UNIT_TEST_USER, '{DAV:}write', $acl); + $this->assertAccess($userCanRead, self::UNIT_TEST_USER1, '{DAV:}read', $acl); + $this->assertAccess($userCanWrite, self::UNIT_TEST_USER1, '{DAV:}write', $acl); + + // delete the calendar + $this->dispatcher->expects(self::once()) + ->method('dispatchTyped') + ->with(self::callback(function ($event) { + return $event instanceof CalendarDeletedEvent; + })); + $this->backend->deleteCalendar($calendars[0]['id'], true); + $calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER); + self::assertEmpty($calendars); + } + + public function testCalendarObjectsOperations(): void { + $calendarId = $this->createTestCalendar(); + + // create a card + $uri = static::getUniqueID('calobj'); + $calData = <<<'EOD' +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:ownCloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3ec8 +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:Test Event +DTSTART;VALUE=DATE-TIME:20130912T130000Z +DTEND;VALUE=DATE-TIME:20130912T140000Z +CLASS:PUBLIC +END:VEVENT +END:VCALENDAR +EOD; + + $this->dispatcher->expects(self::atLeastOnce()) + ->method('dispatchTyped'); + $this->backend->createCalendarObject($calendarId, $uri, $calData); + + // get all the calendar objects + $calendarObjects = $this->backend->getCalendarObjects($calendarId); + $this->assertCount(1, $calendarObjects); + $this->assertEquals($calendarId, $calendarObjects[0]['calendarid']); + $this->assertArrayHasKey('classification', $calendarObjects[0]); + + // get the calendar objects + $calendarObject = $this->backend->getCalendarObject($calendarId, $uri); + $this->assertNotNull($calendarObject); + $this->assertArrayHasKey('id', $calendarObject); + $this->assertArrayHasKey('uri', $calendarObject); + $this->assertArrayHasKey('lastmodified', $calendarObject); + $this->assertArrayHasKey('etag', $calendarObject); + $this->assertArrayHasKey('size', $calendarObject); + $this->assertArrayHasKey('classification', $calendarObject); + $this->assertArrayHasKey('{' . SharingPlugin::NS_NEXTCLOUD . '}deleted-at', $calendarObject); + $this->assertEquals($calData, $calendarObject['calendardata']); + + // update the card + $calData = <<<'EOD' +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:ownCloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3ec8 +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:Test Event +DTSTART;VALUE=DATE-TIME:20130912T130000Z +DTEND;VALUE=DATE-TIME:20130912T140000Z +END:VEVENT +END:VCALENDAR +EOD; + $this->dispatcher->expects(self::atLeastOnce()) + ->method('dispatchTyped'); + $this->backend->updateCalendarObject($calendarId, $uri, $calData); + $calendarObject = $this->backend->getCalendarObject($calendarId, $uri); + $this->assertEquals($calData, $calendarObject['calendardata']); + + // delete the card + $this->dispatcher->expects(self::atLeastOnce()) + ->method('dispatchTyped'); + $this->backend->deleteCalendarObject($calendarId, $uri); + $calendarObjects = $this->backend->getCalendarObjects($calendarId); + $this->assertCount(0, $calendarObjects); + } + + + public function testMultipleCalendarObjectsWithSameUID(): void { + $this->expectException(\Sabre\DAV\Exception\BadRequest::class); + $this->expectExceptionMessage('Calendar object with uid already exists in this calendar collection.'); + + $calendarId = $this->createTestCalendar(); + + $calData = <<<'EOD' +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:ownCloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3ec8-1 +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:Test Event +DTSTART;VALUE=DATE-TIME:20130912T130000Z +DTEND;VALUE=DATE-TIME:20130912T140000Z +CLASS:PUBLIC +END:VEVENT +END:VCALENDAR +EOD; + + $uri0 = static::getUniqueID('event'); + $uri1 = static::getUniqueID('event'); + $this->backend->createCalendarObject($calendarId, $uri0, $calData); + $this->backend->createCalendarObject($calendarId, $uri1, $calData); + } + + public function testMultiCalendarObjects(): void { + $calendarId = $this->createTestCalendar(); + + // create an event + $calData = []; + $calData[] = <<<'EOD' +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:ownCloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3ec8-1 +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:Test Event +DTSTART;VALUE=DATE-TIME:20130912T130000Z +DTEND;VALUE=DATE-TIME:20130912T140000Z +CLASS:PUBLIC +END:VEVENT +END:VCALENDAR +EOD; + + $calData[] = <<<'EOD' +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:ownCloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3ec8-2 +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:Test Event +DTSTART;VALUE=DATE-TIME:20130912T130000Z +DTEND;VALUE=DATE-TIME:20130912T140000Z +CLASS:PUBLIC +END:VEVENT +END:VCALENDAR +EOD; + + $calData[] = <<<'EOD' +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:ownCloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3ec8-3 +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:Test Event +DTSTART;VALUE=DATE-TIME:20130912T130000Z +DTEND;VALUE=DATE-TIME:20130912T140000Z +CLASS:PUBLIC +END:VEVENT +END:VCALENDAR +EOD; + + $uri0 = static::getUniqueID('card'); + $this->dispatcher->expects(self::atLeastOnce()) + ->method('dispatchTyped'); + $this->backend->createCalendarObject($calendarId, $uri0, $calData[0]); + $uri1 = static::getUniqueID('card'); + $this->dispatcher->expects(self::atLeastOnce()) + ->method('dispatchTyped'); + $this->backend->createCalendarObject($calendarId, $uri1, $calData[1]); + $uri2 = static::getUniqueID('card'); + $this->dispatcher->expects(self::atLeastOnce()) + ->method('dispatchTyped'); + $this->backend->createCalendarObject($calendarId, $uri2, $calData[2]); + + // get all the cards + $calendarObjects = $this->backend->getCalendarObjects($calendarId); + $this->assertCount(3, $calendarObjects); + + // get the cards + $calendarObjects = $this->backend->getMultipleCalendarObjects($calendarId, [$uri1, $uri2]); + $this->assertCount(2, $calendarObjects); + foreach ($calendarObjects as $card) { + $this->assertArrayHasKey('id', $card); + $this->assertArrayHasKey('uri', $card); + $this->assertArrayHasKey('lastmodified', $card); + $this->assertArrayHasKey('etag', $card); + $this->assertArrayHasKey('size', $card); + $this->assertArrayHasKey('classification', $card); + } + + usort($calendarObjects, function ($a, $b) { + return $a['id'] - $b['id']; + }); + + $this->assertEquals($calData[1], $calendarObjects[0]['calendardata']); + $this->assertEquals($calData[2], $calendarObjects[1]['calendardata']); + + // delete the card + $this->dispatcher->expects(self::atLeastOnce()) + ->method('dispatchTyped'); + $this->backend->deleteCalendarObject($calendarId, $uri0); + $this->dispatcher->expects(self::atLeastOnce()) + ->method('dispatchTyped'); + $this->backend->deleteCalendarObject($calendarId, $uri1); + $this->dispatcher->expects(self::atLeastOnce()) + ->method('dispatchTyped'); + $this->backend->deleteCalendarObject($calendarId, $uri2); + $calendarObjects = $this->backend->getCalendarObjects($calendarId); + $this->assertCount(0, $calendarObjects); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providesCalendarQueryParameters')] + public function testCalendarQuery($expectedEventsInResult, $propFilters, $compFilter): void { + $calendarId = $this->createTestCalendar(); + $events = []; + $events[0] = $this->createEvent($calendarId, '20130912T130000Z', '20130912T140000Z'); + $events[1] = $this->createEvent($calendarId, '20130912T150000Z', '20130912T170000Z'); + $events[2] = $this->createEvent($calendarId, '20130912T173000Z', '20130912T220000Z'); + if (PHP_INT_SIZE > 8) { + $events[3] = $this->createEvent($calendarId, '21130912T130000Z', '22130912T130000Z'); + } else { + /* On 32bit we do not support events after 2038 */ + $events[3] = $this->createEvent($calendarId, '20370912T130000Z', '20370912T130000Z'); + } + + $result = $this->backend->calendarQuery($calendarId, [ + 'name' => '', + 'prop-filters' => $propFilters, + 'comp-filters' => $compFilter + ]); + + $expectedEventsInResult = array_map(function ($index) use ($events) { + return $events[$index]; + }, $expectedEventsInResult); + $this->assertEqualsCanonicalizing($expectedEventsInResult, $result); + } + + public function testGetCalendarObjectByUID(): void { + $calendarId = $this->createTestCalendar(); + $uri = static::getUniqueID('calobj'); + $calData = <<<'EOD' +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:ownCloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3ec8 +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:Test Event +DTSTART;VALUE=DATE-TIME:20130912T130000Z +DTEND;VALUE=DATE-TIME:20130912T140000Z +CLASS:PUBLIC +END:VEVENT +END:VCALENDAR +EOD; + + + $this->backend->createCalendarObject($calendarId, $uri, $calData); + + $co = $this->backend->getCalendarObjectByUID(self::UNIT_TEST_USER, '47d15e3ec8'); + $this->assertNotNull($co); + } + + public static function providesCalendarQueryParameters(): array { + return [ + 'all' => [[0, 1, 2, 3], [], []], + 'only-todos' => [[], ['name' => 'VTODO'], []], + 'only-events' => [[0, 1, 2, 3], [], [['name' => 'VEVENT', 'is-not-defined' => false, 'comp-filters' => [], 'time-range' => ['start' => null, 'end' => null], 'prop-filters' => []]],], + 'start' => [[1, 2, 3], [], [['name' => 'VEVENT', 'is-not-defined' => false, 'comp-filters' => [], 'time-range' => ['start' => new DateTime('2013-09-12 14:00:00', new DateTimeZone('UTC')), 'end' => null], 'prop-filters' => []]],], + 'end' => [[0], [], [['name' => 'VEVENT', 'is-not-defined' => false, 'comp-filters' => [], 'time-range' => ['start' => null, 'end' => new DateTime('2013-09-12 14:00:00', new DateTimeZone('UTC'))], 'prop-filters' => []]],], + 'future' => [[3], [], [['name' => 'VEVENT', 'is-not-defined' => false, 'comp-filters' => [], 'time-range' => ['start' => new DateTime('2036-09-12 14:00:00', new DateTimeZone('UTC')), 'end' => null], 'prop-filters' => []]],], + ]; + } + + public function testCalendarSynchronization(): void { + + // construct calendar for testing + $calendarId = $this->createTestCalendar(); + + /** test fresh sync state with NO events in calendar */ + // construct test state + $stateTest = ['syncToken' => 1, 'added' => [], 'modified' => [], 'deleted' => []]; + // retrieve live state + $stateLive = $this->backend->getChangesForCalendar($calendarId, '', 1); + // test live state + $this->assertEquals($stateTest, $stateLive, 'Failed test fresh sync state with NO events in calendar'); + + /** test delta sync state with NO events in calendar */ + // construct test state + $stateTest = ['syncToken' => 1, 'added' => [], 'modified' => [], 'deleted' => []]; + // retrieve live state + $stateLive = $this->backend->getChangesForCalendar($calendarId, '2', 1); + // test live state + $this->assertEquals($stateTest, $stateLive, 'Failed test delta sync state with NO events in calendar'); + + /** add events to calendar */ + $event1 = $this->createEvent($calendarId, '20240701T130000Z', '20240701T140000Z'); + $event2 = $this->createEvent($calendarId, '20240701T140000Z', '20240701T150000Z'); + $event3 = $this->createEvent($calendarId, '20240701T150000Z', '20240701T160000Z'); + + /** test fresh sync state with events in calendar */ + // construct expected state + $stateTest = ['syncToken' => 4, 'added' => [$event1, $event2, $event3], 'modified' => [], 'deleted' => []]; + sort($stateTest['added']); + // retrieve live state + $stateLive = $this->backend->getChangesForCalendar($calendarId, '', 1); + // sort live state results + sort($stateLive['added']); + sort($stateLive['modified']); + sort($stateLive['deleted']); + // test live state + $this->assertEquals($stateTest, $stateLive, 'Failed test fresh sync state with events in calendar'); + + /** test delta sync state with events in calendar */ + // construct expected state + $stateTest = ['syncToken' => 4, 'added' => [$event2, $event3], 'modified' => [], 'deleted' => []]; + sort($stateTest['added']); + // retrieve live state + $stateLive = $this->backend->getChangesForCalendar($calendarId, '2', 1); + // sort live state results + sort($stateLive['added']); + sort($stateLive['modified']); + sort($stateLive['deleted']); + // test live state + $this->assertEquals($stateTest, $stateLive, 'Failed test delta sync state with events in calendar'); + + /** modify/delete events in calendar */ + $this->deleteEvent($calendarId, $event1); + $this->modifyEvent($calendarId, $event2, '20250701T140000Z', '20250701T150000Z'); + + /** test fresh sync state with modified/deleted events in calendar */ + // construct expected state + $stateTest = ['syncToken' => 6, 'added' => [$event2, $event3], 'modified' => [], 'deleted' => []]; + sort($stateTest['added']); + // retrieve live state + $stateLive = $this->backend->getChangesForCalendar($calendarId, '', 1); + // sort live state results + sort($stateLive['added']); + sort($stateLive['modified']); + sort($stateLive['deleted']); + // test live state + $this->assertEquals($stateTest, $stateLive, 'Failed test fresh sync state with modified/deleted events in calendar'); + + /** test delta sync state with modified/deleted events in calendar */ + // construct expected state + $stateTest = ['syncToken' => 6, 'added' => [$event3], 'modified' => [$event2], 'deleted' => [$event1]]; + // retrieve live state + $stateLive = $this->backend->getChangesForCalendar($calendarId, '3', 1); + // test live state + $this->assertEquals($stateTest, $stateLive, 'Failed test delta sync state with modified/deleted events in calendar'); + + /** test delta sync state with modified/deleted events in calendar and invalid token */ + // construct expected state + $stateTest = ['syncToken' => 6, 'added' => [], 'modified' => [], 'deleted' => []]; + // retrieve live state + $stateLive = $this->backend->getChangesForCalendar($calendarId, '6', 1); + // test live state + $this->assertEquals($stateTest, $stateLive, 'Failed test delta sync state with modified/deleted events in calendar and invalid token'); + + } + + public function testPublications(): void { + $this->dispatcher->expects(self::atLeastOnce()) + ->method('dispatchTyped'); + + $this->backend->createCalendar(self::UNIT_TEST_USER, 'Example', []); + + $calendarInfo = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER)[0]; + + /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject $l10n */ + $l10n = $this->createMock(IL10N::class); + $config = $this->createMock(IConfig::class); + $logger = $this->createMock(\Psr\Log\LoggerInterface::class); + $calendar = new Calendar($this->backend, $calendarInfo, $l10n, $config, $logger); + $calendar->setPublishStatus(true); + $this->assertNotEquals(false, $calendar->getPublishStatus()); + + $publicCalendars = $this->backend->getPublicCalendars(); + $this->assertCount(1, $publicCalendars); + $this->assertEquals(true, $publicCalendars[0]['{http://owncloud.org/ns}public']); + $this->assertEquals('User\'s displayname', $publicCalendars[0]['{http://nextcloud.com/ns}owner-displayname']); + + $publicCalendarURI = $publicCalendars[0]['uri']; + $publicCalendar = $this->backend->getPublicCalendar($publicCalendarURI); + $this->assertEquals(true, $publicCalendar['{http://owncloud.org/ns}public']); + + $calendar->setPublishStatus(false); + $this->assertEquals(false, $calendar->getPublishStatus()); + + $this->expectException(NotFound::class); + $this->backend->getPublicCalendar($publicCalendarURI); + } + + public function testSubscriptions(): void { + $id = $this->backend->createSubscription(self::UNIT_TEST_USER, 'Subscription', [ + '{http://calendarserver.org/ns/}source' => new Href('test-source'), + '{http://apple.com/ns/ical/}calendar-color' => '#1C4587', + '{http://calendarserver.org/ns/}subscribed-strip-todos' => '' + ]); + + $subscriptions = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER); + $this->assertCount(1, $subscriptions); + $this->assertEquals('#1C4587', $subscriptions[0]['{http://apple.com/ns/ical/}calendar-color']); + $this->assertEquals(true, $subscriptions[0]['{http://calendarserver.org/ns/}subscribed-strip-todos']); + $this->assertEquals($id, $subscriptions[0]['id']); + + $patch = new PropPatch([ + '{DAV:}displayname' => 'Unit test', + '{http://apple.com/ns/ical/}calendar-color' => '#ac0606', + ]); + $this->backend->updateSubscription($id, $patch); + $patch->commit(); + + $subscriptions = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER); + $this->assertCount(1, $subscriptions); + $this->assertEquals($id, $subscriptions[0]['id']); + $this->assertEquals('Unit test', $subscriptions[0]['{DAV:}displayname']); + $this->assertEquals('#ac0606', $subscriptions[0]['{http://apple.com/ns/ical/}calendar-color']); + + $this->backend->deleteSubscription($id); + $subscriptions = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER); + $this->assertCount(0, $subscriptions); + } + + public static function providesSchedulingData(): array { + $data = <<<EOS +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject 3.5.0//EN +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VTIMEZONE +TZID:Europe/Warsaw +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20170320T131655Z +LAST-MODIFIED:20170320T135019Z +DTSTAMP:20170320T135019Z +UID:7e908a6d-4c4e-48d7-bd62-59ab80fbf1a3 +SUMMARY:TEST Z pg_escape_bytea +ORGANIZER;RSVP=TRUE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:k.klimczak@gromar.e + u +ATTENDEE;RSVP=TRUE;CN=Zuzanna Leszek;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICI + PANT:mailto:z.leszek@gromar.eu +ATTENDEE;RSVP=TRUE;CN=Marcin Pisarski;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTIC + IPANT:mailto:m.pisarski@gromar.eu +ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT:mailto:klimcz + ak.k@gmail.com +ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT:mailto:k_klim + czak@tlen.pl +DTSTART;TZID=Europe/Warsaw:20170325T150000 +DTEND;TZID=Europe/Warsaw:20170325T160000 +TRANSP:OPAQUE +DESCRIPTION:Magiczna treść uzyskana za pomocą magicznego proszku.\n\nę + żźćńłóÓŻŹĆŁĘ€śśśŚŚ\n \,\,))))))))\;\,\n + __))))))))))))))\,\n \\|/ -\\(((((''''((((((((.\n -*-==/// + ///(('' . `))))))\,\n /|\\ ))| o \;-. '((((( + \,(\,\n ( `| / ) \;))))' + \,_))^\;(~\n | | | \,))((((_ _____- + -----~~~-. %\,\;(\;(>'\;'~\n o_)\; \; )))(((` ~--- + ~ `:: \\ %%~~)(v\;(`('~\n \; ''''```` + `: `:::|\\\,__\,%% )\;`'\; ~\n | _ + ) / `:|`----' `-'\n ______/\\/~ | + / /\n /~\;\;.____/\;\;' / ___--\ + ,-( `\;\;\;/\n / // _\;______\;'------~~~~~ /\;\;/\\ /\n + // | | / \; \\\;\;\,\\\n (<_ | \; + /'\,/-----' _>\n \\_| ||_ + //~\;~~~~~~~~~\n `\\_| (\,~~ -Tua Xiong\n + \\~\\\n + ~~\n\n +SEQUENCE:1 +X-MOZ-GENERATION:1 +END:VEVENT +END:VCALENDAR +EOS; + + return [ + 'no data' => [''], + 'failing on postgres' => [$data] + ]; + } + + /** + * @param $objectData + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providesSchedulingData')] + public function testScheduling($objectData): void { + $this->backend->createSchedulingObject(self::UNIT_TEST_USER, 'Sample Schedule', $objectData); + + $sos = $this->backend->getSchedulingObjects(self::UNIT_TEST_USER); + $this->assertCount(1, $sos); + + $so = $this->backend->getSchedulingObject(self::UNIT_TEST_USER, 'Sample Schedule'); + $this->assertNotNull($so); + + $this->backend->deleteSchedulingObject(self::UNIT_TEST_USER, 'Sample Schedule'); + + $sos = $this->backend->getSchedulingObjects(self::UNIT_TEST_USER); + $this->assertCount(0, $sos); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providesCalDataForGetDenormalizedData')] + public function testGetDenormalizedData($expected, $key, $calData): void { + try { + $actual = $this->backend->getDenormalizedData($calData); + $this->assertEquals($expected, $actual[$key]); + } catch (\Throwable $e) { + if (($e->getMessage() === 'Epoch doesn\'t fit in a PHP integer') && (PHP_INT_SIZE < 8)) { + $this->markTestSkipped('This fail on 32bits because of PHP limitations in DateTime'); + } + throw $e; + } + } + + public static function providesCalDataForGetDenormalizedData(): array { + return [ + 'first occurrence before unix epoch starts' => [0, 'firstOccurence', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:413F269B-B51B-46B1-AFB6-40055C53A4DC\r\nDTSTAMP:20160309T095056Z\r\nDTSTART;VALUE=DATE:16040222\r\nDTEND;VALUE=DATE:16040223\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:SUMMARY\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"], + 'no first occurrence because yearly' => [null, 'firstOccurence', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:413F269B-B51B-46B1-AFB6-40055C53A4DC\r\nDTSTAMP:20160309T095056Z\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:SUMMARY\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"], + + 'last occurrence is max when only last VEVENT in group is weekly' => [(new DateTime(CalDavBackend::MAX_DATE))->getTimestamp(), 'lastOccurence', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.3.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nDTSTART;TZID=America/Los_Angeles:20200812T103000\r\nDTEND;TZID=America/Los_Angeles:20200812T110000\r\nDTSTAMP:20200927T180638Z\r\nUID:asdfasdfasdf@google.com\r\nRECURRENCE-ID;TZID=America/Los_Angeles:20200811T123000\r\nCREATED:20200626T181848Z\r\nLAST-MODIFIED:20200922T192707Z\r\nSUMMARY:Weekly 1:1\r\nTRANSP:OPAQUE\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nDTSTART;TZID=America/Los_Angeles:20200728T123000\r\nDTEND;TZID=America/Los_Angeles:20200728T130000\r\nEXDATE;TZID=America/Los_Angeles:20200818T123000\r\nRRULE:FREQ=WEEKLY;BYDAY=TU\r\nDTSTAMP:20200927T180638Z\r\nUID:asdfasdfasdf@google.com\r\nCREATED:20200626T181848Z\r\nDESCRIPTION:Setting up recurring time on our calendars\r\nLAST-MODIFIED:20200922T192707Z\r\nSUMMARY:Weekly 1:1\r\nTRANSP:OPAQUE\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"], + + 'last occurrence before unix epoch starts' => [0, 'lastOccurence', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.3.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nDTSTART;VALUE=DATE:19110324\r\nDTEND;VALUE=DATE:19110325\r\nDTSTAMP:20200927T180638Z\r\nUID:asdfasdfasdf@google.com\r\nCREATED:20200626T181848Z\r\nDESCRIPTION:Very old event\r\nLAST-MODIFIED:20200922T192707Z\r\nSUMMARY:Some old event\r\nTRANSP:OPAQUE\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"], + + 'first occurrence is found when not first VEVENT in group' => [(new DateTime('2020-09-01T110000', new DateTimeZone('America/Los_Angeles')))->getTimestamp(), 'firstOccurence', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.3.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nDTSTART;TZID=America/Los_Angeles:20201013T110000\r\nDTEND;TZID=America/Los_Angeles:20201013T120000\r\nDTSTAMP:20200927T180638Z\r\nUID:asdf0000@google.com\r\nRECURRENCE-ID;TZID=America/Los_Angeles:20201013T110000\r\nCREATED:20160330T034726Z\r\nLAST-MODIFIED:20200925T042014Z\r\nSTATUS:CONFIRMED\r\nTRANSP:OPAQUE\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nDTSTART;TZID=America/Los_Angeles:20200901T110000\r\nDTEND;TZID=America/Los_Angeles:20200901T120000\r\nRRULE:FREQ=WEEKLY;BYDAY=TU\r\nEXDATE;TZID=America/Los_Angeles:20200922T110000\r\nEXDATE;TZID=America/Los_Angeles:20200915T110000\r\nEXDATE;TZID=America/Los_Angeles:20200908T110000\r\nDTSTAMP:20200927T180638Z\r\nUID:asdf0000@google.com\r\nCREATED:20160330T034726Z\r\nLAST-MODIFIED:20200915T162810Z\r\nSTATUS:CONFIRMED\r\nTRANSP:OPAQUE\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"], + + 'CLASS:PRIVATE' => [CalDavBackend::CLASSIFICATION_PRIVATE, 'classification', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//dmfs.org//mimedir.icalendar//EN\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\nX-LIC-LOCATION:Europe/Berlin\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nDTSTART;TZID=Europe/Berlin:20160419T130000\r\nSUMMARY:Test\r\nCLASS:PRIVATE\r\nTRANSP:OPAQUE\r\nSTATUS:CONFIRMED\r\nDTEND;TZID=Europe/Berlin:20160419T140000\r\nLAST-MODIFIED:20160419T074202Z\r\nDTSTAMP:20160419T074202Z\r\nCREATED:20160419T074202Z\r\nUID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310\r\nEND:VEVENT\r\nEND:VCALENDAR"], + + 'CLASS:PUBLIC' => [CalDavBackend::CLASSIFICATION_PUBLIC, 'classification', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//dmfs.org//mimedir.icalendar//EN\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\nX-LIC-LOCATION:Europe/Berlin\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nDTSTART;TZID=Europe/Berlin:20160419T130000\r\nSUMMARY:Test\r\nCLASS:PUBLIC\r\nTRANSP:OPAQUE\r\nSTATUS:CONFIRMED\r\nDTEND;TZID=Europe/Berlin:20160419T140000\r\nLAST-MODIFIED:20160419T074202Z\r\nDTSTAMP:20160419T074202Z\r\nCREATED:20160419T074202Z\r\nUID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310\r\nEND:VEVENT\r\nEND:VCALENDAR"], + + 'CLASS:CONFIDENTIAL' => [CalDavBackend::CLASSIFICATION_CONFIDENTIAL, 'classification', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//dmfs.org//mimedir.icalendar//EN\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\nX-LIC-LOCATION:Europe/Berlin\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nDTSTART;TZID=Europe/Berlin:20160419T130000\r\nSUMMARY:Test\r\nCLASS:CONFIDENTIAL\r\nTRANSP:OPAQUE\r\nSTATUS:CONFIRMED\r\nDTEND;TZID=Europe/Berlin:20160419T140000\r\nLAST-MODIFIED:20160419T074202Z\r\nDTSTAMP:20160419T074202Z\r\nCREATED:20160419T074202Z\r\nUID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310\r\nEND:VEVENT\r\nEND:VCALENDAR"], + + 'no class set -> public' => [CalDavBackend::CLASSIFICATION_PUBLIC, 'classification', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//dmfs.org//mimedir.icalendar//EN\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\nX-LIC-LOCATION:Europe/Berlin\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nDTSTART;TZID=Europe/Berlin:20160419T130000\r\nSUMMARY:Test\r\nTRANSP:OPAQUE\r\nDTEND;TZID=Europe/Berlin:20160419T140000\r\nLAST-MODIFIED:20160419T074202Z\r\nDTSTAMP:20160419T074202Z\r\nCREATED:20160419T074202Z\r\nUID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310\r\nEND:VEVENT\r\nEND:VCALENDAR"], + + 'unknown class -> private' => [CalDavBackend::CLASSIFICATION_PRIVATE, 'classification', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//dmfs.org//mimedir.icalendar//EN\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\nX-LIC-LOCATION:Europe/Berlin\r\nBEGIN:DAYLIGHT\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nTZNAME:CEST\r\nDTSTART:19700329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nTZNAME:CET\r\nDTSTART:19701025T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nDTSTART;TZID=Europe/Berlin:20160419T130000\r\nSUMMARY:Test\r\nCLASS:VERTRAULICH\r\nTRANSP:OPAQUE\r\nSTATUS:CONFIRMED\r\nDTEND;TZID=Europe/Berlin:20160419T140000\r\nLAST-MODIFIED:20160419T074202Z\r\nDTSTAMP:20160419T074202Z\r\nCREATED:20160419T074202Z\r\nUID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310\r\nEND:VEVENT\r\nEND:VCALENDAR"], + ]; + } + + public function testCalendarSearch(): void { + $calendarId = $this->createTestCalendar(); + + $uri = static::getUniqueID('calobj'); + $calData = <<<EOD +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:ownCloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3ec8 +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:Test Event +DTSTART;VALUE=DATE-TIME:20130912T130000Z +DTEND;VALUE=DATE-TIME:20130912T140000Z +CLASS:PUBLIC +END:VEVENT +END:VCALENDAR +EOD; + + $this->backend->createCalendarObject($calendarId, $uri, $calData); + + $search1 = $this->backend->calendarSearch(self::UNIT_TEST_USER, [ + 'comps' => [ + 'VEVENT', + 'VTODO' + ], + 'props' => [ + 'SUMMARY', + 'LOCATION' + ], + 'search-term' => 'Test', + ]); + $this->assertEquals(count($search1), 1); + + + // update the card + $calData = <<<'EOD' +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:ownCloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3ec8 +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:123 Event 🙈 +DTSTART;VALUE=DATE-TIME:20130912T130000Z +DTEND;VALUE=DATE-TIME:20130912T140000Z +ATTENDEE;CN=test:mailto:foo@bar.com +END:VEVENT +END:VCALENDAR +EOD; + $this->backend->updateCalendarObject($calendarId, $uri, $calData); + + $search2 = $this->backend->calendarSearch(self::UNIT_TEST_USER, [ + 'comps' => [ + 'VEVENT', + 'VTODO' + ], + 'props' => [ + 'SUMMARY', + 'LOCATION' + ], + 'search-term' => 'Test', + ]); + $this->assertEquals(count($search2), 0); + + $search3 = $this->backend->calendarSearch(self::UNIT_TEST_USER, [ + 'comps' => [ + 'VEVENT', + 'VTODO' + ], + 'props' => [ + 'SUMMARY', + 'LOCATION' + ], + 'params' => [ + [ + 'property' => 'ATTENDEE', + 'parameter' => 'CN' + ] + ], + 'search-term' => 'Test', + ]); + $this->assertEquals(count($search3), 1); + + // t matches both summary and attendee's CN, but we want unique results + $search4 = $this->backend->calendarSearch(self::UNIT_TEST_USER, [ + 'comps' => [ + 'VEVENT', + 'VTODO' + ], + 'props' => [ + 'SUMMARY', + 'LOCATION' + ], + 'params' => [ + [ + 'property' => 'ATTENDEE', + 'parameter' => 'CN' + ] + ], + 'search-term' => 't', + ]); + $this->assertEquals(count($search4), 1); + + $this->backend->deleteCalendarObject($calendarId, $uri); + + $search5 = $this->backend->calendarSearch(self::UNIT_TEST_USER, [ + 'comps' => [ + 'VEVENT', + 'VTODO' + ], + 'props' => [ + 'SUMMARY', + 'LOCATION' + ], + 'params' => [ + [ + 'property' => 'ATTENDEE', + 'parameter' => 'CN' + ] + ], + 'search-term' => 't', + ]); + $this->assertEquals(count($search5), 0); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('searchDataProvider')] + public function testSearch(bool $isShared, array $searchOptions, int $count): void { + $calendarId = $this->createTestCalendar(); + + $uris = []; + $calData = []; + + $uris[] = static::getUniqueID('calobj'); + $calData[] = <<<EOD +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:Nextcloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3ec8-1 +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:Test Event +DTSTART;VALUE=DATE-TIME:20130912T130000Z +DTEND;VALUE=DATE-TIME:20130912T140000Z +CLASS:PUBLIC +END:VEVENT +END:VCALENDAR +EOD; + + $uris[] = static::getUniqueID('calobj'); + $calData[] = <<<EOD +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:Nextcloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3ec8-2 +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:123 +LOCATION:Test +DTSTART;VALUE=DATE-TIME:20130912T130000Z +DTEND;VALUE=DATE-TIME:20130912T140000Z +CLASS:PUBLIC +END:VEVENT +END:VCALENDAR +EOD; + + $uris[] = static::getUniqueID('calobj'); + $calData[] = <<<EOD +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:Nextcloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3ec8-3 +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:123 +ATTENDEE;CN=test:mailto:foo@bar.com +DTSTART;VALUE=DATE-TIME:20130912T130000Z +DTEND;VALUE=DATE-TIME:20130912T140000Z +CLASS:PRIVATE +END:VEVENT +END:VCALENDAR +EOD; + + $uris[] = static::getUniqueID('calobj'); + $calData[] = <<<EOD +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:Nextcloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3ec8-4 +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:123 +ATTENDEE;CN=foobar:mailto:test@bar.com +DTSTART;VALUE=DATE-TIME:20130912T130000Z +DTEND;VALUE=DATE-TIME:20130912T140000Z +CLASS:CONFIDENTIAL +END:VEVENT +END:VCALENDAR +EOD; + + $uriCount = count($uris); + for ($i = 0; $i < $uriCount; $i++) { + $this->backend->createCalendarObject($calendarId, + $uris[$i], $calData[$i]); + } + + $calendarInfo = [ + 'id' => $calendarId, + 'principaluri' => 'user1', + '{http://owncloud.org/ns}owner-principal' => $isShared ? 'user2' : 'user1', + ]; + + $result = $this->backend->search($calendarInfo, 'Test', + ['SUMMARY', 'LOCATION', 'ATTENDEE'], $searchOptions, null, null); + + $this->assertCount($count, $result); + } + + public static function searchDataProvider(): array { + return [ + [false, [], 4], + [true, ['timerange' => ['start' => new DateTime('2013-09-12 13:00:00'), 'end' => new DateTime('2013-09-12 14:00:00')]], 2], + [true, ['timerange' => ['start' => new DateTime('2013-09-12 15:00:00'), 'end' => new DateTime('2013-09-12 16:00:00')]], 0], + ]; + } + + public function testSameUriSameIdForDifferentCalendarTypes(): void { + $calendarId = $this->createTestCalendar(); + $subscriptionId = $this->createTestSubscription(); + + $uri = static::getUniqueID('calobj'); + $calData = <<<EOD +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:ownCloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3ec8 +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:Test Event +DTSTART;VALUE=DATE-TIME:20130912T130000Z +DTEND;VALUE=DATE-TIME:20130912T140000Z +CLASS:PUBLIC +END:VEVENT +END:VCALENDAR +EOD; + + $calData2 = <<<EOD +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:ownCloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3ec8 +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:Test Event 123 +DTSTART;VALUE=DATE-TIME:20130912T130000Z +DTEND;VALUE=DATE-TIME:20130912T140000Z +CLASS:PUBLIC +END:VEVENT +END:VCALENDAR +EOD; + + $this->backend->createCalendarObject($calendarId, $uri, $calData); + $this->backend->createCalendarObject($subscriptionId, $uri, $calData2, CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION); + + $this->assertEquals($calData, $this->backend->getCalendarObject($calendarId, $uri, CalDavBackend::CALENDAR_TYPE_CALENDAR)['calendardata']); + $this->assertEquals($calData2, $this->backend->getCalendarObject($subscriptionId, $uri, CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION)['calendardata']); + } + + public function testPurgeAllCachedEventsForSubscription(): void { + $subscriptionId = $this->createTestSubscription(); + $uri = static::getUniqueID('calobj'); + $calData = <<<EOD +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:ownCloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3ec8 +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:Test Event +DTSTART;VALUE=DATE-TIME:20130912T130000Z +DTEND;VALUE=DATE-TIME:20130912T140000Z +CLASS:PUBLIC +END:VEVENT +END:VCALENDAR +EOD; + + $this->backend->createCalendarObject($subscriptionId, $uri, $calData, CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION); + $this->backend->purgeAllCachedEventsForSubscription($subscriptionId); + + $this->assertEquals(null, $this->backend->getCalendarObject($subscriptionId, $uri, CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION)); + } + + public function testCalendarMovement(): void { + $this->backend->createCalendar(self::UNIT_TEST_USER, 'Example', []); + + $this->assertCount(1, $this->backend->getCalendarsForUser(self::UNIT_TEST_USER)); + + $calendarInfoUser = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER)[0]; + + $this->backend->moveCalendar('Example', self::UNIT_TEST_USER, self::UNIT_TEST_USER1); + $this->assertCount(0, $this->backend->getCalendarsForUser(self::UNIT_TEST_USER)); + $this->assertCount(1, $this->backend->getCalendarsForUser(self::UNIT_TEST_USER1)); + + $calendarInfoUser1 = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER1)[0]; + $this->assertEquals($calendarInfoUser['id'], $calendarInfoUser1['id']); + $this->assertEquals($calendarInfoUser['uri'], $calendarInfoUser1['uri']); + } + + public function testSearchPrincipal(): void { + $myPublic = <<<EOD +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//dmfs.org//mimedir.icalendar//EN +BEGIN:VTIMEZONE +TZID:Europe/Berlin +X-LIC-LOCATION:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTART;TZID=Europe/Berlin:20160419T130000 +SUMMARY:My Test (public) +CLASS:PUBLIC +TRANSP:OPAQUE +STATUS:CONFIRMED +DTEND;TZID=Europe/Berlin:20160419T140000 +LAST-MODIFIED:20160419T074202Z +DTSTAMP:20160419T074202Z +CREATED:20160419T074202Z +UID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310-1 +END:VEVENT +END:VCALENDAR +EOD; + $myPrivate = <<<EOD +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//dmfs.org//mimedir.icalendar//EN +BEGIN:VTIMEZONE +TZID:Europe/Berlin +X-LIC-LOCATION:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTART;TZID=Europe/Berlin:20160419T130000 +SUMMARY:My Test (private) +CLASS:PRIVATE +TRANSP:OPAQUE +STATUS:CONFIRMED +DTEND;TZID=Europe/Berlin:20160419T140000 +LAST-MODIFIED:20160419T074202Z +DTSTAMP:20160419T074202Z +CREATED:20160419T074202Z +UID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310-2 +END:VEVENT +END:VCALENDAR +EOD; + $myConfidential = <<<EOD +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//dmfs.org//mimedir.icalendar//EN +BEGIN:VTIMEZONE +TZID:Europe/Berlin +X-LIC-LOCATION:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTART;TZID=Europe/Berlin:20160419T130000 +SUMMARY:My Test (confidential) +CLASS:CONFIDENTIAL +TRANSP:OPAQUE +STATUS:CONFIRMED +DTEND;TZID=Europe/Berlin:20160419T140000 +LAST-MODIFIED:20160419T074202Z +DTSTAMP:20160419T074202Z +CREATED:20160419T074202Z +UID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310-3 +END:VEVENT +END:VCALENDAR +EOD; + + $sharerPublic = <<<EOD +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//dmfs.org//mimedir.icalendar//EN +BEGIN:VTIMEZONE +TZID:Europe/Berlin +X-LIC-LOCATION:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTART;TZID=Europe/Berlin:20160419T130000 +SUMMARY:Sharer Test (public) +CLASS:PUBLIC +TRANSP:OPAQUE +STATUS:CONFIRMED +DTEND;TZID=Europe/Berlin:20160419T140000 +LAST-MODIFIED:20160419T074202Z +DTSTAMP:20160419T074202Z +CREATED:20160419T074202Z +UID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310-4 +END:VEVENT +END:VCALENDAR +EOD; + $sharerPrivate = <<<EOD +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//dmfs.org//mimedir.icalendar//EN +BEGIN:VTIMEZONE +TZID:Europe/Berlin +X-LIC-LOCATION:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTART;TZID=Europe/Berlin:20160419T130000 +SUMMARY:Sharer Test (private) +CLASS:PRIVATE +TRANSP:OPAQUE +STATUS:CONFIRMED +DTEND;TZID=Europe/Berlin:20160419T140000 +LAST-MODIFIED:20160419T074202Z +DTSTAMP:20160419T074202Z +CREATED:20160419T074202Z +UID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310-5 +END:VEVENT +END:VCALENDAR +EOD; + $sharerConfidential = <<<EOD +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//dmfs.org//mimedir.icalendar//EN +BEGIN:VTIMEZONE +TZID:Europe/Berlin +X-LIC-LOCATION:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTART;TZID=Europe/Berlin:20160419T130000 +SUMMARY:Sharer Test (confidential) +CLASS:CONFIDENTIAL +TRANSP:OPAQUE +STATUS:CONFIRMED +DTEND;TZID=Europe/Berlin:20160419T140000 +LAST-MODIFIED:20160419T074202Z +DTSTAMP:20160419T074202Z +CREATED:20160419T074202Z +UID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310-6 +END:VEVENT +END:VCALENDAR +EOD; + + $l10n = $this->createMock(IL10N::class); + $l10n + ->expects($this->any()) + ->method('t') + ->willReturnCallback(function ($text, $parameters = []) { + return vsprintf($text, $parameters); + }); + $config = $this->createMock(IConfig::class); + $this->userManager->expects($this->any()) + ->method('userExists') + ->willReturn(true); + $this->groupManager->expects($this->any()) + ->method('groupExists') + ->willReturn(true); + $this->principal->expects(self::atLeastOnce()) + ->method('findByUri') + ->willReturn(self::UNIT_TEST_USER); + + $me = self::UNIT_TEST_USER; + $sharer = self::UNIT_TEST_USER1; + $this->backend->createCalendar($me, 'calendar-uri-me', []); + $this->backend->createCalendar($sharer, 'calendar-uri-sharer', []); + + $myCalendars = $this->backend->getCalendarsForUser($me); + $this->assertCount(1, $myCalendars); + + $sharerCalendars = $this->backend->getCalendarsForUser($sharer); + $this->assertCount(1, $sharerCalendars); + + $logger = $this->createMock(\Psr\Log\LoggerInterface::class); + $sharerCalendar = new Calendar($this->backend, $sharerCalendars[0], $l10n, $config, $logger); + $this->backend->updateShares($sharerCalendar, [ + [ + 'href' => 'principal:' . $me, + 'readOnly' => false, + ], + ], []); + + $this->assertCount(2, $this->backend->getCalendarsForUser($me)); + + $this->backend->createCalendarObject($myCalendars[0]['id'], 'event0.ics', $myPublic); + $this->backend->createCalendarObject($myCalendars[0]['id'], 'event1.ics', $myPrivate); + $this->backend->createCalendarObject($myCalendars[0]['id'], 'event2.ics', $myConfidential); + + $this->backend->createCalendarObject($sharerCalendars[0]['id'], 'event3.ics', $sharerPublic); + $this->backend->createCalendarObject($sharerCalendars[0]['id'], 'event4.ics', $sharerPrivate); + $this->backend->createCalendarObject($sharerCalendars[0]['id'], 'event5.ics', $sharerConfidential); + + $mySearchResults = $this->backend->searchPrincipalUri($me, 'Test', ['VEVENT'], ['SUMMARY'], []); + $sharerSearchResults = $this->backend->searchPrincipalUri($sharer, 'Test', ['VEVENT'], ['SUMMARY'], []); + + $this->assertCount(4, $mySearchResults); + $this->assertCount(3, $sharerSearchResults); + + $this->assertEquals($myPublic, $mySearchResults[0]['calendardata']); + $this->assertEquals($myPrivate, $mySearchResults[1]['calendardata']); + $this->assertEquals($myConfidential, $mySearchResults[2]['calendardata']); + $this->assertEquals($sharerPublic, $mySearchResults[3]['calendardata']); + + $this->assertEquals($sharerPublic, $sharerSearchResults[0]['calendardata']); + $this->assertEquals($sharerPrivate, $sharerSearchResults[1]['calendardata']); + $this->assertEquals($sharerConfidential, $sharerSearchResults[2]['calendardata']); + } + + /** + * @throws \OCP\DB\Exception + * @throws \Sabre\DAV\Exception\BadRequest + */ + public function testPruneOutdatedSyncTokens(): void { + $calendarId = $this->createTestCalendar(); + $changes = $this->backend->getChangesForCalendar($calendarId, '', 1); + $syncToken = $changes['syncToken']; + + $uri = static::getUniqueID('calobj'); + $calData = <<<EOD +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:Nextcloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3ec8 +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:Test Event +DTSTART;VALUE=DATE-TIME:20130912T130000Z +DTEND;VALUE=DATE-TIME:20130912T140000Z +CLASS:PUBLIC +END:VEVENT +END:VCALENDAR +EOD; + + $this->backend->createCalendarObject($calendarId, $uri, $calData); + + // update the card + $calData = <<<'EOD' +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:Nextcloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3ec8 +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:123 Event 🙈 +DTSTART;VALUE=DATE-TIME:20130912T130000Z +DTEND;VALUE=DATE-TIME:20130912T140000Z +ATTENDEE;CN=test:mailto:foo@bar.com +END:VEVENT +END:VCALENDAR +EOD; + $this->backend->updateCalendarObject($calendarId, $uri, $calData); + + // Keep everything + $deleted = $this->backend->pruneOutdatedSyncTokens(0, 0); + self::assertSame(0, $deleted); + + $deleted = $this->backend->pruneOutdatedSyncTokens(0, time()); + // At least one from the object creation and one from the object update + $this->assertGreaterThanOrEqual(2, $deleted); + $changes = $this->backend->getChangesForCalendar($calendarId, $syncToken, 1); + $this->assertEmpty($changes['added']); + $this->assertEmpty($changes['modified']); + $this->assertEmpty($changes['deleted']); + + // Test that objects remain + + // Currently changes are empty + $changes = $this->backend->getChangesForCalendar($calendarId, $syncToken, 100); + $this->assertEquals(0, count($changes['added'] + $changes['modified'] + $changes['deleted'])); + + // Create card + $uri = static::getUniqueID('calobj'); + $calData = <<<EOD +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:Nextcloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20230910T125139Z +UID:47d15e3ec9 +LAST-MODIFIED;VALUE=DATE-TIME:20230910T125139Z +DTSTAMP;VALUE=DATE-TIME:20230910T125139Z +SUMMARY:Test Event +DTSTART;VALUE=DATE-TIME:20230912T130000Z +DTEND;VALUE=DATE-TIME:20230912T140000Z +CLASS:PUBLIC +END:VEVENT +END:VCALENDAR +EOD; + $this->backend->createCalendarObject($calendarId, $uri, $calData); + + // We now have one add + $changes = $this->backend->getChangesForCalendar($calendarId, $syncToken, 100); + $this->assertEquals(1, count($changes['added'])); + $this->assertEmpty($changes['modified']); + $this->assertEmpty($changes['deleted']); + + // update the card + $calData = <<<'EOD' +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:Nextcloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20230910T125139Z +UID:47d15e3ec9 +LAST-MODIFIED;VALUE=DATE-TIME:20230910T125139Z +DTSTAMP;VALUE=DATE-TIME:20230910T125139Z +SUMMARY:123 Event 🙈 +DTSTART;VALUE=DATE-TIME:20230912T130000Z +DTEND;VALUE=DATE-TIME:20230912T140000Z +ATTENDEE;CN=test:mailto:foo@bar.com +END:VEVENT +END:VCALENDAR +EOD; + $this->backend->updateCalendarObject($calendarId, $uri, $calData); + + // One add, one modify, but shortened to modify + $changes = $this->backend->getChangesForCalendar($calendarId, $syncToken, 100); + $this->assertEmpty($changes['added']); + $this->assertEquals(1, count($changes['modified'])); + $this->assertEmpty($changes['deleted']); + + // Delete all but last change + $deleted = $this->backend->pruneOutdatedSyncTokens(1, time()); + $this->assertEquals(1, $deleted); // We had two changes before, now one + + // Only update should remain + $changes = $this->backend->getChangesForCalendar($calendarId, $syncToken, 100); + $this->assertEmpty($changes['added']); + $this->assertEquals(1, count($changes['modified'])); + $this->assertEmpty($changes['deleted']); + + // Check that no crash occurs when prune is called without current changes + $deleted = $this->backend->pruneOutdatedSyncTokens(1, time()); + self::assertSame(0, $deleted); + } + + public function testSearchAndExpandRecurrences(): void { + $calendarId = $this->createTestCalendar(); + $calendarInfo = [ + 'id' => $calendarId, + 'principaluri' => 'user1', + '{http://owncloud.org/ns}owner-principal' => 'user1', + ]; + + $calData = <<<'EOD' +BEGIN:VCALENDAR +PRODID:-//IDN nextcloud.com//Calendar app 4.5.0-alpha.2//EN +CALSCALE:GREGORIAN +VERSION:2.0 +BEGIN:VEVENT +CREATED:20230921T133401Z +DTSTAMP:20230921T133448Z +LAST-MODIFIED:20230921T133448Z +SEQUENCE:2 +UID:7b7d5d12-683c-48ce-973a-b3e1cb0bae2a +DTSTART;VALUE=DATE:20230912 +DTEND;VALUE=DATE:20230913 +STATUS:CONFIRMED +SUMMARY:Daily Event +RRULE:FREQ=DAILY +END:VEVENT +END:VCALENDAR +EOD; + $uri = static::getUniqueID('calobj'); + $this->backend->createCalendarObject($calendarId, $uri, $calData); + + $start = new DateTimeImmutable('2023-09-20T00:00:00Z'); + $end = $start->add(new DateInterval('P14D')); + + $results = $this->backend->search( + $calendarInfo, + '', + [], + [ + 'timerange' => [ + 'start' => $start, + 'end' => $end, + ] + ], + null, + null, + ); + + $this->assertCount(1, $results); + $this->assertCount(14, $results[0]['objects']); + foreach ($results as $result) { + foreach ($result['objects'] as $object) { + $this->assertEquals($object['UID'][0], '7b7d5d12-683c-48ce-973a-b3e1cb0bae2a'); + $this->assertEquals($object['SUMMARY'][0], 'Daily Event'); + $this->assertGreaterThanOrEqual( + $start->getTimestamp(), + $object['DTSTART'][0]->getTimestamp(), + 'Recurrence starting before requested start', + ); + $this->assertLessThanOrEqual( + $end->getTimestamp(), + $object['DTSTART'][0]->getTimestamp(), + 'Recurrence starting after requested end', + ); + } + } + } + + public function testRestoreChanges(): void { + $calendarId = $this->createTestCalendar(); + $uri1 = static::getUniqueID('calobj1') . '.ics'; + $calData = <<<EOD +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:Nextcloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3ec8 +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:Test Event +DTSTART;VALUE=DATE-TIME:20130912T130000Z +DTEND;VALUE=DATE-TIME:20130912T140000Z +CLASS:PUBLIC +END:VEVENT +END:VCALENDAR +EOD; + $this->backend->createCalendarObject($calendarId, $uri1, $calData); + $calData = <<<EOD +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:Nextcloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3ec8 +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:Test Event – UPDATED +DTSTART;VALUE=DATE-TIME:20130912T130000Z +DTEND;VALUE=DATE-TIME:20130912T140000Z +CLASS:PUBLIC +SEQUENCE:1 +END:VEVENT +END:VCALENDAR +EOD; + $this->backend->updateCalendarObject($calendarId, $uri1, $calData); + $uri2 = static::getUniqueID('calobj2') . '.ics'; + $calData = <<<EOD +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:Nextcloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3ec9 +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:Test Event +DTSTART;VALUE=DATE-TIME:20130912T130000Z +DTEND;VALUE=DATE-TIME:20130912T140000Z +CLASS:PUBLIC +END:VEVENT +END:VCALENDAR +EOD; + $this->backend->createCalendarObject($calendarId, $uri2, $calData); + $changesBefore = $this->backend->getChangesForCalendar($calendarId, null, 1); + $this->backend->deleteCalendarObject($calendarId, $uri2); + $uri3 = static::getUniqueID('calobj3') . '.ics'; + $calData = <<<EOD +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:Nextcloud Calendar +BEGIN:VEVENT +CREATED;VALUE=DATE-TIME:20130910T125139Z +UID:47d15e3e10 +LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z +DTSTAMP;VALUE=DATE-TIME:20130910T125139Z +SUMMARY:Test Event +DTSTART;VALUE=DATE-TIME:20130912T130000Z +DTEND;VALUE=DATE-TIME:20130912T140000Z +CLASS:PUBLIC +END:VEVENT +END:VCALENDAR +EOD; + $this->backend->createCalendarObject($calendarId, $uri3, $calData); + $deleteChanges = $this->db->getQueryBuilder(); + $deleteChanges->delete('calendarchanges') + ->where($deleteChanges->expr()->eq('calendarid', $deleteChanges->createNamedParameter($calendarId))); + $deleteChanges->executeStatement(); + + $this->backend->restoreChanges($calendarId); + + $changesAfter = $this->backend->getChangesForCalendar($calendarId, $changesBefore['syncToken'], 1); + self::assertEquals([], $changesAfter['added']); + self::assertEqualsCanonicalizing([$uri1, $uri3], $changesAfter['modified']); + self::assertEquals([$uri2], $changesAfter['deleted']); + } + + public function testSearchWithLimitAndTimeRange(): void { + $calendarId = $this->createTestCalendar(); + $calendarInfo = [ + 'id' => $calendarId, + 'principaluri' => 'user1', + '{http://owncloud.org/ns}owner-principal' => 'user1', + ]; + + $testFiles = [ + __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-1.ics', + __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-2.ics', + __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-3.ics', + __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-4.ics', + __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-5.ics', + __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-6.ics', + ]; + + foreach ($testFiles as $testFile) { + $objectUri = static::getUniqueID('search-limit-timerange-'); + $calendarData = \file_get_contents($testFile); + $this->backend->createCalendarObject($calendarId, $objectUri, $calendarData); + } + + $start = new DateTimeImmutable('2024-05-06T00:00:00Z'); + $end = $start->add(new DateInterval('P14D')); + + $results = $this->backend->search( + $calendarInfo, + '', + [], + [ + 'timerange' => [ + 'start' => $start, + 'end' => $end, + ] + ], + 4, + null, + ); + + $this->assertCount(2, $results); + + $this->assertEquals('Cake Tasting', $results[0]['objects'][0]['SUMMARY'][0]); + $this->assertGreaterThanOrEqual( + $start->getTimestamp(), + $results[0]['objects'][0]['DTSTART'][0]->getTimestamp(), + 'Recurrence starting before requested start', + ); + + $this->assertEquals('Pasta Day', $results[1]['objects'][0]['SUMMARY'][0]); + $this->assertGreaterThanOrEqual( + $start->getTimestamp(), + $results[1]['objects'][0]['DTSTART'][0]->getTimestamp(), + 'Recurrence starting before requested start', + ); + } + + public function testSearchWithLimitAndTimeRangeShouldNotReturnMoreObjectsThenLimit(): void { + $calendarId = $this->createTestCalendar(); + $calendarInfo = [ + 'id' => $calendarId, + 'principaluri' => 'user1', + '{http://owncloud.org/ns}owner-principal' => 'user1', + ]; + + $testFiles = [ + __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-1.ics', + __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-2.ics', + __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-3.ics', + __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-4.ics', + __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-5.ics', + __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-6.ics', + ]; + + foreach ($testFiles as $testFile) { + $objectUri = static::getUniqueID('search-limit-timerange-'); + $calendarData = \file_get_contents($testFile); + $this->backend->createCalendarObject($calendarId, $objectUri, $calendarData); + } + + $start = new DateTimeImmutable('2024-05-06T00:00:00Z'); + $end = $start->add(new DateInterval('P14D')); + + $results = $this->backend->search( + $calendarInfo, + '', + [], + [ + 'timerange' => [ + 'start' => $start, + 'end' => $end, + ] + ], + 1, + null, + ); + + $this->assertCount(1, $results); + + $this->assertEquals('Cake Tasting', $results[0]['objects'][0]['SUMMARY'][0]); + $this->assertGreaterThanOrEqual( + $start->getTimestamp(), + $results[0]['objects'][0]['DTSTART'][0]->getTimestamp(), + 'Recurrence starting before requested start', + ); + } + + public function testSearchWithLimitAndTimeRangeShouldReturnObjectsInTheSameOrder(): void { + $calendarId = $this->createTestCalendar(); + $calendarInfo = [ + 'id' => $calendarId, + 'principaluri' => 'user1', + '{http://owncloud.org/ns}owner-principal' => 'user1', + ]; + + $testFiles = [ + __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-1.ics', + __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-2.ics', + __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-3.ics', + __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-4.ics', + __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-6.ics', // <-- intentional! + __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-5.ics', + ]; + + foreach ($testFiles as $testFile) { + $objectUri = static::getUniqueID('search-limit-timerange-'); + $calendarData = \file_get_contents($testFile); + $this->backend->createCalendarObject($calendarId, $objectUri, $calendarData); + } + + $start = new DateTimeImmutable('2024-05-06T00:00:00Z'); + $end = $start->add(new DateInterval('P14D')); + + $results = $this->backend->search( + $calendarInfo, + '', + [], + [ + 'timerange' => [ + 'start' => $start, + 'end' => $end, + ] + ], + 2, + null, + ); + + $this->assertCount(2, $results); + + $this->assertEquals('Cake Tasting', $results[0]['objects'][0]['SUMMARY'][0]); + $this->assertGreaterThanOrEqual( + $start->getTimestamp(), + $results[0]['objects'][0]['DTSTART'][0]->getTimestamp(), + 'Recurrence starting before requested start', + ); + + $this->assertEquals('Pasta Day', $results[1]['objects'][0]['SUMMARY'][0]); + $this->assertGreaterThanOrEqual( + $start->getTimestamp(), + $results[1]['objects'][0]['DTSTART'][0]->getTimestamp(), + 'Recurrence starting before requested start', + ); + } + + public function testSearchShouldReturnObjectsInTheSameOrderMissingDate(): void { + $calendarId = $this->createTestCalendar(); + $calendarInfo = [ + 'id' => $calendarId, + 'principaluri' => 'user1', + '{http://owncloud.org/ns}owner-principal' => 'user1', + ]; + + $testFiles = [ + __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-6.ics', // <-- intentional! + __DIR__ . '/../test_fixtures/caldav-search-limit-timerange-5.ics', + __DIR__ . '/../test_fixtures/caldav-search-missing-start-1.ics', + __DIR__ . '/../test_fixtures/caldav-search-missing-start-2.ics', + ]; + + foreach ($testFiles as $testFile) { + $objectUri = static::getUniqueID('search-return-objects-in-same-order-'); + $calendarData = \file_get_contents($testFile); + $this->backend->createCalendarObject($calendarId, $objectUri, $calendarData); + } + + $results = $this->backend->search( + $calendarInfo, + '', + [], + [], + 4, + null, + ); + + $this->assertCount(4, $results); + + $this->assertEquals('Cake Tasting', $results[0]['objects'][0]['SUMMARY'][0]); + $this->assertEquals('Pasta Day', $results[1]['objects'][0]['SUMMARY'][0]); + $this->assertEquals('Missing DTSTART 1', $results[2]['objects'][0]['SUMMARY'][0]); + $this->assertEquals('Missing DTSTART 2', $results[3]['objects'][0]['SUMMARY'][0]); + } + + public function testUnshare(): void { + $principalGroup = 'principal:' . self::UNIT_TEST_GROUP; + $principalUser = 'principal:' . self::UNIT_TEST_USER; + + $l10n = $this->createMock(IL10N::class); + $l10n->method('t') + ->willReturnCallback(fn ($text, $parameters = []) => vsprintf($text, $parameters)); + $config = $this->createMock(IConfig::class); + $logger = new NullLogger(); + + $this->principal->expects($this->exactly(2)) + ->method('findByUri') + ->willReturnMap([ + [$principalGroup, '', self::UNIT_TEST_GROUP], + [$principalUser, '', self::UNIT_TEST_USER], + ]); + $this->groupManager->expects($this->once()) + ->method('groupExists') + ->willReturn(true); + $this->dispatcher->expects($this->exactly(2)) + ->method('dispatchTyped'); + + $calendarId = $this->createTestCalendar(); + $calendarInfo = $this->backend->getCalendarById($calendarId); + + $calendar = new Calendar($this->backend, $calendarInfo, $l10n, $config, $logger); + + $this->backend->updateShares( + shareable: $calendar, + add: [ + ['href' => $principalGroup, 'readOnly' => false] + ], + remove: [] + ); + + $this->backend->unshare( + shareable: $calendar, + principal: $principalUser + ); + + } +} diff --git a/apps/dav/tests/unit/CalDAV/CalendarHomeTest.php b/apps/dav/tests/unit/CalDAV/CalendarHomeTest.php new file mode 100644 index 00000000000..e25cc099bd6 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/CalendarHomeTest.php @@ -0,0 +1,347 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\CalDAV; + +use OCA\DAV\AppInfo\PluginManager; +use OCA\DAV\CalDAV\CachedSubscription; +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CalDAV\CalendarHome; +use OCA\DAV\CalDAV\Integration\ExternalCalendar; +use OCA\DAV\CalDAV\Integration\ICalendarProvider; +use OCA\DAV\CalDAV\Outbox; +use OCA\DAV\CalDAV\Trashbin\TrashbinHome; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Sabre\CalDAV\Schedule\Inbox; +use Sabre\CalDAV\Subscriptions\Subscription; +use Sabre\DAV\MkCol; +use Test\TestCase; + +class CalendarHomeTest extends TestCase { + private CalDavBackend&MockObject $backend; + private array $principalInfo = []; + private PluginManager&MockObject $pluginManager; + private LoggerInterface&MockObject $logger; + private CalendarHome $calendarHome; + + protected function setUp(): void { + parent::setUp(); + + $this->backend = $this->createMock(CalDavBackend::class); + $this->principalInfo = [ + 'uri' => 'user-principal-123', + ]; + $this->pluginManager = $this->createMock(PluginManager::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->calendarHome = new CalendarHome( + $this->backend, + $this->principalInfo, + $this->logger, + false + ); + + // Replace PluginManager with our mock + $reflection = new \ReflectionClass($this->calendarHome); + $reflectionProperty = $reflection->getProperty('pluginManager'); + $reflectionProperty->setValue($this->calendarHome, $this->pluginManager); + } + + public function testCreateCalendarValidName(): void { + /** @var MkCol&MockObject $mkCol */ + $mkCol = $this->createMock(MkCol::class); + + $mkCol->method('getResourceType') + ->willReturn(['{DAV:}collection', + '{urn:ietf:params:xml:ns:caldav}calendar']); + $mkCol->method('getRemainingValues') + ->willReturn(['... properties ...']); + + $this->backend->expects(self::once()) + ->method('createCalendar') + ->with('user-principal-123', 'name123', ['... properties ...']); + + $this->calendarHome->createExtendedCollection('name123', $mkCol); + } + + public function testCreateCalendarReservedName(): void { + $this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class); + $this->expectExceptionMessage('The resource you tried to create has a reserved name'); + + /** @var MkCol&MockObject $mkCol */ + $mkCol = $this->createMock(MkCol::class); + + $this->calendarHome->createExtendedCollection('contact_birthdays', $mkCol); + } + + public function testCreateCalendarReservedNameAppGenerated(): void { + $this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class); + $this->expectExceptionMessage('The resource you tried to create has a reserved name'); + + /** @var MkCol&MockObject $mkCol */ + $mkCol = $this->createMock(MkCol::class); + + $this->calendarHome->createExtendedCollection('app-generated--example--foo-1', $mkCol); + } + + public function testGetChildren():void { + $this->backend + ->expects(self::once()) + ->method('getCalendarsForUser') + ->with('user-principal-123') + ->willReturn([]); + + $this->backend + ->expects(self::once()) + ->method('getSubscriptionsForUser') + ->with('user-principal-123') + ->willReturn([]); + + $calendarPlugin1 = $this->createMock(ICalendarProvider::class); + $calendarPlugin1 + ->expects(self::once()) + ->method('fetchAllForCalendarHome') + ->with('user-principal-123') + ->willReturn(['plugin1calendar1', 'plugin1calendar2']); + + $calendarPlugin2 = $this->createMock(ICalendarProvider::class); + $calendarPlugin2 + ->expects(self::once()) + ->method('fetchAllForCalendarHome') + ->with('user-principal-123') + ->willReturn(['plugin2calendar1', 'plugin2calendar2']); + + $this->pluginManager + ->expects(self::once()) + ->method('getCalendarPlugins') + ->with() + ->willReturn([$calendarPlugin1, $calendarPlugin2]); + + $actual = $this->calendarHome->getChildren(); + + $this->assertCount(7, $actual); + $this->assertInstanceOf(Inbox::class, $actual[0]); + $this->assertInstanceOf(Outbox::class, $actual[1]); + $this->assertInstanceOf(TrashbinHome::class, $actual[2]); + $this->assertEquals('plugin1calendar1', $actual[3]); + $this->assertEquals('plugin1calendar2', $actual[4]); + $this->assertEquals('plugin2calendar1', $actual[5]); + $this->assertEquals('plugin2calendar2', $actual[6]); + } + + public function testGetChildNonAppGenerated():void { + $this->backend + ->expects(self::once()) + ->method('getCalendarByUri') + ->with('user-principal-123') + ->willReturn([]); + + $this->backend + ->expects(self::once()) + ->method('getCalendarsForUser') + ->with('user-principal-123') + ->willReturn([]); + + $this->backend + ->expects(self::once()) + ->method('getSubscriptionsForUser') + ->with('user-principal-123') + ->willReturn([]); + + $this->pluginManager + ->expects(self::never()) + ->method('getCalendarPlugins'); + + $this->expectException(\Sabre\DAV\Exception\NotFound::class); + $this->expectExceptionMessage('Node with name \'personal\' could not be found'); + + $this->calendarHome->getChild('personal'); + } + + public function testGetChildAppGenerated():void { + $this->backend + ->expects(self::once()) + ->method('getCalendarByUri') + ->with('user-principal-123') + ->willReturn([]); + + $this->backend + ->expects(self::once()) + ->method('getCalendarsForUser') + ->with('user-principal-123') + ->willReturn([]); + + $this->backend + ->expects(self::once()) + ->method('getSubscriptionsForUser') + ->with('user-principal-123') + ->willReturn([]); + + $calendarPlugin1 = $this->createMock(ICalendarProvider::class); + $calendarPlugin1 + ->expects(self::once()) + ->method('getAppId') + ->with() + ->willReturn('calendar_plugin_1'); + $calendarPlugin1 + ->expects(self::never()) + ->method('hasCalendarInCalendarHome'); + $calendarPlugin1 + ->expects(self::never()) + ->method('getCalendarInCalendarHome'); + + $externalCalendarMock = $this->createMock(ExternalCalendar::class); + + $calendarPlugin2 = $this->createMock(ICalendarProvider::class); + $calendarPlugin2 + ->expects(self::once()) + ->method('getAppId') + ->with() + ->willReturn('calendar_plugin_2'); + $calendarPlugin2 + ->expects(self::once()) + ->method('hasCalendarInCalendarHome') + ->with('user-principal-123', 'calendar-uri-from-backend') + ->willReturn(true); + $calendarPlugin2 + ->expects(self::once()) + ->method('getCalendarInCalendarHome') + ->with('user-principal-123', 'calendar-uri-from-backend') + ->willReturn($externalCalendarMock); + + $this->pluginManager + ->expects(self::once()) + ->method('getCalendarPlugins') + ->with() + ->willReturn([$calendarPlugin1, $calendarPlugin2]); + + $actual = $this->calendarHome->getChild('app-generated--calendar_plugin_2--calendar-uri-from-backend'); + $this->assertEquals($externalCalendarMock, $actual); + } + + public function testGetChildrenSubscriptions(): void { + $this->backend + ->expects(self::once()) + ->method('getCalendarsForUser') + ->with('user-principal-123') + ->willReturn([]); + + $this->backend + ->expects(self::once()) + ->method('getSubscriptionsForUser') + ->with('user-principal-123') + ->willReturn([ + [ + 'id' => 'subscription-1', + 'uri' => 'subscription-1', + 'principaluri' => 'user-principal-123', + 'source' => 'https://localhost/subscription-1', + // A subscription array has actually more properties. + ], + [ + 'id' => 'subscription-2', + 'uri' => 'subscription-2', + 'principaluri' => 'user-principal-123', + 'source' => 'https://localhost/subscription-2', + // A subscription array has actually more properties. + ] + ]); + + /* + * @FIXME: PluginManager should be injected via constructor. + */ + + $pluginManager = $this->createMock(PluginManager::class); + $pluginManager + ->expects(self::once()) + ->method('getCalendarPlugins') + ->with() + ->willReturn([]); + + $calendarHome = new CalendarHome( + $this->backend, + $this->principalInfo, + $this->logger, + false + ); + + $reflection = new \ReflectionClass($calendarHome); + $reflectionProperty = $reflection->getProperty('pluginManager'); + $reflectionProperty->setValue($calendarHome, $pluginManager); + + $actual = $calendarHome->getChildren(); + + $this->assertCount(5, $actual); + $this->assertInstanceOf(Inbox::class, $actual[0]); + $this->assertInstanceOf(Outbox::class, $actual[1]); + $this->assertInstanceOf(TrashbinHome::class, $actual[2]); + $this->assertInstanceOf(Subscription::class, $actual[3]); + $this->assertInstanceOf(Subscription::class, $actual[4]); + } + + public function testGetChildrenCachedSubscriptions(): void { + $this->backend + ->expects(self::once()) + ->method('getCalendarsForUser') + ->with('user-principal-123') + ->willReturn([]); + + $this->backend + ->expects(self::once()) + ->method('getSubscriptionsForUser') + ->with('user-principal-123') + ->willReturn([ + [ + 'id' => 'subscription-1', + 'uri' => 'subscription-1', + 'principaluris' => 'user-principal-123', + 'source' => 'https://localhost/subscription-1', + // A subscription array has actually more properties. + ], + [ + 'id' => 'subscription-2', + 'uri' => 'subscription-2', + 'principaluri' => 'user-principal-123', + 'source' => 'https://localhost/subscription-2', + // A subscription array has actually more properties. + ] + ]); + + /* + * @FIXME: PluginManager should be injected via constructor. + */ + + $pluginManager = $this->createMock(PluginManager::class); + $pluginManager + ->expects(self::once()) + ->method('getCalendarPlugins') + ->with() + ->willReturn([]); + + $calendarHome = new CalendarHome( + $this->backend, + $this->principalInfo, + $this->logger, + true + ); + + $reflection = new \ReflectionClass($calendarHome); + $reflectionProperty = $reflection->getProperty('pluginManager'); + $reflectionProperty->setValue($calendarHome, $pluginManager); + + $actual = $calendarHome->getChildren(); + + $this->assertCount(5, $actual); + $this->assertInstanceOf(Inbox::class, $actual[0]); + $this->assertInstanceOf(Outbox::class, $actual[1]); + $this->assertInstanceOf(TrashbinHome::class, $actual[2]); + $this->assertInstanceOf(CachedSubscription::class, $actual[3]); + $this->assertInstanceOf(CachedSubscription::class, $actual[4]); + } +} diff --git a/apps/dav/tests/unit/CalDAV/CalendarImplTest.php b/apps/dav/tests/unit/CalDAV/CalendarImplTest.php new file mode 100644 index 00000000000..d6a8f3b910e --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/CalendarImplTest.php @@ -0,0 +1,308 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV; + +use Generator; +use OCA\DAV\CalDAV\Auth\CustomPrincipalPlugin; +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CalDAV\Calendar; +use OCA\DAV\CalDAV\CalendarImpl; +use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer; +use OCA\DAV\CalDAV\Schedule\Plugin; +use OCA\DAV\Connector\Sabre\Server; +use OCP\Calendar\Exceptions\CalendarException; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Component\VEvent; +use Sabre\VObject\ITip\Message; +use Sabre\VObject\Reader; + +class CalendarImplTest extends \Test\TestCase { + private Calendar&MockObject $calendar; + private array $calendarInfo; + private CalDavBackend&MockObject $backend; + private CalendarImpl $calendarImpl; + private array $mockExportCollection; + + protected function setUp(): void { + parent::setUp(); + + $this->calendar = $this->createMock(Calendar::class); + $this->calendarInfo = [ + 'id' => 1, + '{DAV:}displayname' => 'user readable name 123', + '{http://apple.com/ns/ical/}calendar-color' => '#AABBCC', + 'uri' => '/this/is/a/uri', + 'principaluri' => 'principal/users/foobar' + ]; + $this->backend = $this->createMock(CalDavBackend::class); + + $this->calendarImpl = new CalendarImpl( + $this->calendar, + $this->calendarInfo, + $this->backend + ); + } + + + public function testGetKey(): void { + $this->assertEquals($this->calendarImpl->getKey(), 1); + } + + public function testGetDisplayname(): void { + $this->assertEquals($this->calendarImpl->getDisplayName(), 'user readable name 123'); + } + + public function testGetDisplayColor(): void { + $this->assertEquals($this->calendarImpl->getDisplayColor(), '#AABBCC'); + } + + public function testSearch(): void { + $this->backend->expects($this->once()) + ->method('search') + ->with($this->calendarInfo, 'abc', ['def'], ['ghi'], 42, 1337) + ->willReturn(['SEARCHRESULTS']); + + $result = $this->calendarImpl->search('abc', ['def'], ['ghi'], 42, 1337); + $this->assertEquals($result, ['SEARCHRESULTS']); + } + + public function testGetPermissionRead(): void { + $this->calendar->expects($this->once()) + ->method('getACL') + ->with() + ->willReturn([ + ['privilege' => '{DAV:}read', 'principal' => 'principal/users/foobar'], + ['privilege' => '{DAV:}read', 'principal' => 'principal/users/other'], + ['privilege' => '{DAV:}write', 'principal' => 'principal/users/other'], + ['privilege' => '{DAV:}all', 'principal' => 'principal/users/other'], + ]); + + $this->assertEquals(1, $this->calendarImpl->getPermissions()); + } + + public function testGetPermissionWrite(): void { + $this->calendar->expects($this->once()) + ->method('getACL') + ->with() + ->willReturn([ + ['privilege' => '{DAV:}write', 'principal' => 'principal/users/foobar'], + ['privilege' => '{DAV:}read', 'principal' => 'principal/users/other'], + ['privilege' => '{DAV:}all', 'principal' => 'principal/users/other'], + ]); + + $this->assertEquals(6, $this->calendarImpl->getPermissions()); + } + + public function testGetPermissionReadWrite(): void { + $this->calendar->expects($this->once()) + ->method('getACL') + ->with() + ->willReturn([ + ['privilege' => '{DAV:}write', 'principal' => 'principal/users/foobar'], + ['privilege' => '{DAV:}read', 'principal' => 'principal/users/foobar'], + ['privilege' => '{DAV:}all', 'principal' => 'principal/users/other'], + ]); + + $this->assertEquals(7, $this->calendarImpl->getPermissions()); + } + + public function testGetPermissionAll(): void { + $this->calendar->expects($this->once()) + ->method('getACL') + ->with() + ->willReturn([ + ['privilege' => '{DAV:}all', 'principal' => 'principal/users/foobar'], + ]); + + $this->assertEquals(31, $this->calendarImpl->getPermissions()); + } + + public function testHandleImipMessage(): void { + $message = <<<EOF +BEGIN:VCALENDAR +PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN +METHOD:REPLY +VERSION:2.0 +BEGIN:VEVENT +ATTENDEE;PARTSTAT=ACCEPTED:mailto:lewis@stardew-tent-living.com +ORGANIZER:mailto:pierre@generalstore.com +UID:aUniqueUid +SEQUENCE:2 +REQUEST-STATUS:2.0;Success +END:VEVENT +END:VCALENDAR +EOF; + + /** @var CustomPrincipalPlugin|MockObject $authPlugin */ + $authPlugin = $this->createMock(CustomPrincipalPlugin::class); + $authPlugin->expects(self::once()) + ->method('setCurrentPrincipal') + ->with($this->calendar->getPrincipalURI()); + + /** @var \Sabre\DAVACL\Plugin|MockObject $aclPlugin */ + $aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class); + + /** @var Plugin|MockObject $schedulingPlugin */ + $schedulingPlugin = $this->createMock(Plugin::class); + $iTipMessage = $this->getITipMessage($message); + $iTipMessage->recipient = 'mailto:lewis@stardew-tent-living.com'; + + $server = $this->createMock(Server::class); + $server->expects($this->any()) + ->method('getPlugin') + ->willReturnMap([ + ['auth', $authPlugin], + ['acl', $aclPlugin], + ['caldav-schedule', $schedulingPlugin] + ]); + $server->expects(self::once()) + ->method('emit'); + + $invitationResponseServer = $this->createPartialMock(InvitationResponseServer::class, ['getServer', 'isExternalAttendee']); + $invitationResponseServer->server = $server; + $invitationResponseServer->expects($this->any()) + ->method('getServer') + ->willReturn($server); + $invitationResponseServer->expects(self::once()) + ->method('isExternalAttendee') + ->willReturn(false); + + $calendarImpl = $this->getMockBuilder(CalendarImpl::class) + ->setConstructorArgs([$this->calendar, $this->calendarInfo, $this->backend]) + ->onlyMethods(['getInvitationResponseServer']) + ->getMock(); + $calendarImpl->expects($this->once()) + ->method('getInvitationResponseServer') + ->willReturn($invitationResponseServer); + + $calendarImpl->handleIMipMessage('filename.ics', $message); + } + + public function testHandleImipMessageNoCalendarUri(): void { + /** @var CustomPrincipalPlugin|MockObject $authPlugin */ + $authPlugin = $this->createMock(CustomPrincipalPlugin::class); + $authPlugin->expects(self::once()) + ->method('setCurrentPrincipal') + ->with($this->calendar->getPrincipalURI()); + unset($this->calendarInfo['uri']); + + /** @var Plugin|MockObject $schedulingPlugin */ + $schedulingPlugin = $this->createMock(Plugin::class); + + /** @var \Sabre\DAVACL\Plugin|MockObject $schedulingPlugin */ + $aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class); + + $server + = $this->createMock(Server::class); + $server->expects($this->any()) + ->method('getPlugin') + ->willReturnMap([ + ['auth', $authPlugin], + ['acl', $aclPlugin], + ['caldav-schedule', $schedulingPlugin] + ]); + $server->expects(self::never()) + ->method('emit'); + + $invitationResponseServer = $this->createPartialMock(InvitationResponseServer::class, ['getServer']); + $invitationResponseServer->server = $server; + $invitationResponseServer->expects($this->any()) + ->method('getServer') + ->willReturn($server); + + $calendarImpl = $this->getMockBuilder(CalendarImpl::class) + ->setConstructorArgs([$this->calendar, $this->calendarInfo, $this->backend]) + ->onlyMethods(['getInvitationResponseServer']) + ->getMock(); + $calendarImpl->expects($this->once()) + ->method('getInvitationResponseServer') + ->willReturn($invitationResponseServer); + + $message = <<<EOF +BEGIN:VCALENDAR +PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN +METHOD:REPLY +VERSION:2.0 +BEGIN:VEVENT +ATTENDEE;PARTSTAT=ACCEPTED:mailto:lewis@stardew-tent-living.com +ORGANIZER:mailto:pierre@generalstore.com +UID:aUniqueUid +SEQUENCE:2 +REQUEST-STATUS:2.0;Success +END:VEVENT +END:VCALENDAR +EOF; + + $this->expectException(CalendarException::class); + $calendarImpl->handleIMipMessage('filename.ics', $message); + } + + private function getITipMessage($calendarData): Message { + $iTipMessage = new Message(); + /** @var VCalendar $vObject */ + $vObject = Reader::read($calendarData); + /** @var VEvent $vEvent */ + $vEvent = $vObject->{'VEVENT'}; + $orgaizer = $vEvent->{'ORGANIZER'}->getValue(); + $attendee = $vEvent->{'ATTENDEE'}->getValue(); + + $iTipMessage->method = $vObject->{'METHOD'}->getValue(); + $iTipMessage->recipient = $orgaizer; + $iTipMessage->sender = $attendee; + $iTipMessage->uid = isset($vEvent->{'UID'}) ? $vEvent->{'UID'}->getValue() : ''; + $iTipMessage->component = 'VEVENT'; + $iTipMessage->sequence = isset($vEvent->{'SEQUENCE'}) ? (int)$vEvent->{'SEQUENCE'}->getValue() : 0; + $iTipMessage->message = $vObject; + return $iTipMessage; + } + + protected function mockExportGenerator(): Generator { + foreach ($this->mockExportCollection as $entry) { + yield $entry; + } + } + + public function testExport(): void { + // Arrange + // construct calendar with a 1 hour event and same start/end time zones + $vCalendar = new VCalendar(); + /** @var VEvent $vEvent */ + $vEvent = $vCalendar->add('VEVENT', []); + $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc'); + $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']); + $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']); + $vEvent->add('SUMMARY', 'Test Recurrence Event'); + $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']); + $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [ + 'CN' => 'Attendee One', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); + // construct data store return + $this->mockExportCollection[] = [ + 'id' => 1, + 'calendardata' => $vCalendar->serialize() + ]; + $this->backend->expects($this->once()) + ->method('exportCalendar') + ->with(1, $this->backend::CALENDAR_TYPE_CALENDAR, null) + ->willReturn($this->mockExportGenerator()); + + // Act + foreach ($this->calendarImpl->export(null) as $entry) { + $exported[] = $entry; + } + + // Assert + $this->assertCount(1, $exported, 'Invalid exported items count'); + } + +} diff --git a/apps/dav/tests/unit/CalDAV/CalendarManagerTest.php b/apps/dav/tests/unit/CalDAV/CalendarManagerTest.php new file mode 100644 index 00000000000..e8159ffe07c --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/CalendarManagerTest.php @@ -0,0 +1,64 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV; + +use OC\Calendar\Manager; +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CalDAV\CalendarImpl; +use OCA\DAV\CalDAV\CalendarManager; +use OCP\Calendar\IManager; +use OCP\IConfig; +use OCP\IL10N; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; + +class CalendarManagerTest extends \Test\TestCase { + private CalDavBackend&MockObject $backend; + private IL10N&MockObject $l10n; + private IConfig&MockObject $config; + private LoggerInterface&MockObject $logger; + private CalendarManager $manager; + + protected function setUp(): void { + parent::setUp(); + $this->backend = $this->createMock(CalDavBackend::class); + $this->l10n = $this->createMock(IL10N::class); + $this->config = $this->createMock(IConfig::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->manager = new CalendarManager( + $this->backend, + $this->l10n, + $this->config, + $this->logger + ); + } + + public function testSetupCalendarProvider(): void { + $this->backend->expects($this->once()) + ->method('getCalendarsForUser') + ->with('principals/users/user123') + ->willReturn([ + ['id' => 123, 'uri' => 'blablub1'], + ['id' => 456, 'uri' => 'blablub2'], + ]); + + /** @var IManager&MockObject $calendarManager */ + $calendarManager = $this->createMock(Manager::class); + $registeredIds = []; + $calendarManager->expects($this->exactly(2)) + ->method('registerCalendar') + ->willReturnCallback(function ($parameter) use (&$registeredIds): void { + $this->assertInstanceOf(CalendarImpl::class, $parameter); + $registeredIds[] = $parameter->getKey(); + }); + + $this->manager->setupCalendarProvider($calendarManager, 'user123'); + + $this->assertEquals(['123','456'], $registeredIds); + } +} diff --git a/apps/dav/tests/unit/CalDAV/CalendarTest.php b/apps/dav/tests/unit/CalDAV/CalendarTest.php new file mode 100644 index 00000000000..b0d3c35bfe7 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/CalendarTest.php @@ -0,0 +1,608 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\CalDAV; + +use OCA\DAV\CalDAV\BirthdayService; +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CalDAV\Calendar; +use OCP\IConfig; +use OCP\IL10N; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Sabre\DAV\PropPatch; +use Sabre\VObject\Reader; +use Test\TestCase; + +class CalendarTest extends TestCase { + protected IL10N&MockObject $l10n; + protected IConfig&MockObject $config; + protected LoggerInterface&MockObject $logger; + + protected function setUp(): void { + parent::setUp(); + $this->l10n = $this->createMock(IL10N::class); + $this->config = $this->createMock(IConfig::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->l10n + ->expects($this->any()) + ->method('t') + ->willReturnCallback(function ($text, $parameters = []) { + return vsprintf($text, $parameters); + }); + } + + public function testDelete(): void { + /** @var CalDavBackend&MockObject $backend */ + $backend = $this->createMock(CalDavBackend::class); + $backend->expects($this->never()) + ->method('updateShares'); + $backend->expects($this->once()) + ->method('unshare'); + + $calendarInfo = [ + '{http://owncloud.org/ns}owner-principal' => 'user1', + 'principaluri' => 'user2', + 'id' => 666, + 'uri' => 'cal', + ]; + $c = new Calendar($backend, $calendarInfo, $this->l10n, $this->config, $this->logger); + $c->delete(); + } + + + public function testDeleteFromGroup(): void { + /** @var CalDavBackend&MockObject $backend */ + $backend = $this->createMock(CalDavBackend::class); + $backend->expects($this->never()) + ->method('updateShares'); + $backend->expects($this->once()) + ->method('unshare'); + + $calendarInfo = [ + '{http://owncloud.org/ns}owner-principal' => 'user1', + 'principaluri' => 'user2', + 'id' => 666, + 'uri' => 'cal', + ]; + $c = new Calendar($backend, $calendarInfo, $this->l10n, $this->config, $this->logger); + $c->delete(); + } + + public function testDeleteOwn(): void { + /** @var CalDavBackend&MockObject $backend */ + $backend = $this->createMock(CalDavBackend::class); + $backend->expects($this->never())->method('updateShares'); + $backend->expects($this->never())->method('getShares'); + + $this->config->expects($this->never())->method('setUserValue'); + + $backend->expects($this->once())->method('deleteCalendar') + ->with(666); + + $calendarInfo = [ + '{http://owncloud.org/ns}owner-principal' => 'user1', + 'principaluri' => 'user1', + 'id' => 666, + 'uri' => 'cal', + ]; + $c = new Calendar($backend, $calendarInfo, $this->l10n, $this->config, $this->logger); + $c->delete(); + } + + public function testDeleteBirthdayCalendar(): void { + /** @var CalDavBackend&MockObject $backend */ + $backend = $this->createMock(CalDavBackend::class); + $backend->expects($this->once())->method('deleteCalendar') + ->with(666); + + $this->config->expects($this->once()) + ->method('setUserValue') + ->with('user1', 'dav', 'generateBirthdayCalendar', 'no'); + + $calendarInfo = [ + '{http://owncloud.org/ns}owner-principal' => 'principals/users/user1', + 'principaluri' => 'principals/users/user1', + 'id' => 666, + 'uri' => 'contact_birthdays', + '{DAV:}displayname' => 'Test', + ]; + + $c = new Calendar($backend, $calendarInfo, $this->l10n, $this->config, $this->logger); + $c->delete(); + } + + public static function dataPropPatch(): array { + return [ + ['user1', 'user2', [], true], + ['user1', 'user2', [ + '{http://owncloud.org/ns}calendar-enabled' => true, + ], true], + ['user1', 'user2', [ + '{DAV:}displayname' => true, + ], true], + ['user1', 'user2', [ + '{DAV:}displayname' => true, + '{http://owncloud.org/ns}calendar-enabled' => true, + ], true], + ['user1', 'user1', [], false], + ['user1', 'user1', [ + '{http://owncloud.org/ns}calendar-enabled' => true, + ], false], + ['user1', 'user1', [ + '{DAV:}displayname' => true, + ], false], + ['user1', 'user1', [ + '{DAV:}displayname' => true, + '{http://owncloud.org/ns}calendar-enabled' => true, + ], false], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataPropPatch')] + public function testPropPatch(string $ownerPrincipal, string $principalUri, array $mutations, bool $shared): void { + /** @var CalDavBackend&MockObject $backend */ + $backend = $this->createMock(CalDavBackend::class); + $calendarInfo = [ + '{http://owncloud.org/ns}owner-principal' => $ownerPrincipal, + 'principaluri' => $principalUri, + 'id' => 666, + 'uri' => 'default' + ]; + $c = new Calendar($backend, $calendarInfo, $this->l10n, $this->config, $this->logger); + $propPatch = new PropPatch($mutations); + + if (!$shared) { + $backend->expects($this->once()) + ->method('updateCalendar') + ->with(666, $propPatch); + } + $c->propPatch($propPatch); + $this->addToAssertionCount(1); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providesReadOnlyInfo')] + public function testAcl($expectsWrite, $readOnlyValue, $hasOwnerSet, $uri = 'default'): void { + /** @var CalDavBackend&MockObject $backend */ + $backend = $this->createMock(CalDavBackend::class); + $backend->expects($this->any())->method('applyShareAcl')->willReturnArgument(1); + $calendarInfo = [ + 'principaluri' => 'user2', + 'id' => 666, + 'uri' => $uri + ]; + $calendarInfo['{DAV:}displayname'] = 'Test'; + if (!is_null($readOnlyValue)) { + $calendarInfo['{http://owncloud.org/ns}read-only'] = $readOnlyValue; + } + if ($hasOwnerSet) { + $calendarInfo['{http://owncloud.org/ns}owner-principal'] = 'user1'; + } + $c = new Calendar($backend, $calendarInfo, $this->l10n, $this->config, $this->logger); + $acl = $c->getACL(); + $childAcl = $c->getChildACL(); + + $expectedAcl = [[ + 'privilege' => '{DAV:}read', + 'principal' => $hasOwnerSet ? 'user1' : 'user2', + 'protected' => true + ], [ + 'privilege' => '{DAV:}read', + 'principal' => ($hasOwnerSet ? 'user1' : 'user2') . '/calendar-proxy-write', + 'protected' => true, + ], [ + 'privilege' => '{DAV:}read', + 'principal' => ($hasOwnerSet ? 'user1' : 'user2') . '/calendar-proxy-read', + 'protected' => true, + ]]; + if ($uri === BirthdayService::BIRTHDAY_CALENDAR_URI) { + $expectedAcl[] = [ + 'privilege' => '{DAV:}write-properties', + 'principal' => $hasOwnerSet ? 'user1' : 'user2', + 'protected' => true + ]; + $expectedAcl[] = [ + 'privilege' => '{DAV:}write-properties', + 'principal' => ($hasOwnerSet ? 'user1' : 'user2') . '/calendar-proxy-write', + 'protected' => true + ]; + } else { + $expectedAcl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $hasOwnerSet ? 'user1' : 'user2', + 'protected' => true + ]; + $expectedAcl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => ($hasOwnerSet ? 'user1' : 'user2') . '/calendar-proxy-write', + 'protected' => true + ]; + } + + $expectedAcl[] = [ + 'privilege' => '{DAV:}write-properties', + 'principal' => ($hasOwnerSet ? 'user1' : 'user2') . '/calendar-proxy-read', + 'protected' => true + ]; + + if ($hasOwnerSet) { + $expectedAcl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => 'user2', + 'protected' => true + ]; + if ($expectsWrite) { + $expectedAcl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => 'user2', + 'protected' => true + ]; + } else { + $expectedAcl[] = [ + 'privilege' => '{DAV:}write-properties', + 'principal' => 'user2', + 'protected' => true + ]; + } + } + $this->assertEquals($expectedAcl, $acl); + $this->assertEquals($expectedAcl, $childAcl); + } + + public static function providesReadOnlyInfo(): array { + return [ + 'read-only property not set' => [true, null, true], + 'read-only property is false' => [true, false, true], + 'read-only property is true' => [false, true, true], + 'read-only property not set and no owner' => [true, null, false], + 'read-only property is false and no owner' => [true, false, false], + 'read-only property is true and no owner' => [false, true, false], + 'birthday calendar' => [false, false, false, BirthdayService::BIRTHDAY_CALENDAR_URI] + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providesConfidentialClassificationData')] + public function testPrivateClassification(int $expectedChildren, bool $isShared): void { + $calObject0 = ['uri' => 'event-0', 'classification' => CalDavBackend::CLASSIFICATION_PUBLIC]; + $calObject1 = ['uri' => 'event-1', 'classification' => CalDavBackend::CLASSIFICATION_CONFIDENTIAL]; + $calObject2 = ['uri' => 'event-2', 'classification' => CalDavBackend::CLASSIFICATION_PRIVATE]; + + /** @var CalDavBackend&MockObject $backend */ + $backend = $this->createMock(CalDavBackend::class); + $backend->expects($this->any())->method('getCalendarObjects')->willReturn([ + $calObject0, $calObject1, $calObject2 + ]); + $backend->expects($this->any())->method('getMultipleCalendarObjects') + ->with(666, ['event-0', 'event-1', 'event-2']) + ->willReturn([ + $calObject0, $calObject1, $calObject2 + ]); + $backend->expects($this->any())->method('getCalendarObject') + ->willReturn($calObject2)->with(666, 'event-2'); + $backend->expects($this->any())->method('applyShareAcl')->willReturnArgument(1); + + $calendarInfo = [ + 'principaluri' => 'user2', + 'id' => 666, + 'uri' => 'cal', + ]; + + if ($isShared) { + $calendarInfo['{http://owncloud.org/ns}owner-principal'] = 'user1'; + } + $c = new Calendar($backend, $calendarInfo, $this->l10n, $this->config, $this->logger); + $children = $c->getChildren(); + $this->assertEquals($expectedChildren, count($children)); + $children = $c->getMultipleChildren(['event-0', 'event-1', 'event-2']); + $this->assertEquals($expectedChildren, count($children)); + + $this->assertEquals(!$isShared, $c->childExists('event-2')); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providesConfidentialClassificationData')] + public function testConfidentialClassification(int $expectedChildren, bool $isShared): void { + $start = '20160609'; + $end = '20160610'; + + $calData = <<<EOD +BEGIN:VCALENDAR +PRODID:-//ownCloud calendar v1.2.2 +BEGIN:VEVENT +CREATED:20160602T133732 +DTSTAMP:20160602T133732 +LAST-MODIFIED:20160602T133732 +UID:wej2z68l9h +SUMMARY:Test Event +LOCATION:Somewhere ... +ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CUTYPE=INDIVIDUAL;CN=de + epdiver:MAILTO:thomas.mueller@tmit.eu +ORGANIZER;CN=deepdiver:MAILTO:thomas.mueller@tmit.eu +DESCRIPTION:maybe .... +DTSTART;TZID=Europe/Berlin;VALUE=DATE:$start +DTEND;TZID=Europe/Berlin;VALUE=DATE:$end +RRULE:FREQ=DAILY +BEGIN:VALARM +ACTION:AUDIO +TRIGGER:-PT15M +END:VALARM +END:VEVENT +BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:DAYLIGHT +DTSTART:19810329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +TZNAME:MESZ +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +END:DAYLIGHT +BEGIN:STANDARD +DTSTART:19961027T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +TZNAME:MEZ +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +END:STANDARD +END:VTIMEZONE +END:VCALENDAR +EOD; + + $calObject0 = ['uri' => 'event-0', 'classification' => CalDavBackend::CLASSIFICATION_PUBLIC]; + $calObject1 = ['uri' => 'event-1', 'classification' => CalDavBackend::CLASSIFICATION_CONFIDENTIAL, 'calendardata' => $calData]; + $calObject2 = ['uri' => 'event-2', 'classification' => CalDavBackend::CLASSIFICATION_PRIVATE]; + + /** @var CalDavBackend&MockObject $backend */ + $backend = $this->createMock(CalDavBackend::class); + $backend->expects($this->any())->method('getCalendarObjects')->willReturn([ + $calObject0, $calObject1, $calObject2 + ]); + $backend->expects($this->any())->method('getMultipleCalendarObjects') + ->with(666, ['event-0', 'event-1', 'event-2']) + ->willReturn([ + $calObject0, $calObject1, $calObject2 + ]); + $backend->expects($this->any())->method('getCalendarObject') + ->willReturn($calObject1)->with(666, 'event-1'); + $backend->expects($this->any())->method('applyShareAcl')->willReturnArgument(1); + + $calendarInfo = [ + '{http://owncloud.org/ns}owner-principal' => $isShared ? 'user1' : 'user2', + 'principaluri' => 'user2', + 'id' => 666, + 'uri' => 'cal', + ]; + $c = new Calendar($backend, $calendarInfo, $this->l10n, $this->config, $this->logger); + + $this->assertEquals(count($c->getChildren()), $expectedChildren); + + // test private event + $privateEvent = $c->getChild('event-1'); + $calData = $privateEvent->get(); + $event = Reader::read($calData); + + $this->assertEquals($start, $event->VEVENT->DTSTART->getValue()); + $this->assertEquals($end, $event->VEVENT->DTEND->getValue()); + + if ($isShared) { + $this->assertEquals('Busy', $event->VEVENT->SUMMARY->getValue()); + $this->assertArrayNotHasKey('ATTENDEE', $event->VEVENT); + $this->assertArrayNotHasKey('LOCATION', $event->VEVENT); + $this->assertArrayNotHasKey('DESCRIPTION', $event->VEVENT); + $this->assertArrayNotHasKey('ORGANIZER', $event->VEVENT); + } else { + $this->assertEquals('Test Event', $event->VEVENT->SUMMARY->getValue()); + } + + // Test l10n + $l10n = $this->createMock(IL10N::class); + if ($isShared) { + $l10n->expects($this->once()) + ->method('t') + ->with('Busy') + ->willReturn('Translated busy'); + } else { + $l10n->expects($this->never()) + ->method('t'); + } + $c = new Calendar($backend, $calendarInfo, $l10n, $this->config, $this->logger); + + $calData = $c->getChild('event-1')->get(); + $event = Reader::read($calData); + + if ($isShared) { + $this->assertEquals('Translated busy', $event->VEVENT->SUMMARY->getValue()); + } else { + $this->assertEquals('Test Event', $event->VEVENT->SUMMARY->getValue()); + } + } + + public static function providesConfidentialClassificationData(): array { + return [ + [3, false], + [2, true] + ]; + } + + public function testRemoveVAlarms(): void { + $publicObjectData = <<<EOD +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Nextcloud calendar v1.5.6 +CALSCALE:GREGORIAN +BEGIN:VEVENT +CREATED:20171022T125130 +DTSTAMP:20171022T125130 +LAST-MODIFIED:20171022T125130 +UID:PPL24TH8UGOWE94XET87ER +SUMMARY:Foo bar blub +CLASS:PUBLIC +STATUS:CONFIRMED +DTSTART;VALUE=DATE:20171024 +DTEND;VALUE=DATE:20171025 +BEGIN:VALARM +ACTION:AUDIO +TRIGGER:-PT15M +END:VALARM +END:VEVENT +END:VCALENDAR + +EOD; + + $confidentialObjectData = <<<EOD +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Nextcloud calendar v1.5.6 +CALSCALE:GREGORIAN +BEGIN:VEVENT +CREATED:20171022T125130 +DTSTAMP:20171022T125130 +LAST-MODIFIED:20171022T125130 +UID:PPL24TH8UGOWE94XET87ER +SUMMARY:Foo bar blub +CLASS:CONFIDENTIAL +STATUS:CONFIRMED +DTSTART;VALUE=DATE:20171024 +DTEND;VALUE=DATE:20171025 +BEGIN:VALARM +ACTION:AUDIO +TRIGGER:-PT15M +END:VALARM +END:VEVENT +END:VCALENDAR + +EOD; + + $publicObjectDataWithoutVAlarm = <<<EOD +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Nextcloud calendar v1.5.6 +CALSCALE:GREGORIAN +BEGIN:VEVENT +CREATED:20171022T125130 +DTSTAMP:20171022T125130 +LAST-MODIFIED:20171022T125130 +UID:PPL24TH8UGOWE94XET87ER +SUMMARY:Foo bar blub +CLASS:PUBLIC +STATUS:CONFIRMED +DTSTART;VALUE=DATE:20171024 +DTEND;VALUE=DATE:20171025 +END:VEVENT +END:VCALENDAR + +EOD; + + $confidentialObjectCleaned = <<<EOD +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Nextcloud calendar v1.5.6 +CALSCALE:GREGORIAN +BEGIN:VEVENT +CREATED:20171022T125130 +UID:PPL24TH8UGOWE94XET87ER +SUMMARY:Busy +CLASS:CONFIDENTIAL +DTSTART;VALUE=DATE:20171024 +DTEND;VALUE=DATE:20171025 +END:VEVENT +END:VCALENDAR + +EOD; + + + + $publicObject = ['uri' => 'event-0', + 'classification' => CalDavBackend::CLASSIFICATION_PUBLIC, + 'calendardata' => $publicObjectData]; + + $confidentialObject = ['uri' => 'event-1', + 'classification' => CalDavBackend::CLASSIFICATION_CONFIDENTIAL, + 'calendardata' => $confidentialObjectData]; + + /** @var CalDavBackend&MockObject $backend */ + $backend = $this->createMock(CalDavBackend::class); + $backend->expects($this->any()) + ->method('getCalendarObjects') + ->willReturn([$publicObject, $confidentialObject]); + + $backend->expects($this->any()) + ->method('getMultipleCalendarObjects') + ->with(666, ['event-0', 'event-1']) + ->willReturn([$publicObject, $confidentialObject]); + + $backend->expects($this->any()) + ->method('getCalendarObject') + ->willReturnCallback(function ($cId, $uri) use ($publicObject, $confidentialObject) { + switch ($uri) { + case 'event-0': + return $publicObject; + + case 'event-1': + return $confidentialObject; + + default: + throw new \Exception('unexpected uri'); + } + }); + + $backend->expects($this->any()) + ->method('applyShareAcl') + ->willReturnArgument(1); + + $calendarInfoOwner = [ + '{http://owncloud.org/ns}owner-principal' => 'user1', + 'principaluri' => 'user1', + 'id' => 666, + 'uri' => 'cal', + ]; + $calendarInfoSharedRW = [ + '{http://owncloud.org/ns}owner-principal' => 'user1', + 'principaluri' => 'user2', + 'id' => 666, + 'uri' => 'cal', + ]; + $calendarInfoSharedRO = [ + '{http://owncloud.org/ns}owner-principal' => 'user1', + '{http://owncloud.org/ns}read-only' => true, + 'principaluri' => 'user2', + 'id' => 666, + 'uri' => 'cal', + ]; + + $ownerCalendar = new Calendar($backend, $calendarInfoOwner, $this->l10n, $this->config, $this->logger); + $rwCalendar = new Calendar($backend, $calendarInfoSharedRW, $this->l10n, $this->config, $this->logger); + $roCalendar = new Calendar($backend, $calendarInfoSharedRO, $this->l10n, $this->config, $this->logger); + + $this->assertCount(2, $ownerCalendar->getChildren()); + $this->assertCount(2, $rwCalendar->getChildren()); + $this->assertCount(2, $roCalendar->getChildren()); + + // calendar data shall not be altered for the owner + $this->assertEquals($ownerCalendar->getChild('event-0')->get(), $publicObjectData); + $this->assertEquals($ownerCalendar->getChild('event-1')->get(), $confidentialObjectData); + + // valarms shall not be removed for read-write shares + $this->assertEquals( + $this->fixLinebreak($rwCalendar->getChild('event-0')->get()), + $this->fixLinebreak($publicObjectData)); + $this->assertEquals( + $this->fixLinebreak($rwCalendar->getChild('event-1')->get()), + $this->fixLinebreak($confidentialObjectCleaned)); + + // valarms shall be removed for read-only shares + $this->assertEquals( + $this->fixLinebreak($roCalendar->getChild('event-0')->get()), + $this->fixLinebreak($publicObjectDataWithoutVAlarm)); + $this->assertEquals( + $this->fixLinebreak($roCalendar->getChild('event-1')->get()), + $this->fixLinebreak($confidentialObjectCleaned)); + } + + private function fixLinebreak(string $str): string { + return preg_replace('~(*BSR_ANYCRLF)\R~', "\r\n", $str); + } +} diff --git a/apps/dav/tests/unit/CalDAV/DefaultCalendarValidatorTest.php b/apps/dav/tests/unit/CalDAV/DefaultCalendarValidatorTest.php new file mode 100644 index 00000000000..194009827da --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/DefaultCalendarValidatorTest.php @@ -0,0 +1,171 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Tests\unit\CalDAV; + +use OCA\DAV\CalDAV\Calendar; +use OCA\DAV\CalDAV\DefaultCalendarValidator; +use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet; +use Test\TestCase; + +class DefaultCalendarValidatorTest extends TestCase { + private DefaultCalendarValidator $validator; + + protected function setUp(): void { + parent::setUp(); + + $this->validator = new DefaultCalendarValidator(); + } + + public function testValidateScheduleDefaultCalendar(): void { + $node = $this->createMock(Calendar::class); + $node->expects(self::once()) + ->method('isSubscription') + ->willReturn(false); + $node->expects(self::once()) + ->method('canWrite') + ->willReturn(true); + $node->expects(self::once()) + ->method('isShared') + ->willReturn(false); + $node->expects(self::once()) + ->method('isDeleted') + ->willReturn(false); + $node->expects(self::once()) + ->method('getProperties') + ->willReturn([ + '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VEVENT']), + ]); + + $this->validator->validateScheduleDefaultCalendar($node); + } + + public function testValidateScheduleDefaultCalendarWithEmptyProperties(): void { + $node = $this->createMock(Calendar::class); + $node->expects(self::once()) + ->method('isSubscription') + ->willReturn(false); + $node->expects(self::once()) + ->method('canWrite') + ->willReturn(true); + $node->expects(self::once()) + ->method('isShared') + ->willReturn(false); + $node->expects(self::once()) + ->method('isDeleted') + ->willReturn(false); + $node->expects(self::once()) + ->method('getProperties') + ->willReturn([]); + + $this->validator->validateScheduleDefaultCalendar($node); + } + + public function testValidateScheduleDefaultCalendarWithSubscription(): void { + $node = $this->createMock(Calendar::class); + $node->expects(self::once()) + ->method('isSubscription') + ->willReturn(true); + $node->expects(self::never()) + ->method('canWrite'); + $node->expects(self::never()) + ->method('isShared'); + $node->expects(self::never()) + ->method('isDeleted'); + $node->expects(self::never()) + ->method('getProperties'); + + $this->expectException(\Sabre\DAV\Exception::class); + $this->validator->validateScheduleDefaultCalendar($node); + } + + public function testValidateScheduleDefaultCalendarWithoutWrite(): void { + $node = $this->createMock(Calendar::class); + $node->expects(self::once()) + ->method('isSubscription') + ->willReturn(false); + $node->expects(self::once()) + ->method('canWrite') + ->willReturn(false); + $node->expects(self::never()) + ->method('isShared'); + $node->expects(self::never()) + ->method('isDeleted'); + $node->expects(self::never()) + ->method('getProperties'); + + $this->expectException(\Sabre\DAV\Exception::class); + $this->validator->validateScheduleDefaultCalendar($node); + } + + public function testValidateScheduleDefaultCalendarWithShared(): void { + $node = $this->createMock(Calendar::class); + $node->expects(self::once()) + ->method('isSubscription') + ->willReturn(false); + $node->expects(self::once()) + ->method('canWrite') + ->willReturn(true); + $node->expects(self::once()) + ->method('isShared') + ->willReturn(true); + $node->expects(self::never()) + ->method('isDeleted'); + $node->expects(self::never()) + ->method('getProperties'); + + $this->expectException(\Sabre\DAV\Exception::class); + $this->validator->validateScheduleDefaultCalendar($node); + } + + public function testValidateScheduleDefaultCalendarWithDeleted(): void { + $node = $this->createMock(Calendar::class); + $node->expects(self::once()) + ->method('isSubscription') + ->willReturn(false); + $node->expects(self::once()) + ->method('canWrite') + ->willReturn(true); + $node->expects(self::once()) + ->method('isShared') + ->willReturn(false); + $node->expects(self::once()) + ->method('isDeleted') + ->willReturn(true); + $node->expects(self::never()) + ->method('getProperties'); + + $this->expectException(\Sabre\DAV\Exception::class); + $this->validator->validateScheduleDefaultCalendar($node); + } + + public function testValidateScheduleDefaultCalendarWithoutVeventSupport(): void { + $node = $this->createMock(Calendar::class); + $node->expects(self::once()) + ->method('isSubscription') + ->willReturn(false); + $node->expects(self::once()) + ->method('canWrite') + ->willReturn(true); + $node->expects(self::once()) + ->method('isShared') + ->willReturn(false); + $node->expects(self::once()) + ->method('isDeleted') + ->willReturn(false); + $node->expects(self::once()) + ->method('getProperties') + ->willReturn([ + '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO']), + ]); + + $this->expectException(\Sabre\DAV\Exception::class); + $this->validator->validateScheduleDefaultCalendar($node); + } +} diff --git a/apps/dav/tests/unit/CalDAV/EventComparisonServiceTest.php b/apps/dav/tests/unit/CalDAV/EventComparisonServiceTest.php new file mode 100644 index 00000000000..90b6f9ec0db --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/EventComparisonServiceTest.php @@ -0,0 +1,188 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Tests\unit\CalDAV; + +use OCA\DAV\CalDAV\EventComparisonService; +use Sabre\VObject\Component\VCalendar; +use Test\TestCase; + +class EventComparisonServiceTest extends TestCase { + private EventComparisonService $eventComparisonService; + + protected function setUp(): void { + $this->eventComparisonService = new EventComparisonService(); + } + + public function testNoModifiedEvent(): void { + $vCalendarOld = new VCalendar(); + $vCalendarNew = new VCalendar(); + + $vEventOld = $vCalendarOld->add('VEVENT', [ + 'UID' => 'uid-1234', + 'LAST-MODIFIED' => 123456, + 'SEQUENCE' => 2, + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00'), + 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z', + ]); + $vEventOld->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $vEventOld->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); + + $vEventNew = $vCalendarNew->add('VEVENT', [ + 'UID' => 'uid-1234', + 'LAST-MODIFIED' => 123456, + 'SEQUENCE' => 2, + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00'), + 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z', + ]); + $vEventNew->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $vEventNew->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); + + $result = $this->eventComparisonService->findModified($vCalendarNew, $vCalendarOld); + $this->assertEmpty($result['old']); + $this->assertEmpty($result['new']); + } + + public function testNewEvent(): void { + $vCalendarOld = null; + $vCalendarNew = new VCalendar(); + + $vEventNew = $vCalendarNew->add('VEVENT', [ + 'UID' => 'uid-1234', + 'LAST-MODIFIED' => 123456, + 'SEQUENCE' => 2, + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00'), + 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z', + ]); + $vEventNew->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $vEventNew->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); + + $result = $this->eventComparisonService->findModified($vCalendarNew, $vCalendarOld); + $this->assertNull($result['old']); + $this->assertEquals([$vEventNew], $result['new']); + } + + public function testModifiedUnmodifiedEvent(): void { + $vCalendarOld = new VCalendar(); + $vCalendarNew = new VCalendar(); + + $vEventOld1 = $vCalendarOld->add('VEVENT', [ + 'UID' => 'uid-1234', + 'LAST-MODIFIED' => 123456, + 'SEQUENCE' => 2, + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00'), + ]); + $vEventOld1->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $vEventOld1->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); + + $vEventOld2 = $vCalendarOld->add('VEVENT', [ + 'UID' => 'uid-1235', + 'LAST-MODIFIED' => 123456, + 'SEQUENCE' => 2, + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00'), + ]); + $vEventOld2->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $vEventOld2->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); + + $vEventNew1 = $vCalendarNew->add('VEVENT', [ + 'UID' => 'uid-1234', + 'LAST-MODIFIED' => 123456, + 'SEQUENCE' => 2, + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00'), + ]); + $vEventNew1->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $vEventNew1->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); + + $vEventNew2 = $vCalendarNew->add('VEVENT', [ + 'UID' => 'uid-1235', + 'LAST-MODIFIED' => 123457, + 'SEQUENCE' => 3, + 'SUMMARY' => 'Fellowship meeting 2', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00'), + ]); + $vEventNew2->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $vEventNew2->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); + + $result = $this->eventComparisonService->findModified($vCalendarNew, $vCalendarOld); + $this->assertEquals([$vEventOld2], $result['old']); + $this->assertEquals([$vEventNew2], $result['new']); + } + + // First test to certify fix for issue nextcloud/server#41084 + public function testSequenceNumberIncrementDetectedForFirstModificationToEventWithoutZeroInit(): void { + $vCalendarOld = new VCalendar(); + $vCalendarNew = new VCalendar(); + + $vEventOld = $vCalendarOld->add('VEVENT', [ + 'UID' => 'uid-1234', + 'LAST-MODIFIED' => 123456, + // 'SEQUENCE' => 0, // sequence number may not be set to zero during event creation and instead fully omitted + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00'), + 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z', + ]); + $vEventOld->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $vEventOld->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); + + $vEventNew = $vCalendarNew->add('VEVENT', [ + 'UID' => 'uid-1234', + 'LAST-MODIFIED' => 123456, + 'SEQUENCE' => 1, + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00'), + 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z', + ]); + $vEventNew->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $vEventNew->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); + + $result = $this->eventComparisonService->findModified($vCalendarNew, $vCalendarOld); + $this->assertEquals([$vEventOld], $result['old']); + $this->assertEquals([$vEventNew], $result['new']); + } + + // Second test to certify fix for issue nextcloud/server#41084 + public function testSequenceNumberIncrementDetectedForFirstModificationToEventWithZeroInit(): void { + $vCalendarOld = new VCalendar(); + $vCalendarNew = new VCalendar(); + + $vEventOld = $vCalendarOld->add('VEVENT', [ + 'UID' => 'uid-1234', + 'LAST-MODIFIED' => 123456, + 'SEQUENCE' => 0, + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00'), + 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z', + ]); + $vEventOld->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $vEventOld->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); + + $vEventNew = $vCalendarNew->add('VEVENT', [ + 'UID' => 'uid-1234', + 'LAST-MODIFIED' => 123456, + 'SEQUENCE' => 1, + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00'), + 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z', + ]); + $vEventNew->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $vEventNew->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); + + $result = $this->eventComparisonService->findModified($vCalendarNew, $vCalendarOld); + $this->assertEquals([$vEventOld], $result['old']); + $this->assertEquals([$vEventNew], $result['new']); + } + + +} diff --git a/apps/dav/tests/unit/CalDAV/EventReaderTest.php b/apps/dav/tests/unit/CalDAV/EventReaderTest.php new file mode 100644 index 00000000000..3bd4f9d85c2 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/EventReaderTest.php @@ -0,0 +1,1087 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Tests\unit\CalDAV; + +use DateTimeZone; +use OCA\DAV\CalDAV\EventReader; +use Sabre\VObject\Component\VCalendar; +use Test\TestCase; + +class EventReaderTest extends TestCase { + + private VCalendar $vCalendar1a; + private VCalendar $vCalendar1b; + private VCalendar $vCalendar1c; + private VCalendar $vCalendar1d; + private VCalendar $vCalendar1e; + private VCalendar $vCalendar2; + private VCalendar $vCalendar3; + + protected function setUp(): void { + + parent::setUp(); + + // construct calendar with a 1 hour event and same start/end time zones + $this->vCalendar1a = new VCalendar(); + $vEvent = $this->vCalendar1a->add('VEVENT', []); + $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc'); + $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']); + $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']); + $vEvent->add('SUMMARY', 'Test Recurrence Event'); + $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']); + $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [ + 'CN' => 'Attendee One', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); + + // construct calendar with a 1 hour event and different start/end time zones + $this->vCalendar1b = new VCalendar(); + $vEvent = $this->vCalendar1b->add('VEVENT', []); + $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc'); + $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']); + $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Vancouver']); + $vEvent->add('SUMMARY', 'Test Recurrence Event'); + $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']); + $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [ + 'CN' => 'Attendee One', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); + + // construct calendar with a 1 hour event and global time zone + $this->vCalendar1c = new VCalendar(); + // time zone component + $vTimeZone = $this->vCalendar1c->add('VTIMEZONE'); + $vTimeZone->add('TZID', 'America/Toronto'); + // event component + $vEvent = $this->vCalendar1c->add('VEVENT', []); + $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc'); + $vEvent->add('DTSTART', '20240701T080000'); + $vEvent->add('DTEND', '20240701T090000'); + $vEvent->add('SUMMARY', 'Test Recurrence Event'); + $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']); + $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [ + 'CN' => 'Attendee One', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); + + // construct calendar with a 1 hour event and no time zone + $this->vCalendar1d = new VCalendar(); + $vEvent = $this->vCalendar1d->add('VEVENT', []); + $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc'); + $vEvent->add('DTSTART', '20240701T080000'); + $vEvent->add('DTEND', '20240701T090000'); + $vEvent->add('SUMMARY', 'Test Recurrence Event'); + $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']); + $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [ + 'CN' => 'Attendee One', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); + + // construct calendar with a 1 hour event and Microsoft time zone + $this->vCalendar1e = new VCalendar(); + $vEvent = $this->vCalendar1e->add('VEVENT', []); + $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc'); + $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'Eastern Standard Time']); + $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'Eastern Standard Time']); + $vEvent->add('SUMMARY', 'Test Recurrence Event'); + $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']); + $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [ + 'CN' => 'Attendee One', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); + + // construct calendar with a full day event + $this->vCalendar2 = new VCalendar(); + // time zone component + $vTimeZone = $this->vCalendar2->add('VTIMEZONE'); + $vTimeZone->add('TZID', 'America/Toronto'); + // event component + $vEvent = $this->vCalendar2->add('VEVENT', []); + $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc'); + $vEvent->add('DTSTART', '20240701'); + $vEvent->add('DTEND', '20240702'); + $vEvent->add('SUMMARY', 'Test Recurrence Event'); + $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']); + $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [ + 'CN' => 'Attendee One', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); + + // construct calendar with a multi day event + $this->vCalendar3 = new VCalendar(); + // time zone component + $vTimeZone = $this->vCalendar3->add('VTIMEZONE'); + $vTimeZone->add('TZID', 'America/Toronto'); + // event component + $vEvent = $this->vCalendar3->add('VEVENT', []); + $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc'); + $vEvent->add('DTSTART', '20240701'); + $vEvent->add('DTEND', '20240706'); + $vEvent->add('SUMMARY', 'Test Recurrence Event'); + $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']); + $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [ + 'CN' => 'Attendee One', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); + + } + + public function testConstructFromCalendarString(): void { + + // construct event reader + $er = new EventReader($this->vCalendar1a->serialize(), '96a0e6b1-d886-4a55-a60d-152b31401dcc'); + // test object creation + $this->assertInstanceOf(EventReader::class, $er); + + } + + public function testConstructFromCalendarObject(): void { + + // construct event reader + $er = new EventReader($this->vCalendar1a, '96a0e6b1-d886-4a55-a60d-152b31401dcc'); + // test object creation + $this->assertInstanceOf(EventReader::class, $er); + + } + + public function testConstructFromEventObject(): void { + + // construct event reader + $er = new EventReader($this->vCalendar1a->VEVENT[0]); + // test object creation + $this->assertInstanceOf(EventReader::class, $er); + + } + + public function testStartDateTime(): void { + + /** test day part event with same start/end time zone */ + // construct event reader + $er = new EventReader($this->vCalendar1a, $this->vCalendar1a->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->startDateTime()); + + /** test day part event with different start/end time zone */ + // construct event reader + $er = new EventReader($this->vCalendar1b, $this->vCalendar1b->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->startDateTime()); + + /** test day part event with global time zone */ + // construct event reader + $er = new EventReader($this->vCalendar1c, $this->vCalendar1c->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->startDateTime()); + + /** test day part event with no time zone */ + // construct event reader + $er = new EventReader($this->vCalendar1d, $this->vCalendar1d->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('UTC')))), $er->startDateTime()); + + /** test day part event with microsoft time zone */ + // construct event reader + $er = new EventReader($this->vCalendar1e, $this->vCalendar1e->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->startDateTime()); + + /** test full day event */ + // construct event reader + $er = new EventReader($this->vCalendar2, $this->vCalendar2->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new \DateTime('20240701T000000', (new DateTimeZone('America/Toronto')))), $er->startDateTime()); + + /** test multi day event */ + // construct event reader + $er = new EventReader($this->vCalendar3, $this->vCalendar3->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new \DateTime('20240701T000000', (new DateTimeZone('America/Toronto')))), $er->startDateTime()); + + } + + public function testStartTimeZone(): void { + + /** test day part event with same start/end time zone */ + // construct event reader + $er = new EventReader($this->vCalendar1a, $this->vCalendar1a->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new DateTimeZone('America/Toronto')), $er->startTimeZone()); + + /** test day part event with different start/end time zone */ + // construct event reader + $er = new EventReader($this->vCalendar1b, $this->vCalendar1b->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new DateTimeZone('America/Toronto')), $er->startTimeZone()); + + /** test day part event with global time zone */ + // construct event reader + $er = new EventReader($this->vCalendar1c, $this->vCalendar1c->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new DateTimeZone('America/Toronto')), $er->startTimeZone()); + + /** test day part event with no time zone */ + // construct event reader + $er = new EventReader($this->vCalendar1d, $this->vCalendar1d->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new DateTimeZone('UTC')), $er->startTimeZone()); + + /** test day part event with microsoft time zone */ + // construct event reader + $er = new EventReader($this->vCalendar1e, $this->vCalendar1e->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new DateTimeZone('America/Toronto')), $er->startTimeZone()); + + /** test full day event */ + // construct event reader + $er = new EventReader($this->vCalendar2, $this->vCalendar2->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new DateTimeZone('America/Toronto')), $er->startTimeZone()); + + /** test multi day event */ + // construct event reader + $er = new EventReader($this->vCalendar3, $this->vCalendar3->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new DateTimeZone('America/Toronto')), $er->startTimeZone()); + + } + + public function testEndDate(): void { + + /** test day part event with same start/end time zone */ + // construct event reader + $er = new EventReader($this->vCalendar1a, $this->vCalendar1a->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new \DateTime('20240701T090000', (new DateTimeZone('America/Toronto')))), $er->endDateTime()); + + /** test day part event with different start/end time zone */ + // construct event reader + $er = new EventReader($this->vCalendar1b, $this->vCalendar1b->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new \DateTime('20240701T090000', (new DateTimeZone('America/Vancouver')))), $er->endDateTime()); + + /** test day part event with global time zone */ + // construct event reader + $er = new EventReader($this->vCalendar1c, $this->vCalendar1c->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new \DateTime('20240701T090000', (new DateTimeZone('America/Toronto')))), $er->endDateTime()); + + /** test day part event with no time zone */ + // construct event reader + $er = new EventReader($this->vCalendar1d, $this->vCalendar1d->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new \DateTime('20240701T090000', (new DateTimeZone('UTC')))), $er->endDateTime()); + + /** test day part event with microsoft time zone */ + // construct event reader + $er = new EventReader($this->vCalendar1e, $this->vCalendar1e->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new \DateTime('20240701T090000', (new DateTimeZone('America/Toronto')))), $er->endDateTime()); + + /** test full day event */ + // construct event reader + $er = new EventReader($this->vCalendar2, $this->vCalendar2->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new \DateTime('20240702T000000', (new DateTimeZone('America/Toronto')))), $er->endDateTime()); + + /** test multi day event */ + // construct event reader + $er = new EventReader($this->vCalendar3, $this->vCalendar3->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new \DateTime('20240706T000000', (new DateTimeZone('America/Toronto')))), $er->endDateTime()); + + } + + public function testEndTimeZone(): void { + + /** test day part event with same start/end time zone */ + // construct event reader + $er = new EventReader($this->vCalendar1a, $this->vCalendar1a->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new DateTimeZone('America/Toronto')), $er->endTimeZone()); + + /** test day part event with different start/end time zone */ + // construct event reader + $er = new EventReader($this->vCalendar1b, $this->vCalendar1b->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new DateTimeZone('America/Vancouver')), $er->endTimeZone()); + + /** test day part event with global time zone */ + // construct event reader + $er = new EventReader($this->vCalendar1c, $this->vCalendar1c->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new DateTimeZone('America/Toronto')), $er->endTimeZone()); + + /** test day part event with no time zone */ + // construct event reader + $er = new EventReader($this->vCalendar1d, $this->vCalendar1d->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new DateTimeZone('UTC')), $er->endTimeZone()); + + /** test day part event with microsoft time zone */ + // construct event reader + $er = new EventReader($this->vCalendar1e, $this->vCalendar1e->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new DateTimeZone('America/Toronto')), $er->endTimeZone()); + + /** test full day event */ + // construct event reader + $er = new EventReader($this->vCalendar2, $this->vCalendar2->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new DateTimeZone('America/Toronto')), $er->endTimeZone()); + + /** test multi day event */ + // construct event reader + $er = new EventReader($this->vCalendar3, $this->vCalendar3->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new DateTimeZone('America/Toronto')), $er->endTimeZone()); + + } + + public function testEntireDay(): void { + + /** test day part event with same start/end time zone */ + // construct event reader + $er = new EventReader($this->vCalendar1a, $this->vCalendar1a->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertFalse($er->entireDay()); + + /** test full day event */ + // construct event reader + $er = new EventReader($this->vCalendar2, $this->vCalendar2->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertTrue($er->entireDay()); + + /** test multi day event */ + // construct event reader + $er = new EventReader($this->vCalendar3, $this->vCalendar3->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertTrue($er->entireDay()); + + } + + public function testRecurs(): void { + + /** test no recurrance */ + $vCalendar = clone $this->vCalendar1a; + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertFalse($er->recurs()); + + /** test rrule recurrance */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertTrue($er->recurs()); + + /** test rdate recurrance */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RDATE', '20240703,20240705'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertTrue($er->recurs()); + + } + + public function testRecurringPattern(): void { + + /** test no recurrance */ + $vCalendar = clone $this->vCalendar1a; + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertNull($er->recurringPattern()); + + /** test absolute rrule recurrance */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals('A', $er->recurringPattern()); + + /** test relative rrule recurrance */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=MO;BYSETPOS=1'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals('R', $er->recurringPattern()); + + /** test rdate recurrance */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RDATE', '20240703,20240705'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals('A', $er->recurringPattern()); + + } + + public function testRecurringPrecision(): void { + + /** test no recurrance */ + $vCalendar = clone $this->vCalendar1a; + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertNull($er->recurringPrecision()); + + /** test daily rrule recurrance */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals('daily', $er->recurringPrecision()); + + /** test weekly rrule recurrance */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals('weekly', $er->recurringPrecision()); + + /** test monthly rrule recurrance */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8,15'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals('monthly', $er->recurringPrecision()); + + /** test yearly rrule recurrance */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYMONTHDAY=1'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals('yearly', $er->recurringPrecision()); + + /** test rdate recurrance */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RDATE', '20240703,20240705'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals('fixed', $er->recurringPrecision()); + + } + + public function testRecurringInterval(): void { + + /** test no recurrance */ + $vCalendar = clone $this->vCalendar1a; + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertNull($er->recurringInterval()); + + /** test daily rrule recurrance */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals(2, $er->recurringInterval()); + + /** test rdate recurrance */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RDATE', '20240703,20240705'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertNull($er->recurringInterval()); + + } + + public function testRecurringConcludes(): void { + + /** test no recurrance */ + $vCalendar = clone $this->vCalendar1a; + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertFalse($er->recurringConcludes()); + + /** test rrule recurrance with no end */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertFalse($er->recurringConcludes()); + + /** test rrule recurrance with until date end */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;UNTIL=20240712T080000Z;BYDAY=MO,WE,FR'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertTrue($er->recurringConcludes()); + + /** test rrule recurrance with iteration end */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertTrue($er->recurringConcludes()); + + /** test rdate recurrance */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RDATE', '20240703,20240705'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertTrue($er->recurringConcludes()); + + /** test rdate (multiple property instances) recurrance */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RDATE', '20240703'); + $vCalendar->VEVENT[0]->add('RDATE', '20240705'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertTrue($er->recurringConcludes()); + + /** test rrule and rdate recurrance with rdate as last date */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR'); + $vCalendar->VEVENT[0]->add('RDATE', '20240706,20240715'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertTrue($er->recurringConcludes()); + + /** test rrule and rdate recurrance with rrule as last date */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=7;BYDAY=MO,WE,FR'); + $vCalendar->VEVENT[0]->add('RDATE', '20240706,20240713'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertTrue($er->recurringConcludes()); + + } + + public function testRecurringConcludesAfter(): void { + + /** test no recurrance */ + $vCalendar = clone $this->vCalendar1a; + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertNull($er->recurringConcludesAfter()); + + /** test rrule recurrance with count */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals(6, $er->recurringConcludesAfter()); + + /** test rdate recurrance */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RDATE', '20240703,20240705'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals(2, $er->recurringConcludesAfter()); + + /** test rdate (multiple property instances) recurrance */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RDATE', '20240703'); + $vCalendar->VEVENT[0]->add('RDATE', '20240705'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals(2, $er->recurringConcludesAfter()); + + /** test rrule and rdate recurrance */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR'); + $vCalendar->VEVENT[0]->add('RDATE', '20240706,20240715'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals(8, $er->recurringConcludesAfter()); + + } + + public function testRecurringConcludesOn(): void { + + /** test no recurrance */ + $vCalendar = clone $this->vCalendar1a; + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertNull($er->recurringConcludesOn()); + + /** test rrule recurrance with no end */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertNull($er->recurringConcludesOn()); + + /** test rrule recurrance with until date end */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;UNTIL=20240712T080000Z;BYDAY=MO,WE,FR'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + + // TODO: Fix until time zone + //$this->assertEquals((new \DateTime('20240712T080000', (new DateTimeZone('America/Toronto')))), $er->recurringConcludesOn()); + + /** test rdate recurrance */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RDATE', '20240703,20240705'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new \DateTime('20240705T000000', (new DateTimeZone('America/Toronto')))), $er->recurringConcludesOn()); + + /** test rdate (multiple property instances) recurrance */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RDATE', '20240703'); + $vCalendar->VEVENT[0]->add('RDATE', '20240705'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new \DateTime('20240705T000000', (new DateTimeZone('America/Toronto')))), $er->recurringConcludesOn()); + + /** test rrule and rdate recurrance with rdate as last date */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR'); + $vCalendar->VEVENT[0]->add('RDATE', '20240706,20240715'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new \DateTime('20240715T000000', (new DateTimeZone('America/Toronto')))), $er->recurringConcludesOn()); + + /** test rrule and rdate recurrance with rrule as last date */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;COUNT=7;BYDAY=MO,WE,FR'); + $vCalendar->VEVENT[0]->add('RDATE', '20240706,20240713'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals((new \DateTime('20240715T080000', (new DateTimeZone('America/Toronto')))), $er->recurringConcludesOn()); + + } + + public function testRecurringDaysOfWeek(): void { + + /** test no recurrance */ + $vCalendar = clone $this->vCalendar1a; + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals([], $er->recurringDaysOfWeek()); + + /** test rrule recurrance with weekly days*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;UNTIL=20240712T080000Z;BYDAY=MO,WE,FR'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals(['MO','WE','FR'], $er->recurringDaysOfWeek()); + + } + + public function testRecurringDaysOfWeekNamed(): void { + + /** test no recurrance */ + $vCalendar = clone $this->vCalendar1a; + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals([], $er->recurringDaysOfWeekNamed()); + + /** test rrule recurrance with weekly days*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;UNTIL=20240712T080000Z;BYDAY=MO,WE,FR'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals(['Monday','Wednesday','Friday'], $er->recurringDaysOfWeekNamed()); + + } + + public function testRecurringDaysOfMonth(): void { + + /** test no recurrance */ + $vCalendar = clone $this->vCalendar1a; + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals([], $er->recurringDaysOfMonth()); + + /** test rrule recurrance with monthly absolute dates*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=6,13,20,27'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals([6,13,20,27], $er->recurringDaysOfMonth()); + + } + + public function testRecurringDaysOfYear(): void { + + /** test no recurrance */ + $vCalendar = clone $this->vCalendar1a; + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals([], $er->recurringDaysOfYear()); + + /** test rrule recurrance with monthly absolute dates*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYYEARDAY=1,30,180,365'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals([1,30,180,365], $er->recurringDaysOfYear()); + + } + + public function testRecurringWeeksOfMonth(): void { + + /** test no recurrance */ + $vCalendar = clone $this->vCalendar1a; + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals([], $er->recurringWeeksOfMonth()); + + /** test rrule recurrance with monthly days*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=MO;BYSETPOS=1'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals([1], $er->recurringWeeksOfMonth()); + + } + + public function testRecurringWeeksOfMonthNamed(): void { + + /** test no recurrance */ + $vCalendar = clone $this->vCalendar1a; + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals([], $er->recurringWeeksOfMonthNamed()); + + /** test rrule recurrance with weekly days*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=MO;BYSETPOS=1'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals(['First'], $er->recurringWeeksOfMonthNamed()); + + } + + public function testRecurringWeeksOfYear(): void { + + /** test no recurrance */ + $vCalendar = clone $this->vCalendar1a; + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals([], $er->recurringWeeksOfYear()); + + /** test rrule recurrance with monthly days*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;INTERVAL=1;BYWEEKNO=35,42;BYDAY=TU'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals([35,42], $er->recurringWeeksOfYear()); + + } + + public function testRecurringMonthsOfYear(): void { + + /** test no recurrance */ + $vCalendar = clone $this->vCalendar1a; + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals([], $er->recurringMonthsOfYear()); + + /** test rrule recurrance with monthly days*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;INTERVAL=1;BYMONTH=7;BYMONTHDAY=1'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals([7], $er->recurringMonthsOfYear()); + + } + + public function testRecurringMonthsOfYearNamed(): void { + + /** test no recurrance */ + $vCalendar = clone $this->vCalendar1a; + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals([], $er->recurringMonthsOfYearNamed()); + + /** test rrule recurrance with weekly days*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;INTERVAL=1;BYMONTH=7;BYMONTHDAY=1'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test set by constructor + $this->assertEquals(['July'], $er->recurringMonthsOfYearNamed()); + + } + + public function testRecurringIterationDaily(): void { + + /** test rrule recurrance with daily frequency*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=3;UNTIL=20240714T040000Z'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test initial recurrance + $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance + $er->recurrenceAdvance(); + $this->assertEquals((new \DateTime('20240704T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance + $er->recurrenceAdvance(); + $this->assertEquals((new \DateTime('20240707T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance + $er->recurrenceAdvance(); + $this->assertEquals((new \DateTime('20240710T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance + $er->recurrenceAdvance(); + $this->assertEquals((new \DateTime('20240713T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance (This is past the last recurrance and should return null) + $er->recurrenceAdvance(); + $this->assertNull($er->recurrenceDate()); + // test rewind to initial recurrance + $er->recurrenceRewind(); + $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance + $er->recurrenceAdvanceTo((new \DateTime('20240709T080000'))); + $this->assertEquals((new \DateTime('20240710T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + + } + + public function testRecurringIterationWeekly(): void { + + /** test rrule recurrance with weekly frequency*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20240713T040000Z'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test initial recurrance + $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance + $er->recurrenceAdvance(); + $this->assertEquals((new \DateTime('20240703T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance + $er->recurrenceAdvance(); + $this->assertEquals((new \DateTime('20240705T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance + $er->recurrenceAdvance(); + $this->assertEquals((new \DateTime('20240708T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance + $er->recurrenceAdvance(); + $this->assertEquals((new \DateTime('20240710T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance + $er->recurrenceAdvance(); + $this->assertEquals((new \DateTime('20240712T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance (This is past the last recurrance and should return null) + $er->recurrenceAdvance(); + $this->assertNull($er->recurrenceDate()); + // test rewind to initial recurrance + $er->recurrenceRewind(); + $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance + $er->recurrenceAdvanceTo((new \DateTime('20240709T080000'))); + $this->assertEquals((new \DateTime('20240710T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + + } + + public function testRecurringIterationMonthlyAbsolute(): void { + + /** test rrule recurrance with monthly absolute frequency on the 1st of each month*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;COUNT=3;BYMONTHDAY=1'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test initial recurrance + $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance + $er->recurrenceAdvance(); + $this->assertEquals((new \DateTime('20240801T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance + $er->recurrenceAdvance(); + $this->assertEquals((new \DateTime('20240901T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance (This is past the last recurrance and should return null) + $er->recurrenceAdvance(); + $this->assertNull($er->recurrenceDate()); + // test rewind to initial recurrance + $er->recurrenceRewind(); + $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance + $er->recurrenceAdvanceTo((new \DateTime('20240809T080000'))); + $this->assertEquals((new \DateTime('20240901T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + + } + + public function testRecurringIterationMonthlyRelative(): void { + + /** test rrule recurrance with monthly relative frequency on the first monday of each month*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;COUNT=3;BYDAY=MO;BYSETPOS=1'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test initial recurrance + $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance + $er->recurrenceAdvance(); + $this->assertEquals((new \DateTime('20240805T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance + $er->recurrenceAdvance(); + $this->assertEquals((new \DateTime('20240902T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance (This is past the last recurrance and should return null) + $er->recurrenceAdvance(); + $this->assertNull($er->recurrenceDate()); + // test rewind to initial recurrance + $er->recurrenceRewind(); + $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance + $er->recurrenceAdvanceTo((new \DateTime('20240809T080000'))); + $this->assertEquals((new \DateTime('20240902T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + + } + + public function testRecurringIterationYearlyAbsolute(): void { + + /** test rrule recurrance with yearly absolute frequency on the 1st of july*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;COUNT=3;BYMONTH=7'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test initial recurrance + $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance + $er->recurrenceAdvance(); + $this->assertEquals((new \DateTime('20250701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance + $er->recurrenceAdvance(); + $this->assertEquals((new \DateTime('20260701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance (This is past the last recurrance and should return null) + $er->recurrenceAdvance(); + $this->assertNull($er->recurrenceDate()); + // test rewind to initial recurrance + $er->recurrenceRewind(); + $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance + $er->recurrenceAdvanceTo((new \DateTime('20250809T080000'))); + $this->assertEquals((new \DateTime('20260701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + + } + + public function testRecurringIterationYearlyRelative(): void { + + /** test rrule recurrance with yearly relative frequency on the first monday of july*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;COUNT=3;BYMONTH=7;BYDAY=MO;BYSETPOS=1'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test initial recurrance + $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance + $er->recurrenceAdvance(); + $this->assertEquals((new \DateTime('20250707T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance + $er->recurrenceAdvance(); + $this->assertEquals((new \DateTime('20260706T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance (This is past the last recurrance and should return null) + $er->recurrenceAdvance(); + $this->assertNull($er->recurrenceDate()); + // test rewind to initial recurrance + $er->recurrenceRewind(); + $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance + $er->recurrenceAdvanceTo((new \DateTime('20250809T080000'))); + $this->assertEquals((new \DateTime('20260706T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + + } + + public function testRecurringIterationFixed(): void { + + /** test rrule recurrance with yearly relative frequency on the first monday of july*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RDATE', '20240703T080000,20240905T080000,20241231T080000'); + // construct event reader + $er = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test initial recurrance + $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance + $er->recurrenceAdvance(); + $this->assertEquals((new \DateTime('20240703T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance + $er->recurrenceAdvance(); + $this->assertEquals((new \DateTime('20240905T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance + $er->recurrenceAdvance(); + $this->assertEquals((new \DateTime('20241231T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance (This is past the last recurrance and should return null) + $er->recurrenceAdvance(); + $this->assertNull($er->recurrenceDate()); + // test rewind to initial recurrance + $er->recurrenceRewind(); + $this->assertEquals((new \DateTime('20240701T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + // test next recurrance + $er->recurrenceAdvanceTo((new \DateTime('20240809T080000'))); + $this->assertEquals((new \DateTime('20240905T080000', (new DateTimeZone('America/Toronto')))), $er->recurrenceDate()); + + } + +} diff --git a/apps/dav/tests/unit/CalDAV/Export/ExportServiceTest.php b/apps/dav/tests/unit/CalDAV/Export/ExportServiceTest.php new file mode 100644 index 00000000000..838dfc18f2f --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Export/ExportServiceTest.php @@ -0,0 +1,81 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\Export; + +use Generator; +use OCA\DAV\CalDAV\Export\ExportService; +use OCP\Calendar\CalendarExportOptions; +use OCP\Calendar\ICalendarExport; +use OCP\ServerVersion; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\VObject\Component\VCalendar; + +class ExportServiceTest extends \Test\TestCase { + private ServerVersion&MockObject $serverVersion; + private ExportService $service; + private ICalendarExport&MockObject $calendar; + private array $mockExportCollection; + + protected function setUp(): void { + parent::setUp(); + + $this->serverVersion = $this->createMock(ServerVersion::class); + $this->serverVersion->method('getVersionString') + ->willReturn('32.0.0.0'); + $this->service = new ExportService($this->serverVersion); + $this->calendar = $this->createMock(ICalendarExport::class); + + } + + protected function mockGenerator(): Generator { + foreach ($this->mockExportCollection as $entry) { + yield $entry; + } + } + + public function testExport(): void { + // Arrange + // construct calendar with a 1 hour event and same start/end time zones + $vCalendar = new VCalendar(); + /** @var \Sabre\VObject\Component\VEvent $vEvent */ + $vEvent = $vCalendar->add('VEVENT', []); + $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc'); + $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']); + $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']); + $vEvent->add('SUMMARY', 'Test Recurrence Event'); + $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']); + $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [ + 'CN' => 'Attendee One', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); + // construct calendar return + $options = new CalendarExportOptions(); + $this->mockExportCollection[] = $vCalendar; + $this->calendar->expects($this->once()) + ->method('export') + ->with($options) + ->willReturn($this->mockGenerator()); + + // Act + $document = ''; + foreach ($this->service->export($this->calendar, $options) as $chunk) { + $document .= $chunk; + } + + // Assert + $this->assertStringContainsString('BEGIN:VCALENDAR', $document, 'Exported document calendar start missing'); + $this->assertStringContainsString('BEGIN:VEVENT', $document, 'Exported document event start missing'); + $this->assertStringContainsString('END:VEVENT', $document, 'Exported document event end missing'); + $this->assertStringContainsString('END:VCALENDAR', $document, 'Exported document calendar end missing'); + + } + +} diff --git a/apps/dav/tests/unit/CalDAV/Integration/ExternalCalendarTest.php b/apps/dav/tests/unit/CalDAV/Integration/ExternalCalendarTest.php new file mode 100644 index 00000000000..b2f479ac0e3 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Integration/ExternalCalendarTest.php @@ -0,0 +1,101 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\Integration; + +use OCA\DAV\CalDAV\Integration\ExternalCalendar; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class ExternalCalendarTest extends TestCase { + private ExternalCalendar&MockObject $abstractExternalCalendar; + + protected function setUp(): void { + parent::setUp(); + + $this->abstractExternalCalendar + = $this->getMockForAbstractClass(ExternalCalendar::class, ['example-app-id', 'calendar-uri-in-backend']); + } + + public function testGetName():void { + // Check that the correct name is returned + $this->assertEquals('app-generated--example-app-id--calendar-uri-in-backend', + $this->abstractExternalCalendar->getName()); + + // Check that the method is final and can't be overridden by other classes + $reflectionMethod = new \ReflectionMethod(ExternalCalendar::class, 'getName'); + $this->assertTrue($reflectionMethod->isFinal()); + } + + public function testSetName():void { + // Check that the method is final and can't be overridden by other classes + $reflectionMethod = new \ReflectionMethod(ExternalCalendar::class, 'setName'); + $this->assertTrue($reflectionMethod->isFinal()); + + $this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class); + $this->expectExceptionMessage('Renaming calendars is not yet supported'); + + $this->abstractExternalCalendar->setName('other-name'); + } + + public function createDirectory(): void { + // Check that the method is final and can't be overridden by other classes + $reflectionMethod = new \ReflectionMethod(ExternalCalendar::class, 'createDirectory'); + $this->assertTrue($reflectionMethod->isFinal()); + + $this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class); + $this->expectExceptionMessage('Creating collections in calendar objects is not allowed'); + + $this->abstractExternalCalendar->createDirectory('other-name'); + } + + public function testIsAppGeneratedCalendar():void { + $this->assertFalse(ExternalCalendar::isAppGeneratedCalendar('personal')); + $this->assertFalse(ExternalCalendar::isAppGeneratedCalendar('work')); + $this->assertFalse(ExternalCalendar::isAppGeneratedCalendar('contact_birthdays')); + $this->assertFalse(ExternalCalendar::isAppGeneratedCalendar('company')); + $this->assertFalse(ExternalCalendar::isAppGeneratedCalendar('app-generated')); + $this->assertFalse(ExternalCalendar::isAppGeneratedCalendar('app-generated--example')); + + $this->assertTrue(ExternalCalendar::isAppGeneratedCalendar('app-generated--deck--board-1')); + $this->assertTrue(ExternalCalendar::isAppGeneratedCalendar('app-generated--example--foo-2')); + $this->assertTrue(ExternalCalendar::isAppGeneratedCalendar('app-generated--example--foo--2')); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('splitAppGeneratedCalendarUriDataProvider')] + public function testSplitAppGeneratedCalendarUriInvalid(string $name):void { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Provided calendar uri was not app-generated'); + + ExternalCalendar::splitAppGeneratedCalendarUri($name); + } + + public static function splitAppGeneratedCalendarUriDataProvider():array { + return [ + ['personal'], + ['foo_shared_by_admin'], + ['contact_birthdays'], + ]; + } + + public function testSplitAppGeneratedCalendarUri():void { + $this->assertEquals(['deck', 'board-1'], ExternalCalendar::splitAppGeneratedCalendarUri('app-generated--deck--board-1')); + $this->assertEquals(['example', 'foo-2'], ExternalCalendar::splitAppGeneratedCalendarUri('app-generated--example--foo-2')); + $this->assertEquals(['example', 'foo--2'], ExternalCalendar::splitAppGeneratedCalendarUri('app-generated--example--foo--2')); + } + + public function testDoesViolateReservedName():void { + $this->assertFalse(ExternalCalendar::doesViolateReservedName('personal')); + $this->assertFalse(ExternalCalendar::doesViolateReservedName('work')); + $this->assertFalse(ExternalCalendar::doesViolateReservedName('contact_birthdays')); + $this->assertFalse(ExternalCalendar::doesViolateReservedName('company')); + + $this->assertTrue(ExternalCalendar::doesViolateReservedName('app-generated')); + $this->assertTrue(ExternalCalendar::doesViolateReservedName('app-generated-calendar')); + $this->assertTrue(ExternalCalendar::doesViolateReservedName('app-generated--deck-123')); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Listener/CalendarPublicationListenerTest.php b/apps/dav/tests/unit/CalDAV/Listener/CalendarPublicationListenerTest.php new file mode 100644 index 00000000000..3ba0b832593 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Listener/CalendarPublicationListenerTest.php @@ -0,0 +1,55 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\Listeners; + +use OCA\DAV\CalDAV\Activity\Backend; +use OCA\DAV\Events\CalendarPublishedEvent; +use OCA\DAV\Events\CalendarUnpublishedEvent; +use OCA\DAV\Listener\CalendarPublicationListener; +use OCP\EventDispatcher\Event; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class CalendarPublicationListenerTest extends TestCase { + private Backend&MockObject $activityBackend; + private LoggerInterface&MockObject $logger; + private CalendarPublicationListener $calendarPublicationListener; + private CalendarPublishedEvent&MockObject $publicationEvent; + private CalendarUnpublishedEvent&MockObject $unpublicationEvent; + + protected function setUp(): void { + parent::setUp(); + + $this->activityBackend = $this->createMock(Backend::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->publicationEvent = $this->createMock(CalendarPublishedEvent::class); + $this->unpublicationEvent = $this->createMock(CalendarUnpublishedEvent::class); + $this->calendarPublicationListener = new CalendarPublicationListener($this->activityBackend, $this->logger); + } + + public function testInvalidEvent(): void { + $this->activityBackend->expects($this->never())->method('onCalendarPublication'); + $this->logger->expects($this->never())->method('debug'); + $this->calendarPublicationListener->handle(new Event()); + } + + public function testPublicationEvent(): void { + $this->publicationEvent->expects($this->once())->method('getCalendarData')->with()->willReturn([]); + $this->activityBackend->expects($this->once())->method('onCalendarPublication')->with([], true); + $this->logger->expects($this->once())->method('debug'); + $this->calendarPublicationListener->handle($this->publicationEvent); + } + + public function testUnPublicationEvent(): void { + $this->unpublicationEvent->expects($this->once())->method('getCalendarData')->with()->willReturn([]); + $this->activityBackend->expects($this->once())->method('onCalendarPublication')->with([], false); + $this->logger->expects($this->once())->method('debug'); + $this->calendarPublicationListener->handle($this->unpublicationEvent); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Listener/CalendarShareUpdateListenerTest.php b/apps/dav/tests/unit/CalDAV/Listener/CalendarShareUpdateListenerTest.php new file mode 100644 index 00000000000..d5697a862db --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Listener/CalendarShareUpdateListenerTest.php @@ -0,0 +1,48 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\Listeners; + +use OCA\DAV\CalDAV\Activity\Backend; +use OCA\DAV\Events\CalendarShareUpdatedEvent; +use OCA\DAV\Listener\CalendarShareUpdateListener; +use OCP\EventDispatcher\Event; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class CalendarShareUpdateListenerTest extends TestCase { + private Backend&MockObject $activityBackend; + private LoggerInterface&MockObject $logger; + private CalendarShareUpdateListener $calendarPublicationListener; + private CalendarShareUpdatedEvent&MockObject $event; + + protected function setUp(): void { + parent::setUp(); + + $this->activityBackend = $this->createMock(Backend::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->event = $this->createMock(CalendarShareUpdatedEvent::class); + $this->calendarPublicationListener = new CalendarShareUpdateListener($this->activityBackend, $this->logger); + } + + public function testInvalidEvent(): void { + $this->activityBackend->expects($this->never())->method('onCalendarUpdateShares'); + $this->logger->expects($this->never())->method('debug'); + $this->calendarPublicationListener->handle(new Event()); + } + + public function testEvent(): void { + $this->event->expects($this->once())->method('getCalendarData')->with()->willReturn([]); + $this->event->expects($this->once())->method('getOldShares')->with()->willReturn([]); + $this->event->expects($this->once())->method('getAdded')->with()->willReturn([]); + $this->event->expects($this->once())->method('getRemoved')->with()->willReturn([]); + $this->activityBackend->expects($this->once())->method('onCalendarUpdateShares')->with([], [], [], []); + $this->logger->expects($this->once())->method('debug'); + $this->calendarPublicationListener->handle($this->event); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Listener/SubscriptionListenerTest.php b/apps/dav/tests/unit/CalDAV/Listener/SubscriptionListenerTest.php new file mode 100644 index 00000000000..cbfdfd6b9b7 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Listener/SubscriptionListenerTest.php @@ -0,0 +1,67 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\Listeners; + +use OCA\DAV\BackgroundJob\RefreshWebcalJob; +use OCA\DAV\CalDAV\Reminder\Backend; +use OCA\DAV\CalDAV\WebcalCaching\RefreshWebcalService; +use OCA\DAV\Events\SubscriptionCreatedEvent; +use OCA\DAV\Events\SubscriptionDeletedEvent; +use OCA\DAV\Listener\SubscriptionListener; +use OCP\BackgroundJob\IJobList; +use OCP\EventDispatcher\Event; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class SubscriptionListenerTest extends TestCase { + private RefreshWebcalService&MockObject $refreshWebcalService; + private Backend&MockObject $reminderBackend; + private IJobList&MockObject $jobList; + private LoggerInterface&MockObject $logger; + private SubscriptionListener $calendarPublicationListener; + private SubscriptionCreatedEvent&MockObject $subscriptionCreatedEvent; + private SubscriptionDeletedEvent&MockObject $subscriptionDeletedEvent; + + protected function setUp(): void { + parent::setUp(); + + $this->refreshWebcalService = $this->createMock(RefreshWebcalService::class); + $this->reminderBackend = $this->createMock(Backend::class); + $this->jobList = $this->createMock(IJobList::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->subscriptionCreatedEvent = $this->createMock(SubscriptionCreatedEvent::class); + $this->subscriptionDeletedEvent = $this->createMock(SubscriptionDeletedEvent::class); + $this->calendarPublicationListener = new SubscriptionListener($this->jobList, $this->refreshWebcalService, $this->reminderBackend, $this->logger); + } + + public function testInvalidEvent(): void { + $this->refreshWebcalService->expects($this->never())->method('refreshSubscription'); + $this->jobList->expects($this->never())->method('add'); + $this->logger->expects($this->never())->method('debug'); + $this->calendarPublicationListener->handle(new Event()); + } + + public function testCreateSubscriptionEvent(): void { + $this->subscriptionCreatedEvent->expects($this->once())->method('getSubscriptionId')->with()->willReturn(5); + $this->subscriptionCreatedEvent->expects($this->once())->method('getSubscriptionData')->with()->willReturn(['principaluri' => 'principaluri', 'uri' => 'uri']); + $this->refreshWebcalService->expects($this->once())->method('refreshSubscription')->with('principaluri', 'uri'); + $this->jobList->expects($this->once())->method('add')->with(RefreshWebcalJob::class, ['principaluri' => 'principaluri', 'uri' => 'uri']); + $this->logger->expects($this->exactly(2))->method('debug'); + $this->calendarPublicationListener->handle($this->subscriptionCreatedEvent); + } + + public function testDeleteSubscriptionEvent(): void { + $this->subscriptionDeletedEvent->expects($this->once())->method('getSubscriptionId')->with()->willReturn(5); + $this->subscriptionDeletedEvent->expects($this->once())->method('getSubscriptionData')->with()->willReturn(['principaluri' => 'principaluri', 'uri' => 'uri']); + $this->jobList->expects($this->once())->method('remove')->with(RefreshWebcalJob::class, ['principaluri' => 'principaluri', 'uri' => 'uri']); + $this->reminderBackend->expects($this->once())->method('cleanRemindersForCalendar')->with(5); + $this->logger->expects($this->exactly(2))->method('debug'); + $this->calendarPublicationListener->handle($this->subscriptionDeletedEvent); + } +} diff --git a/apps/dav/tests/unit/CalDAV/OutboxTest.php b/apps/dav/tests/unit/CalDAV/OutboxTest.php new file mode 100644 index 00000000000..cc0a3f0405f --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/OutboxTest.php @@ -0,0 +1,105 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV; + +use OCA\DAV\CalDAV\Outbox; +use OCP\IConfig; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class OutboxTest extends TestCase { + private IConfig&MockObject $config; + private Outbox $outbox; + + protected function setUp(): void { + parent::setUp(); + + $this->config = $this->createMock(IConfig::class); + $this->outbox = new Outbox($this->config, 'user-principal-123'); + } + + public function testGetACLFreeBusyEnabled(): void { + $this->config->expects($this->once()) + ->method('getAppValue') + ->with('dav', 'disableFreeBusy', 'no') + ->willReturn('no'); + + $this->assertEquals([ + [ + 'privilege' => '{DAV:}read', + 'principal' => 'user-principal-123', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => 'user-principal-123/calendar-proxy-read', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => 'user-principal-123/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{urn:ietf:params:xml:ns:caldav}schedule-send', + 'principal' => 'user-principal-123', + 'protected' => true, + ], + [ + 'privilege' => '{urn:ietf:params:xml:ns:caldav}schedule-send', + 'principal' => 'user-principal-123/calendar-proxy-write', + 'protected' => true, + ], + ], $this->outbox->getACL()); + } + + public function testGetACLFreeBusyDisabled(): void { + $this->config->expects($this->once()) + ->method('getAppValue') + ->with('dav', 'disableFreeBusy', 'no') + ->willReturn('yes'); + + $this->assertEquals([ + [ + 'privilege' => '{DAV:}read', + 'principal' => 'user-principal-123', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => 'user-principal-123/calendar-proxy-read', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => 'user-principal-123/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{urn:ietf:params:xml:ns:caldav}schedule-send-invite', + 'principal' => 'user-principal-123', + 'protected' => true, + ], + [ + 'privilege' => '{urn:ietf:params:xml:ns:caldav}schedule-send-invite', + 'principal' => 'user-principal-123/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{urn:ietf:params:xml:ns:caldav}schedule-send-reply', + 'principal' => 'user-principal-123', + 'protected' => true, + ], + [ + 'privilege' => '{urn:ietf:params:xml:ns:caldav}schedule-send-reply', + 'principal' => 'user-principal-123/calendar-proxy-write', + 'protected' => true, + ], + ], $this->outbox->getACL()); + } +} diff --git a/apps/dav/tests/unit/CalDAV/PluginTest.php b/apps/dav/tests/unit/CalDAV/PluginTest.php new file mode 100644 index 00000000000..c5725a1fa81 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/PluginTest.php @@ -0,0 +1,47 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV; + +use OCA\DAV\CalDAV\Plugin; +use Test\TestCase; + +class PluginTest extends TestCase { + private Plugin $plugin; + + protected function setUp(): void { + parent::setUp(); + + $this->plugin = new Plugin(); + } + + public static function linkProvider(): array { + return [ + [ + 'principals/users/MyUserName', + 'calendars/MyUserName', + ], + [ + 'principals/calendar-resources/Resource-ABC', + 'system-calendars/calendar-resources/Resource-ABC', + ], + [ + 'principals/calendar-rooms/Room-ABC', + 'system-calendars/calendar-rooms/Room-ABC', + ], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('linkProvider')] + public function testGetCalendarHomeForPrincipal(string $input, string $expected): void { + $this->assertSame($expected, $this->plugin->getCalendarHomeForPrincipal($input)); + } + + public function testGetCalendarHomeForUnknownPrincipal(): void { + $this->assertNull($this->plugin->getCalendarHomeForPrincipal('FOO/BAR/BLUB')); + } +} diff --git a/apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php b/apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php new file mode 100644 index 00000000000..6acceed6f64 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/PublicCalendarRootTest.php @@ -0,0 +1,141 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV; + +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CalDAV\Calendar; +use OCA\DAV\CalDAV\PublicCalendar; +use OCA\DAV\CalDAV\PublicCalendarRoot; +use OCA\DAV\Connector\Sabre\Principal; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IGroupManager; +use OCP\IL10N; +use OCP\IUserManager; +use OCP\Security\ISecureRandom; +use OCP\Server; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +/** + * Class PublicCalendarRootTest + * + * @group DB + * + * @package OCA\DAV\Tests\unit\CalDAV + */ +class PublicCalendarRootTest extends TestCase { + public const UNIT_TEST_USER = ''; + private CalDavBackend $backend; + private PublicCalendarRoot $publicCalendarRoot; + private IL10N&MockObject $l10n; + private Principal&MockObject $principal; + protected IUserManager&MockObject $userManager; + protected IGroupManager&MockObject $groupManager; + protected IConfig&MockObject $config; + private ISecureRandom $random; + private LoggerInterface&MockObject $logger; + + protected function setUp(): void { + parent::setUp(); + + $db = Server::get(IDBConnection::class); + $this->principal = $this->createMock('OCA\DAV\Connector\Sabre\Principal'); + $this->userManager = $this->createMock(IUserManager::class); + $this->groupManager = $this->createMock(IGroupManager::class); + $this->random = Server::get(ISecureRandom::class); + $this->logger = $this->createMock(LoggerInterface::class); + $dispatcher = $this->createMock(IEventDispatcher::class); + $config = $this->createMock(IConfig::class); + $sharingBackend = $this->createMock(\OCA\DAV\CalDAV\Sharing\Backend::class); + + $this->principal->expects($this->any())->method('getGroupMembership') + ->withAnyParameters() + ->willReturn([]); + + $this->principal->expects($this->any())->method('getCircleMembership') + ->withAnyParameters() + ->willReturn([]); + + $this->backend = new CalDavBackend( + $db, + $this->principal, + $this->userManager, + $this->random, + $this->logger, + $dispatcher, + $config, + $sharingBackend, + false, + ); + $this->l10n = $this->createMock(IL10N::class); + $this->config = $this->createMock(IConfig::class); + + $this->publicCalendarRoot = new PublicCalendarRoot($this->backend, + $this->l10n, $this->config, $this->logger); + } + + protected function tearDown(): void { + parent::tearDown(); + + if (is_null($this->backend)) { + return; + } + $this->principal->expects($this->any())->method('getGroupMembership') + ->withAnyParameters() + ->willReturn([]); + + $this->principal->expects($this->any())->method('getCircleMembership') + ->withAnyParameters() + ->willReturn([]); + + $books = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER); + foreach ($books as $book) { + $this->backend->deleteCalendar($book['id'], true); + } + } + + public function testGetName(): void { + $name = $this->publicCalendarRoot->getName(); + $this->assertEquals('public-calendars', $name); + } + + public function testGetChild(): void { + $calendar = $this->createPublicCalendar(); + + $publicCalendars = $this->backend->getPublicCalendars(); + $this->assertEquals(1, count($publicCalendars)); + $this->assertEquals(true, $publicCalendars[0]['{http://owncloud.org/ns}public']); + + $publicCalendarURI = $publicCalendars[0]['uri']; + + $calendarResult = $this->publicCalendarRoot->getChild($publicCalendarURI); + $this->assertEquals($calendar, $calendarResult); + } + + public function testGetChildren(): void { + $this->createPublicCalendar(); + $calendarResults = $this->publicCalendarRoot->getChildren(); + $this->assertSame([], $calendarResults); + } + + protected function createPublicCalendar(): Calendar { + $this->backend->createCalendar(self::UNIT_TEST_USER, 'Example', []); + + $calendarInfo = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER)[0]; + $calendar = new PublicCalendar($this->backend, $calendarInfo, $this->l10n, $this->config, $this->logger); + $publicUri = $calendar->setPublishStatus(true); + + $calendarInfo = $this->backend->getPublicCalendar($publicUri); + $calendar = new PublicCalendar($this->backend, $calendarInfo, $this->l10n, $this->config, $this->logger); + + return $calendar; + } +} diff --git a/apps/dav/tests/unit/CalDAV/PublicCalendarTest.php b/apps/dav/tests/unit/CalDAV/PublicCalendarTest.php new file mode 100644 index 00000000000..98153a067fb --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/PublicCalendarTest.php @@ -0,0 +1,151 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV; + +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CalDAV\PublicCalendar; +use OCP\IConfig; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Sabre\VObject\Reader; + +class PublicCalendarTest extends CalendarTest { + + #[\PHPUnit\Framework\Attributes\DataProvider('providesConfidentialClassificationData')] + public function testPrivateClassification(int $expectedChildren, bool $isShared): void { + $calObject0 = ['uri' => 'event-0', 'classification' => CalDavBackend::CLASSIFICATION_PUBLIC]; + $calObject1 = ['uri' => 'event-1', 'classification' => CalDavBackend::CLASSIFICATION_CONFIDENTIAL]; + $calObject2 = ['uri' => 'event-2', 'classification' => CalDavBackend::CLASSIFICATION_PRIVATE]; + + /** @var CalDavBackend&MockObject $backend */ + $backend = $this->getMockBuilder(CalDavBackend::class)->disableOriginalConstructor()->getMock(); + $backend->expects($this->any())->method('getCalendarObjects')->willReturn([ + $calObject0, $calObject1, $calObject2 + ]); + $backend->expects($this->any())->method('getMultipleCalendarObjects') + ->with(666, ['event-0', 'event-1', 'event-2']) + ->willReturn([ + $calObject0, $calObject1, $calObject2 + ]); + $backend->expects($this->any())->method('getCalendarObject') + ->willReturn($calObject2)->with(666, 'event-2'); + $backend->expects($this->any())->method('applyShareAcl')->willReturnArgument(1); + + $calendarInfo = [ + '{http://owncloud.org/ns}owner-principal' => 'user2', + 'principaluri' => 'user2', + 'id' => 666, + 'uri' => 'cal', + ]; + /** @var IConfig&MockObject $config */ + $config = $this->createMock(IConfig::class); + /** @var LoggerInterface&MockObject $logger */ + $logger = $this->createMock(LoggerInterface::class); + $c = new PublicCalendar($backend, $calendarInfo, $this->l10n, $config, $logger); + $children = $c->getChildren(); + $this->assertEquals(2, count($children)); + $children = $c->getMultipleChildren(['event-0', 'event-1', 'event-2']); + $this->assertEquals(2, count($children)); + + $this->assertFalse($c->childExists('event-2')); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providesConfidentialClassificationData')] + public function testConfidentialClassification(int $expectedChildren, bool $isShared): void { + $start = '20160609'; + $end = '20160610'; + + $calData = <<<EOD +BEGIN:VCALENDAR +PRODID:-//ownCloud calendar v1.2.2 +BEGIN:VEVENT +CREATED:20160602T133732 +DTSTAMP:20160602T133732 +LAST-MODIFIED:20160602T133732 +UID:wej2z68l9h +SUMMARY:Test Event +LOCATION:Somewhere ... +ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CUTYPE=INDIVIDUAL;CN=de + epdiver:MAILTO:thomas.mueller@tmit.eu +ORGANIZER;CN=deepdiver:MAILTO:thomas.mueller@tmit.eu +DESCRIPTION:maybe .... +DTSTART;TZID=Europe/Berlin;VALUE=DATE:$start +DTEND;TZID=Europe/Berlin;VALUE=DATE:$end +RRULE:FREQ=DAILY +BEGIN:VALARM +ACTION:AUDIO +TRIGGER:-PT15M +END:VALARM +END:VEVENT +BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:DAYLIGHT +DTSTART:19810329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +TZNAME:MESZ +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +END:DAYLIGHT +BEGIN:STANDARD +DTSTART:19961027T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +TZNAME:MEZ +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +END:STANDARD +END:VTIMEZONE +END:VCALENDAR +EOD; + + $calObject0 = ['uri' => 'event-0', 'classification' => CalDavBackend::CLASSIFICATION_PUBLIC]; + $calObject1 = ['uri' => 'event-1', 'classification' => CalDavBackend::CLASSIFICATION_CONFIDENTIAL, 'calendardata' => $calData]; + $calObject2 = ['uri' => 'event-2', 'classification' => CalDavBackend::CLASSIFICATION_PRIVATE]; + + /** @var CalDavBackend&MockObject $backend */ + $backend = $this->getMockBuilder(CalDavBackend::class)->disableOriginalConstructor()->getMock(); + $backend->expects($this->any())->method('getCalendarObjects')->willReturn([ + $calObject0, $calObject1, $calObject2 + ]); + $backend->expects($this->any())->method('getMultipleCalendarObjects') + ->with(666, ['event-0', 'event-1', 'event-2']) + ->willReturn([ + $calObject0, $calObject1, $calObject2 + ]); + $backend->expects($this->any())->method('getCalendarObject') + ->willReturn($calObject1)->with(666, 'event-1'); + $backend->expects($this->any())->method('applyShareAcl')->willReturnArgument(1); + + $calendarInfo = [ + '{http://owncloud.org/ns}owner-principal' => 'user1', + 'principaluri' => 'user2', + 'id' => 666, + 'uri' => 'cal', + ]; + /** @var IConfig&MockObject $config */ + $config = $this->createMock(IConfig::class); + /** @var LoggerInterface&MockObject $logger */ + $logger = $this->createMock(LoggerInterface::class); + $c = new PublicCalendar($backend, $calendarInfo, $this->l10n, $config, $logger); + + $this->assertEquals(count($c->getChildren()), 2); + + // test private event + $privateEvent = $c->getChild('event-1'); + $calData = $privateEvent->get(); + $event = Reader::read($calData); + + $this->assertEquals($start, $event->VEVENT->DTSTART->getValue()); + $this->assertEquals($end, $event->VEVENT->DTEND->getValue()); + + $this->assertEquals('Busy', $event->VEVENT->SUMMARY->getValue()); + $this->assertArrayNotHasKey('ATTENDEE', $event->VEVENT); + $this->assertArrayNotHasKey('LOCATION', $event->VEVENT); + $this->assertArrayNotHasKey('DESCRIPTION', $event->VEVENT); + $this->assertArrayNotHasKey('ORGANIZER', $event->VEVENT); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Publishing/PublisherTest.php b/apps/dav/tests/unit/CalDAV/Publishing/PublisherTest.php new file mode 100644 index 00000000000..5344ec5d7cd --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Publishing/PublisherTest.php @@ -0,0 +1,61 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\Publishing; + +use OCA\DAV\CalDAV\Publishing\Xml\Publisher; +use Sabre\Xml\Writer; +use Test\TestCase; + +class PublisherTest extends TestCase { + public const NS_CALENDARSERVER = 'http://calendarserver.org/ns/'; + + public function testSerializePublished(): void { + $publish = new Publisher('urltopublish', true); + + $xml = $this->write([ + '{' . self::NS_CALENDARSERVER . '}publish-url' => $publish, + ]); + + $this->assertEquals('urltopublish', $publish->getValue()); + + $this->assertXmlStringEqualsXmlString( + '<?xml version="1.0"?> + <x1:publish-url xmlns:d="DAV:" xmlns:x1="' . self::NS_CALENDARSERVER . '"> + <d:href>urltopublish</d:href> + </x1:publish-url>', $xml); + } + + public function testSerializeNotPublished(): void { + $publish = new Publisher('urltopublish', false); + + $xml = $this->write([ + '{' . self::NS_CALENDARSERVER . '}pre-publish-url' => $publish, + ]); + + $this->assertEquals('urltopublish', $publish->getValue()); + + $this->assertXmlStringEqualsXmlString( + '<?xml version="1.0"?> + <x1:pre-publish-url xmlns:d="DAV:" xmlns:x1="' . self::NS_CALENDARSERVER . '">urltopublish</x1:pre-publish-url>', $xml); + } + + + protected array $elementMap = []; + protected array $namespaceMap = ['DAV:' => 'd']; + protected string $contextUri = '/'; + + private function write($input) { + $writer = new Writer(); + $writer->contextUri = $this->contextUri; + $writer->namespaceMap = $this->namespaceMap; + $writer->openMemory(); + $writer->setIndent(true); + $writer->write($input); + return $writer->outputMemory(); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Publishing/PublishingTest.php b/apps/dav/tests/unit/CalDAV/Publishing/PublishingTest.php new file mode 100644 index 00000000000..ec2ae37a929 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Publishing/PublishingTest.php @@ -0,0 +1,74 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\Publishing; + +use OCA\DAV\CalDAV\Calendar; +use OCA\DAV\CalDAV\Publishing\PublishPlugin; +use OCP\IConfig; +use OCP\IRequest; +use OCP\IURLGenerator; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\Server; +use Sabre\DAV\SimpleCollection; +use Sabre\HTTP\Request; +use Sabre\HTTP\Response; +use Test\TestCase; + +class PublishingTest extends TestCase { + private PublishPlugin $plugin; + private Server $server; + private Calendar&MockObject $book; + private IConfig&MockObject $config; + private IURLGenerator&MockObject $urlGenerator; + + protected function setUp(): void { + parent::setUp(); + + $this->config = $this->createMock(IConfig::class); + $this->config->expects($this->any())->method('getSystemValue') + ->with($this->equalTo('secret')) + ->willReturn('mysecret'); + + $this->urlGenerator = $this->createMock(IURLGenerator::class); + + /** @var IRequest $request */ + $this->plugin = new PublishPlugin($this->config, $this->urlGenerator); + + $root = new SimpleCollection('calendars'); + $this->server = new Server($root); + /** @var SimpleCollection $node */ + $this->book = $this->getMockBuilder(Calendar::class) + ->disableOriginalConstructor() + ->getMock(); + $this->book->method('getName')->willReturn('cal1'); + $root->addChild($this->book); + $this->plugin->initialize($this->server); + } + + public function testPublishing(): void { + $this->book->expects($this->once())->method('setPublishStatus')->with(true); + + // setup request + $request = new Request('POST', 'cal1'); + $request->addHeader('Content-Type', 'application/xml'); + $request->setBody('<o:publish-calendar xmlns:o="http://calendarserver.org/ns/"/>'); + $response = new Response(); + $this->plugin->httpPost($request, $response); + } + + public function testUnPublishing(): void { + $this->book->expects($this->once())->method('setPublishStatus')->with(false); + + // setup request + $request = new Request('POST', 'cal1'); + $request->addHeader('Content-Type', 'application/xml'); + $request->setBody('<o:unpublish-calendar xmlns:o="http://calendarserver.org/ns/"/>'); + $response = new Response(); + $this->plugin->httpPost($request, $response); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Reminder/BackendTest.php b/apps/dav/tests/unit/CalDAV/Reminder/BackendTest.php new file mode 100644 index 00000000000..356acf2dd7f --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Reminder/BackendTest.php @@ -0,0 +1,377 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\Reminder; + +use OCA\DAV\CalDAV\Reminder\Backend as ReminderBackend; +use OCP\AppFramework\Utility\ITimeFactory; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class BackendTest extends TestCase { + private ReminderBackend $reminderBackend; + private ITimeFactory&MockObject $timeFactory; + + protected function setUp(): void { + parent::setUp(); + + $query = self::$realDatabase->getQueryBuilder(); + $query->delete('calendar_reminders')->executeStatement(); + $query->delete('calendarobjects')->executeStatement(); + $query->delete('calendars')->executeStatement(); + + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->reminderBackend = new ReminderBackend(self::$realDatabase, $this->timeFactory); + + $this->createRemindersTestSet(); + } + + protected function tearDown(): void { + $query = self::$realDatabase->getQueryBuilder(); + $query->delete('calendar_reminders')->executeStatement(); + $query->delete('calendarobjects')->executeStatement(); + $query->delete('calendars')->executeStatement(); + + parent::tearDown(); + } + + + public function testCleanRemindersForEvent(): void { + $query = self::$realDatabase->getQueryBuilder(); + $rows = $query->select('*') + ->from('calendar_reminders') + ->execute() + ->fetchAll(); + + $this->assertCount(4, $rows); + + $this->reminderBackend->cleanRemindersForEvent(1); + + $query = self::$realDatabase->getQueryBuilder(); + $rows = $query->select('*') + ->from('calendar_reminders') + ->execute() + ->fetchAll(); + + $this->assertCount(2, $rows); + } + + public function testCleanRemindersForCalendar(): void { + $query = self::$realDatabase->getQueryBuilder(); + $rows = $query->select('*') + ->from('calendar_reminders') + ->execute() + ->fetchAll(); + + $this->assertCount(4, $rows); + + $this->reminderBackend->cleanRemindersForCalendar(1); + + $query = self::$realDatabase->getQueryBuilder(); + $rows = $query->select('*') + ->from('calendar_reminders') + ->execute() + ->fetchAll(); + + $this->assertCount(1, $rows); + } + + public function testRemoveReminder(): void { + $query = self::$realDatabase->getQueryBuilder(); + $rows = $query->select('*') + ->from('calendar_reminders') + ->execute() + ->fetchAll(); + + $this->assertCount(4, $rows); + + $this->reminderBackend->removeReminder((int)$rows[3]['id']); + + $query = self::$realDatabase->getQueryBuilder(); + $rows = $query->select('*') + ->from('calendar_reminders') + ->execute() + ->fetchAll(); + + $this->assertCount(3, $rows); + } + + public function testGetRemindersToProcess(): void { + $this->timeFactory->expects($this->exactly(1)) + ->method('getTime') + ->with() + ->willReturn(123457); + + $rows = $this->reminderBackend->getRemindersToProcess(); + + $this->assertCount(2, $rows); + unset($rows[0]['id']); + unset($rows[1]['id']); + + $expected1 = [ + 'calendar_id' => 1, + 'object_id' => 1, + 'uid' => 'asd', + 'is_recurring' => false, + 'recurrence_id' => 123458, + 'is_recurrence_exception' => false, + 'event_hash' => 'asd123', + 'alarm_hash' => 'asd567', + 'type' => 'EMAIL', + 'is_relative' => true, + 'notification_date' => 123456, + 'is_repeat_based' => false, + 'calendardata' => 'Calendar data 123', + 'displayname' => 'Displayname 123', + 'principaluri' => 'principals/users/user001', + ]; + $expected2 = [ + 'calendar_id' => 1, + 'object_id' => 1, + 'uid' => 'asd', + 'is_recurring' => false, + 'recurrence_id' => 123458, + 'is_recurrence_exception' => false, + 'event_hash' => 'asd123', + 'alarm_hash' => 'asd567', + 'type' => 'AUDIO', + 'is_relative' => true, + 'notification_date' => 123456, + 'is_repeat_based' => false, + 'calendardata' => 'Calendar data 123', + 'displayname' => 'Displayname 123', + 'principaluri' => 'principals/users/user001', + ]; + + $this->assertEqualsCanonicalizing([$rows[0],$rows[1]], [$expected1,$expected2]); + } + + public function testGetAllScheduledRemindersForEvent(): void { + $rows = $this->reminderBackend->getAllScheduledRemindersForEvent(1); + + $this->assertCount(2, $rows); + unset($rows[0]['id']); + unset($rows[1]['id']); + + $this->assertEquals($rows[0], [ + 'calendar_id' => 1, + 'object_id' => 1, + 'uid' => 'asd', + 'is_recurring' => false, + 'recurrence_id' => 123458, + 'is_recurrence_exception' => false, + 'event_hash' => 'asd123', + 'alarm_hash' => 'asd567', + 'type' => 'EMAIL', + 'is_relative' => true, + 'notification_date' => 123456, + 'is_repeat_based' => false, + ]); + $this->assertEquals($rows[1], [ + 'calendar_id' => 1, + 'object_id' => 1, + 'uid' => 'asd', + 'is_recurring' => false, + 'recurrence_id' => 123458, + 'is_recurrence_exception' => false, + 'event_hash' => 'asd123', + 'alarm_hash' => 'asd567', + 'type' => 'AUDIO', + 'is_relative' => true, + 'notification_date' => 123456, + 'is_repeat_based' => false, + ]); + } + + public function testInsertReminder(): void { + $query = self::$realDatabase->getQueryBuilder(); + $rows = $query->select('*') + ->from('calendar_reminders') + ->execute() + ->fetchAll(); + + $this->assertCount(4, $rows); + + $this->reminderBackend->insertReminder(42, 1337, 'uid99', true, 12345678, + true, 'hash99', 'hash42', 'AUDIO', false, 12345670, false); + + $query = self::$realDatabase->getQueryBuilder(); + $rows = $query->select('*') + ->from('calendar_reminders') + ->execute() + ->fetchAll(); + + $this->assertCount(5, $rows); + + unset($rows[4]['id']); + + $this->assertEquals($rows[4], [ + 'calendar_id' => '42', + 'object_id' => '1337', + 'is_recurring' => '1', + 'uid' => 'uid99', + 'recurrence_id' => '12345678', + 'is_recurrence_exception' => '1', + 'event_hash' => 'hash99', + 'alarm_hash' => 'hash42', + 'type' => 'AUDIO', + 'is_relative' => '0', + 'notification_date' => '12345670', + 'is_repeat_based' => '0', + ]); + } + + public function testUpdateReminder(): void { + $query = self::$realDatabase->getQueryBuilder(); + $rows = $query->select('*') + ->from('calendar_reminders') + ->executeQuery() + ->fetchAll(); + + $this->assertCount(4, $rows); + + $this->assertEquals($rows[3]['notification_date'], 123600); + + $reminderId = (int)$rows[3]['id']; + $newNotificationDate = 123700; + + $this->reminderBackend->updateReminder($reminderId, $newNotificationDate); + + $query = self::$realDatabase->getQueryBuilder(); + $row = $query->select('notification_date') + ->from('calendar_reminders') + ->where($query->expr()->eq('id', $query->createNamedParameter($reminderId))) + ->executeQuery() + ->fetch(); + + $this->assertEquals((int)$row['notification_date'], 123700); + } + + + private function createRemindersTestSet(): void { + $query = self::$realDatabase->getQueryBuilder(); + $query->insert('calendars') + ->values([ + 'id' => $query->createNamedParameter(1), + 'principaluri' => $query->createNamedParameter('principals/users/user001'), + 'displayname' => $query->createNamedParameter('Displayname 123'), + ]) + ->executeStatement(); + + $query = self::$realDatabase->getQueryBuilder(); + $query->insert('calendars') + ->values([ + 'id' => $query->createNamedParameter(99), + 'principaluri' => $query->createNamedParameter('principals/users/user002'), + 'displayname' => $query->createNamedParameter('Displayname 99'), + ]) + ->executeStatement(); + + $query = self::$realDatabase->getQueryBuilder(); + $query->insert('calendarobjects') + ->values([ + 'id' => $query->createNamedParameter(1), + 'calendardata' => $query->createNamedParameter('Calendar data 123'), + 'calendarid' => $query->createNamedParameter(1), + 'size' => $query->createNamedParameter(42), + ]) + ->executeStatement(); + + $query = self::$realDatabase->getQueryBuilder(); + $query->insert('calendarobjects') + ->values([ + 'id' => $query->createNamedParameter(2), + 'calendardata' => $query->createNamedParameter('Calendar data 456'), + 'calendarid' => $query->createNamedParameter(1), + 'size' => $query->createNamedParameter(42), + ]) + ->executeStatement(); + + $query = self::$realDatabase->getQueryBuilder(); + $query->insert('calendarobjects') + ->values([ + 'id' => $query->createNamedParameter(10), + 'calendardata' => $query->createNamedParameter('Calendar data 789'), + 'calendarid' => $query->createNamedParameter(99), + 'size' => $query->createNamedParameter(42), + ]) + ->executeStatement(); + + $query = self::$realDatabase->getQueryBuilder(); + $query->insert('calendar_reminders') + ->values([ + 'calendar_id' => $query->createNamedParameter(1), + 'object_id' => $query->createNamedParameter(1), + 'uid' => $query->createNamedParameter('asd'), + 'is_recurring' => $query->createNamedParameter(0), + 'recurrence_id' => $query->createNamedParameter(123458), + 'is_recurrence_exception' => $query->createNamedParameter(0), + 'event_hash' => $query->createNamedParameter('asd123'), + 'alarm_hash' => $query->createNamedParameter('asd567'), + 'type' => $query->createNamedParameter('EMAIL'), + 'is_relative' => $query->createNamedParameter(1), + 'notification_date' => $query->createNamedParameter(123456), + 'is_repeat_based' => $query->createNamedParameter(0), + ]) + ->executeStatement(); + + $query = self::$realDatabase->getQueryBuilder(); + $query->insert('calendar_reminders') + ->values([ + 'calendar_id' => $query->createNamedParameter(1), + 'object_id' => $query->createNamedParameter(1), + 'uid' => $query->createNamedParameter('asd'), + 'is_recurring' => $query->createNamedParameter(0), + 'recurrence_id' => $query->createNamedParameter(123458), + 'is_recurrence_exception' => $query->createNamedParameter(0), + 'event_hash' => $query->createNamedParameter('asd123'), + 'alarm_hash' => $query->createNamedParameter('asd567'), + 'type' => $query->createNamedParameter('AUDIO'), + 'is_relative' => $query->createNamedParameter(1), + 'notification_date' => $query->createNamedParameter(123456), + 'is_repeat_based' => $query->createNamedParameter(0), + ]) + ->executeStatement(); + + $query = self::$realDatabase->getQueryBuilder(); + $query->insert('calendar_reminders') + ->values([ + 'calendar_id' => $query->createNamedParameter(1), + 'object_id' => $query->createNamedParameter(2), + 'uid' => $query->createNamedParameter('asd'), + 'is_recurring' => $query->createNamedParameter(0), + 'recurrence_id' => $query->createNamedParameter(123900), + 'is_recurrence_exception' => $query->createNamedParameter(0), + 'event_hash' => $query->createNamedParameter('asd123'), + 'alarm_hash' => $query->createNamedParameter('asd567'), + 'type' => $query->createNamedParameter('EMAIL'), + 'is_relative' => $query->createNamedParameter(1), + 'notification_date' => $query->createNamedParameter(123499), + 'is_repeat_based' => $query->createNamedParameter(0), + ]) + ->executeStatement(); + + $query = self::$realDatabase->getQueryBuilder(); + $query->insert('calendar_reminders') + ->values([ + 'calendar_id' => $query->createNamedParameter(99), + 'object_id' => $query->createNamedParameter(10), + 'uid' => $query->createNamedParameter('asd'), + 'is_recurring' => $query->createNamedParameter(0), + 'recurrence_id' => $query->createNamedParameter(123900), + 'is_recurrence_exception' => $query->createNamedParameter(0), + 'event_hash' => $query->createNamedParameter('asd123'), + 'alarm_hash' => $query->createNamedParameter('asd567'), + 'type' => $query->createNamedParameter('DISPLAY'), + 'is_relative' => $query->createNamedParameter(1), + 'notification_date' => $query->createNamedParameter(123600), + 'is_repeat_based' => $query->createNamedParameter(0), + ]) + ->executeStatement(); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AbstractNotificationProviderTestCase.php b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AbstractNotificationProviderTestCase.php new file mode 100644 index 00000000000..70b374298ea --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AbstractNotificationProviderTestCase.php @@ -0,0 +1,52 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\Reminder\NotificationProvider; + +use OCA\DAV\CalDAV\Reminder\NotificationProvider\AbstractProvider; +use OCP\IConfig; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\L10N\IFactory as L10NFactory; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Sabre\VObject\Component\VCalendar; +use Test\TestCase; + +abstract class AbstractNotificationProviderTestCase extends TestCase { + protected LoggerInterface&MockObject $logger; + protected L10NFactory&MockObject $l10nFactory; + protected IL10N&MockObject $l10n; + protected IURLGenerator&MockObject $urlGenerator; + protected IConfig&MockObject $config; + protected AbstractProvider $provider; + protected VCalendar $vcalendar; + protected string $calendarDisplayName; + protected IUser&MockObject $user; + + protected function setUp(): void { + parent::setUp(); + + $this->logger = $this->createMock(LoggerInterface::class); + $this->l10nFactory = $this->createMock(L10NFactory::class); + $this->l10n = $this->createMock(IL10N::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + $this->config = $this->createMock(IConfig::class); + + $this->vcalendar = new VCalendar(); + $this->vcalendar->add('VEVENT', [ + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2017-01-01 00:00:00+00:00'), // 1483228800, + 'UID' => 'uid1234', + ]); + $this->calendarDisplayName = 'Personal'; + + $this->user = $this->createMock(IUser::class); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AudioProviderTest.php b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AudioProviderTest.php new file mode 100644 index 00000000000..d03205eaeb9 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AudioProviderTest.php @@ -0,0 +1,17 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\Reminder\NotificationProvider; + +use OCA\DAV\CalDAV\Reminder\NotificationProvider\AudioProvider; + +class AudioProviderTest extends PushProviderTest { + public function testNotificationType():void { + $this->assertEquals(AudioProvider::NOTIFICATION_TYPE, 'AUDIO'); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/EmailProviderTest.php b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/EmailProviderTest.php new file mode 100644 index 00000000000..f7fbac2c407 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/EmailProviderTest.php @@ -0,0 +1,509 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\Reminder\NotificationProvider; + +use OCA\DAV\CalDAV\Reminder\NotificationProvider\EmailProvider; +use OCP\IL10N; +use OCP\IUser; +use OCP\Mail\IEMailTemplate; +use OCP\Mail\IMailer; +use OCP\Mail\IMessage; +use OCP\Util; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\VObject\Component\VCalendar; + +class EmailProviderTest extends AbstractNotificationProviderTestCase { + public const USER_EMAIL = 'frodo@hobb.it'; + private IMailer&MockObject $mailer; + + protected function setUp(): void { + parent::setUp(); + + $this->mailer = $this->createMock(IMailer::class); + + $this->provider = new EmailProvider( + $this->config, + $this->mailer, + $this->logger, + $this->l10nFactory, + $this->urlGenerator + ); + } + + public function testSendWithoutAttendees():void { + [$user1, $user2, $user3, , $user5] = $users = $this->getUsers(); + $principalEmailAddresses = [$user1->getEmailAddress()]; + + $enL10N = $this->createMock(IL10N::class); + $enL10N->method('t') + ->willReturnArgument(0); + $enL10N->method('l') + ->willReturnArgument(0); + + $deL10N = $this->createMock(IL10N::class); + $deL10N->method('t') + ->willReturnArgument(0); + $deL10N->method('l') + ->willReturnArgument(0); + + $this->l10nFactory + ->method('getUserLanguage') + ->willReturnMap([ + [$user1, 'en'], + [$user2, 'de'], + [$user3, 'de'], + [$user5, 'de'], + ]); + + $this->l10nFactory + ->method('findGenericLanguage') + ->willReturn('en'); + + $this->l10nFactory + ->method('languageExists') + ->willReturnMap([ + ['dav', 'en', true], + ['dav', 'de', true], + ]); + + $this->l10nFactory + ->method('get') + ->willReturnMap([ + ['dav', 'en', null, $enL10N], + ['dav', 'de', null, $deL10N], + ]); + + $template1 = $this->getTemplateMock(); + $message11 = $this->getMessageMock('uid1@example.com', $template1); + $template2 = $this->getTemplateMock(); + $message21 = $this->getMessageMock('uid2@example.com', $template2); + $message22 = $this->getMessageMock('uid3@example.com', $template2); + + $this->mailer->expects($this->exactly(2)) + ->method('createEMailTemplate') + ->with('dav.calendarReminder') + ->willReturnOnConsecutiveCalls( + $template1, + $template2 + ); + + $this->mailer->expects($this->exactly(4)) + ->method('validateMailAddress') + ->willReturnMap([ + ['uid1@example.com', true], + ['uid2@example.com', true], + ['uid3@example.com', true], + ['invalid', false], + ]); + + $this->mailer->expects($this->exactly(3)) + ->method('createMessage') + ->with() + ->willReturnOnConsecutiveCalls( + $message11, + $message21, + $message22 + ); + + $calls = [ + [$message11], + [$message21], + [$message22], + ]; + $this->mailer->expects($this->exactly(count($calls))) + ->method('send') + ->willReturnCallback(function () use (&$calls) { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + return []; + }); + + $this->setupURLGeneratorMock(2); + + $vcalendar = $this->getNoAttendeeVCalendar(); + $this->provider->send($vcalendar->VEVENT, $this->calendarDisplayName, $principalEmailAddresses, $users); + } + + public function testSendWithAttendeesWhenOwnerIsOrganizer(): void { + [$user1, $user2, $user3, , $user5] = $users = $this->getUsers(); + $principalEmailAddresses = [$user1->getEmailAddress()]; + + $enL10N = $this->createMock(IL10N::class); + $enL10N->method('t') + ->willReturnArgument(0); + $enL10N->method('l') + ->willReturnArgument(0); + + $deL10N = $this->createMock(IL10N::class); + $deL10N->method('t') + ->willReturnArgument(0); + $deL10N->method('l') + ->willReturnArgument(0); + + $this->l10nFactory + ->method('getUserLanguage') + ->willReturnMap([ + [$user1, 'en'], + [$user2, 'de'], + [$user3, 'de'], + [$user5, 'de'], + ]); + + $this->l10nFactory + ->method('findGenericLanguage') + ->willReturn('en'); + + $this->l10nFactory + ->method('languageExists') + ->willReturnMap([ + ['dav', 'en', true], + ['dav', 'de', true], + ]); + + $this->l10nFactory + ->method('get') + ->willReturnMap([ + ['dav', 'en', null, $enL10N], + ['dav', 'de', null, $deL10N], + ]); + + $template1 = $this->getTemplateMock(); + $message11 = $this->getMessageMock('foo1@example.org', $template1); + $message12 = $this->getMessageMock('uid2@example.com', $template1); + $message13 = $this->getMessageMock('uid3@example.com', $template1); + $template2 = $this->getTemplateMock(); + $message21 = $this->getMessageMock('foo3@example.org', $template2); + $message22 = $this->getMessageMock('foo4@example.org', $template2); + $message23 = $this->getMessageMock('uid1@example.com', $template2); + + $this->mailer->expects(self::exactly(2)) + ->method('createEMailTemplate') + ->with('dav.calendarReminder') + ->willReturnOnConsecutiveCalls( + $template1, + $template2, + ); + $this->mailer->expects($this->atLeastOnce()) + ->method('validateMailAddress') + ->willReturnMap([ + ['foo1@example.org', true], + ['foo3@example.org', true], + ['foo4@example.org', true], + ['uid1@example.com', true], + ['uid2@example.com', true], + ['uid3@example.com', true], + ['invalid', false], + ]); + $this->mailer->expects($this->exactly(6)) + ->method('createMessage') + ->with() + ->willReturnOnConsecutiveCalls( + $message11, + $message12, + $message13, + $message21, + $message22, + $message23, + ); + + $calls = [ + [$message11], + [$message12], + [$message13], + [$message21], + [$message22], + [$message23], + ]; + $this->mailer->expects($this->exactly(count($calls))) + ->method('send') + ->willReturnCallback(function () use (&$calls) { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + return []; + }); + $this->setupURLGeneratorMock(2); + + $vcalendar = $this->getAttendeeVCalendar(); + $this->provider->send($vcalendar->VEVENT, $this->calendarDisplayName, $principalEmailAddresses, $users); + } + + public function testSendWithAttendeesWhenOwnerIsAttendee(): void { + [$user1, $user2, $user3] = $this->getUsers(); + $users = [$user2, $user3]; + $principalEmailAddresses = [$user2->getEmailAddress()]; + + $deL10N = $this->createMock(IL10N::class); + $deL10N->method('t') + ->willReturnArgument(0); + $deL10N->method('l') + ->willReturnArgument(0); + + $this->l10nFactory + ->method('getUserLanguage') + ->willReturnMap([ + [$user2, 'de'], + [$user3, 'de'], + ]); + + $this->l10nFactory + ->method('findGenericLanguage') + ->willReturn('en'); + + $this->l10nFactory + ->method('languageExists') + ->willReturnMap([ + ['dav', 'de', true], + ]); + + $this->l10nFactory + ->method('get') + ->willReturnMap([ + ['dav', 'de', null, $deL10N], + ]); + + $template1 = $this->getTemplateMock(); + $message12 = $this->getMessageMock('uid2@example.com', $template1); + $message13 = $this->getMessageMock('uid3@example.com', $template1); + + $this->mailer->expects(self::once()) + ->method('createEMailTemplate') + ->with('dav.calendarReminder') + ->willReturnOnConsecutiveCalls( + $template1, + ); + $this->mailer->expects($this->atLeastOnce()) + ->method('validateMailAddress') + ->willReturnMap([ + ['foo1@example.org', true], + ['foo3@example.org', true], + ['foo4@example.org', true], + ['uid1@example.com', true], + ['uid2@example.com', true], + ['uid3@example.com', true], + ['invalid', false], + ]); + $this->mailer->expects($this->exactly(2)) + ->method('createMessage') + ->with() + ->willReturnOnConsecutiveCalls( + $message12, + $message13, + ); + + $calls = [ + [$message12], + [$message13], + ]; + $this->mailer->expects($this->exactly(count($calls))) + ->method('send') + ->willReturnCallback(function () use (&$calls) { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + return []; + }); + $this->setupURLGeneratorMock(1); + + $vcalendar = $this->getAttendeeVCalendar(); + $this->provider->send($vcalendar->VEVENT, $this->calendarDisplayName, $principalEmailAddresses, $users); + } + + /** + * @return IEMailTemplate + */ + private function getTemplateMock():IEMailTemplate { + $template = $this->createMock(IEMailTemplate::class); + + $template->expects($this->once()) + ->method('addHeader') + ->with() + ->willReturn($template); + + $template->expects($this->once()) + ->method('setSubject') + ->with() + ->willReturn($template); + + $template->expects($this->once()) + ->method('addHeading') + ->with() + ->willReturn($template); + + $template->expects($this->exactly(4)) + ->method('addBodyListItem') + ->with() + ->willReturn($template); + + $template->expects($this->once()) + ->method('addFooter') + ->with() + ->willReturn($template); + + return $template; + } + + /** + * @param string $toMail + * @param IEMailTemplate $templateMock + * @param array|null $replyTo + * @return IMessage + */ + private function getMessageMock(string $toMail, IEMailTemplate $templateMock, ?array $replyTo = null):IMessage { + $message = $this->createMock(IMessage::class); + $i = 0; + + $message->expects($this->once()) + ->method('setFrom') + ->with([Util::getDefaultEmailAddress('reminders-noreply')]) + ->willReturn($message); + + if ($replyTo) { + $message->expects($this->once()) + ->method('setReplyTo') + ->with($replyTo) + ->willReturn($message); + } else { + $message->expects($this->never()) + ->method('setReplyTo'); + } + + $message->expects($this->once()) + ->method('setTo') + ->with([$toMail]) + ->willReturn($message); + + $message->expects($this->once()) + ->method('useTemplate') + ->with($templateMock) + ->willReturn($message); + + return $message; + } + + private function getNoAttendeeVCalendar():VCalendar { + $vcalendar = new VCalendar(); + $vcalendar->add('VEVENT', [ + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2017-01-01 00:00:00+00:00'), // 1483228800, + 'UID' => 'uid1234', + 'LOCATION' => 'Location 123', + 'DESCRIPTION' => 'DESCRIPTION 456', + ]); + + return $vcalendar; + } + + private function getAttendeeVCalendar():VCalendar { + $vcalendar = new VCalendar(); + $vcalendar->add('VEVENT', [ + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2017-01-01 00:00:00+00:00'), // 1483228800, + 'UID' => 'uid1234', + 'LOCATION' => 'Location 123', + 'DESCRIPTION' => 'DESCRIPTION 456', + ]); + + $vcalendar->VEVENT->add( + 'ORGANIZER', + 'mailto:uid1@example.com', + [ + 'LANG' => 'en' + ] + ); + + $vcalendar->VEVENT->add( + 'ATTENDEE', + 'mailto:foo1@example.org', + [ + 'LANG' => 'de', + 'PARTSTAT' => 'NEEDS-ACTION', + ] + ); + + $vcalendar->VEVENT->add( + 'ATTENDEE', + 'mailto:foo2@example.org', + [ + 'LANG' => 'de', + 'PARTSTAT' => 'DECLINED', + ] + ); + + $vcalendar->VEVENT->add( + 'ATTENDEE', + 'mailto:foo3@example.org', + [ + 'LANG' => 'en', + 'PARTSTAT' => 'CONFIRMED', + ] + ); + + $vcalendar->VEVENT->add( + 'ATTENDEE', + 'mailto:foo4@example.org' + ); + + $vcalendar->VEVENT->add( + 'ATTENDEE', + 'tomail:foo5@example.org' + ); + + return $vcalendar; + } + + private function setupURLGeneratorMock(int $times = 1): void { + $this->urlGenerator + ->expects($this->exactly($times * 4)) + ->method('imagePath') + ->willReturnMap([ + ['core', 'actions/info.png', 'imagePath1'], + ['core', 'places/calendar.png', 'imagePath2'], + ['core', 'actions/address.png', 'imagePath3'], + ['core', 'actions/more.png', 'imagePath4'], + ]); + $this->urlGenerator + ->expects($this->exactly($times * 4)) + ->method('getAbsoluteURL') + ->willReturnMap([ + ['imagePath1', 'AbsURL1'], + ['imagePath2', 'AbsURL2'], + ['imagePath3', 'AbsURL3'], + ['imagePath4', 'AbsURL4'], + ]); + } + + private function getUsers(): array { + $user1 = $this->createMock(IUser::class); + $user1->method('getUID') + ->willReturn('uid1'); + $user1->method('getEMailAddress') + ->willReturn('uid1@example.com'); + $user2 = $this->createMock(IUser::class); + $user2->method('getUID') + ->willReturn('uid2'); + $user2->method('getEMailAddress') + ->willReturn('uid2@example.com'); + $user3 = $this->createMock(IUser::class); + $user3->method('getUID') + ->willReturn('uid3'); + $user3->method('getEMailAddress') + ->willReturn('uid3@example.com'); + $user4 = $this->createMock(IUser::class); + $user4->method('getUID') + ->willReturn('uid4'); + $user4->method('getEMailAddress') + ->willReturn(null); + $user5 = $this->createMock(IUser::class); + $user5->method('getUID') + ->willReturn('uid5'); + $user5->method('getEMailAddress') + ->willReturn('invalid'); + + return [$user1, $user2, $user3, $user4, $user5]; + } +} diff --git a/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/PushProviderTest.php b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/PushProviderTest.php new file mode 100644 index 00000000000..5034af49cae --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/PushProviderTest.php @@ -0,0 +1,173 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\Reminder\NotificationProvider; + +use OCA\DAV\CalDAV\Reminder\NotificationProvider\PushProvider; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IUser; +use OCP\Notification\IManager; +use OCP\Notification\INotification; +use PHPUnit\Framework\MockObject\MockObject; + +class PushProviderTest extends AbstractNotificationProviderTestCase { + private IManager&MockObject $manager; + private ITimeFactory&MockObject $timeFactory; + + protected function setUp(): void { + parent::setUp(); + + $this->manager = $this->createMock(IManager::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + + $this->provider = new PushProvider( + $this->config, + $this->manager, + $this->logger, + $this->l10nFactory, + $this->urlGenerator, + $this->timeFactory + ); + } + + public function testNotificationType():void { + $this->assertEquals(PushProvider::NOTIFICATION_TYPE, 'DISPLAY'); + } + + public function testNotSend(): void { + $this->config->expects($this->once()) + ->method('getAppValue') + ->with('dav', 'sendEventRemindersPush', 'yes') + ->willReturn('no'); + + $this->manager->expects($this->never()) + ->method('createNotification'); + $this->manager->expects($this->never()) + ->method('notify'); + + $user1 = $this->createMock(IUser::class); + $user1->method('getUID') + ->willReturn('uid1'); + $user2 = $this->createMock(IUser::class); + $user2->method('getUID') + ->willReturn('uid2'); + $user3 = $this->createMock(IUser::class); + $user3->method('getUID') + ->willReturn('uid3'); + + $users = [$user1, $user2, $user3]; + + $this->provider->send($this->vcalendar->VEVENT, $this->calendarDisplayName, [], $users); + } + + public function testSend(): void { + $this->config->expects($this->once()) + ->method('getAppValue') + ->with('dav', 'sendEventRemindersPush', 'yes') + ->willReturn('yes'); + + $user1 = $this->createMock(IUser::class); + $user1->method('getUID') + ->willReturn('uid1'); + $user2 = $this->createMock(IUser::class); + $user2->method('getUID') + ->willReturn('uid2'); + $user3 = $this->createMock(IUser::class); + $user3->method('getUID') + ->willReturn('uid3'); + + $users = [$user1, $user2, $user3]; + + $dateTime = new \DateTime('@946684800'); + $this->timeFactory->method('getDateTime') + ->with() + ->willReturn($dateTime); + + $notification1 = $this->createNotificationMock('uid1', $dateTime); + $notification2 = $this->createNotificationMock('uid2', $dateTime); + $notification3 = $this->createNotificationMock('uid3', $dateTime); + + $this->manager->expects($this->exactly(3)) + ->method('createNotification') + ->willReturnOnConsecutiveCalls( + $notification1, + $notification2, + $notification3 + ); + + $calls = [ + $notification1, + $notification2, + $notification3, + ]; + $this->manager->expects($this->exactly(3)) + ->method('notify') + ->willReturnCallback(function ($notification) use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, $notification); + }); + + $this->provider->send($this->vcalendar->VEVENT, $this->calendarDisplayName, [], $users); + } + + /** + * @param string $uid + * @param \DateTime $dt + */ + private function createNotificationMock(string $uid, \DateTime $dt):INotification { + $notification = $this->createMock(INotification::class); + $notification + ->expects($this->once()) + ->method('setApp') + ->with('dav') + ->willReturn($notification); + + $notification->expects($this->once()) + ->method('setUser') + ->with($uid) + ->willReturn($notification); + + $notification->expects($this->once()) + ->method('setDateTime') + ->with($dt) + ->willReturn($notification); + + $notification->expects($this->once()) + ->method('setObject') + ->with('dav', hash('sha256', 'uid1234', false)) + ->willReturn($notification); + + $notification->expects($this->once()) + ->method('setSubject') + ->with('calendar_reminder', [ + 'title' => 'Fellowship meeting', + 'start_atom' => '2017-01-01T00:00:00+00:00', + ]) + ->willReturn($notification); + + $notification + ->expects($this->once()) + ->method('setMessage') + ->with('calendar_reminder', [ + 'title' => 'Fellowship meeting', + 'start_atom' => '2017-01-01T00:00:00+00:00', + 'description' => null, + 'location' => null, + 'all_day' => false, + 'start_is_floating' => false, + 'start_timezone' => 'UTC', + 'end_atom' => '2017-01-01T00:00:00+00:00', + 'end_is_floating' => false, + 'end_timezone' => 'UTC', + 'calendar_displayname' => 'Personal', + ]) + ->willReturn($notification); + + return $notification; + } +} diff --git a/apps/dav/tests/unit/CalDAV/Reminder/NotificationProviderManagerTest.php b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProviderManagerTest.php new file mode 100644 index 00000000000..6b813ed0228 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProviderManagerTest.php @@ -0,0 +1,83 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\Reminder; + +use OCA\DAV\CalDAV\Reminder\NotificationProvider\EmailProvider; +use OCA\DAV\CalDAV\Reminder\NotificationProvider\ProviderNotAvailableException; +use OCA\DAV\CalDAV\Reminder\NotificationProvider\PushProvider; +use OCA\DAV\CalDAV\Reminder\NotificationProviderManager; +use OCA\DAV\CalDAV\Reminder\NotificationTypeDoesNotExistException; +use OCA\DAV\Capabilities; +use OCP\AppFramework\QueryException; +use Test\TestCase; + +/** + * @group DB + */ +class NotificationProviderManagerTest extends TestCase { + private NotificationProviderManager $providerManager; + + /** + * @throws QueryException + */ + protected function setUp(): void { + parent::setUp(); + + $this->providerManager = new NotificationProviderManager(); + $this->providerManager->registerProvider(EmailProvider::class); + } + + /** + * @throws ProviderNotAvailableException + * @throws NotificationTypeDoesNotExistException + */ + public function testGetProviderForUnknownType(): void { + $this->expectException(NotificationTypeDoesNotExistException::class); + $this->expectExceptionMessage('Type NOT EXISTENT is not an accepted type of notification'); + + $this->providerManager->getProvider('NOT EXISTENT'); + } + + /** + * @throws NotificationTypeDoesNotExistException + * @throws ProviderNotAvailableException + */ + public function testGetProviderForUnRegisteredType(): void { + $this->expectException(ProviderNotAvailableException::class); + $this->expectExceptionMessage('No notification provider for type AUDIO available'); + + $this->providerManager->getProvider('AUDIO'); + } + + public function testGetProvider(): void { + $provider = $this->providerManager->getProvider('EMAIL'); + $this->assertInstanceOf(EmailProvider::class, $provider); + } + + public function testRegisterProvider(): void { + $this->providerManager->registerProvider(PushProvider::class); + $provider = $this->providerManager->getProvider('DISPLAY'); + $this->assertInstanceOf(PushProvider::class, $provider); + } + + /** + * @throws QueryException + */ + public function testRegisterBadProvider(): void { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid notification provider registered'); + + $this->providerManager->registerProvider(Capabilities::class); + } + + public function testHasProvider(): void { + $this->assertTrue($this->providerManager->hasProvider('EMAIL')); + $this->assertFalse($this->providerManager->hasProvider('EMAIL123')); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Reminder/NotifierTest.php b/apps/dav/tests/unit/CalDAV/Reminder/NotifierTest.php new file mode 100644 index 00000000000..c091f590711 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Reminder/NotifierTest.php @@ -0,0 +1,263 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\Reminder; + +use OCA\DAV\AppInfo\Application; +use OCA\DAV\CalDAV\Reminder\Notifier; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\L10N\IFactory; +use OCP\Notification\AlreadyProcessedException; +use OCP\Notification\INotification; +use OCP\Notification\UnknownNotificationException; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class NotifierTest extends TestCase { + protected IFactory&MockObject $factory; + protected IURLGenerator&MockObject $urlGenerator; + protected IL10N&MockObject $l10n; + protected ITimeFactory&MockObject $timeFactory; + protected Notifier $notifier; + + protected function setUp(): void { + parent::setUp(); + + $this->urlGenerator = $this->createMock(IURLGenerator::class); + $this->l10n = $this->createMock(IL10N::class); + $this->l10n->expects($this->any()) + ->method('t') + ->willReturnCallback(function ($string, $args) { + if (!is_array($args)) { + $args = [$args]; + } + return vsprintf($string, $args); + }); + $this->l10n->expects($this->any()) + ->method('l') + ->willReturnCallback(function ($string, $args) { + /** \DateTime $args */ + return $args->format(\DateTime::ATOM); + }); + $this->l10n->expects($this->any()) + ->method('n') + ->willReturnCallback(function ($textSingular, $textPlural, $count, $args) { + $text = $count === 1 ? $textSingular : $textPlural; + $text = str_replace('%n', (string)$count, $text); + return vsprintf($text, $args); + }); + $this->factory = $this->createMock(IFactory::class); + $this->factory->expects($this->any()) + ->method('get') + ->willReturn($this->l10n); + + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->timeFactory + ->method('getDateTime') + ->willReturn(\DateTime::createFromFormat(\DateTime::ATOM, '2005-08-15T14:00:00+02:00')); + + $this->notifier = new Notifier( + $this->factory, + $this->urlGenerator, + $this->timeFactory + ); + } + + public function testGetId():void { + $this->assertEquals($this->notifier->getID(), 'dav'); + } + + public function testGetName():void { + $this->assertEquals($this->notifier->getName(), 'Calendar'); + } + + + public function testPrepareWrongApp(): void { + $this->expectException(UnknownNotificationException::class); + $this->expectExceptionMessage('Notification not from this app'); + + /** @var INotification&MockObject $notification */ + $notification = $this->createMock(INotification::class); + + $notification->expects($this->once()) + ->method('getApp') + ->willReturn('notifications'); + $notification->expects($this->never()) + ->method('getSubject'); + + $this->notifier->prepare($notification, 'en'); + } + + + public function testPrepareWrongSubject(): void { + $this->expectException(UnknownNotificationException::class); + $this->expectExceptionMessage('Unknown subject'); + + /** @var INotification&MockObject $notification */ + $notification = $this->createMock(INotification::class); + + $notification->expects($this->once()) + ->method('getApp') + ->willReturn(Application::APP_ID); + $notification->expects($this->once()) + ->method('getSubject') + ->willReturn('wrong subject'); + + $this->notifier->prepare($notification, 'en'); + } + + private static function hasPhpDatetimeDiffBug(): bool { + $d1 = \DateTime::createFromFormat(\DateTimeInterface::ATOM, '2023-11-22T11:52:00+01:00'); + $d2 = new \DateTime('2023-11-22T10:52:03', new \DateTimeZone('UTC')); + + // The difference is 3 seconds, not -1year+11months+… + return $d1->diff($d2)->y < 0; + } + + public static function dataPrepare(): array { + return [ + [ + 'calendar_reminder', + [ + 'title' => 'Title of this event', + 'start_atom' => '2005-08-15T15:52:01+02:00' + ], + self::hasPhpDatetimeDiffBug() ? 'Title of this event' : 'Title of this event (in 1 hour, 52 minutes)', + [ + 'title' => 'Title of this event', + 'description' => null, + 'location' => 'NC Headquarters', + 'all_day' => false, + 'start_atom' => '2005-08-15T15:52:01+02:00', + 'start_is_floating' => false, + 'start_timezone' => 'Europe/Berlin', + 'end_atom' => '2005-08-15T17:52:01+02:00', + 'end_is_floating' => false, + 'end_timezone' => 'Europe/Berlin', + 'calendar_displayname' => 'Personal', + ], + "Calendar: Personal\r\nDate: 2005-08-15T15:52:01+02:00, 2005-08-15T15:52:01+02:00 - 2005-08-15T17:52:01+02:00 (Europe/Berlin)\r\nWhere: NC Headquarters" + ], + [ + 'calendar_reminder', + [ + 'title' => 'Title of this event', + 'start_atom' => '2005-08-15T13:00:00+02:00', + ], + self::hasPhpDatetimeDiffBug() ? 'Title of this event' : 'Title of this event (1 hour ago)', + [ + 'title' => 'Title of this event', + 'description' => null, + 'location' => 'NC Headquarters', + 'all_day' => false, + 'start_atom' => '2005-08-15T13:00:00+02:00', + 'start_is_floating' => false, + 'start_timezone' => 'Europe/Berlin', + 'end_atom' => '2005-08-15T15:00:00+02:00', + 'end_is_floating' => false, + 'end_timezone' => 'Europe/Berlin', + 'calendar_displayname' => 'Personal', + ], + "Calendar: Personal\r\nDate: 2005-08-15T13:00:00+02:00, 2005-08-15T13:00:00+02:00 - 2005-08-15T15:00:00+02:00 (Europe/Berlin)\r\nWhere: NC Headquarters" + ], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataPrepare')] + public function testPrepare(string $subjectType, array $subjectParams, string $subject, array $messageParams, string $message): void { + /** @var INotification&MockObject $notification */ + $notification = $this->createMock(INotification::class); + + $notification->expects($this->once()) + ->method('getApp') + ->willReturn(Application::APP_ID); + $notification->expects($this->once()) + ->method('getSubject') + ->willReturn($subjectType); + $notification->expects($this->once()) + ->method('getSubjectParameters') + ->willReturn($subjectParams); + $notification->expects($this->once()) + ->method('getMessageParameters') + ->willReturn($messageParams); + + $notification->expects($this->once()) + ->method('setParsedSubject') + ->with($subject) + ->willReturnSelf(); + + $notification->expects($this->once()) + ->method('setParsedMessage') + ->with($message) + ->willReturnSelf(); + + $this->urlGenerator->expects($this->once()) + ->method('imagePath') + ->with('core', 'places/calendar.svg') + ->willReturn('icon-url'); + $this->urlGenerator->expects($this->once()) + ->method('getAbsoluteURL') + ->with('icon-url') + ->willReturn('absolute-icon-url'); + $notification->expects($this->once()) + ->method('setIcon') + ->with('absolute-icon-url') + ->willReturnSelf(); + + $return = $this->notifier->prepare($notification, 'en'); + + $this->assertEquals($notification, $return); + } + + public function testPassedEvent(): void { + /** @var INotification&MockObject $notification */ + $notification = $this->createMock(INotification::class); + + $notification->expects($this->once()) + ->method('getApp') + ->willReturn(Application::APP_ID); + $notification->expects($this->once()) + ->method('getSubject') + ->willReturn('calendar_reminder'); + $notification->expects($this->once()) + ->method('getSubjectParameters') + ->willReturn([ + 'title' => 'Title of this event', + 'start_atom' => '2005-08-15T08:00:00+02:00' + ]); + + $notification->expects($this->once()) + ->method('getMessageParameters') + ->willReturn([ + 'title' => 'Title of this event', + 'description' => null, + 'location' => 'NC Headquarters', + 'all_day' => false, + 'start_atom' => '2005-08-15T08:00:00+02:00', + 'start_is_floating' => false, + 'start_timezone' => 'Europe/Berlin', + 'end_atom' => '2005-08-15T13:00:00+02:00', + 'end_is_floating' => false, + 'end_timezone' => 'Europe/Berlin', + 'calendar_displayname' => 'Personal', + ]); + + $notification->expects($this->once()) + ->method('setParsedSubject') + ->with(self::hasPhpDatetimeDiffBug() ? 'Title of this event' : 'Title of this event (6 hours ago)') + ->willReturnSelf(); + + $this->expectException(AlreadyProcessedException::class); + + $return = $this->notifier->prepare($notification, 'en'); + + $this->assertEquals($notification, $return); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Reminder/ReminderServiceTest.php b/apps/dav/tests/unit/CalDAV/Reminder/ReminderServiceTest.php new file mode 100644 index 00000000000..c18901c5f58 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Reminder/ReminderServiceTest.php @@ -0,0 +1,777 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\Reminder; + +use DateTime; +use DateTimeZone; +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CalDAV\Reminder\Backend; +use OCA\DAV\CalDAV\Reminder\INotificationProvider; +use OCA\DAV\CalDAV\Reminder\NotificationProviderManager; +use OCA\DAV\CalDAV\Reminder\ReminderService; +use OCA\DAV\Connector\Sabre\Principal; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\IUserManager; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class ReminderServiceTest extends TestCase { + private Backend&MockObject $backend; + private NotificationProviderManager&MockObject $notificationProviderManager; + private IUserManager&MockObject $userManager; + private IGroupManager&MockObject $groupManager; + private CalDavBackend&MockObject $caldavBackend; + private ITimeFactory&MockObject $timeFactory; + private IConfig&MockObject $config; + private LoggerInterface&MockObject $logger; + private Principal&MockObject $principalConnector; + private ReminderService $reminderService; + + public const CALENDAR_DATA = <<<EOD +BEGIN:VCALENDAR +PRODID:-//Nextcloud calendar v1.6.4 +BEGIN:VEVENT +CREATED:20160602T133732 +DTSTAMP:20160602T133732 +LAST-MODIFIED:20160602T133732 +UID:wej2z68l9h +SUMMARY:Test Event +LOCATION:Somewhere ... +DESCRIPTION:maybe .... +DTSTART;TZID=Europe/Berlin;VALUE=DATE:20160609 +DTEND;TZID=Europe/Berlin;VALUE=DATE:20160610 +BEGIN:VALARM +ACTION:EMAIL +TRIGGER:-PT15M +END:VALARM +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER;VALUE=DATE-TIME:20160608T000000Z +END:VALARM +END:VEVENT +END:VCALENDAR +EOD; + + public const CALENDAR_DATA_REPEAT = <<<EOD +BEGIN:VCALENDAR +PRODID:-//Nextcloud calendar v1.6.4 +BEGIN:VEVENT +CREATED:20160602T133732 +DTSTAMP:20160602T133732 +LAST-MODIFIED:20160602T133732 +UID:wej2z68l9h +SUMMARY:Test Event +LOCATION:Somewhere ... +DESCRIPTION:maybe .... +DTSTART;TZID=Europe/Berlin;VALUE=DATE:20160609 +DTEND;TZID=Europe/Berlin;VALUE=DATE:20160610 +BEGIN:VALARM +ACTION:EMAIL +TRIGGER:-PT15M +REPEAT:4 +DURATION:PT2M +END:VALARM +END:VEVENT +END:VCALENDAR +EOD; + + public const CALENDAR_DATA_RECURRING = <<<EOD +BEGIN:VCALENDAR +PRODID:-//Nextcloud calendar v1.6.4 +BEGIN:VEVENT +CREATED:20160602T133732 +DTSTAMP:20160602T133732 +LAST-MODIFIED:20160602T133732 +UID:wej2z68l9h +SUMMARY:Test Event +LOCATION:Somewhere ... +DESCRIPTION:maybe .... +DTSTART;TZID=Europe/Berlin;VALUE=DATE:20160609 +DTEND;TZID=Europe/Berlin;VALUE=DATE:20160610 +RRULE:FREQ=WEEKLY +BEGIN:VALARM +ACTION:EMAIL +TRIGGER:-PT15M +END:VALARM +BEGIN:VALARM +ACTION:EMAIL +TRIGGER:-P8D +END:VALARM +END:VEVENT +END:VCALENDAR +EOD; + + public const CALENDAR_DATA_RECURRING_REPEAT = <<<EOD +BEGIN:VCALENDAR +PRODID:-//Nextcloud calendar v1.6.4 +BEGIN:VEVENT +CREATED:20160602T133732 +DTSTAMP:20160602T133732 +LAST-MODIFIED:20160602T133732 +UID:wej2z68l9h +SUMMARY:Test Event +LOCATION:Somewhere ... +DESCRIPTION:maybe .... +DTSTART;TZID=Europe/Berlin;VALUE=DATE:20160609 +DTEND;TZID=Europe/Berlin;VALUE=DATE:20160610 +RRULE:FREQ=WEEKLY +BEGIN:VALARM +ACTION:EMAIL +TRIGGER:-PT15M +REPEAT:4 +DURATION:PT2M +END:VALARM +BEGIN:VALARM +ACTION:EMAIL +TRIGGER:-P8D +END:VALARM +END:VEVENT +END:VCALENDAR +EOD; + + public const CALENDAR_DATA_NO_ALARM = <<<EOD +BEGIN:VCALENDAR +PRODID:-//Nextcloud calendar v1.6.4 +BEGIN:VEVENT +CREATED:20160602T133732 +DTSTAMP:20160602T133732 +LAST-MODIFIED:20160602T133732 +UID:wej2z68l9h +SUMMARY:Test Event +LOCATION:Somewhere ... +DESCRIPTION:maybe .... +DTSTART;TZID=Europe/Berlin;VALUE=DATE:20160609 +DTEND;TZID=Europe/Berlin;VALUE=DATE:20160610 +END:VEVENT +END:VCALENDAR +EOD; + + private const CALENDAR_DATA_ONE_TIME = <<<EOD +BEGIN:VCALENDAR +PRODID:-//IDN nextcloud.com//Calendar app 4.3.0-alpha.0//EN +CALSCALE:GREGORIAN +VERSION:2.0 +BEGIN:VEVENT +CREATED:20230203T154600Z +DTSTAMP:20230203T154602Z +LAST-MODIFIED:20230203T154602Z +SEQUENCE:2 +UID:f6a565b6-f9a8-4d1e-9d01-c8dcbe716b7e +DTSTART;TZID=Europe/Vienna:20230204T090000 +DTEND;TZID=Europe/Vienna:20230204T120000 +STATUS:CONFIRMED +SUMMARY:TEST +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER;RELATED=START:-PT1H +END:VALARM +END:VEVENT +BEGIN:VTIMEZONE +TZID:Europe/Vienna +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +END:VCALENDAR +EOD; + + private const CALENDAR_DATA_ALL_DAY = <<<EOD +BEGIN:VCALENDAR +PRODID:-//IDN nextcloud.com//Calendar app 4.3.0-alpha.0//EN +CALSCALE:GREGORIAN +VERSION:2.0 +BEGIN:VEVENT +CREATED:20230203T113430Z +DTSTAMP:20230203T113432Z +LAST-MODIFIED:20230203T113432Z +SEQUENCE:2 +UID:a163a056-ba26-44a2-8080-955f19611a8f +DTSTART;VALUE=DATE:20230204 +DTEND;VALUE=DATE:20230205 +STATUS:CONFIRMED +SUMMARY:TEST +BEGIN:VALARM +ACTION:EMAIL +TRIGGER;RELATED=START:-PT1H +END:VALARM +END:VEVENT +END:VCALENDAR +EOD; + + private const PAGO_PAGO_VTIMEZONE_ICS = <<<ICS +BEGIN:VCALENDAR +BEGIN:VTIMEZONE +TZID:Pacific/Pago_Pago +BEGIN:STANDARD +TZOFFSETFROM:-1100 +TZOFFSETTO:-1100 +TZNAME:SST +DTSTART:19700101T000000 +END:STANDARD +END:VTIMEZONE +END:VCALENDAR +ICS; + + private ?string $oldTimezone; + + protected function setUp(): void { + parent::setUp(); + + $this->backend = $this->createMock(Backend::class); + $this->notificationProviderManager = $this->createMock(NotificationProviderManager::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->groupManager = $this->createMock(IGroupManager::class); + $this->caldavBackend = $this->createMock(CalDavBackend::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->config = $this->createMock(IConfig::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->principalConnector = $this->createMock(Principal::class); + + $this->caldavBackend->method('getShares')->willReturn([]); + + $this->reminderService = new ReminderService( + $this->backend, + $this->notificationProviderManager, + $this->userManager, + $this->groupManager, + $this->caldavBackend, + $this->timeFactory, + $this->config, + $this->logger, + $this->principalConnector, + ); + } + + public function testOnCalendarObjectDelete():void { + $this->backend->expects($this->once()) + ->method('cleanRemindersForEvent') + ->with(44); + + $objectData = [ + 'id' => '44', + 'component' => 'vevent', + ]; + + $this->reminderService->onCalendarObjectDelete($objectData); + } + + public function testOnCalendarObjectCreateSingleEntry():void { + $objectData = [ + 'calendardata' => self::CALENDAR_DATA, + 'id' => '42', + 'calendarid' => '1337', + 'component' => 'vevent', + ]; + + $calls = [ + [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'de919af7429d3b5c11e8b9d289b411a6', 'EMAIL', true, 1465429500, false], + [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', '35b3eae8e792aa2209f0b4e1a302f105', 'DISPLAY', false, 1465344000, false] + ]; + $this->backend->expects($this->exactly(count($calls))) + ->method('insertReminder') + ->willReturnCallback(function () use (&$calls) { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + return 1; + }); + + $this->timeFactory->expects($this->once()) + ->method('getDateTime') + ->with() + ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2016-06-08T00:00:00+00:00')); + + $this->reminderService->onCalendarObjectCreate($objectData); + } + + /** + * RFC5545 says DTSTART is REQUIRED, but we have seen event without the prop + */ + public function testOnCalendarObjectCreateNoDtstart(): void { + $calendarData = <<<EOD +BEGIN:VCALENDAR +PRODID:-//Nextcloud calendar v1.6.4 +BEGIN:VEVENT +CREATED:20160602T133732 +DTSTAMP:20160602T133732 +LAST-MODIFIED:20160602T133732 +UID:wej2z68l9h +SUMMARY:Test Event +BEGIN:VALARM +ACTION:EMAIL +TRIGGER:-PT15M +END:VALARM +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER;VALUE=DATE-TIME:20160608T000000Z +END:VALARM +END:VEVENT +END:VCALENDAR +EOD; + $objectData = [ + 'calendardata' => $calendarData, + 'id' => '42', + 'calendarid' => '1337', + 'component' => 'vevent', + ]; + + $this->backend->expects($this->never()) + ->method('insertReminder'); + + $this->reminderService->onCalendarObjectCreate($objectData); + } + + public function testOnCalendarObjectCreateSingleEntryWithRepeat(): void { + $objectData = [ + 'calendardata' => self::CALENDAR_DATA_REPEAT, + 'id' => '42', + 'calendarid' => '1337', + 'component' => 'vevent', + ]; + + $calls = [ + [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1465429500, false], + [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1465429620, true], + [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1465429740, true], + [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1465429860, true], + [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1465429980, true] + ]; + $this->backend->expects($this->exactly(count($calls))) + ->method('insertReminder') + ->willReturnCallback(function () use (&$calls) { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + return 1; + }); + + $this->timeFactory->expects($this->once()) + ->method('getDateTime') + ->with() + ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2016-06-08T00:00:00+00:00')); + + $this->reminderService->onCalendarObjectCreate($objectData); + } + + public function testOnCalendarObjectCreateRecurringEntry(): void { + $objectData = [ + 'calendardata' => self::CALENDAR_DATA_RECURRING, + 'id' => '42', + 'calendarid' => '1337', + 'component' => 'vevent', + ]; + + $calls = [ + [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'de919af7429d3b5c11e8b9d289b411a6', 'EMAIL', true, 1467243900, false], + [1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', '8996992118817f9f311ac5cc56d1cc97', 'EMAIL', true, 1467158400, false] + ]; + $this->backend->expects($this->exactly(count($calls))) + ->method('insertReminder') + ->willReturnCallback(function () use (&$calls) { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + return 1; + }); + + $this->timeFactory->expects($this->once()) + ->method('getDateTime') + ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2016-06-29T00:00:00+00:00')); + + $this->reminderService->onCalendarObjectCreate($objectData); + } + + public function testOnCalendarObjectCreateEmpty():void { + $objectData = [ + 'calendardata' => self::CALENDAR_DATA_NO_ALARM, + 'id' => '42', + 'calendarid' => '1337', + 'component' => 'vevent', + ]; + + $this->backend->expects($this->never()) + ->method('insertReminder'); + + $this->reminderService->onCalendarObjectCreate($objectData); + } + + public function testOnCalendarObjectCreateAllDayWithNullTimezone(): void { + $objectData = [ + 'calendardata' => self::CALENDAR_DATA_ALL_DAY, + 'id' => '42', + 'calendarid' => '1337', + 'component' => 'vevent', + ]; + $this->timeFactory->expects($this->once()) + ->method('getDateTime') + ->with() + ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2023-02-03T13:28:00+00:00')); + $this->caldavBackend->expects(self::once()) + ->method('getCalendarById') + ->with(1337) + ->willReturn([ + '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => null, + ]); + + // One hour before midnight relative to the server's time + $expectedReminderTimstamp = (new DateTime('2023-02-03T23:00:00'))->getTimestamp(); + $this->backend->expects(self::once()) + ->method('insertReminder') + ->with(1337, 42, self::anything(), false, 1675468800, false, self::anything(), self::anything(), 'EMAIL', true, $expectedReminderTimstamp, false); + + $this->reminderService->onCalendarObjectCreate($objectData); + } + + public function testOnCalendarObjectCreateAllDayWithBlankTimezone(): void { + $objectData = [ + 'calendardata' => self::CALENDAR_DATA_ALL_DAY, + 'id' => '42', + 'calendarid' => '1337', + 'component' => 'vevent', + ]; + $this->timeFactory->expects($this->once()) + ->method('getDateTime') + ->with() + ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2023-02-03T13:28:00+00:00')); + $this->caldavBackend->expects(self::once()) + ->method('getCalendarById') + ->with(1337) + ->willReturn([ + '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => '', + ]); + + // One hour before midnight relative to the server's time + $expectedReminderTimstamp = (new DateTime('2023-02-03T23:00:00'))->getTimestamp(); + $this->backend->expects(self::once()) + ->method('insertReminder') + ->with(1337, 42, self::anything(), false, 1675468800, false, self::anything(), self::anything(), 'EMAIL', true, $expectedReminderTimstamp, false); + + $this->reminderService->onCalendarObjectCreate($objectData); + } + + public function testOnCalendarObjectCreateAllDayWithTimezone(): void { + $objectData = [ + 'calendardata' => self::CALENDAR_DATA_ALL_DAY, + 'id' => '42', + 'calendarid' => '1337', + 'component' => 'vevent', + ]; + $this->timeFactory->expects($this->once()) + ->method('getDateTime') + ->with() + ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2023-02-03T13:28:00+00:00')); + $this->caldavBackend->expects(self::once()) + ->method('getCalendarById') + ->with(1337) + ->willReturn([ + '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => self::PAGO_PAGO_VTIMEZONE_ICS, + ]); + + // One hour before midnight relative to the timezone + $expectedReminderTimstamp = (new DateTime('2023-02-03T23:00:00', new DateTimeZone('Pacific/Pago_Pago')))->getTimestamp(); + $this->backend->expects(self::once()) + ->method('insertReminder') + ->with(1337, 42, 'a163a056-ba26-44a2-8080-955f19611a8f', false, self::anything(), false, self::anything(), self::anything(), 'EMAIL', true, $expectedReminderTimstamp, false); + + $this->reminderService->onCalendarObjectCreate($objectData); + } + + public function testOnCalendarObjectCreateRecurringEntryWithRepeat():void { + $objectData = [ + 'calendardata' => self::CALENDAR_DATA_RECURRING_REPEAT, + 'id' => '42', + 'calendarid' => '1337', + 'component' => 'vevent', + ]; + $this->caldavBackend->expects(self::once()) + ->method('getCalendarById') + ->with(1337) + ->willReturn([ + '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => null, + ]); + + $calls = [ + [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467243900, false], + [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467244020, true], + [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467244140, true], + [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467244260, true], + [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467244380, true], + [1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', '8996992118817f9f311ac5cc56d1cc97', 'EMAIL', true, 1467158400, false] + ]; + $this->backend->expects($this->exactly(count($calls))) + ->method('insertReminder') + ->willReturnCallback(function () use (&$calls) { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + return 1; + }); + + $this->timeFactory->expects($this->once()) + ->method('getDateTime') + ->with() + ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2016-06-29T00:00:00+00:00')); + + $this->reminderService->onCalendarObjectCreate($objectData); + } + + public function testOnCalendarObjectCreateWithEventTimezoneAndCalendarTimezone():void { + $objectData = [ + 'calendardata' => self::CALENDAR_DATA_ONE_TIME, + 'id' => '42', + 'calendarid' => '1337', + 'component' => 'vevent', + ]; + $this->caldavBackend->expects(self::once()) + ->method('getCalendarById') + ->with(1337) + ->willReturn([ + '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => self::PAGO_PAGO_VTIMEZONE_ICS, + ]); + $expectedReminderTimstamp = (new DateTime('2023-02-04T08:00:00', new DateTimeZone('Europe/Vienna')))->getTimestamp(); + $this->backend->expects(self::once()) + ->method('insertReminder') + ->with(1337, 42, self::anything(), false, self::anything(), false, self::anything(), self::anything(), self::anything(), true, $expectedReminderTimstamp, false) + ->willReturn(1); + $this->caldavBackend->expects(self::once()) + ->method('getCalendarById') + ->with(1337) + ->willReturn([ + '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => null, + ]); + $this->timeFactory->expects($this->once()) + ->method('getDateTime') + ->with() + ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2023-02-03T13:28:00+00:00')); + ; + + $this->reminderService->onCalendarObjectCreate($objectData); + } + + public function testProcessReminders():void { + $this->backend->expects($this->once()) + ->method('getRemindersToProcess') + ->with() + ->willReturn([ + [ + 'id' => 1, + 'calendar_id' => 1337, + 'object_id' => 42, + 'uid' => 'wej2z68l9h', + 'is_recurring' => false, + 'recurrence_id' => 1465430400, + 'is_recurrence_exception' => false, + 'event_hash' => '5c70531aab15c92b52518ae10a2f78a4', + 'alarm_hash' => 'de919af7429d3b5c11e8b9d289b411a6', + 'type' => 'EMAIL', + 'is_relative' => true, + 'notification_date' => 1465429500, + 'is_repeat_based' => false, + 'calendardata' => self::CALENDAR_DATA, + 'displayname' => 'Displayname 123', + 'principaluri' => 'principals/users/user001', + ], + [ + 'id' => 2, + 'calendar_id' => 1337, + 'object_id' => 42, + 'uid' => 'wej2z68l9h', + 'is_recurring' => false, + 'recurrence_id' => 1465430400, + 'is_recurrence_exception' => false, + 'event_hash' => '5c70531aab15c92b52518ae10a2f78a4', + 'alarm_hash' => 'ecacbf07d413c3c78d1ac7ad8c469602', + 'type' => 'EMAIL', + 'is_relative' => true, + 'notification_date' => 1465429740, + 'is_repeat_based' => true, + 'calendardata' => self::CALENDAR_DATA_REPEAT, + 'displayname' => 'Displayname 123', + 'principaluri' => 'principals/users/user001', + ], + [ + 'id' => 3, + 'calendar_id' => 1337, + 'object_id' => 42, + 'uid' => 'wej2z68l9h', + 'is_recurring' => false, + 'recurrence_id' => 1465430400, + 'is_recurrence_exception' => false, + 'event_hash' => '5c70531aab15c92b52518ae10a2f78a4', + 'alarm_hash' => '35b3eae8e792aa2209f0b4e1a302f105', + 'type' => 'DISPLAY', + 'is_relative' => false, + 'notification_date' => 1465344000, + 'is_repeat_based' => false, + 'calendardata' => self::CALENDAR_DATA, + 'displayname' => 'Displayname 123', + 'principaluri' => 'principals/users/user001', + ], + [ + 'id' => 4, + 'calendar_id' => 1337, + 'object_id' => 42, + 'uid' => 'wej2z68l9h', + 'is_recurring' => true, + 'recurrence_id' => 1467244800, + 'is_recurrence_exception' => false, + 'event_hash' => 'fbdb2726bc0f7dfacac1d881c1453e20', + 'alarm_hash' => 'ecacbf07d413c3c78d1ac7ad8c469602', + 'type' => 'EMAIL', + 'is_relative' => true, + 'notification_date' => 1467243900, + 'is_repeat_based' => false, + 'calendardata' => self::CALENDAR_DATA_RECURRING_REPEAT, + 'displayname' => 'Displayname 123', + 'principaluri' => 'principals/users/user001', + ], + [ + 'id' => 5, + 'calendar_id' => 1337, + 'object_id' => 42, + 'uid' => 'wej2z68l9h', + 'is_recurring' => true, + 'recurrence_id' => 1467849600, + 'is_recurrence_exception' => false, + 'event_hash' => 'fbdb2726bc0f7dfacac1d881c1453e20', + 'alarm_hash' => '8996992118817f9f311ac5cc56d1cc97', + 'type' => 'EMAIL', + 'is_relative' => true, + 'notification_date' => 1467158400, + 'is_repeat_based' => false, + 'calendardata' => self::CALENDAR_DATA_RECURRING, + 'displayname' => 'Displayname 123', + 'principaluri' => 'principals/users/user001', + ] + ]); + + $this->notificationProviderManager->expects($this->exactly(5)) + ->method('hasProvider') + ->willReturnMap([ + ['EMAIL', true], + ['DISPLAY', true], + ]); + + $provider1 = $this->createMock(INotificationProvider::class); + $provider2 = $this->createMock(INotificationProvider::class); + $provider3 = $this->createMock(INotificationProvider::class); + $provider4 = $this->createMock(INotificationProvider::class); + $provider5 = $this->createMock(INotificationProvider::class); + + $getProviderCalls = [ + ['EMAIL', $provider1], + ['EMAIL', $provider2], + ['DISPLAY', $provider3], + ['EMAIL', $provider4], + ['EMAIL', $provider5], + ]; + $this->notificationProviderManager->expects($this->exactly(count($getProviderCalls))) + ->method('getProvider') + ->willReturnCallback(function () use (&$getProviderCalls) { + $expected = array_shift($getProviderCalls); + $return = array_pop($expected); + $this->assertEquals($expected, func_get_args()); + return $return; + }); + + $user = $this->createMock(IUser::class); + $this->userManager->expects($this->exactly(5)) + ->method('get') + ->with('user001') + ->willReturn($user); + + $provider1->expects($this->once()) + ->method('send') + ->with($this->callback(function ($vevent) { + if ($vevent->DTSTART->getDateTime()->format(DateTime::ATOM) !== '2016-06-09T00:00:00+00:00') { + return false; + } + return true; + }, 'Displayname 123', $user)); + $provider2->expects($this->once()) + ->method('send') + ->with($this->callback(function ($vevent) { + if ($vevent->DTSTART->getDateTime()->format(DateTime::ATOM) !== '2016-06-09T00:00:00+00:00') { + return false; + } + return true; + }, 'Displayname 123', $user)); + $provider3->expects($this->once()) + ->method('send') + ->with($this->callback(function ($vevent) { + if ($vevent->DTSTART->getDateTime()->format(DateTime::ATOM) !== '2016-06-09T00:00:00+00:00') { + return false; + } + return true; + }, 'Displayname 123', $user)); + $provider4->expects($this->once()) + ->method('send') + ->with($this->callback(function ($vevent) { + if ($vevent->DTSTART->getDateTime()->format(DateTime::ATOM) !== '2016-06-30T00:00:00+00:00') { + return false; + } + return true; + }, 'Displayname 123', $user)); + $provider5->expects($this->once()) + ->method('send') + ->with($this->callback(function ($vevent) { + if ($vevent->DTSTART->getDateTime()->format(DateTime::ATOM) !== '2016-07-07T00:00:00+00:00') { + return false; + } + return true; + }, 'Displayname 123', $user)); + + $removeReminderCalls = [ + [1], + [2], + [3], + [4], + [5], + ]; + $this->backend->expects($this->exactly(5)) + ->method('removeReminder') + ->willReturnCallback(function () use (&$removeReminderCalls): void { + $expected = array_shift($removeReminderCalls); + $this->assertEquals($expected, func_get_args()); + }); + + + $insertReminderCalls = [ + [1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467848700, false], + [1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467848820, true], + [1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467848940, true], + [1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467849060, true], + [1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467849180, true], + [1337, 42, 'wej2z68l9h', true, 1468454400, false, 'fbdb2726bc0f7dfacac1d881c1453e20', '8996992118817f9f311ac5cc56d1cc97', 'EMAIL', true, 1467763200, false], + ]; + $this->backend->expects($this->exactly(count($insertReminderCalls))) + ->method('insertReminder') + ->willReturnCallback(function () use (&$insertReminderCalls) { + $expected = array_shift($insertReminderCalls); + $this->assertEquals($expected, func_get_args()); + return 99; + }); + + $this->timeFactory->method('getDateTime') + ->willReturn(DateTime::createFromFormat(DateTime::ATOM, '2016-06-08T00:00:00+00:00')); + + $this->reminderService->processReminders(); + } +} diff --git a/apps/dav/tests/unit/CalDAV/ResourceBooking/AbstractPrincipalBackendTestCase.php b/apps/dav/tests/unit/CalDAV/ResourceBooking/AbstractPrincipalBackendTestCase.php new file mode 100644 index 00000000000..364bc74de49 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/ResourceBooking/AbstractPrincipalBackendTestCase.php @@ -0,0 +1,556 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\ResourceBooking; + +use OCA\DAV\CalDAV\Proxy\Proxy; +use OCA\DAV\CalDAV\Proxy\ProxyMapper; +use OCA\DAV\CalDAV\ResourceBooking\ResourcePrincipalBackend; +use OCA\DAV\CalDAV\ResourceBooking\RoomPrincipalBackend; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\IUserSession; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Sabre\DAV\PropPatch; +use Test\TestCase; + +abstract class AbstractPrincipalBackendTestCase extends TestCase { + protected ResourcePrincipalBackend|RoomPrincipalBackend $principalBackend; + protected IUserSession&MockObject $userSession; + protected IGroupManager&MockObject $groupManager; + protected LoggerInterface&MockObject $logger; + protected ProxyMapper&MockObject $proxyMapper; + protected string $mainDbTable; + protected string $metadataDbTable; + protected string $foreignKey; + protected string $principalPrefix; + protected string $expectedCUType; + + protected function setUp(): void { + parent::setUp(); + + $this->userSession = $this->createMock(IUserSession::class); + $this->groupManager = $this->createMock(IGroupManager::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->proxyMapper = $this->createMock(ProxyMapper::class); + } + + protected function tearDown(): void { + $query = self::$realDatabase->getQueryBuilder(); + + $query->delete('calendar_resources')->executeStatement(); + $query->delete('calendar_resources_md')->executeStatement(); + $query->delete('calendar_rooms')->executeStatement(); + $query->delete('calendar_rooms_md')->executeStatement(); + } + + public function testGetPrincipalsByPrefix(): void { + $actual = $this->principalBackend->getPrincipalsByPrefix($this->principalPrefix); + + $this->assertEquals([ + [ + 'uri' => $this->principalPrefix . '/backend1-res1', + '{DAV:}displayname' => 'Beamer1', + '{http://sabredav.org/ns}email-address' => 'res1@foo.bar', + '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => $this->expectedCUType, + ], + [ + 'uri' => $this->principalPrefix . '/backend1-res2', + '{DAV:}displayname' => 'TV1', + '{http://sabredav.org/ns}email-address' => 'res2@foo.bar', + '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => $this->expectedCUType, + ], + [ + 'uri' => $this->principalPrefix . '/backend2-res3', + '{DAV:}displayname' => 'Beamer2', + '{http://sabredav.org/ns}email-address' => 'res3@foo.bar', + '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => $this->expectedCUType, + '{http://nextcloud.com/ns}foo' => 'value1', + '{http://nextcloud.com/ns}meta2' => 'value2', + ], + [ + 'uri' => $this->principalPrefix . '/backend2-res4', + '{DAV:}displayname' => 'TV2', + '{http://sabredav.org/ns}email-address' => 'res4@foo.bar', + '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => $this->expectedCUType, + '{http://nextcloud.com/ns}meta1' => 'value1', + '{http://nextcloud.com/ns}meta3' => 'value3-old', + ], + [ + 'uri' => $this->principalPrefix . '/backend3-res5', + '{DAV:}displayname' => 'Beamer3', + '{http://sabredav.org/ns}email-address' => 'res5@foo.bar', + '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => $this->expectedCUType, + ], + [ + 'uri' => $this->principalPrefix . '/backend3-res6', + '{DAV:}displayname' => 'Pointer', + '{http://sabredav.org/ns}email-address' => 'res6@foo.bar', + '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => $this->expectedCUType, + '{http://nextcloud.com/ns}meta99' => 'value99' + ] + ], $actual); + } + + public function testGetNoPrincipalsByPrefixForWrongPrincipalPrefix(): void { + $actual = $this->principalBackend->getPrincipalsByPrefix('principals/users'); + $this->assertEquals([], $actual); + } + + public function testGetPrincipalByPath(): void { + $actual = $this->principalBackend->getPrincipalByPath($this->principalPrefix . '/backend2-res3'); + $this->assertEquals([ + 'uri' => $this->principalPrefix . '/backend2-res3', + '{DAV:}displayname' => 'Beamer2', + '{http://sabredav.org/ns}email-address' => 'res3@foo.bar', + '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => $this->expectedCUType, + '{http://nextcloud.com/ns}foo' => 'value1', + '{http://nextcloud.com/ns}meta2' => 'value2', + ], $actual); + } + + public function testGetPrincipalByPathNotFound(): void { + $actual = $this->principalBackend->getPrincipalByPath($this->principalPrefix . '/db-123'); + $this->assertEquals(null, $actual); + } + + public function testGetPrincipalByPathWrongPrefix(): void { + $actual = $this->principalBackend->getPrincipalByPath('principals/users/foo-bar'); + $this->assertEquals(null, $actual); + } + + public function testGetGroupMemberSet(): void { + $actual = $this->principalBackend->getGroupMemberSet($this->principalPrefix . '/backend1-res1'); + $this->assertEquals([], $actual); + } + + public function testGetGroupMemberSetProxyRead(): void { + $proxy1 = new Proxy(); + $proxy1->setProxyId('proxyId1'); + $proxy1->setPermissions(1); + + $proxy2 = new Proxy(); + $proxy2->setProxyId('proxyId2'); + $proxy2->setPermissions(3); + + $proxy3 = new Proxy(); + $proxy3->setProxyId('proxyId3'); + $proxy3->setPermissions(3); + + $this->proxyMapper->expects($this->once()) + ->method('getProxiesOf') + ->with($this->principalPrefix . '/backend1-res1') + ->willReturn([$proxy1, $proxy2, $proxy3]); + + $actual = $this->principalBackend->getGroupMemberSet($this->principalPrefix . '/backend1-res1/calendar-proxy-read'); + $this->assertEquals(['proxyId1'], $actual); + } + + public function testGetGroupMemberSetProxyWrite(): void { + $proxy1 = new Proxy(); + $proxy1->setProxyId('proxyId1'); + $proxy1->setPermissions(1); + + $proxy2 = new Proxy(); + $proxy2->setProxyId('proxyId2'); + $proxy2->setPermissions(3); + + $proxy3 = new Proxy(); + $proxy3->setProxyId('proxyId3'); + $proxy3->setPermissions(3); + + $this->proxyMapper->expects($this->once()) + ->method('getProxiesOf') + ->with($this->principalPrefix . '/backend1-res1') + ->willReturn([$proxy1, $proxy2, $proxy3]); + + $actual = $this->principalBackend->getGroupMemberSet($this->principalPrefix . '/backend1-res1/calendar-proxy-write'); + $this->assertEquals(['proxyId2', 'proxyId3'], $actual); + } + + public function testGetGroupMembership(): void { + $proxy1 = new Proxy(); + $proxy1->setOwnerId('proxyId1'); + $proxy1->setPermissions(1); + + $proxy2 = new Proxy(); + $proxy2->setOwnerId('proxyId2'); + $proxy2->setPermissions(3); + + $this->proxyMapper->expects($this->once()) + ->method('getProxiesFor') + ->with($this->principalPrefix . '/backend1-res1') + ->willReturn([$proxy1, $proxy2]); + + $actual = $this->principalBackend->getGroupMembership($this->principalPrefix . '/backend1-res1'); + + $this->assertEquals(['proxyId1/calendar-proxy-read', 'proxyId2/calendar-proxy-write'], $actual); + } + + public function testSetGroupMemberSet(): void { + $this->proxyMapper->expects($this->once()) + ->method('getProxiesOf') + ->with($this->principalPrefix . '/backend1-res1') + ->willReturn([]); + + $calls = [ + function ($proxy) { + /** @var Proxy $proxy */ + if ($proxy->getOwnerId() !== $this->principalPrefix . '/backend1-res1') { + return false; + } + if ($proxy->getProxyId() !== $this->principalPrefix . '/backend1-res2') { + return false; + } + if ($proxy->getPermissions() !== 3) { + return false; + } + + return true; + }, + function ($proxy) { + /** @var Proxy $proxy */ + if ($proxy->getOwnerId() !== $this->principalPrefix . '/backend1-res1') { + return false; + } + if ($proxy->getProxyId() !== $this->principalPrefix . '/backend2-res3') { + return false; + } + if ($proxy->getPermissions() !== 3) { + return false; + } + + return true; + } + ]; + $this->proxyMapper->expects($this->exactly(2)) + ->method('insert') + ->willReturnCallback(function ($proxy) use (&$calls) { + $expected = array_shift($calls); + $this->assertTrue($expected($proxy)); + return $proxy; + }); + + $this->principalBackend->setGroupMemberSet($this->principalPrefix . '/backend1-res1/calendar-proxy-write', [$this->principalPrefix . '/backend1-res2', $this->principalPrefix . '/backend2-res3']); + } + + public function testUpdatePrincipal(): void { + $propPatch = $this->createMock(PropPatch::class); + $actual = $this->principalBackend->updatePrincipal($this->principalPrefix . '/foo-bar', $propPatch); + + $this->assertEquals(0, $actual); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataSearchPrincipals')] + public function testSearchPrincipals($expected, $test): void { + $user = $this->createMock(IUser::class); + $this->userSession->expects($this->once()) + ->method('getUser') + ->with() + ->willReturn($user); + $this->groupManager->expects($this->once()) + ->method('getUserGroupIds') + ->with($user) + ->willReturn(['group1', 'group2']); + + $actual = $this->principalBackend->searchPrincipals($this->principalPrefix, [ + '{http://sabredav.org/ns}email-address' => 'foo', + '{DAV:}displayname' => 'Beamer', + ], $test); + + $this->assertEquals( + str_replace('%prefix%', $this->principalPrefix, $expected), + $actual); + } + + public static function dataSearchPrincipals(): array { + // data providers are called before we subclass + // this class, $this->principalPrefix is null + // at that point, so we need this hack + return [ + [[ + '%prefix%/backend1-res1', + '%prefix%/backend2-res3', + ], 'allof'], + [[ + '%prefix%/backend1-res1', + '%prefix%/backend1-res2', + '%prefix%/backend2-res3', + '%prefix%/backend2-res4', + '%prefix%/backend3-res6', + ], 'anyof'], + ]; + } + + public function testSearchPrincipalsByMetadataKey(): void { + $user = $this->createMock(IUser::class); + $this->userSession->expects($this->once()) + ->method('getUser') + ->with() + ->willReturn($user); + $this->groupManager->expects($this->once()) + ->method('getUserGroupIds') + ->with($user) + ->willReturn(['group1', 'group2']); + + $actual = $this->principalBackend->searchPrincipals($this->principalPrefix, [ + '{http://nextcloud.com/ns}meta3' => 'value', + ]); + + $this->assertEquals([ + $this->principalPrefix . '/backend2-res4', + ], $actual); + } + + public function testSearchPrincipalsByCalendarUserAddressSet(): void { + $user = $this->createMock(IUser::class); + $this->userSession->method('getUser') + ->with() + ->willReturn($user); + $this->groupManager->method('getUserGroupIds') + ->with($user) + ->willReturn(['group1', 'group2']); + + $actual = $this->principalBackend->searchPrincipals($this->principalPrefix, [ + '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set' => 'res2@foo.bar', + ]); + + $this->assertEquals( + str_replace('%prefix%', $this->principalPrefix, [ + '%prefix%/backend1-res2', + ]), + $actual); + } + + public function testSearchPrincipalsEmptySearchProperties(): void { + $this->userSession->expects($this->never()) + ->method('getUser'); + $this->groupManager->expects($this->never()) + ->method('getUserGroupIds'); + + $this->principalBackend->searchPrincipals($this->principalPrefix, []); + } + + public function testSearchPrincipalsWrongPrincipalPrefix(): void { + $this->userSession->expects($this->never()) + ->method('getUser'); + $this->groupManager->expects($this->never()) + ->method('getUserGroupIds'); + + $this->principalBackend->searchPrincipals('principals/users', [ + '{http://sabredav.org/ns}email-address' => 'foo' + ]); + } + + public function testFindByUriByEmail(): void { + $user = $this->createMock(IUser::class); + $this->userSession->expects($this->once()) + ->method('getUser') + ->with() + ->willReturn($user); + $this->groupManager->expects($this->once()) + ->method('getUserGroupIds') + ->with($user) + ->willReturn(['group1', 'group2']); + + $actual = $this->principalBackend->findByUri('mailto:res1@foo.bar', $this->principalPrefix); + $this->assertEquals($this->principalPrefix . '/backend1-res1', $actual); + } + + public function testFindByUriByEmailForbiddenResource(): void { + $user = $this->createMock(IUser::class); + $this->userSession->expects($this->once()) + ->method('getUser') + ->with() + ->willReturn($user); + $this->groupManager->expects($this->once()) + ->method('getUserGroupIds') + ->with($user) + ->willReturn(['group1', 'group2']); + + $actual = $this->principalBackend->findByUri('mailto:res5@foo.bar', $this->principalPrefix); + $this->assertEquals(null, $actual); + } + + public function testFindByUriByEmailNotFound(): void { + $user = $this->createMock(IUser::class); + $this->userSession->expects($this->once()) + ->method('getUser') + ->with() + ->willReturn($user); + $this->groupManager->expects($this->once()) + ->method('getUserGroupIds') + ->with($user) + ->willReturn(['group1', 'group2']); + + $actual = $this->principalBackend->findByUri('mailto:res99@foo.bar', $this->principalPrefix); + $this->assertEquals(null, $actual); + } + + public function testFindByUriByPrincipal(): void { + $user = $this->createMock(IUser::class); + $this->userSession->expects($this->once()) + ->method('getUser') + ->with() + ->willReturn($user); + $this->groupManager->expects($this->once()) + ->method('getUserGroupIds') + ->with($user) + ->willReturn(['group1', 'group2']); + + $actual = $this->principalBackend->findByUri('mailto:res6@foo.bar', $this->principalPrefix); + $this->assertEquals($this->principalPrefix . '/backend3-res6', $actual); + } + + public function testFindByUriByPrincipalForbiddenResource(): void { + $user = $this->createMock(IUser::class); + $this->userSession->expects($this->once()) + ->method('getUser') + ->with() + ->willReturn($user); + $this->groupManager->expects($this->once()) + ->method('getUserGroupIds') + ->with($user) + ->willReturn(['group1', 'group2']); + + $actual = $this->principalBackend->findByUri('principal:' . $this->principalPrefix . '/backend3-res5', $this->principalPrefix); + $this->assertEquals(null, $actual); + } + + public function testFindByUriByPrincipalNotFound(): void { + $user = $this->createMock(IUser::class); + $this->userSession->expects($this->once()) + ->method('getUser') + ->with() + ->willReturn($user); + $this->groupManager->expects($this->once()) + ->method('getUserGroupIds') + ->with($user) + ->willReturn(['group1', 'group2']); + + $actual = $this->principalBackend->findByUri('principal:' . $this->principalPrefix . '/db-123', $this->principalPrefix); + $this->assertEquals(null, $actual); + } + + public function testFindByUriByUnknownUri(): void { + $user = $this->createMock(IUser::class); + $this->userSession->expects($this->once()) + ->method('getUser') + ->with() + ->willReturn($user); + $this->groupManager->expects($this->once()) + ->method('getUserGroupIds') + ->with($user) + ->willReturn(['group1', 'group2']); + + $actual = $this->principalBackend->findByUri('foobar:blub', $this->principalPrefix); + $this->assertEquals(null, $actual); + } + + protected function createTestDatasetInDb() { + $query = self::$realDatabase->getQueryBuilder(); + $query->insert($this->mainDbTable) + ->values([ + 'backend_id' => $query->createNamedParameter('backend1'), + 'resource_id' => $query->createNamedParameter('res1'), + 'email' => $query->createNamedParameter('res1@foo.bar'), + 'displayname' => $query->createNamedParameter('Beamer1'), + 'group_restrictions' => $query->createNamedParameter('[]'), + ]) + ->execute(); + + $query->insert($this->mainDbTable) + ->values([ + 'backend_id' => $query->createNamedParameter('backend1'), + 'resource_id' => $query->createNamedParameter('res2'), + 'email' => $query->createNamedParameter('res2@foo.bar'), + 'displayname' => $query->createNamedParameter('TV1'), + 'group_restrictions' => $query->createNamedParameter('[]'), + ]) + ->execute(); + + $query->insert($this->mainDbTable) + ->values([ + 'backend_id' => $query->createNamedParameter('backend2'), + 'resource_id' => $query->createNamedParameter('res3'), + 'email' => $query->createNamedParameter('res3@foo.bar'), + 'displayname' => $query->createNamedParameter('Beamer2'), + 'group_restrictions' => $query->createNamedParameter('[]'), + ]) + ->execute(); + $id3 = $query->getLastInsertId(); + + $query->insert($this->mainDbTable) + ->values([ + 'backend_id' => $query->createNamedParameter('backend2'), + 'resource_id' => $query->createNamedParameter('res4'), + 'email' => $query->createNamedParameter('res4@foo.bar'), + 'displayname' => $query->createNamedParameter('TV2'), + 'group_restrictions' => $query->createNamedParameter('[]'), + ]) + ->execute(); + $id4 = $query->getLastInsertId(); + + $query->insert($this->mainDbTable) + ->values([ + 'backend_id' => $query->createNamedParameter('backend3'), + 'resource_id' => $query->createNamedParameter('res5'), + 'email' => $query->createNamedParameter('res5@foo.bar'), + 'displayname' => $query->createNamedParameter('Beamer3'), + 'group_restrictions' => $query->createNamedParameter('["foo", "bar"]'), + ]) + ->execute(); + + $query->insert($this->mainDbTable) + ->values([ + 'backend_id' => $query->createNamedParameter('backend3'), + 'resource_id' => $query->createNamedParameter('res6'), + 'email' => $query->createNamedParameter('res6@foo.bar'), + 'displayname' => $query->createNamedParameter('Pointer'), + 'group_restrictions' => $query->createNamedParameter('["group1", "bar"]'), + ]) + ->execute(); + $id6 = $query->getLastInsertId(); + + $query->insert($this->metadataDbTable) + ->values([ + $this->foreignKey => $query->createNamedParameter($id3), + 'key' => $query->createNamedParameter('{http://nextcloud.com/ns}foo'), + 'value' => $query->createNamedParameter('value1') + ]) + ->execute(); + $query->insert($this->metadataDbTable) + ->values([ + $this->foreignKey => $query->createNamedParameter($id3), + 'key' => $query->createNamedParameter('{http://nextcloud.com/ns}meta2'), + 'value' => $query->createNamedParameter('value2') + ]) + ->execute(); + $query->insert($this->metadataDbTable) + ->values([ + $this->foreignKey => $query->createNamedParameter($id4), + 'key' => $query->createNamedParameter('{http://nextcloud.com/ns}meta1'), + 'value' => $query->createNamedParameter('value1') + ]) + ->execute(); + $query->insert($this->metadataDbTable) + ->values([ + $this->foreignKey => $query->createNamedParameter($id4), + 'key' => $query->createNamedParameter('{http://nextcloud.com/ns}meta3'), + 'value' => $query->createNamedParameter('value3-old') + ]) + ->execute(); + $query->insert($this->metadataDbTable) + ->values([ + $this->foreignKey => $query->createNamedParameter($id6), + 'key' => $query->createNamedParameter('{http://nextcloud.com/ns}meta99'), + 'value' => $query->createNamedParameter('value99') + ]) + ->execute(); + } +} diff --git a/apps/dav/tests/unit/CalDAV/ResourceBooking/ResourcePrincipalBackendTest.php b/apps/dav/tests/unit/CalDAV/ResourceBooking/ResourcePrincipalBackendTest.php new file mode 100644 index 00000000000..168e21c3a91 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/ResourceBooking/ResourcePrincipalBackendTest.php @@ -0,0 +1,28 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\ResourceBooking; + +use OCA\DAV\CalDAV\ResourceBooking\ResourcePrincipalBackend; + +class ResourcePrincipalBackendTest extends AbstractPrincipalBackendTestCase { + protected function setUp(): void { + parent::setUp(); + + $this->principalBackend = new ResourcePrincipalBackend(self::$realDatabase, + $this->userSession, $this->groupManager, $this->logger, $this->proxyMapper); + + $this->mainDbTable = 'calendar_resources'; + $this->metadataDbTable = 'calendar_resources_md'; + $this->foreignKey = 'resource_id'; + + $this->principalPrefix = 'principals/calendar-resources'; + $this->expectedCUType = 'RESOURCE'; + + $this->createTestDatasetInDb(); + } +} diff --git a/apps/dav/tests/unit/CalDAV/ResourceBooking/RoomPrincipalBackendTest.php b/apps/dav/tests/unit/CalDAV/ResourceBooking/RoomPrincipalBackendTest.php new file mode 100644 index 00000000000..8a53b0ee25e --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/ResourceBooking/RoomPrincipalBackendTest.php @@ -0,0 +1,28 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\ResourceBooking; + +use OCA\DAV\CalDAV\ResourceBooking\RoomPrincipalBackend; + +class RoomPrincipalBackendTest extends AbstractPrincipalBackendTestCase { + protected function setUp(): void { + parent::setUp(); + + $this->principalBackend = new RoomPrincipalBackend(self::$realDatabase, + $this->userSession, $this->groupManager, $this->logger, $this->proxyMapper); + + $this->mainDbTable = 'calendar_rooms'; + $this->metadataDbTable = 'calendar_rooms_md'; + $this->foreignKey = 'room_id'; + + $this->principalPrefix = 'principals/calendar-rooms'; + $this->expectedCUType = 'ROOM'; + + $this->createTestDatasetInDb(); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginCharsetTest.php b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginCharsetTest.php new file mode 100644 index 00000000000..fa52d5319c9 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginCharsetTest.php @@ -0,0 +1,193 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Tests\unit\CalDAV\Schedule; + +use OC\L10N\L10N; +use OC\URLGenerator; +use OCA\DAV\CalDAV\EventComparisonService; +use OCA\DAV\CalDAV\Schedule\IMipPlugin; +use OCA\DAV\CalDAV\Schedule\IMipService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Defaults; +use OCP\IAppConfig; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\IUserSession; +use OCP\L10N\IFactory; +use OCP\Mail\IMailer; +use OCP\Mail\IMessage; +use OCP\Mail\Provider\IManager; +use OCP\Mail\Provider\IMessageSend; +use OCP\Mail\Provider\IService; +use OCP\Mail\Provider\Message as MailProviderMessage; +use OCP\Security\ISecureRandom; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Component\VEvent; +use Sabre\VObject\ITip\Message; +use Sabre\VObject\Property\ICalendar\CalAddress; +use Symfony\Component\Mime\Email; +use Test\TestCase; + +class IMipPluginCharsetTest extends TestCase { + // Dependencies + private Defaults&MockObject $defaults; + private IAppConfig&MockObject $appConfig; + private IConfig&MockObject $config; + private IDBConnection&MockObject $db; + private IFactory $l10nFactory; + private IManager&MockObject $mailManager; + private IMailer&MockObject $mailer; + private ISecureRandom&MockObject $random; + private ITimeFactory&MockObject $timeFactory; + private IUrlGenerator&MockObject $urlGenerator; + private IUserSession&MockObject $userSession; + private LoggerInterface $logger; + + // Services + private EventComparisonService $eventComparisonService; + private IMipPlugin $imipPlugin; + private IMipService $imipService; + + // ITip Message + private Message $itipMessage; + + protected function setUp(): void { + // Used by IMipService and IMipPlugin + $today = new \DateTime('2025-06-15 14:30'); + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->timeFactory->method('getTime') + ->willReturn($today->getTimestamp()); + $this->timeFactory->method('getDateTime') + ->willReturn($today); + + // IMipService + $this->urlGenerator = $this->createMock(URLGenerator::class); + $this->config = $this->createMock(IConfig::class); + $this->db = $this->createMock(IDBConnection::class); + $this->random = $this->createMock(ISecureRandom::class); + $l10n = $this->createMock(L10N::class); + $this->l10nFactory = $this->createMock(IFactory::class); + $this->l10nFactory->method('findGenericLanguage') + ->willReturn('en'); + $this->l10nFactory->method('findLocale') + ->willReturn('en_US'); + $this->l10nFactory->method('get') + ->willReturn($l10n); + $this->imipService = new IMipService( + $this->urlGenerator, + $this->config, + $this->db, + $this->random, + $this->l10nFactory, + $this->timeFactory, + ); + + // EventComparisonService + $this->eventComparisonService = new EventComparisonService(); + + // IMipPlugin + $this->appConfig = $this->createMock(IAppConfig::class); + $message = new \OC\Mail\Message(new Email(), false); + $this->mailer = $this->createMock(IMailer::class); + $this->mailer->method('createMessage') + ->willReturn($message); + $this->mailer->method('validateMailAddress') + ->willReturn(true); + $this->logger = new NullLogger(); + $this->defaults = $this->createMock(Defaults::class); + $this->defaults->method('getName') + ->willReturn('Instance Name 123'); + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('luigi'); + $this->userSession = $this->createMock(IUserSession::class); + $this->userSession->method('getUser') + ->willReturn($user); + $this->mailManager = $this->createMock(IManager::class); + $this->imipPlugin = new IMipPlugin( + $this->appConfig, + $this->mailer, + $this->logger, + $this->timeFactory, + $this->defaults, + $this->userSession, + $this->imipService, + $this->eventComparisonService, + $this->mailManager, + ); + + // ITipMessage + $calendar = new VCalendar(); + $event = new VEvent($calendar, 'VEVENT'); + $event->UID = 'uid-1234'; + $event->SEQUENCE = 1; + $event->SUMMARY = 'Lunch'; + $event->DTSTART = new \DateTime('2025-06-20 12:30:00'); + $organizer = new CalAddress($calendar, 'ORGANIZER', 'mailto:luigi@example.org'); + $event->add($organizer); + $attendee = new CalAddress($calendar, 'ATTENDEE', 'mailto:jose@example.org', ['RSVP' => 'TRUE', 'CN' => 'José']); + $event->add($attendee); + $calendar->add($event); + $this->itipMessage = new Message(); + $this->itipMessage->method = 'REQUEST'; + $this->itipMessage->message = $calendar; + $this->itipMessage->sender = 'mailto:luigi@example.org'; + $this->itipMessage->senderName = 'Luigi'; + $this->itipMessage->recipient = 'mailto:' . 'jose@example.org'; + } + + public function testCharsetMailer(): void { + // Arrange + $symfonyEmail = null; + $this->mailer->expects(self::once()) + ->method('send') + ->willReturnCallback(function (IMessage $message) use (&$symfonyEmail): array { + if ($message instanceof \OC\Mail\Message) { + $symfonyEmail = $message->getSymfonyEmail(); + } + return []; + }); + + // Act + $this->imipPlugin->schedule($this->itipMessage); + + // Assert + $this->assertNotNull($symfonyEmail); + $body = $symfonyEmail->getBody()->toString(); + $this->assertStringContainsString('Content-Type: text/calendar; method=REQUEST; charset="utf-8"; name=event.ics', $body); + } + + public function testCharsetMailProvider(): void { + // Arrange + $this->appConfig->method('getValueBool') + ->with('core', 'mail_providers_enabled', true) + ->willReturn(true); + $mailMessage = new MailProviderMessage(); + $mailService = $this->createStubForIntersectionOfInterfaces([IService::class, IMessageSend::class]); + $mailService->method('initiateMessage') + ->willReturn($mailMessage); + $mailService->expects(self::once()) + ->method('sendMessage'); + $this->mailManager->method('findServiceByAddress') + ->willReturn($mailService); + + // Act + $this->imipPlugin->schedule($this->itipMessage); + + // Assert + $attachments = $mailMessage->getAttachments(); + $this->assertCount(1, $attachments); + $this->assertStringContainsString('text/calendar; method=REQUEST; charset="utf-8"; name=event.ics', $attachments[0]->getType()); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php new file mode 100644 index 00000000000..8e71bfa6edf --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php @@ -0,0 +1,1080 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\CalDAV\Schedule; + +use OCA\DAV\CalDAV\EventComparisonService; +use OCA\DAV\CalDAV\Schedule\IMipPlugin; +use OCA\DAV\CalDAV\Schedule\IMipService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Defaults; +use OCP\IAppConfig; +use OCP\IUser; +use OCP\IUserSession; +use OCP\Mail\IAttachment; +use OCP\Mail\IEMailTemplate; +use OCP\Mail\IMailer; +use OCP\Mail\IMessage; +use OCP\Mail\Provider\IManager as IMailManager; +use OCP\Mail\Provider\IMessage as IMailMessageNew; +use OCP\Mail\Provider\IMessageSend as IMailMessageSend; +use OCP\Mail\Provider\IService as IMailService; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Component\VEvent; +use Sabre\VObject\ITip\Message; +use Test\TestCase; +use function array_merge; + +interface IMailServiceMock extends IMailService, IMailMessageSend { + // workaround for creating mock class with multiple interfaces + // TODO: remove after phpUnit 10 is supported. +} + +class IMipPluginTest extends TestCase { + private IMessage&MockObject $mailMessage; + private IMailer&MockObject $mailer; + private IEMailTemplate&MockObject $emailTemplate; + private IAttachment&MockObject $emailAttachment; + private ITimeFactory&MockObject $timeFactory; + private IAppConfig&MockObject $config; + private IUserSession&MockObject $userSession; + private IUser&MockObject $user; + private IMipPlugin $plugin; + private IMipService&MockObject $service; + private Defaults&MockObject $defaults; + private LoggerInterface&MockObject $logger; + private EventComparisonService&MockObject $eventComparisonService; + private IMailManager&MockObject $mailManager; + private IMailServiceMock&MockObject $mailService; + private IMailMessageNew&MockObject $mailMessageNew; + + protected function setUp(): void { + $this->mailMessage = $this->createMock(IMessage::class); + $this->mailMessage->method('setFrom')->willReturn($this->mailMessage); + $this->mailMessage->method('setReplyTo')->willReturn($this->mailMessage); + $this->mailMessage->method('setTo')->willReturn($this->mailMessage); + + $this->mailer = $this->createMock(IMailer::class); + $this->mailer->method('createMessage')->willReturn($this->mailMessage); + + $this->emailTemplate = $this->createMock(IEMailTemplate::class); + $this->mailer->method('createEMailTemplate')->willReturn($this->emailTemplate); + + $this->emailAttachment = $this->createMock(IAttachment::class); + $this->mailer->method('createAttachment')->willReturn($this->emailAttachment); + + $this->logger = $this->createMock(LoggerInterface::class); + + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->timeFactory->method('getTime')->willReturn(1496912528); // 2017-01-01 + + $this->config = $this->createMock(IAppConfig::class); + + $this->user = $this->createMock(IUser::class); + + $this->userSession = $this->createMock(IUserSession::class); + $this->userSession->method('getUser') + ->willReturn($this->user); + + $this->defaults = $this->createMock(Defaults::class); + $this->defaults->method('getName') + ->willReturn('Instance Name 123'); + + $this->service = $this->createMock(IMipService::class); + + $this->eventComparisonService = $this->createMock(EventComparisonService::class); + + $this->mailManager = $this->createMock(IMailManager::class); + + $this->mailService = $this->createMock(IMailServiceMock::class); + + $this->mailMessageNew = $this->createMock(IMailMessageNew::class); + + $this->plugin = new IMipPlugin( + $this->config, + $this->mailer, + $this->logger, + $this->timeFactory, + $this->defaults, + $this->userSession, + $this->service, + $this->eventComparisonService, + $this->mailManager, + ); + } + + public function testDeliveryNoSignificantChange(): void { + $message = new Message(); + $message->method = 'REQUEST'; + $message->message = new VCalendar(); + $message->message->add('VEVENT', array_merge([ + 'UID' => 'uid-1234', + 'SEQUENCE' => 0, + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00') + ], [])); + $message->message->VEVENT->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $message->message->VEVENT->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE']); + $message->sender = 'mailto:gandalf@wiz.ard'; + $message->senderName = 'Mr. Wizard'; + $message->recipient = 'mailto:' . 'frodo@hobb.it'; + $message->significantChange = false; + $this->plugin->schedule($message); + $this->assertEquals('1.0', $message->getScheduleStatus()); + } + + public function testParsingSingle(): void { + $message = new Message(); + $message->method = 'REQUEST'; + $newVCalendar = new VCalendar(); + $newVevent = new VEvent($newVCalendar, 'one', array_merge([ + 'UID' => 'uid-1234', + 'SEQUENCE' => 1, + 'SUMMARY' => 'Fellowship meeting without (!) Boromir', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00') + ], [])); + $newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $newVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); + $message->message = $newVCalendar; + $message->sender = 'mailto:gandalf@wiz.ard'; + $message->senderName = 'Mr. Wizard'; + $message->recipient = 'mailto:' . 'frodo@hobb.it'; + // save the old copy in the plugin + $oldVCalendar = new VCalendar(); + $oldVEvent = new VEvent($oldVCalendar, 'one', [ + 'UID' => 'uid-1234', + 'SEQUENCE' => 0, + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00') + ]); + $oldVEvent->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $oldVEvent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); + $oldVEvent->add('ATTENDEE', 'mailto:' . 'boromir@tra.it.or', ['RSVP' => 'TRUE']); + $oldVCalendar->add($oldVEvent); + $data = ['invitee_name' => 'Mr. Wizard', + 'meeting_title' => 'Fellowship meeting without (!) Boromir', + 'attendee_name' => 'frodo@hobb.it' + ]; + $attendees = $newVevent->select('ATTENDEE'); + $atnd = ''; + foreach ($attendees as $attendee) { + if (strcasecmp($attendee->getValue(), $message->recipient) === 0) { + $atnd = $attendee; + } + } + $this->plugin->setVCalendar($oldVCalendar); + $this->service->expects(self::once()) + ->method('getLastOccurrence') + ->willReturn(1496912700); + $this->mailer->expects(self::once()) + ->method('validateMailAddress') + ->with('frodo@hobb.it') + ->willReturn(true); + $this->eventComparisonService->expects(self::once()) + ->method('findModified') + ->willReturn(['new' => [$newVevent], 'old' => [$oldVEvent]]); + $this->service->expects(self::once()) + ->method('getCurrentAttendee') + ->with($message) + ->willReturn($atnd); + $this->service->expects(self::once()) + ->method('isRoomOrResource') + ->with($atnd) + ->willReturn(false); + $this->service->expects(self::once()) + ->method('isCircle') + ->with($atnd) + ->willReturn(false); + $this->service->expects(self::once()) + ->method('buildBodyData') + ->with($newVevent, $oldVEvent) + ->willReturn($data); + $this->user->expects(self::any()) + ->method('getUID') + ->willReturn('user1'); + $this->user->expects(self::any()) + ->method('getDisplayName') + ->willReturn('Mr. Wizard'); + $this->userSession->expects(self::any()) + ->method('getUser') + ->willReturn($this->user); + $this->service->expects(self::once()) + ->method('getFrom'); + $this->service->expects(self::once()) + ->method('addSubjectAndHeading') + ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting without (!) Boromir', true); + $this->service->expects(self::once()) + ->method('addBulletList') + ->with($this->emailTemplate, $newVevent, $data); + $this->service->expects(self::once()) + ->method('getAttendeeRsvpOrReqForParticipant') + ->willReturn(true); + $this->config->expects(self::once()) + ->method('getValueString') + ->with('dav', 'invitation_link_recipients', 'yes') + ->willReturn('yes'); + $this->service->expects(self::once()) + ->method('createInvitationToken') + ->with($message, $newVevent, 1496912700) + ->willReturn('token'); + $this->service->expects(self::once()) + ->method('addResponseButtons') + ->with($this->emailTemplate, 'token'); + $this->service->expects(self::once()) + ->method('addMoreOptionsButton') + ->with($this->emailTemplate, 'token'); + $this->mailer->expects(self::once()) + ->method('send') + ->willReturn([]); + $this->plugin->schedule($message); + $this->assertEquals('1.1', $message->getScheduleStatus()); + } + + public function testAttendeeIsResource(): void { + $message = new Message(); + $message->method = 'REQUEST'; + $newVCalendar = new VCalendar(); + $newVevent = new VEvent($newVCalendar, 'one', array_merge([ + 'UID' => 'uid-1234', + 'SEQUENCE' => 1, + 'SUMMARY' => 'Fellowship meeting without (!) Boromir', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00') + ], [])); + $newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $newVevent->add('ATTENDEE', 'mailto:' . 'the-shire@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'The Shire', 'CUTYPE' => 'ROOM']); + $message->message = $newVCalendar; + $message->sender = 'mailto:gandalf@wiz.ard'; + $message->senderName = 'Mr. Wizard'; + $message->recipient = 'mailto:' . 'the-shire@hobb.it'; + // save the old copy in the plugin + $oldVCalendar = new VCalendar(); + $oldVEvent = new VEvent($oldVCalendar, 'one', [ + 'UID' => 'uid-1234', + 'SEQUENCE' => 0, + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00') + ]); + $oldVEvent->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $oldVEvent->add('ATTENDEE', 'mailto:' . 'the-shire@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'The Shire', 'CUTYPE' => 'ROOM']); + $oldVEvent->add('ATTENDEE', 'mailto:' . 'boromir@tra.it.or', ['RSVP' => 'TRUE']); + $oldVCalendar->add($oldVEvent); + $data = ['invitee_name' => 'Mr. Wizard', + 'meeting_title' => 'Fellowship meeting without (!) Boromir', + 'attendee_name' => 'frodo@hobb.it' + ]; + $attendees = $newVevent->select('ATTENDEE'); + $room = ''; + foreach ($attendees as $attendee) { + if (strcasecmp($attendee->getValue(), $message->recipient) === 0) { + $room = $attendee; + } + } + $this->plugin->setVCalendar($oldVCalendar); + $this->service->expects(self::once()) + ->method('getLastOccurrence') + ->willReturn(1496912700); + $this->mailer->expects(self::once()) + ->method('validateMailAddress') + ->with('the-shire@hobb.it') + ->willReturn(true); + $this->eventComparisonService->expects(self::once()) + ->method('findModified') + ->willReturn(['new' => [$newVevent], 'old' => [$oldVEvent]]); + $this->service->expects(self::once()) + ->method('getCurrentAttendee') + ->with($message) + ->willReturn($room); + $this->service->expects(self::once()) + ->method('isRoomOrResource') + ->with($room) + ->willReturn(true); + $this->service->expects(self::never()) + ->method('isCircle'); + $this->service->expects(self::never()) + ->method('buildBodyData'); + $this->user->expects(self::any()) + ->method('getUID') + ->willReturn('user1'); + $this->user->expects(self::any()) + ->method('getDisplayName') + ->willReturn('Mr. Wizard'); + $this->userSession->expects(self::any()) + ->method('getUser') + ->willReturn($this->user); + $this->service->expects(self::never()) + ->method('getFrom'); + $this->service->expects(self::never()) + ->method('addSubjectAndHeading'); + $this->service->expects(self::never()) + ->method('addBulletList'); + $this->service->expects(self::never()) + ->method('getAttendeeRsvpOrReqForParticipant'); + $this->config->expects(self::never()) + ->method('getValueString'); + $this->service->expects(self::never()) + ->method('createInvitationToken'); + $this->service->expects(self::never()) + ->method('addResponseButtons'); + $this->service->expects(self::never()) + ->method('addMoreOptionsButton'); + $this->mailer->expects(self::never()) + ->method('send'); + $this->plugin->schedule($message); + $this->assertEquals('1.0', $message->getScheduleStatus()); + } + + public function testAttendeeIsCircle(): void { + $message = new Message(); + $message->method = 'REQUEST'; + $newVCalendar = new VCalendar(); + $newVevent = new VEvent($newVCalendar, 'one', array_merge([ + 'UID' => 'uid-1234', + 'SEQUENCE' => 1, + 'SUMMARY' => 'Fellowship meeting without (!) Boromir', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00') + ], [])); + $newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $newVevent->add('ATTENDEE', 'mailto:' . 'circle+82utEV1Fle8wvxndZLK5TVAPtxj8IIe@middle.earth', ['RSVP' => 'TRUE', 'CN' => 'The Fellowship', 'CUTYPE' => 'GROUP']); + $newVevent->add('ATTENDEE', 'mailto:' . 'boromir@tra.it.or', ['RSVP' => 'TRUE', 'MEMBER' => 'circle+82utEV1Fle8wvxndZLK5TVAPtxj8IIe@middle.earth']); + $message->message = $newVCalendar; + $message->sender = 'mailto:gandalf@wiz.ard'; + $message->senderName = 'Mr. Wizard'; + $message->recipient = 'mailto:' . 'circle+82utEV1Fle8wvxndZLK5TVAPtxj8IIe@middle.earth'; + $attendees = $newVevent->select('ATTENDEE'); + $circle = ''; + foreach ($attendees as $attendee) { + if (strcasecmp($attendee->getValue(), $message->recipient) === 0) { + $circle = $attendee; + } + } + $this->assertNotEmpty($circle, 'Failed to find attendee belonging to the circle'); + $this->service->expects(self::once()) + ->method('getLastOccurrence') + ->willReturn(1496912700); + $this->mailer->expects(self::once()) + ->method('validateMailAddress') + ->with('circle+82utEV1Fle8wvxndZLK5TVAPtxj8IIe@middle.earth') + ->willReturn(true); + $this->eventComparisonService->expects(self::once()) + ->method('findModified') + ->willReturn(['new' => [$newVevent], 'old' => null]); + $this->service->expects(self::once()) + ->method('getCurrentAttendee') + ->with($message) + ->willReturn($circle); + $this->service->expects(self::once()) + ->method('isRoomOrResource') + ->with($circle) + ->willReturn(false); + $this->service->expects(self::once()) + ->method('isCircle') + ->with($circle) + ->willReturn(true); + $this->service->expects(self::never()) + ->method('buildBodyData'); + $this->user->expects(self::any()) + ->method('getUID') + ->willReturn('user1'); + $this->user->expects(self::any()) + ->method('getDisplayName') + ->willReturn('Mr. Wizard'); + $this->userSession->expects(self::any()) + ->method('getUser') + ->willReturn($this->user); + $this->service->expects(self::never()) + ->method('getFrom'); + $this->service->expects(self::never()) + ->method('addSubjectAndHeading'); + $this->service->expects(self::never()) + ->method('addBulletList'); + $this->service->expects(self::never()) + ->method('getAttendeeRsvpOrReqForParticipant'); + $this->config->expects(self::never()) + ->method('getValueString'); + $this->service->expects(self::never()) + ->method('createInvitationToken'); + $this->service->expects(self::never()) + ->method('addResponseButtons'); + $this->service->expects(self::never()) + ->method('addMoreOptionsButton'); + $this->mailer->expects(self::never()) + ->method('send'); + $this->plugin->schedule($message); + $this->assertEquals('1.0', $message->getScheduleStatus()); + } + + public function testParsingRecurrence(): void { + $message = new Message(); + $message->method = 'REQUEST'; + $newVCalendar = new VCalendar(); + $newVevent = new VEvent($newVCalendar, 'one', [ + 'UID' => 'uid-1234', + 'LAST-MODIFIED' => 123456, + 'SEQUENCE' => 2, + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00'), + 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z' + ]); + $newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $newVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); + $newvEvent2 = new VEvent($newVCalendar, 'two', [ + 'UID' => 'uid-1234', + 'SEQUENCE' => 1, + 'SUMMARY' => 'Elevenses', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00'), + 'RECURRENCE-ID' => new \DateTime('2016-01-01 00:00:00') + ]); + $newvEvent2->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $newvEvent2->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); + $message->message = $newVCalendar; + $message->sender = 'mailto:gandalf@wiz.ard'; + $message->recipient = 'mailto:' . 'frodo@hobb.it'; + // save the old copy in the plugin + $oldVCalendar = new VCalendar(); + $oldVEvent = new VEvent($oldVCalendar, 'one', [ + 'UID' => 'uid-1234', + 'LAST-MODIFIED' => 123456, + 'SEQUENCE' => 2, + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00'), + 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z' + ]); + $oldVEvent->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $oldVEvent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); + $data = ['invitee_name' => 'Mr. Wizard', + 'meeting_title' => 'Elevenses', + 'attendee_name' => 'frodo@hobb.it' + ]; + $attendees = $newVevent->select('ATTENDEE'); + $atnd = ''; + foreach ($attendees as $attendee) { + if (strcasecmp($attendee->getValue(), $message->recipient) === 0) { + $atnd = $attendee; + } + } + $this->plugin->setVCalendar($oldVCalendar); + $this->service->expects(self::once()) + ->method('getLastOccurrence') + ->willReturn(1496912700); + $this->mailer->expects(self::once()) + ->method('validateMailAddress') + ->with('frodo@hobb.it') + ->willReturn(true); + $this->eventComparisonService->expects(self::once()) + ->method('findModified') + ->willReturn(['old' => [] ,'new' => [$newVevent]]); + $this->service->expects(self::once()) + ->method('getCurrentAttendee') + ->with($message) + ->willReturn($atnd); + $this->service->expects(self::once()) + ->method('isRoomOrResource') + ->with($atnd) + ->willReturn(false); + $this->service->expects(self::once()) + ->method('isCircle') + ->with($atnd) + ->willReturn(false); + $this->service->expects(self::once()) + ->method('buildBodyData') + ->with($newVevent, null) + ->willReturn($data); + $this->user->expects(self::any()) + ->method('getUID') + ->willReturn('user1'); + $this->user->expects(self::any()) + ->method('getDisplayName') + ->willReturn('Mr. Wizard'); + $this->userSession->expects(self::any()) + ->method('getUser') + ->willReturn($this->user); + $this->service->expects(self::once()) + ->method('getFrom'); + $this->service->expects(self::once()) + ->method('addSubjectAndHeading') + ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Elevenses', false); + $this->service->expects(self::once()) + ->method('addBulletList') + ->with($this->emailTemplate, $newVevent, $data); + $this->service->expects(self::once()) + ->method('getAttendeeRsvpOrReqForParticipant') + ->willReturn(true); + $this->config->expects(self::once()) + ->method('getValueString') + ->with('dav', 'invitation_link_recipients', 'yes') + ->willReturn('yes'); + $this->service->expects(self::once()) + ->method('createInvitationToken') + ->with($message, $newVevent, 1496912700) + ->willReturn('token'); + $this->service->expects(self::once()) + ->method('addResponseButtons') + ->with($this->emailTemplate, 'token'); + $this->service->expects(self::once()) + ->method('addMoreOptionsButton') + ->with($this->emailTemplate, 'token'); + $this->mailer->expects(self::once()) + ->method('send') + ->willReturn([]); + $this->plugin->schedule($message); + $this->assertEquals('1.1', $message->getScheduleStatus()); + } + + public function testEmailValidationFailed(): void { + $message = new Message(); + $message->method = 'REQUEST'; + $message->message = new VCalendar(); + $message->message->add('VEVENT', array_merge([ + 'UID' => 'uid-1234', + 'SEQUENCE' => 0, + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00') + ], [])); + $message->message->VEVENT->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $message->message->VEVENT->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE']); + $message->sender = 'mailto:gandalf@wiz.ard'; + $message->senderName = 'Mr. Wizard'; + $message->recipient = 'mailto:' . 'frodo@hobb.it'; + + $this->service->expects(self::once()) + ->method('getLastOccurrence') + ->willReturn(1496912700); + $this->mailer->expects(self::once()) + ->method('validateMailAddress') + ->with('frodo@hobb.it') + ->willReturn(false); + + $this->plugin->schedule($message); + $this->assertEquals('5.0', $message->getScheduleStatus()); + } + + public function testFailedDelivery(): void { + $message = new Message(); + $message->method = 'REQUEST'; + $newVcalendar = new VCalendar(); + $newVevent = new VEvent($newVcalendar, 'one', array_merge([ + 'UID' => 'uid-1234', + 'SEQUENCE' => 1, + 'SUMMARY' => 'Fellowship meeting without (!) Boromir', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00') + ], [])); + $newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $newVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); + $message->message = $newVcalendar; + $message->sender = 'mailto:gandalf@wiz.ard'; + $message->senderName = 'Mr. Wizard'; + $message->recipient = 'mailto:' . 'frodo@hobb.it'; + // save the old copy in the plugin + $oldVcalendar = new VCalendar(); + $oldVevent = new VEvent($oldVcalendar, 'one', [ + 'UID' => 'uid-1234', + 'SEQUENCE' => 0, + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00') + ]); + $oldVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $oldVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); + $oldVevent->add('ATTENDEE', 'mailto:' . 'boromir@tra.it.or', ['RSVP' => 'TRUE']); + $oldVcalendar->add($oldVevent); + $data = ['invitee_name' => 'Mr. Wizard', + 'meeting_title' => 'Fellowship meeting without (!) Boromir', + 'attendee_name' => 'frodo@hobb.it' + ]; + $attendees = $newVevent->select('ATTENDEE'); + $atnd = ''; + foreach ($attendees as $attendee) { + if (strcasecmp($attendee->getValue(), $message->recipient) === 0) { + $atnd = $attendee; + } + } + $this->plugin->setVCalendar($oldVcalendar); + $this->service->expects(self::once()) + ->method('getLastOccurrence') + ->willReturn(1496912700); + $this->mailer->expects(self::once()) + ->method('validateMailAddress') + ->with('frodo@hobb.it') + ->willReturn(true); + $this->eventComparisonService->expects(self::once()) + ->method('findModified') + ->willReturn(['old' => [] ,'new' => [$newVevent]]); + $this->service->expects(self::once()) + ->method('getCurrentAttendee') + ->with($message) + ->willReturn($atnd); + $this->service->expects(self::once()) + ->method('isRoomOrResource') + ->with($atnd) + ->willReturn(false); + $this->service->expects(self::once()) + ->method('isCircle') + ->with($atnd) + ->willReturn(false); + $this->service->expects(self::once()) + ->method('buildBodyData') + ->with($newVevent, null) + ->willReturn($data); + $this->user->expects(self::any()) + ->method('getUID') + ->willReturn('user1'); + $this->user->expects(self::any()) + ->method('getDisplayName') + ->willReturn('Mr. Wizard'); + $this->userSession->expects(self::any()) + ->method('getUser') + ->willReturn($this->user); + $this->service->expects(self::once()) + ->method('getFrom'); + $this->service->expects(self::once()) + ->method('addSubjectAndHeading') + ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting without (!) Boromir', false); + $this->service->expects(self::once()) + ->method('addBulletList') + ->with($this->emailTemplate, $newVevent, $data); + $this->service->expects(self::once()) + ->method('getAttendeeRsvpOrReqForParticipant') + ->willReturn(true); + $this->config->expects(self::once()) + ->method('getValueString') + ->with('dav', 'invitation_link_recipients', 'yes') + ->willReturn('yes'); + $this->service->expects(self::once()) + ->method('createInvitationToken') + ->with($message, $newVevent, 1496912700) + ->willReturn('token'); + $this->service->expects(self::once()) + ->method('addResponseButtons') + ->with($this->emailTemplate, 'token'); + $this->service->expects(self::once()) + ->method('addMoreOptionsButton') + ->with($this->emailTemplate, 'token'); + $this->mailer->expects(self::once()) + ->method('send') + ->willReturn([]); + $this->mailer + ->method('send') + ->willThrowException(new \Exception()); + $this->logger->expects(self::once()) + ->method('error'); + $this->plugin->schedule($message); + $this->assertEquals('5.0', $message->getScheduleStatus()); + } + + public function testMailProviderSend(): void { + // construct iTip message with event and attendees + $message = new Message(); + $message->method = 'REQUEST'; + $calendar = new VCalendar(); + $event = new VEvent($calendar, 'one', array_merge([ + 'UID' => 'uid-1234', + 'SEQUENCE' => 1, + 'SUMMARY' => 'Fellowship meeting without (!) Boromir', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00') + ], [])); + $event->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $event->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); + $message->message = $calendar; + $message->sender = 'mailto:gandalf@wiz.ard'; + $message->senderName = 'Mr. Wizard'; + $message->recipient = 'mailto:' . 'frodo@hobb.it'; + // construct + foreach ($event->select('ATTENDEE') as $entry) { + if (strcasecmp($entry->getValue(), $message->recipient) === 0) { + $attendee = $entry; + } + } + // construct body data return + $data = ['invitee_name' => 'Mr. Wizard', + 'meeting_title' => 'Fellowship meeting without (!) Boromir', + 'attendee_name' => 'frodo@hobb.it' + ]; + // construct system config mock returns + $this->config->expects(self::once()) + ->method('getValueString') + ->with('dav', 'invitation_link_recipients', 'yes') + ->willReturn('yes'); + // construct user mock returns + $this->user->expects(self::any()) + ->method('getUID') + ->willReturn('user1'); + $this->user->expects(self::any()) + ->method('getDisplayName') + ->willReturn('Mr. Wizard'); + // construct user session mock returns + $this->userSession->expects(self::any()) + ->method('getUser') + ->willReturn($this->user); + // construct service mock returns + $this->service->expects(self::once()) + ->method('getLastOccurrence') + ->willReturn(1496912700); + $this->service->expects(self::once()) + ->method('getCurrentAttendee') + ->with($message) + ->willReturn($attendee); + $this->service->expects(self::once()) + ->method('isRoomOrResource') + ->with($attendee) + ->willReturn(false); + $this->service->expects(self::once()) + ->method('isCircle') + ->with($attendee) + ->willReturn(false); + $this->service->expects(self::once()) + ->method('buildBodyData') + ->with($event, null) + ->willReturn($data); + $this->service->expects(self::once()) + ->method('getFrom'); + $this->service->expects(self::once()) + ->method('addSubjectAndHeading') + ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting without (!) Boromir', false); + $this->service->expects(self::once()) + ->method('addBulletList') + ->with($this->emailTemplate, $event, $data); + $this->service->expects(self::once()) + ->method('getAttendeeRsvpOrReqForParticipant') + ->willReturn(true); + $this->service->expects(self::once()) + ->method('createInvitationToken') + ->with($message, $event, 1496912700) + ->willReturn('token'); + $this->service->expects(self::once()) + ->method('addResponseButtons') + ->with($this->emailTemplate, 'token'); + $this->service->expects(self::once()) + ->method('addMoreOptionsButton') + ->with($this->emailTemplate, 'token'); + $this->eventComparisonService->expects(self::once()) + ->method('findModified') + ->willReturn(['old' => [] ,'new' => [$event]]); + // construct mail mock returns + $this->mailer->expects(self::once()) + ->method('validateMailAddress') + ->with('frodo@hobb.it') + ->willReturn(true); + // construct mail provider mock returns + $this->mailService + ->method('initiateMessage') + ->willReturn($this->mailMessageNew); + $this->mailService + ->method('sendMessage') + ->with($this->mailMessageNew); + $this->mailManager + ->method('findServiceByAddress') + ->with('user1', 'gandalf@wiz.ard') + ->willReturn($this->mailService); + + $this->plugin->schedule($message); + $this->assertEquals('1.1', $message->getScheduleStatus()); + } + + public function testMailProviderDisabled(): void { + $message = new Message(); + $message->method = 'REQUEST'; + $newVCalendar = new VCalendar(); + $newVevent = new VEvent($newVCalendar, 'one', array_merge([ + 'UID' => 'uid-1234', + 'SEQUENCE' => 1, + 'SUMMARY' => 'Fellowship meeting without (!) Boromir', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00') + ], [])); + $newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $newVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); + $message->message = $newVCalendar; + $message->sender = 'mailto:gandalf@wiz.ard'; + $message->senderName = 'Mr. Wizard'; + $message->recipient = 'mailto:' . 'frodo@hobb.it'; + // save the old copy in the plugin + $oldVCalendar = new VCalendar(); + $oldVEvent = new VEvent($oldVCalendar, 'one', [ + 'UID' => 'uid-1234', + 'SEQUENCE' => 0, + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00') + ]); + $oldVEvent->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $oldVEvent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); + $oldVEvent->add('ATTENDEE', 'mailto:' . 'boromir@tra.it.or', ['RSVP' => 'TRUE']); + $oldVCalendar->add($oldVEvent); + $data = ['invitee_name' => 'Mr. Wizard', + 'meeting_title' => 'Fellowship meeting without (!) Boromir', + 'attendee_name' => 'frodo@hobb.it' + ]; + $attendees = $newVevent->select('ATTENDEE'); + $atnd = ''; + foreach ($attendees as $attendee) { + if (strcasecmp($attendee->getValue(), $message->recipient) === 0) { + $atnd = $attendee; + } + } + $this->plugin->setVCalendar($oldVCalendar); + $this->service->expects(self::once()) + ->method('getLastOccurrence') + ->willReturn(1496912700); + $this->mailer->expects(self::once()) + ->method('validateMailAddress') + ->with('frodo@hobb.it') + ->willReturn(true); + $this->eventComparisonService->expects(self::once()) + ->method('findModified') + ->willReturn(['new' => [$newVevent], 'old' => [$oldVEvent]]); + $this->service->expects(self::once()) + ->method('getCurrentAttendee') + ->with($message) + ->willReturn($atnd); + $this->service->expects(self::once()) + ->method('isRoomOrResource') + ->with($atnd) + ->willReturn(false); + $this->service->expects(self::once()) + ->method('isCircle') + ->with($atnd) + ->willReturn(false); + $this->service->expects(self::once()) + ->method('buildBodyData') + ->with($newVevent, $oldVEvent) + ->willReturn($data); + $this->user->expects(self::any()) + ->method('getUID') + ->willReturn('user1'); + $this->user->expects(self::any()) + ->method('getDisplayName') + ->willReturn('Mr. Wizard'); + $this->userSession->expects(self::any()) + ->method('getUser') + ->willReturn($this->user); + $this->service->expects(self::once()) + ->method('getFrom'); + $this->service->expects(self::once()) + ->method('addSubjectAndHeading') + ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting without (!) Boromir', true); + $this->service->expects(self::once()) + ->method('addBulletList') + ->with($this->emailTemplate, $newVevent, $data); + $this->service->expects(self::once()) + ->method('getAttendeeRsvpOrReqForParticipant') + ->willReturn(true); + $this->config->expects(self::once()) + ->method('getValueString') + ->with('dav', 'invitation_link_recipients', 'yes') + ->willReturn('yes'); + $this->config->expects(self::once()) + ->method('getValueBool') + ->with('core', 'mail_providers_enabled', true) + ->willReturn(false); + $this->service->expects(self::once()) + ->method('createInvitationToken') + ->with($message, $newVevent, 1496912700) + ->willReturn('token'); + $this->service->expects(self::once()) + ->method('addResponseButtons') + ->with($this->emailTemplate, 'token'); + $this->service->expects(self::once()) + ->method('addMoreOptionsButton') + ->with($this->emailTemplate, 'token'); + $this->mailer->expects(self::once()) + ->method('send') + ->willReturn([]); + $this->plugin->schedule($message); + $this->assertEquals('1.1', $message->getScheduleStatus()); + } + + public function testNoOldEvent(): void { + $message = new Message(); + $message->method = 'REQUEST'; + $newVCalendar = new VCalendar(); + $newVevent = new VEvent($newVCalendar, 'VEVENT', array_merge([ + 'UID' => 'uid-1234', + 'SEQUENCE' => 1, + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00') + ], [])); + $newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $newVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); + $message->message = $newVCalendar; + $message->sender = 'mailto:gandalf@wiz.ard'; + $message->senderName = 'Mr. Wizard'; + $message->recipient = 'mailto:' . 'frodo@hobb.it'; + $data = ['invitee_name' => 'Mr. Wizard', + 'meeting_title' => 'Fellowship meeting', + 'attendee_name' => 'frodo@hobb.it' + ]; + $attendees = $newVevent->select('ATTENDEE'); + $atnd = ''; + foreach ($attendees as $attendee) { + if (strcasecmp($attendee->getValue(), $message->recipient) === 0) { + $atnd = $attendee; + } + } + $this->service->expects(self::once()) + ->method('getLastOccurrence') + ->willReturn(1496912700); + $this->mailer->expects(self::once()) + ->method('validateMailAddress') + ->with('frodo@hobb.it') + ->willReturn(true); + $this->eventComparisonService->expects(self::once()) + ->method('findModified') + ->with($newVCalendar, null) + ->willReturn(['old' => [] ,'new' => [$newVevent]]); + $this->service->expects(self::once()) + ->method('getCurrentAttendee') + ->with($message) + ->willReturn($atnd); + $this->service->expects(self::once()) + ->method('isRoomOrResource') + ->with($atnd) + ->willReturn(false); + $this->service->expects(self::once()) + ->method('isCircle') + ->with($atnd) + ->willReturn(false); + $this->service->expects(self::once()) + ->method('buildBodyData') + ->with($newVevent, null) + ->willReturn($data); + $this->user->expects(self::any()) + ->method('getUID') + ->willReturn('user1'); + $this->user->expects(self::any()) + ->method('getDisplayName') + ->willReturn('Mr. Wizard'); + $this->userSession->expects(self::any()) + ->method('getUser') + ->willReturn($this->user); + $this->service->expects(self::once()) + ->method('getFrom'); + $this->service->expects(self::once()) + ->method('addSubjectAndHeading') + ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting', false); + $this->service->expects(self::once()) + ->method('addBulletList') + ->with($this->emailTemplate, $newVevent, $data); + $this->service->expects(self::once()) + ->method('getAttendeeRsvpOrReqForParticipant') + ->willReturn(true); + $this->config->expects(self::once()) + ->method('getValueString') + ->with('dav', 'invitation_link_recipients', 'yes') + ->willReturn('yes'); + $this->service->expects(self::once()) + ->method('createInvitationToken') + ->with($message, $newVevent, 1496912700) + ->willReturn('token'); + $this->service->expects(self::once()) + ->method('addResponseButtons') + ->with($this->emailTemplate, 'token'); + $this->service->expects(self::once()) + ->method('addMoreOptionsButton') + ->with($this->emailTemplate, 'token'); + $this->mailer->expects(self::once()) + ->method('send') + ->willReturn([]); + $this->mailer + ->method('send') + ->willReturn([]); + $this->plugin->schedule($message); + $this->assertEquals('1.1', $message->getScheduleStatus()); + } + + public function testNoButtons(): void { + $message = new Message(); + $message->method = 'REQUEST'; + $newVCalendar = new VCalendar(); + $newVevent = new VEvent($newVCalendar, 'VEVENT', array_merge([ + 'UID' => 'uid-1234', + 'SEQUENCE' => 1, + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00') + ], [])); + $newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $newVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); + $message->message = $newVCalendar; + $message->sender = 'mailto:gandalf@wiz.ard'; + $message->recipient = 'mailto:' . 'frodo@hobb.it'; + $data = ['invitee_name' => 'Mr. Wizard', + 'meeting_title' => 'Fellowship meeting', + 'attendee_name' => 'frodo@hobb.it' + ]; + $attendees = $newVevent->select('ATTENDEE'); + $atnd = ''; + foreach ($attendees as $attendee) { + if (strcasecmp($attendee->getValue(), $message->recipient) === 0) { + $atnd = $attendee; + } + } + $this->service->expects(self::once()) + ->method('getLastOccurrence') + ->willReturn(1496912700); + $this->mailer->expects(self::once()) + ->method('validateMailAddress') + ->with('frodo@hobb.it') + ->willReturn(true); + $this->eventComparisonService->expects(self::once()) + ->method('findModified') + ->with($newVCalendar, null) + ->willReturn(['old' => [] ,'new' => [$newVevent]]); + $this->service->expects(self::once()) + ->method('getCurrentAttendee') + ->with($message) + ->willReturn($atnd); + $this->service->expects(self::once()) + ->method('isRoomOrResource') + ->with($atnd) + ->willReturn(false); + $this->service->expects(self::once()) + ->method('isCircle') + ->with($atnd) + ->willReturn(false); + $this->service->expects(self::once()) + ->method('buildBodyData') + ->with($newVevent, null) + ->willReturn($data); + $this->user->expects(self::any()) + ->method('getUID') + ->willReturn('user1'); + $this->user->expects(self::any()) + ->method('getDisplayName') + ->willReturn('Mr. Wizard'); + $this->userSession->expects(self::any()) + ->method('getUser') + ->willReturn($this->user); + $this->service->expects(self::once()) + ->method('getFrom'); + $this->service->expects(self::once()) + ->method('addSubjectAndHeading') + ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting', false); + $this->service->expects(self::once()) + ->method('addBulletList') + ->with($this->emailTemplate, $newVevent, $data); + $this->service->expects(self::once()) + ->method('getAttendeeRsvpOrReqForParticipant') + ->willReturn(true); + $this->config->expects(self::once()) + ->method('getValueString') + ->with('dav', 'invitation_link_recipients', 'yes') + ->willReturn('no'); + $this->service->expects(self::never()) + ->method('createInvitationToken'); + $this->service->expects(self::never()) + ->method('addResponseButtons'); + $this->service->expects(self::never()) + ->method('addMoreOptionsButton'); + $this->mailer->expects(self::once()) + ->method('send') + ->willReturn([]); + $this->mailer + ->method('send') + ->willReturn([]); + $this->plugin->schedule($message); + $this->assertEquals('1.1', $message->getScheduleStatus()); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Schedule/IMipServiceTest.php b/apps/dav/tests/unit/CalDAV/Schedule/IMipServiceTest.php new file mode 100644 index 00000000000..2be6a1cf8b1 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Schedule/IMipServiceTest.php @@ -0,0 +1,2200 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace OCA\DAV\Tests\unit\CalDAV\Schedule; + +use OC\URLGenerator; +use OCA\DAV\CalDAV\EventReader; +use OCA\DAV\CalDAV\Schedule\IMipService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IL10N; +use OCP\L10N\IFactory; +use OCP\Security\ISecureRandom; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Property\ICalendar\DateTime; +use Test\TestCase; + +class IMipServiceTest extends TestCase { + private URLGenerator&MockObject $urlGenerator; + private IConfig&MockObject $config; + private IDBConnection&MockObject $db; + private ISecureRandom&MockObject $random; + private IFactory&MockObject $l10nFactory; + private IL10N&MockObject $l10n; + private ITimeFactory&MockObject $timeFactory; + private IMipService $service; + + + private VCalendar $vCalendar1a; + private VCalendar $vCalendar1b; + private VCalendar $vCalendar2; + private VCalendar $vCalendar3; + /** @var DateTime DateTime object that will be returned by DateTime() or DateTime('now') */ + public static $datetimeNow; + + protected function setUp(): void { + parent::setUp(); + + $this->urlGenerator = $this->createMock(URLGenerator::class); + $this->config = $this->createMock(IConfig::class); + $this->db = $this->createMock(IDBConnection::class); + $this->random = $this->createMock(ISecureRandom::class); + $this->l10nFactory = $this->createMock(IFactory::class); + $this->l10n = $this->createMock(IL10N::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->l10nFactory->expects(self::once()) + ->method('findGenericLanguage') + ->willReturn('en'); + $this->l10nFactory->expects(self::once()) + ->method('get') + ->with('dav', 'en') + ->willReturn($this->l10n); + $this->service = new IMipService( + $this->urlGenerator, + $this->config, + $this->db, + $this->random, + $this->l10nFactory, + $this->timeFactory + ); + + // construct calendar with a 1 hour event and same start/end time zones + $this->vCalendar1a = new VCalendar(); + $vEvent = $this->vCalendar1a->add('VEVENT', []); + $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc'); + $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']); + $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']); + $vEvent->add('SUMMARY', 'Testing Event'); + $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']); + $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [ + 'CN' => 'Attendee One', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); + + // construct calendar with a 1 hour event and different start/end time zones + $this->vCalendar1b = new VCalendar(); + $vEvent = $this->vCalendar1b->add('VEVENT', []); + $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc'); + $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']); + $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Vancouver']); + $vEvent->add('SUMMARY', 'Testing Event'); + $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']); + $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [ + 'CN' => 'Attendee One', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); + + // construct calendar with a full day event + $this->vCalendar2 = new VCalendar(); + // time zone component + $vTimeZone = $this->vCalendar2->add('VTIMEZONE'); + $vTimeZone->add('TZID', 'America/Toronto'); + // event component + $vEvent = $this->vCalendar2->add('VEVENT', []); + $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc'); + $vEvent->add('DTSTART', '20240701'); + $vEvent->add('DTEND', '20240702'); + $vEvent->add('SUMMARY', 'Testing Event'); + $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']); + $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [ + 'CN' => 'Attendee One', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); + + // construct calendar with a multi day event + $this->vCalendar3 = new VCalendar(); + // time zone component + $vTimeZone = $this->vCalendar3->add('VTIMEZONE'); + $vTimeZone->add('TZID', 'America/Toronto'); + // event component + $vEvent = $this->vCalendar3->add('VEVENT', []); + $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc'); + $vEvent->add('DTSTART', '20240701'); + $vEvent->add('DTEND', '20240706'); + $vEvent->add('SUMMARY', 'Testing Event'); + $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']); + $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [ + 'CN' => 'Attendee One', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); + } + + public function testGetFrom(): void { + $senderName = 'Detective McQueen'; + $default = 'Twin Lakes Police Department - Darkside Division'; + $expected = 'Detective McQueen via Twin Lakes Police Department - Darkside Division'; + + $this->l10n->expects(self::once()) + ->method('t') + ->willReturn($expected); + + $actual = $this->service->getFrom($senderName, $default); + $this->assertEquals($expected, $actual); + } + + public function testBuildBodyDataCreated(): void { + + // construct l10n return(s) + $this->l10n->method('l')->willReturnCallback( + function ($v1, $v2, $v3) { + return match (true) { + $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM', + $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM', + $v1 === 'date' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'full'] => 'July 1, 2024' + }; + } + ); + $this->l10n->method('n')->willReturnMap([ + [ + 'In a day on %1$s between %2$s - %3$s', + 'In %n days on %1$s between %2$s - %3$s', + 1, + ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'], + 'In a day on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)' + ] + ]); + // construct time factory return(s) + $this->timeFactory->method('getDateTime')->willReturnCallback( + function ($v1, $v2) { + return match (true) { + $v1 == 'now' && $v2 == null => (new \DateTime('20240630T000000')) + }; + } + ); + /** test singleton partial day event*/ + $vCalendar = clone $this->vCalendar1a; + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // define expected output + $expected = [ + 'meeting_when' => $this->service->generateWhenString($eventReader), + 'meeting_description' => '', + 'meeting_title' => 'Testing Event', + 'meeting_location' => '', + 'meeting_url' => '', + 'meeting_url_html' => '', + ]; + // generate actual output + $actual = $this->service->buildBodyData($vCalendar->VEVENT[0], null); + // test output + $this->assertEquals($expected, $actual); + } + + public function testBuildBodyDataUpdate(): void { + + // construct l10n return(s) + $this->l10n->method('l')->willReturnCallback( + function ($v1, $v2, $v3) { + return match (true) { + $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM', + $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM', + $v1 === 'date' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'full'] => 'July 1, 2024' + }; + } + ); + $this->l10n->method('n')->willReturnMap([ + [ + 'In a day on %1$s between %2$s - %3$s', + 'In %n days on %1$s between %2$s - %3$s', + 1, + ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'], + 'In a day on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)' + ] + ]); + // construct time factory return(s) + $this->timeFactory->method('getDateTime')->willReturnCallback( + function ($v1, $v2) { + return match (true) { + $v1 == 'now' && $v2 == null => (new \DateTime('20240630T000000')) + }; + } + ); + /** test singleton partial day event*/ + $vCalendarNew = clone $this->vCalendar1a; + $vCalendarOld = clone $this->vCalendar1a; + // construct event reader + $eventReaderNew = new EventReader($vCalendarNew, $vCalendarNew->VEVENT[0]->UID->getValue()); + // alter old event label/title + $vCalendarOld->VEVENT[0]->SUMMARY->setValue('Testing Singleton Event'); + // define expected output + $expected = [ + 'meeting_when' => $this->service->generateWhenString($eventReaderNew), + 'meeting_description' => '', + 'meeting_title' => 'Testing Event', + 'meeting_location' => '', + 'meeting_url' => '', + 'meeting_url_html' => '', + 'meeting_when_html' => $this->service->generateWhenString($eventReaderNew), + 'meeting_title_html' => sprintf("<span style='text-decoration: line-through'>%s</span><br />%s", 'Testing Singleton Event', 'Testing Event'), + 'meeting_description_html' => '', + 'meeting_location_html' => '' + ]; + // generate actual output + $actual = $this->service->buildBodyData($vCalendarNew->VEVENT[0], $vCalendarOld->VEVENT[0]); + // test output + $this->assertEquals($expected, $actual); + } + + public function testGetLastOccurrenceRRULE(): void { + $vCalendar = new VCalendar(); + $vCalendar->add('VEVENT', [ + 'UID' => 'uid-1234', + 'LAST-MODIFIED' => 123456, + 'SEQUENCE' => 2, + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00'), + 'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z', + ]); + + $occurrence = $this->service->getLastOccurrence($vCalendar); + $this->assertEquals(1454284800, $occurrence); + } + + public function testGetLastOccurrenceEndDate(): void { + $vCalendar = new VCalendar(); + $vCalendar->add('VEVENT', [ + 'UID' => 'uid-1234', + 'LAST-MODIFIED' => 123456, + 'SEQUENCE' => 2, + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00'), + 'DTEND' => new \DateTime('2017-01-01 00:00:00'), + ]); + + $occurrence = $this->service->getLastOccurrence($vCalendar); + $this->assertEquals(1483228800, $occurrence); + } + + public function testGetLastOccurrenceDuration(): void { + $vCalendar = new VCalendar(); + $vCalendar->add('VEVENT', [ + 'UID' => 'uid-1234', + 'LAST-MODIFIED' => 123456, + 'SEQUENCE' => 2, + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00'), + 'DURATION' => 'P12W', + ]); + + $occurrence = $this->service->getLastOccurrence($vCalendar); + $this->assertEquals(1458864000, $occurrence); + } + + public function testGetLastOccurrenceAllDay(): void { + $vCalendar = new VCalendar(); + $vEvent = $vCalendar->add('VEVENT', [ + 'UID' => 'uid-1234', + 'LAST-MODIFIED' => 123456, + 'SEQUENCE' => 2, + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00'), + ]); + + // rewrite from DateTime to Date + $vEvent->DTSTART['VALUE'] = 'DATE'; + + $occurrence = $this->service->getLastOccurrence($vCalendar); + $this->assertEquals(1451692800, $occurrence); + } + + public function testGetLastOccurrenceFallback(): void { + $vCalendar = new VCalendar(); + $vCalendar->add('VEVENT', [ + 'UID' => 'uid-1234', + 'LAST-MODIFIED' => 123456, + 'SEQUENCE' => 2, + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00'), + ]); + + $occurrence = $this->service->getLastOccurrence($vCalendar); + $this->assertEquals(1451606400, $occurrence); + } + + public function testGenerateWhenStringSingular(): void { + + // construct l10n return(s) + $this->l10n->method('l')->willReturnCallback( + function ($v1, $v2, $v3) { + return match (true) { + $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM', + $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM', + $v1 === 'date' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'full'] => 'July 1, 2024', + $v1 === 'date' && $v2 == (new \DateTime('20240701T000000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'full'] => 'July 1, 2024' + }; + } + ); + $this->l10n->method('t')->willReturnMap([ + [ + 'In the past on %1$s for the entire day', + ['July 1, 2024'], + 'In the past on July 1, 2024 for the entire day' + ], + [ + 'In the past on %1$s between %2$s - %3$s', + ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'], + 'In the past on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)' + ], + ]); + $this->l10n->method('n')->willReturnMap([ + // singular entire day + [ + 'In a minute on %1$s for the entire day', + 'In %n minutes on %1$s for the entire day', + 1, + ['July 1, 2024'], + 'In a minute on July 1, 2024 for the entire day' + ], + [ + 'In a hour on %1$s for the entire day', + 'In %n hours on %1$s for the entire day', + 1, + ['July 1, 2024'], + 'In a hour on July 1, 2024 for the entire day' + ], + [ + 'In a day on %1$s for the entire day', + 'In %n days on %1$s for the entire day', + 1, + ['July 1, 2024'], + 'In a day on July 1, 2024 for the entire day' + ], + [ + 'In a week on %1$s for the entire day', + 'In %n weeks on %1$s for the entire day', + 1, + ['July 1, 2024'], + 'In a week on July 1, 2024 for the entire day' + ], + [ + 'In a month on %1$s for the entire day', + 'In %n months on %1$s for the entire day', + 1, + ['July 1, 2024'], + 'In a month on July 1, 2024 for the entire day' + ], + [ + 'In a year on %1$s for the entire day', + 'In %n years on %1$s for the entire day', + 1, + ['July 1, 2024'], + 'In a year on July 1, 2024 for the entire day' + ], + // plural entire day + [ + 'In a minute on %1$s for the entire day', + 'In %n minutes on %1$s for the entire day', + 2, + ['July 1, 2024'], + 'In 2 minutes on July 1, 2024 for the entire day' + ], + [ + 'In a hour on %1$s for the entire day', + 'In %n hours on %1$s for the entire day', + 2, + ['July 1, 2024'], + 'In 2 hours on July 1, 2024 for the entire day' + ], + [ + 'In a day on %1$s for the entire day', + 'In %n days on %1$s for the entire day', + 2, + ['July 1, 2024'], + 'In 2 days on July 1, 2024 for the entire day' + ], + [ + 'In a week on %1$s for the entire day', + 'In %n weeks on %1$s for the entire day', + 2, + ['July 1, 2024'], + 'In 2 weeks on July 1, 2024 for the entire day' + ], + [ + 'In a month on %1$s for the entire day', + 'In %n months on %1$s for the entire day', + 2, + ['July 1, 2024'], + 'In 2 months on July 1, 2024 for the entire day' + ], + [ + 'In a year on %1$s for the entire day', + 'In %n years on %1$s for the entire day', + 2, + ['July 1, 2024'], + 'In 2 years on July 1, 2024 for the entire day' + ], + // singular partial day + [ + 'In a minute on %1$s between %2$s - %3$s', + 'In %n minutes on %1$s between %2$s - %3$s', + 1, + ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'], + 'In a minute on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)' + ], + [ + 'In a hour on %1$s between %2$s - %3$s', + 'In %n hours on %1$s between %2$s - %3$s', + 1, + ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'], + 'In a hour on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)' + ], + [ + 'In a day on %1$s between %2$s - %3$s', + 'In %n days on %1$s between %2$s - %3$s', + 1, + ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'], + 'In a day on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)' + ], + [ + 'In a week on %1$s between %2$s - %3$s', + 'In %n weeks on %1$s between %2$s - %3$s', + 1, + ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'], + 'In a week on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)' + ], + [ + 'In a month on %1$s between %2$s - %3$s', + 'In %n months on %1$s between %2$s - %3$s', + 1, + ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'], + 'In a month on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)' + ], + [ + 'In a year on %1$s between %2$s - %3$s', + 'In %n years on %1$s between %2$s - %3$s', + 1, + ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'], + 'In a year on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)' + ], + // plural partial day + [ + 'In a minute on %1$s between %2$s - %3$s', + 'In %n minutes on %1$s between %2$s - %3$s', + 2, + ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'], + 'In 2 minutes on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)' + ], + [ + 'In a hour on %1$s between %2$s - %3$s', + 'In %n hours on %1$s between %2$s - %3$s', + 2, + ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'], + 'In 2 hours on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)' + ], + [ + 'In a day on %1$s between %2$s - %3$s', + 'In %n days on %1$s between %2$s - %3$s', + 2, + ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'], + 'In 2 days on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)' + ], + [ + 'In a week on %1$s between %2$s - %3$s', + 'In %n weeks on %1$s between %2$s - %3$s', + 2, + ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'], + 'In 2 weeks on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)' + ], + [ + 'In a month on %1$s between %2$s - %3$s', + 'In %n months on %1$s between %2$s - %3$s', + 2, + ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'], + 'In 2 months on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)' + ], + [ + 'In a year on %1$s between %2$s - %3$s', + 'In %n years on %1$s between %2$s - %3$s', + 2, + ['July 1, 2024', '8:00 AM', '9:00 AM (America/Toronto)'], + 'In 2 years on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)' + ], + ]); + + // construct time factory return(s) + $this->timeFactory->method('getDateTime')->willReturnOnConsecutiveCalls( + // past interval test dates + (new \DateTime('20240702T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240703T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240702T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240703T170000', (new \DateTimeZone('America/Toronto')))), + // minute interval test dates + (new \DateTime('20240701T075900', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240630T235900', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240701T075800', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240630T235800', (new \DateTimeZone('America/Toronto')))), + // hour interval test dates + (new \DateTime('20240701T070000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240630T230000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240701T060000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240630T220000', (new \DateTimeZone('America/Toronto')))), + // day interval test dates + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + // week interval test dates + (new \DateTime('20240621T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240621T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240614T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240614T170000', (new \DateTimeZone('America/Toronto')))), + // month interval test dates + (new \DateTime('20240530T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240530T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240430T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240430T170000', (new \DateTimeZone('America/Toronto')))), + // year interval test dates + (new \DateTime('20230630T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20230630T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20220630T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20220630T170000', (new \DateTimeZone('America/Toronto')))) + ); + + /** test partial day event in 1 day in the past*/ + $vCalendar = clone $this->vCalendar1a; + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + $this->assertEquals( + 'In the past on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)', + $this->service->generateWhenString($eventReader) + ); + + /** test entire day event in 1 day in the past*/ + $vCalendar = clone $this->vCalendar2; + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + $this->assertEquals( + 'In the past on July 1, 2024 for the entire day', + $this->service->generateWhenString($eventReader) + ); + + /** test partial day event in 2 days in the past*/ + $vCalendar = clone $this->vCalendar1a; + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + $this->assertEquals( + 'In the past on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)', + $this->service->generateWhenString($eventReader) + ); + + /** test entire day event in 2 days in the past*/ + $vCalendar = clone $this->vCalendar2; + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + $this->assertEquals( + 'In the past on July 1, 2024 for the entire day', + $this->service->generateWhenString($eventReader) + ); + + /** test partial day event in 1 minute*/ + $vCalendar = clone $this->vCalendar1a; + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + $this->assertEquals( + 'In a minute on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)', + $this->service->generateWhenString($eventReader) + ); + + /** test entire day event in 1 minute*/ + $vCalendar = clone $this->vCalendar2; + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + $this->assertEquals( + 'In a minute on July 1, 2024 for the entire day', + $this->service->generateWhenString($eventReader) + ); + + /** test partial day event in 2 minutes*/ + $vCalendar = clone $this->vCalendar1a; + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + $this->assertEquals( + 'In 2 minutes on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)', + $this->service->generateWhenString($eventReader) + ); + + /** test entire day event in 2 minutes*/ + $vCalendar = clone $this->vCalendar2; + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + $this->assertEquals( + 'In 2 minutes on July 1, 2024 for the entire day', + $this->service->generateWhenString($eventReader) + ); + + /** test partial day event in 1 hour*/ + $vCalendar = clone $this->vCalendar1a; + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + $this->assertEquals( + 'In a hour on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)', + $this->service->generateWhenString($eventReader) + ); + + /** test entire day event in 1 hour*/ + $vCalendar = clone $this->vCalendar2; + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + $this->assertEquals( + 'In a hour on July 1, 2024 for the entire day', + $this->service->generateWhenString($eventReader) + ); + + /** test partial day event in 2 hours*/ + $vCalendar = clone $this->vCalendar1a; + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + $this->assertEquals( + 'In 2 hours on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)', + $this->service->generateWhenString($eventReader) + ); + + /** test entire day event in 2 hours*/ + $vCalendar = clone $this->vCalendar2; + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + $this->assertEquals( + 'In 2 hours on July 1, 2024 for the entire day', + $this->service->generateWhenString($eventReader) + ); + + /** test patrial day event in 1 day*/ + $vCalendar = clone $this->vCalendar1a; + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In a day on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)', + $this->service->generateWhenString($eventReader) + ); + + /** test entire day event in 1 day*/ + $vCalendar = clone $this->vCalendar2; + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In a day on July 1, 2024 for the entire day', + $this->service->generateWhenString($eventReader) + ); + + /** test patrial day event in 2 days*/ + $vCalendar = clone $this->vCalendar1a; + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In 2 days on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)', + $this->service->generateWhenString($eventReader) + ); + + /** test entire day event in 2 days*/ + $vCalendar = clone $this->vCalendar2; + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In 2 days on July 1, 2024 for the entire day', + $this->service->generateWhenString($eventReader) + ); + + /** test patrial day event in 1 week*/ + $vCalendar = clone $this->vCalendar1a; + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In a week on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)', + $this->service->generateWhenString($eventReader) + ); + + /** test entire day event in 1 week*/ + $vCalendar = clone $this->vCalendar2; + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In a week on July 1, 2024 for the entire day', + $this->service->generateWhenString($eventReader) + ); + + /** test patrial day event in 2 weeks*/ + $vCalendar = clone $this->vCalendar1a; + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In 2 weeks on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)', + $this->service->generateWhenString($eventReader) + ); + + /** test entire day event in 2 weeks*/ + $vCalendar = clone $this->vCalendar2; + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In 2 weeks on July 1, 2024 for the entire day', + $this->service->generateWhenString($eventReader) + ); + + /** test patrial day event in 1 month*/ + $vCalendar = clone $this->vCalendar1a; + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In a month on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)', + $this->service->generateWhenString($eventReader) + ); + + /** test entire day event in 1 month*/ + $vCalendar = clone $this->vCalendar2; + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In a month on July 1, 2024 for the entire day', + $this->service->generateWhenString($eventReader) + ); + + /** test patrial day event in 2 months*/ + $vCalendar = clone $this->vCalendar1a; + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In 2 months on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)', + $this->service->generateWhenString($eventReader) + ); + + /** test entire day event in 2 months*/ + $vCalendar = clone $this->vCalendar2; + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In 2 months on July 1, 2024 for the entire day', + $this->service->generateWhenString($eventReader) + ); + + /** test patrial day event in 1 year*/ + $vCalendar = clone $this->vCalendar1a; + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In a year on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)', + $this->service->generateWhenString($eventReader) + ); + + /** test entire day event in 1 year*/ + $vCalendar = clone $this->vCalendar2; + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In a year on July 1, 2024 for the entire day', + $this->service->generateWhenString($eventReader) + ); + + /** test patrial day event in 2 years*/ + $vCalendar = clone $this->vCalendar1a; + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In 2 years on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)', + $this->service->generateWhenString($eventReader) + ); + + /** test entire day event in 2 years*/ + $vCalendar = clone $this->vCalendar2; + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In 2 years on July 1, 2024 for the entire day', + $this->service->generateWhenString($eventReader) + ); + + } + + public function testGenerateWhenStringRecurringDaily(): void { + + // construct l10n return maps + $this->l10n->method('l')->willReturnCallback( + function ($v1, $v2, $v3) { + return match (true) { + $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM', + $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM', + $v1 === 'date' && $v2 == (new \DateTime('20240713T080000', (new \DateTimeZone('UTC')))) && $v3 == ['width' => 'long'] => 'July 13, 2024' + }; + } + ); + $this->l10n->method('t')->willReturnMap([ + ['Every Day for the entire day', [], 'Every Day for the entire day'], + ['Every Day for the entire day until %1$s', ['July 13, 2024'], 'Every Day for the entire day until July 13, 2024'], + ['Every Day between %1$s - %2$s', ['8:00 AM', '9:00 AM (America/Toronto)'], 'Every Day between 8:00 AM - 9:00 AM (America/Toronto)'], + ['Every Day between %1$s - %2$s until %3$s', ['8:00 AM', '9:00 AM (America/Toronto)', 'July 13, 2024'], 'Every Day between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024'], + ['Every %1$d Days for the entire day', [3], 'Every 3 Days for the entire day'], + ['Every %1$d Days for the entire day until %2$s', [3, 'July 13, 2024'], 'Every 3 Days for the entire day until July 13, 2024'], + ['Every %1$d Days between %2$s - %3$s', [3, '8:00 AM', '9:00 AM (America/Toronto)'], 'Every 3 Days between 8:00 AM - 9:00 AM (America/Toronto)'], + ['Every %1$d Days between %2$s - %3$s until %4$s', [3, '8:00 AM', '9:00 AM (America/Toronto)', 'July 13, 2024'], 'Every 3 Days between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024'], + ['Could not generate event recurrence statement', [], 'Could not generate event recurrence statement'], + ]); + + /** test partial day event with every day interval and no conclusion*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=1;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every Day between 8:00 AM - 9:00 AM (America/Toronto)', + $this->service->generateWhenString($eventReader) + ); + + /** test partial day event with every day interval and conclusion*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=1;UNTIL=20240713T080000Z'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every Day between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024', + $this->service->generateWhenString($eventReader) + ); + + /** test partial day event every 3rd day interval and no conclusion*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=3;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every 3 Days between 8:00 AM - 9:00 AM (America/Toronto)', + $this->service->generateWhenString($eventReader) + ); + + /** test partial day event with every 3rd day interval and conclusion*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=3;UNTIL=20240713T080000Z'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every 3 Days between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024', + $this->service->generateWhenString($eventReader) + ); + + /** test entire day event with every day interval and no conclusion*/ + $vCalendar = clone $this->vCalendar2; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=1;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every Day for the entire day', + $this->service->generateWhenString($eventReader) + ); + + /** test entire day event with every day interval and conclusion*/ + $vCalendar = clone $this->vCalendar2; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=1;UNTIL=20240713T080000Z'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every Day for the entire day until July 13, 2024', + $this->service->generateWhenString($eventReader) + ); + + /** test entire day event with every 3rd day interval and no conclusion*/ + $vCalendar = clone $this->vCalendar2; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=3;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every 3 Days for the entire day', + $this->service->generateWhenString($eventReader) + ); + + /** test entire day event with every 3rd day interval and conclusion*/ + $vCalendar = clone $this->vCalendar2; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=3;UNTIL=20240713T080000Z'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every 3 Days for the entire day until July 13, 2024', + $this->service->generateWhenString($eventReader) + ); + + } + + public function testGenerateWhenStringRecurringWeekly(): void { + + // construct l10n return maps + $this->l10n->method('l')->willReturnCallback( + function ($v1, $v2, $v3) { + return match (true) { + $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM', + $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM', + $v1 === 'date' && $v2 == (new \DateTime('20240722T080000', (new \DateTimeZone('UTC')))) && $v3 == ['width' => 'long'] => 'July 13, 2024' + }; + } + ); + $this->l10n->method('t')->willReturnMap([ + ['Every Week on %1$s for the entire day', ['Monday, Wednesday, Friday'], 'Every Week on Monday, Wednesday, Friday for the entire day'], + ['Every Week on %1$s for the entire day until %2$s', ['Monday, Wednesday, Friday', 'July 13, 2024'], 'Every Week on Monday, Wednesday, Friday for the entire day until July 13, 2024'], + ['Every Week on %1$s between %2$s - %3$s', ['Monday, Wednesday, Friday', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every Week on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto)'], + ['Every Week on %1$s between %2$s - %3$s until %4$s', ['Monday, Wednesday, Friday', '8:00 AM', '9:00 AM (America/Toronto)', 'July 13, 2024'], 'Every Week on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024'], + ['Every %1$d Weeks on %2$s for the entire day', [2, 'Monday, Wednesday, Friday'], 'Every 2 Weeks on Monday, Wednesday, Friday for the entire day'], + ['Every %1$d Weeks on %2$s for the entire day until %3$s', [2, 'Monday, Wednesday, Friday', 'July 13, 2024'], 'Every 2 Weeks on Monday, Wednesday, Friday for the entire day until July 13, 2024'], + ['Every %1$d Weeks on %2$s between %3$s - %4$s', [2, 'Monday, Wednesday, Friday', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every 2 Weeks on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto)'], + ['Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s', [2, 'Monday, Wednesday, Friday', '8:00 AM', '9:00 AM (America/Toronto)', 'July 13, 2024'], 'Every 2 Weeks on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024'], + ['Could not generate event recurrence statement', [], 'Could not generate event recurrence statement'], + ['Monday', [], 'Monday'], + ['Wednesday', [], 'Wednesday'], + ['Friday', [], 'Friday'], + ]); + + /** test partial day event with every week interval on Mon, Wed, Fri and no conclusion*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every Week on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto)', + $this->service->generateWhenString($eventReader) + ); + + /** test partial day event with every week interval on Mon, Wed, Fri and conclusion*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20240722T080000Z;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every Week on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024', + $this->service->generateWhenString($eventReader) + ); + + /** test partial day event with every 2nd week interval on Mon, Wed, Fri and no conclusion*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;INTERVAL=2;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every 2 Weeks on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto)', + $this->service->generateWhenString($eventReader) + ); + + /** test partial day event with every 2nd week interval on Mon, Wed, Fri and conclusion*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;INTERVAL=2;UNTIL=20240722T080000Z;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every 2 Weeks on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024', + $this->service->generateWhenString($eventReader) + ); + + /** test entire day event with every week interval on Mon, Wed, Fri and no conclusion*/ + $vCalendar = clone $this->vCalendar2; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every Week on Monday, Wednesday, Friday for the entire day', + $this->service->generateWhenString($eventReader) + ); + + /** test entire day event with every week interval on Mon, Wed, Fri and conclusion*/ + $vCalendar = clone $this->vCalendar2; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20240722T080000Z;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every Week on Monday, Wednesday, Friday for the entire day until July 13, 2024', + $this->service->generateWhenString($eventReader) + ); + + /** test entire day event with every 2nd week interval on Mon, Wed, Fri and no conclusion*/ + $vCalendar = clone $this->vCalendar2; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;INTERVAL=2;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every 2 Weeks on Monday, Wednesday, Friday for the entire day', + $this->service->generateWhenString($eventReader) + ); + + /** test entire day event with every 2nd week interval on Mon, Wed, Fri and conclusion*/ + $vCalendar = clone $this->vCalendar2; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=WEEKLY;BYDAY=MO,WE,FR;INTERVAL=2;UNTIL=20240722T080000Z;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every 2 Weeks on Monday, Wednesday, Friday for the entire day until July 13, 2024', + $this->service->generateWhenString($eventReader) + ); + + } + + public function testGenerateWhenStringRecurringMonthly(): void { + + // construct l10n return maps + $this->l10n->method('l')->willReturnCallback( + function ($v1, $v2, $v3) { + return match (true) { + $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM', + $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM', + $v1 === 'date' && $v2 == (new \DateTime('20241231T080000', (new \DateTimeZone('UTC')))) && $v3 == ['width' => 'long'] => 'December 31, 2024' + }; + } + ); + $this->l10n->method('t')->willReturnMap([ + ['Every Month on the %1$s for the entire day', ['1, 8'], 'Every Month on the 1, 8 for the entire day'], + ['Every Month on the %1$s for the entire day until %2$s', ['1, 8', 'December 31, 2024'], 'Every Month on the 1, 8 for the entire day until December 31, 2024'], + ['Every Month on the %1$s between %2$s - %3$s', ['1, 8', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every Month on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto)'], + ['Every Month on the %1$s between %2$s - %3$s until %4$s', ['1, 8', '8:00 AM', '9:00 AM (America/Toronto)', 'December 31, 2024'], 'Every Month on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024'], + ['Every %1$d Months on the %2$s for the entire day', [2, '1, 8'], 'Every 2 Months on the 1, 8 for the entire day'], + ['Every %1$d Months on the %2$s for the entire day until %3$s', [2, '1, 8', 'December 31, 2024'], 'Every 2 Months on the 1, 8 for the entire day until December 31, 2024'], + ['Every %1$d Months on the %2$s between %3$s - %4$s', [2, '1, 8', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every 2 Months on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto)'], + ['Every %1$d Months on the %2$s between %3$s - %4$s until %5$s', [2, '1, 8', '8:00 AM', '9:00 AM (America/Toronto)', 'December 31, 2024'], 'Every 2 Months on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024'], + ['Every Month on the %1$s for the entire day', ['First Sunday, Saturday'], 'Every Month on the First Sunday, Saturday for the entire day'], + ['Every Month on the %1$s for the entire day until %2$s', ['First Sunday, Saturday', 'December 31, 2024'], 'Every Month on the First Sunday, Saturday for the entire day until December 31, 2024'], + ['Every Month on the %1$s between %2$s - %3$s', ['First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every Month on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)'], + ['Every Month on the %1$s between %2$s - %3$s until %4$s', ['First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)', 'December 31, 2024'], 'Every Month on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024'], + ['Every %1$d Months on the %2$s for the entire day', [2, 'First Sunday, Saturday'], 'Every 2 Months on the First Sunday, Saturday for the entire day'], + ['Every %1$d Months on the %2$s for the entire day until %3$s', [2, 'First Sunday, Saturday', 'December 31, 2024'], 'Every 2 Months on the First Sunday, Saturday for the entire day until December 31, 2024'], + ['Every %1$d Months on the %2$s between %3$s - %4$s', [2, 'First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every 2 Months on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)'], + ['Every %1$d Months on the %2$s between %3$s - %4$s until %5$s', [2, 'First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)', 'December 31, 2024'], 'Every 2 Months on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024'], + ['Could not generate event recurrence statement', [], 'Could not generate event recurrence statement'], + ['Saturday', [], 'Saturday'], + ['Sunday', [], 'Sunday'], + ['First', [], 'First'], + ]); + + /** test absolute partial day event with every month interval on 1st, 8th and no conclusion*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every Month on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto)', + $this->service->generateWhenString($eventReader) + ); + + /** test absolute partial day event with every Month interval on 1st, 8th and conclusion*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;UNTIL=20241231T080000Z;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every Month on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024', + $this->service->generateWhenString($eventReader) + ); + + /** test absolute partial day event with every 2nd Month interval on 1st, 8th and no conclusion*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;INTERVAL=2;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every 2 Months on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto)', + $this->service->generateWhenString($eventReader) + ); + + /** test absolute partial day event with every 2nd Month interval on 1st, 8th and conclusion*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;INTERVAL=2;UNTIL=20241231T080000Z;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every 2 Months on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024', + $this->service->generateWhenString($eventReader) + ); + + /** test absolute entire day event with every Month interval on 1st, 8th and no conclusion*/ + $vCalendar = clone $this->vCalendar2; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every Month on the 1, 8 for the entire day', + $this->service->generateWhenString($eventReader) + ); + + /** test absolute entire day event with every Month interval on 1st, 8th and conclusion*/ + $vCalendar = clone $this->vCalendar2; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;UNTIL=20241231T080000Z;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every Month on the 1, 8 for the entire day until December 31, 2024', + $this->service->generateWhenString($eventReader) + ); + + /** test absolute entire day event with every 2nd Month interval on 1st, 8th and no conclusion*/ + $vCalendar = clone $this->vCalendar2; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;INTERVAL=2;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every 2 Months on the 1, 8 for the entire day', + $this->service->generateWhenString($eventReader) + ); + + /** test absolute entire day event with every 2nd Month interval on 1st, 8th and conclusion*/ + $vCalendar = clone $this->vCalendar2; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYMONTHDAY=1,8;INTERVAL=2;UNTIL=20241231T080000Z;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every 2 Months on the 1, 8 for the entire day until December 31, 2024', + $this->service->generateWhenString($eventReader) + ); + + /** test relative partial day event with every month interval on the 1st Saturday, Sunday and no conclusion*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every Month on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)', + $this->service->generateWhenString($eventReader) + ); + + /** test relative partial day event with every Month interval on the 1st Saturday, Sunday and conclusion*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;UNTIL=20241231T080000Z;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every Month on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024', + $this->service->generateWhenString($eventReader) + ); + + /** test relative partial day event with every 2nd Month interval on the 1st Saturday, Sunday and no conclusion*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every 2 Months on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)', + $this->service->generateWhenString($eventReader) + ); + + /** test relative partial day event with every 2nd Month interval on the 1st Saturday, Sunday and conclusion*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;UNTIL=20241231T080000Z;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every 2 Months on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024', + $this->service->generateWhenString($eventReader) + ); + + /** test relative entire day event with every Month interval on the 1st Saturday, Sunday and no conclusion*/ + $vCalendar = clone $this->vCalendar2; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every Month on the First Sunday, Saturday for the entire day', + $this->service->generateWhenString($eventReader) + ); + + /** test relative entire day event with every Month interval on the 1st Saturday, Sunday and conclusion*/ + $vCalendar = clone $this->vCalendar2; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;UNTIL=20241231T080000Z;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every Month on the First Sunday, Saturday for the entire day until December 31, 2024', + $this->service->generateWhenString($eventReader) + ); + + /** test relative entire day event with every 2nd Month interval on the 1st Saturday, Sunday and no conclusion*/ + $vCalendar = clone $this->vCalendar2; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every 2 Months on the First Sunday, Saturday for the entire day', + $this->service->generateWhenString($eventReader) + ); + + /** test relative entire day event with every 2nd Month interval on the 1st Saturday, Sunday and conclusion*/ + $vCalendar = clone $this->vCalendar2; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=MONTHLY;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;UNTIL=20241231T080000Z;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every 2 Months on the First Sunday, Saturday for the entire day until December 31, 2024', + $this->service->generateWhenString($eventReader) + ); + + } + + public function testGenerateWhenStringRecurringYearly(): void { + + // construct l10n return maps + $this->l10n->method('l')->willReturnCallback( + function ($v1, $v2, $v3) { + return match (true) { + $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM', + $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM', + $v1 === 'date' && $v2 == (new \DateTime('20260731T040000', (new \DateTimeZone('UTC')))) && $v3 == ['width' => 'long'] => 'July 31, 2026' + }; + } + ); + $this->l10n->method('t')->willReturnMap([ + ['Every Year in %1$s on the %2$s for the entire day', ['July', '1st'], 'Every Year in July on the 1st for the entire day'], + ['Every Year in %1$s on the %2$s for the entire day until %3$s', ['July', '1st', 'July 31, 2026'], 'Every Year in July on the 1st for the entire day until July 31, 2026'], + ['Every Year in %1$s on the %2$s between %3$s - %4$s', ['July', '1st', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every Year in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto)'], + ['Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s', ['July', '1st', '8:00 AM', '9:00 AM (America/Toronto)', 'July 31, 2026'], 'Every Year in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026'], + ['Every %1$d Years in %2$s on the %3$s for the entire day', [2, 'July', '1st'], 'Every 2 Years in July on the 1st for the entire day'], + ['Every %1$d Years in %2$s on the %3$s for the entire day until %4$s', [2, 'July', '1st', 'July 31, 2026'], 'Every 2 Years in July on the 1st for the entire day until July 31, 2026'], + ['Every %1$d Years in %2$s on the %3$s between %4$s - %5$s', [2, 'July', '1st', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every 2 Years in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto)'], + ['Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s', [2, 'July', '1st', '8:00 AM', '9:00 AM (America/Toronto)', 'July 31, 2026'], 'Every 2 Years in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026'], + ['Every Year in %1$s on the %2$s for the entire day', ['July', 'First Sunday, Saturday'], 'Every Year in July on the First Sunday, Saturday for the entire day'], + ['Every Year in %1$s on the %2$s for the entire day until %3$s', ['July', 'First Sunday, Saturday', 'July 31, 2026'], 'Every Year in July on the First Sunday, Saturday for the entire day until July 31, 2026'], + ['Every Year in %1$s on the %2$s between %3$s - %4$s', ['July', 'First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every Year in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)'], + ['Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s', ['July', 'First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)', 'July 31, 2026'], 'Every Year in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026'], + ['Every %1$d Years in %2$s on the %3$s for the entire day', [2, 'July', 'First Sunday, Saturday'], 'Every 2 Years in July on the First Sunday, Saturday for the entire day'], + ['Every %1$d Years in %2$s on the %3$s for the entire day until %4$s', [2, 'July', 'First Sunday, Saturday', 'July 31, 2026'], 'Every 2 Years in July on the First Sunday, Saturday for the entire day until July 31, 2026'], + ['Every %1$d Years in %2$s on the %3$s between %4$s - %5$s', [2, 'July', 'First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)'], 'Every 2 Years in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)'], + ['Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s', [2, 'July', 'First Sunday, Saturday', '8:00 AM', '9:00 AM (America/Toronto)', 'July 31, 2026'], 'Every 2 Years in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026'], + ['Could not generate event recurrence statement', [], 'Could not generate event recurrence statement'], + ['July', [], 'July'], + ['Saturday', [], 'Saturday'], + ['Sunday', [], 'Sunday'], + ['First', [], 'First'], + ]); + + /** test absolute partial day event with every year interval on July 1 and no conclusion*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every Year in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto)', + $this->service->generateWhenString($eventReader) + ); + + /** test absolute partial day event with every year interval on July 1 and conclusion*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;UNTIL=20260731T040000Z'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every Year in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026', + $this->service->generateWhenString($eventReader) + ); + + /** test absolute partial day event with every 2nd year interval on July 1 and no conclusion*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;INTERVAL=2;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every 2 Years in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto)', + $this->service->generateWhenString($eventReader) + ); + + /** test absolute partial day event with every 2nd year interval on July 1 and conclusion*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;INTERVAL=2;UNTIL=20260731T040000Z;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every 2 Years in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026', + $this->service->generateWhenString($eventReader) + ); + + /** test absolute entire day event with every year interval on July 1 and no conclusion*/ + $vCalendar = clone $this->vCalendar2; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every Year in July on the 1st for the entire day', + $this->service->generateWhenString($eventReader) + ); + + /** test absolute entire day event with every year interval on July 1 and conclusion*/ + $vCalendar = clone $this->vCalendar2; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;UNTIL=20260731T040000Z;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every Year in July on the 1st for the entire day until July 31, 2026', + $this->service->generateWhenString($eventReader) + ); + + /** test absolute entire day event with every 2nd year interval on July 1 and no conclusion*/ + $vCalendar = clone $this->vCalendar2; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;INTERVAL=2;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every 2 Years in July on the 1st for the entire day', + $this->service->generateWhenString($eventReader) + ); + + /** test absolute entire day event with every 2nd year interval on July 1 and conclusion*/ + $vCalendar = clone $this->vCalendar2; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;INTERVAL=2;UNTIL=20260731T040000Z;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every 2 Years in July on the 1st for the entire day until July 31, 2026', + $this->service->generateWhenString($eventReader) + ); + + /** test relative partial day event with every year interval on the 1st Saturday, Sunday in July and no conclusion*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every Year in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)', + $this->service->generateWhenString($eventReader) + ); + + /** test relative partial day event with every year interval on the 1st Saturday, Sunday in July and conclusion*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;UNTIL=20260731T040000Z;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every Year in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026', + $this->service->generateWhenString($eventReader) + ); + + /** test relative partial day event with every 2nd year interval on the 1st Saturday, Sunday in July and no conclusion*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every 2 Years in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)', + $this->service->generateWhenString($eventReader) + ); + + /** test relative partial day event with every 2nd year interval on the 1st Saturday, Sunday in July and conclusion*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;UNTIL=20260731T040000Z;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every 2 Years in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026', + $this->service->generateWhenString($eventReader) + ); + + /** test relative entire day event with every year interval on the 1st Saturday, Sunday in July and no conclusion*/ + $vCalendar = clone $this->vCalendar2; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every Year in July on the First Sunday, Saturday for the entire day', + $this->service->generateWhenString($eventReader) + ); + + /** test relative entire day event with every year interval on the 1st Saturday, Sunday in July and conclusion*/ + $vCalendar = clone $this->vCalendar2; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;UNTIL=20260731T040000Z;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every Year in July on the First Sunday, Saturday for the entire day until July 31, 2026', + $this->service->generateWhenString($eventReader) + ); + + /** test relative entire day event with every 2nd year interval on the 1st Saturday, Sunday in July and no conclusion*/ + $vCalendar = clone $this->vCalendar2; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every 2 Years in July on the First Sunday, Saturday for the entire day', + $this->service->generateWhenString($eventReader) + ); + + /** test relative entire day event with every 2nd year interval on the 1st Saturday, Sunday in July and conclusion*/ + $vCalendar = clone $this->vCalendar2; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=YEARLY;BYMONTH=7;BYDAY=SU,SA;BYSETPOS=1;INTERVAL=2;UNTIL=20260731T040000Z;'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'Every 2 Years in July on the First Sunday, Saturday for the entire day until July 31, 2026', + $this->service->generateWhenString($eventReader) + ); + + } + + public function testGenerateWhenStringRecurringFixed(): void { + + // construct l10n return maps + $this->l10n->method('l')->willReturnCallback( + function ($v1, $v2, $v3) { + return match (true) { + $v1 === 'time' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '8:00 AM', + $v1 === 'time' && $v2 == (new \DateTime('20240701T090000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'short'] => '9:00 AM', + $v1 === 'date' && $v2 == (new \DateTime('20240713T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 13, 2024' + }; + } + ); + $this->l10n->method('t')->willReturnMap([ + ['On specific dates for the entire day until %1$s', ['July 13, 2024'], 'On specific dates for the entire day until July 13, 2024'], + ['On specific dates between %1$s - %2$s until %3$s', ['8:00 AM', '9:00 AM (America/Toronto)', 'July 13, 2024'], 'On specific dates between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024'], + ]); + + /** test partial day event with every day interval and conclusion*/ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RDATE', '20240703T080000,20240709T080000,20240713T080000'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'On specific dates between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024', + $this->service->generateWhenString($eventReader) + ); + + /** test entire day event with every day interval and no conclusion*/ + $vCalendar = clone $this->vCalendar2; + $vCalendar->VEVENT[0]->add('RDATE', '20240703T080000,20240709T080000,20240713T080000'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'On specific dates for the entire day until July 13, 2024', + $this->service->generateWhenString($eventReader) + ); + + } + + public function testGenerateOccurringStringWithRrule(): void { + + // construct l10n return(s) + $this->l10n->method('l')->willReturnCallback( + function ($v1, $v2, $v3) { + return match (true) { + $v1 === 'date' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 1, 2024', + $v1 === 'date' && $v2 == (new \DateTime('20240703T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 3, 2024', + $v1 === 'date' && $v2 == (new \DateTime('20240705T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 5, 2024' + }; + } + ); + $this->l10n->method('n')->willReturnMap([ + // singular + [ + 'In a day on %1$s', + 'In %n days on %1$s', + 1, + ['July 1, 2024'], + 'In a day on July 1, 2024' + ], + [ + 'In a day on %1$s then on %2$s', + 'In %n days on %1$s then on %2$s', + 1, + ['July 1, 2024', 'July 3, 2024'], + 'In a day on July 1, 2024 then on July 3, 2024' + ], + [ + 'In a day on %1$s then on %2$s and %3$s', + 'In %n days on %1$s then on %2$s and %3$s', + 1, + ['July 1, 2024', 'July 3, 2024', 'July 5, 2024'], + 'In a day on July 1, 2024 then on July 3, 2024 and July 5, 2024' + ], + // plural + [ + 'In a day on %1$s', + 'In %n days on %1$s', + 2, + ['July 1, 2024'], + 'In 2 days on July 1, 2024' + ], + [ + 'In a day on %1$s then on %2$s', + 'In %n days on %1$s then on %2$s', + 2, + ['July 1, 2024', 'July 3, 2024'], + 'In 2 days on July 1, 2024 then on July 3, 2024' + ], + [ + 'In a day on %1$s then on %2$s and %3$s', + 'In %n days on %1$s then on %2$s and %3$s', + 2, + ['July 1, 2024', 'July 3, 2024', 'July 5, 2024'], + 'In 2 days on July 1, 2024 then on July 3, 2024 and July 5, 2024' + ], + ]); + + // construct time factory return(s) + $this->timeFactory->method('getDateTime')->willReturnOnConsecutiveCalls( + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + ); + + /** test patrial day recurring event in 1 day with single occurrence remaining */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=1'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In a day on July 1, 2024', + $this->service->generateOccurringString($eventReader) + ); + + /** test patrial day recurring event in 1 day with two occurrences remaining */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=2'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In a day on July 1, 2024 then on July 3, 2024', + $this->service->generateOccurringString($eventReader) + ); + + /** test patrial day recurring event in 1 day with three occurrences remaining */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=3'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In a day on July 1, 2024 then on July 3, 2024 and July 5, 2024', + $this->service->generateOccurringString($eventReader) + ); + + /** test patrial day recurring event in 2 days with single occurrence remaining */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=1'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In 2 days on July 1, 2024', + $this->service->generateOccurringString($eventReader) + ); + + /** test patrial day recurring event in 2 days with two occurrences remaining */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=2'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In 2 days on July 1, 2024 then on July 3, 2024', + $this->service->generateOccurringString($eventReader) + ); + + /** test patrial day recurring event in 2 days with three occurrences remaining */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=3'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In 2 days on July 1, 2024 then on July 3, 2024 and July 5, 2024', + $this->service->generateOccurringString($eventReader) + ); + } + + public function testGenerateOccurringStringWithRdate(): void { + + // construct l10n return(s) + $this->l10n->method('l')->willReturnCallback( + function ($v1, $v2, $v3) { + return match (true) { + $v1 === 'date' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 1, 2024', + $v1 === 'date' && $v2 == (new \DateTime('20240703T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 3, 2024', + $v1 === 'date' && $v2 == (new \DateTime('20240705T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 5, 2024' + }; + } + ); + $this->l10n->method('n')->willReturnMap([ + // singular + [ + 'In a day on %1$s', + 'In %n days on %1$s', + 1, + ['July 1, 2024'], + 'In a day on July 1, 2024' + ], + [ + 'In a day on %1$s then on %2$s', + 'In %n days on %1$s then on %2$s', + 1, + ['July 1, 2024', 'July 3, 2024'], + 'In a day on July 1, 2024 then on July 3, 2024' + ], + [ + 'In a day on %1$s then on %2$s and %3$s', + 'In %n days on %1$s then on %2$s and %3$s', + 1, + ['July 1, 2024', 'July 3, 2024', 'July 5, 2024'], + 'In a day on July 1, 2024 then on July 3, 2024 and July 5, 2024' + ], + // plural + [ + 'In a day on %1$s', + 'In %n days on %1$s', + 2, + ['July 1, 2024'], + 'In 2 days on July 1, 2024' + ], + [ + 'In a day on %1$s then on %2$s', + 'In %n days on %1$s then on %2$s', + 2, + ['July 1, 2024', 'July 3, 2024'], + 'In 2 days on July 1, 2024 then on July 3, 2024' + ], + [ + 'In a day on %1$s then on %2$s and %3$s', + 'In %n days on %1$s then on %2$s and %3$s', + 2, + ['July 1, 2024', 'July 3, 2024', 'July 5, 2024'], + 'In 2 days on July 1, 2024 then on July 3, 2024 and July 5, 2024' + ], + ]); + + // construct time factory return(s) + $this->timeFactory->method('getDateTime')->willReturnOnConsecutiveCalls( + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + ); + + /** test patrial day recurring event in 1 day with single occurrence remaining */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RDATE', '20240701T080000'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In a day on July 1, 2024', + $this->service->generateOccurringString($eventReader), + 'test patrial day recurring event in 1 day with single occurrence remaining' + ); + + /** test patrial day recurring event in 1 day with two occurrences remaining */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RDATE', '20240701T080000,20240703T080000'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In a day on July 1, 2024 then on July 3, 2024', + $this->service->generateOccurringString($eventReader), + 'test patrial day recurring event in 1 day with two occurrences remaining' + ); + + /** test patrial day recurring event in 1 day with three occurrences remaining */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RDATE', '20240701T080000,20240703T080000,20240705T080000'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In a day on July 1, 2024 then on July 3, 2024 and July 5, 2024', + $this->service->generateOccurringString($eventReader), + '' + ); + + /** test patrial day recurring event in 2 days with single occurrences remaining */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RDATE', '20240701T080000'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In 2 days on July 1, 2024', + $this->service->generateOccurringString($eventReader), + '' + ); + + /** test patrial day recurring event in 2 days with two occurrences remaining */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RDATE', '20240701T080000'); + $vCalendar->VEVENT[0]->add('RDATE', '20240703T080000'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In 2 days on July 1, 2024 then on July 3, 2024', + $this->service->generateOccurringString($eventReader), + '' + ); + + /** test patrial day recurring event in 2 days with three occurrences remaining */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RDATE', '20240701T080000'); + $vCalendar->VEVENT[0]->add('RDATE', '20240703T080000'); + $vCalendar->VEVENT[0]->add('RDATE', '20240705T080000'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In 2 days on July 1, 2024 then on July 3, 2024 and July 5, 2024', + $this->service->generateOccurringString($eventReader), + 'test patrial day recurring event in 2 days with three occurrences remaining' + ); + } + + public function testGenerateOccurringStringWithOneExdate(): void { + + // construct l10n return(s) + $this->l10n->method('l')->willReturnCallback( + function ($v1, $v2, $v3) { + return match (true) { + $v1 === 'date' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 1, 2024', + $v1 === 'date' && $v2 == (new \DateTime('20240705T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 5, 2024', + $v1 === 'date' && $v2 == (new \DateTime('20240707T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 7, 2024' + }; + } + ); + $this->l10n->method('n')->willReturnMap([ + // singular + [ + 'In a day on %1$s', + 'In %n days on %1$s', + 1, + ['July 1, 2024'], + 'In a day on July 1, 2024' + ], + [ + 'In a day on %1$s then on %2$s', + 'In %n days on %1$s then on %2$s', + 1, + ['July 1, 2024', 'July 5, 2024'], + 'In a day on July 1, 2024 then on July 5, 2024' + ], + [ + 'In a day on %1$s then on %2$s and %3$s', + 'In %n days on %1$s then on %2$s and %3$s', + 1, + ['July 1, 2024', 'July 5, 2024', 'July 7, 2024'], + 'In a day on July 1, 2024 then on July 5, 2024 and July 7, 2024' + ], + // plural + [ + 'In a day on %1$s', + 'In %n days on %1$s', + 2, + ['July 1, 2024'], + 'In 2 days on July 1, 2024' + ], + [ + 'In a day on %1$s then on %2$s', + 'In %n days on %1$s then on %2$s', + 2, + ['July 1, 2024', 'July 5, 2024'], + 'In 2 days on July 1, 2024 then on July 5, 2024' + ], + [ + 'In a day on %1$s then on %2$s and %3$s', + 'In %n days on %1$s then on %2$s and %3$s', + 2, + ['July 1, 2024', 'July 5, 2024', 'July 7, 2024'], + 'In 2 days on July 1, 2024 then on July 5, 2024 and July 7, 2024' + ], + ]); + + // construct time factory return(s) + $this->timeFactory->method('getDateTime')->willReturnOnConsecutiveCalls( + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + ); + + /** test patrial day recurring event in 1 day with single occurrence remaining and one exception */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=1'); + $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In a day on July 1, 2024', + $this->service->generateOccurringString($eventReader), + 'test patrial day recurring event in 1 day with single occurrence remaining and one exception' + ); + + /** test patrial day recurring event in 1 day with two occurrences remaining and one exception */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=2'); + $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In a day on July 1, 2024', + $this->service->generateOccurringString($eventReader), + 'test patrial day recurring event in 1 day with two occurrences remaining and one exception' + ); + + /** test patrial day recurring event in 1 day with three occurrences remaining and one exception */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=3'); + $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In a day on July 1, 2024 then on July 5, 2024', + $this->service->generateOccurringString($eventReader), + 'test patrial day recurring event in 1 day with three occurrences remaining and one exception' + ); + + /** test patrial day recurring event in 1 day with four occurrences remaining and one exception */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=4'); + $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In a day on July 1, 2024 then on July 5, 2024 and July 7, 2024', + $this->service->generateOccurringString($eventReader), + 'test patrial day recurring event in 1 day with four occurrences remaining and one exception' + ); + + /** test patrial day recurring event in 2 days with single occurrences remaining and one exception */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=1'); + $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In 2 days on July 1, 2024', + $this->service->generateOccurringString($eventReader), + 'test patrial day recurring event in 2 days with single occurrences remaining and one exception' + ); + + /** test patrial day recurring event in 2 days with two occurrences remaining and one exception */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=2'); + $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In 2 days on July 1, 2024', + $this->service->generateOccurringString($eventReader), + 'test patrial day recurring event in 2 days with two occurrences remaining and one exception' + ); + + /** test patrial day recurring event in 2 days with three occurrences remaining and one exception */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=3'); + $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In 2 days on July 1, 2024 then on July 5, 2024', + $this->service->generateOccurringString($eventReader), + 'test patrial day recurring event in 2 days with three occurrences remaining and one exception' + ); + + /** test patrial day recurring event in 2 days with four occurrences remaining and one exception */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=4'); + $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In 2 days on July 1, 2024 then on July 5, 2024 and July 7, 2024', + $this->service->generateOccurringString($eventReader), + 'test patrial day recurring event in 2 days with four occurrences remaining and one exception' + ); + } + + public function testGenerateOccurringStringWithTwoExdate(): void { + + // construct l10n return(s) + $this->l10n->method('l')->willReturnCallback( + function ($v1, $v2, $v3) { + return match (true) { + $v1 === 'date' && $v2 == (new \DateTime('20240701T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 1, 2024', + $v1 === 'date' && $v2 == (new \DateTime('20240705T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 5, 2024', + $v1 === 'date' && $v2 == (new \DateTime('20240709T080000', (new \DateTimeZone('America/Toronto')))) && $v3 == ['width' => 'long'] => 'July 9, 2024' + }; + } + ); + $this->l10n->method('n')->willReturnMap([ + // singular + [ + 'In a day on %1$s', + 'In %n days on %1$s', + 1, + ['July 1, 2024'], + 'In a day on July 1, 2024' + ], + [ + 'In a day on %1$s then on %2$s', + 'In %n days on %1$s then on %2$s', + 1, + ['July 1, 2024', 'July 5, 2024'], + 'In a day on July 1, 2024 then on July 5, 2024' + ], + [ + 'In a day on %1$s then on %2$s and %3$s', + 'In %n days on %1$s then on %2$s and %3$s', + 1, + ['July 1, 2024', 'July 5, 2024', 'July 9, 2024'], + 'In a day on July 1, 2024 then on July 5, 2024 and July 9, 2024' + ], + // plural + [ + 'In a day on %1$s', + 'In %n days on %1$s', + 2, + ['July 1, 2024'], + 'In 2 days on July 1, 2024' + ], + [ + 'In a day on %1$s then on %2$s', + 'In %n days on %1$s then on %2$s', + 2, + ['July 1, 2024', 'July 5, 2024'], + 'In 2 days on July 1, 2024 then on July 5, 2024' + ], + [ + 'In a day on %1$s then on %2$s and %3$s', + 'In %n days on %1$s then on %2$s and %3$s', + 2, + ['July 1, 2024', 'July 5, 2024', 'July 9, 2024'], + 'In 2 days on July 1, 2024 then on July 5, 2024 and July 9, 2024' + ], + ]); + + // construct time factory return(s) + $this->timeFactory->method('getDateTime')->willReturnOnConsecutiveCalls( + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240629T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + (new \DateTime('20240628T170000', (new \DateTimeZone('America/Toronto')))), + ); + + /** test patrial day recurring event in 1 day with single occurrence remaining and two exception */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=1'); + $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000'); + $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In a day on July 1, 2024', + $this->service->generateOccurringString($eventReader), + 'test patrial day recurring event in 1 day with single occurrence remaining and two exception' + ); + + /** test patrial day recurring event in 1 day with two occurrences remaining and two exception */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=2'); + $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000'); + $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In a day on July 1, 2024', + $this->service->generateOccurringString($eventReader), + 'test patrial day recurring event in 1 day with two occurrences remaining and two exception' + ); + + /** test patrial day recurring event in 1 day with three occurrences remaining and two exception */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=3'); + $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000'); + $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In a day on July 1, 2024 then on July 5, 2024', + $this->service->generateOccurringString($eventReader), + 'test patrial day recurring event in 1 day with three occurrences remaining and two exception' + ); + + /** test patrial day recurring event in 1 day with four occurrences remaining and two exception */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=5'); + $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000'); + $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In a day on July 1, 2024 then on July 5, 2024 and July 9, 2024', + $this->service->generateOccurringString($eventReader), + 'test patrial day recurring event in 1 day with four occurrences remaining and two exception' + ); + + /** test patrial day recurring event in 2 days with single occurrences remaining and two exception */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=1'); + $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000'); + $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In 2 days on July 1, 2024', + $this->service->generateOccurringString($eventReader), + 'test patrial day recurring event in 2 days with single occurrences remaining and two exception' + ); + + /** test patrial day recurring event in 2 days with two occurrences remaining and two exception */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=2'); + $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000'); + $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In 2 days on July 1, 2024', + $this->service->generateOccurringString($eventReader), + 'test patrial day recurring event in 2 days with two occurrences remaining and two exception' + ); + + /** test patrial day recurring event in 2 days with three occurrences remaining and two exception */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=3'); + $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000'); + $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In 2 days on July 1, 2024 then on July 5, 2024', + $this->service->generateOccurringString($eventReader), + 'test patrial day recurring event in 2 days with three occurrences remaining and two exception' + ); + + /** test patrial day recurring event in 2 days with five occurrences remaining and two exception */ + $vCalendar = clone $this->vCalendar1a; + $vCalendar->VEVENT[0]->add('RRULE', 'FREQ=DAILY;INTERVAL=2;COUNT=5'); + $vCalendar->VEVENT[0]->add('EXDATE', '20240703T080000'); + $vCalendar->VEVENT[0]->add('EXDATE', '20240707T080000'); + // construct event reader + $eventReader = new EventReader($vCalendar, $vCalendar->VEVENT[0]->UID->getValue()); + // test output + $this->assertEquals( + 'In 2 days on July 1, 2024 then on July 5, 2024 and July 9, 2024', + $this->service->generateOccurringString($eventReader), + 'test patrial day recurring event in 2 days with five occurrences remaining and two exception' + ); + } + +} diff --git a/apps/dav/tests/unit/CalDAV/Schedule/PluginTest.php b/apps/dav/tests/unit/CalDAV/Schedule/PluginTest.php new file mode 100644 index 00000000000..524ac556e19 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Schedule/PluginTest.php @@ -0,0 +1,770 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\Schedule; + +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CalDAV\Calendar; +use OCA\DAV\CalDAV\CalendarHome; +use OCA\DAV\CalDAV\DefaultCalendarValidator; +use OCA\DAV\CalDAV\Plugin as CalDAVPlugin; +use OCA\DAV\CalDAV\Schedule\Plugin; +use OCA\DAV\CalDAV\Trashbin\Plugin as TrashbinPlugin; +use OCP\IConfig; +use OCP\IL10N; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Sabre\CalDAV\Backend\BackendInterface; +use Sabre\DAV\PropFind; +use Sabre\DAV\Server; +use Sabre\DAV\Tree; +use Sabre\DAV\Xml\Property\Href; +use Sabre\DAV\Xml\Property\LocalHref; +use Sabre\DAVACL\IPrincipal; +use Sabre\HTTP\Request; +use Sabre\HTTP\Response; +use Sabre\HTTP\ResponseInterface; +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\ITip\Message; +use Sabre\VObject\Parameter; +use Sabre\VObject\Property\ICalendar\CalAddress; +use Sabre\Xml\Service; +use Test\TestCase; + +class PluginTest extends TestCase { + private Plugin $plugin; + private Server&MockObject $server; + private IConfig&MockObject $config; + private LoggerInterface&MockObject $logger; + private DefaultCalendarValidator $calendarValidator; + + protected function setUp(): void { + parent::setUp(); + + $this->config = $this->createMock(IConfig::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->calendarValidator = new DefaultCalendarValidator(); + + $this->server = $this->createMock(Server::class); + $this->server->httpResponse = $this->createMock(ResponseInterface::class); + $this->server->xml = new Service(); + + $this->plugin = new Plugin($this->config, $this->logger, $this->calendarValidator); + $this->plugin->initialize($this->server); + } + + public function testInitialize(): void { + $calls = [ + // Sabre\CalDAV\Schedule\Plugin events + ['method:POST', [$this->plugin, 'httpPost'], 100], + ['propFind', [$this->plugin, 'propFind'], 100], + ['propPatch', [$this->plugin, 'propPatch'], 100], + ['calendarObjectChange', [$this->plugin, 'calendarObjectChange'], 100], + ['beforeUnbind', [$this->plugin, 'beforeUnbind'], 100], + ['schedule', [$this->plugin, 'scheduleLocalDelivery'], 100], + ['getSupportedPrivilegeSet', [$this->plugin, 'getSupportedPrivilegeSet'], 100], + // OCA\DAV\CalDAV\Schedule\Plugin events + ['propFind', [$this->plugin, 'propFindDefaultCalendarUrl'], 90], + ['afterWriteContent', [$this->plugin, 'dispatchSchedulingResponses'], 100], + ['afterCreateFile', [$this->plugin, 'dispatchSchedulingResponses'], 100], + ]; + $this->server->expects($this->exactly(count($calls))) + ->method('on') + ->willReturnCallback(function () use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + }); + + $this->plugin->initialize($this->server); + } + + public function testGetAddressesForPrincipal(): void { + $href = $this->createMock(Href::class); + $href + ->expects($this->once()) + ->method('getHrefs') + ->willReturn(['lukas@nextcloud.com', 'rullzer@nextcloud.com']); + $this->server + ->expects($this->once()) + ->method('getProperties') + ->with( + 'MyPrincipal', + [ + '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set', + ] + ) + ->willReturn([ + '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set' => $href + ]); + + $result = $this->invokePrivate($this->plugin, 'getAddressesForPrincipal', ['MyPrincipal']); + $this->assertSame(['lukas@nextcloud.com', 'rullzer@nextcloud.com'], $result); + } + + public function testGetAddressesForPrincipalEmpty(): void { + $this->server + ->expects($this->once()) + ->method('getProperties') + ->with( + 'MyPrincipal', + [ + '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set', + ] + ) + ->willReturn(null); + + $result = $this->invokePrivate($this->plugin, 'getAddressesForPrincipal', ['MyPrincipal']); + $this->assertSame([], $result); + } + + public function testStripOffMailTo(): void { + $this->assertEquals('test@example.com', $this->invokePrivate($this->plugin, 'stripOffMailTo', ['test@example.com'])); + $this->assertEquals('test@example.com', $this->invokePrivate($this->plugin, 'stripOffMailTo', ['mailto:test@example.com'])); + } + + public function testGetAttendeeRSVP(): void { + $property1 = $this->createMock(CalAddress::class); + $parameter1 = $this->createMock(Parameter::class); + $property1->expects($this->once()) + ->method('offsetGet') + ->with('RSVP') + ->willReturn($parameter1); + $parameter1->expects($this->once()) + ->method('getValue') + ->with() + ->willReturn('TRUE'); + + $property2 = $this->createMock(CalAddress::class); + $parameter2 = $this->createMock(Parameter::class); + $property2->expects($this->once()) + ->method('offsetGet') + ->with('RSVP') + ->willReturn($parameter2); + $parameter2->expects($this->once()) + ->method('getValue') + ->with() + ->willReturn('FALSE'); + + $property3 = $this->createMock(CalAddress::class); + $property3->expects($this->once()) + ->method('offsetGet') + ->with('RSVP') + ->willReturn(null); + + $this->assertTrue($this->invokePrivate($this->plugin, 'getAttendeeRSVP', [$property1])); + $this->assertFalse($this->invokePrivate($this->plugin, 'getAttendeeRSVP', [$property2])); + $this->assertFalse($this->invokePrivate($this->plugin, 'getAttendeeRSVP', [$property3])); + } + + public static function propFindDefaultCalendarUrlProvider(): array { + return [ + [ + 'principals/users/myuser', + 'calendars/myuser', + false, + CalDavBackend::PERSONAL_CALENDAR_URI, + CalDavBackend::PERSONAL_CALENDAR_NAME, + true + ], + [ + 'principals/users/myuser', + 'calendars/myuser', + false, + CalDavBackend::PERSONAL_CALENDAR_URI, + CalDavBackend::PERSONAL_CALENDAR_NAME, + true, + true + ], + [ + 'principals/users/myuser', + 'calendars/myuser', + false, + CalDavBackend::PERSONAL_CALENDAR_URI, + CalDavBackend::PERSONAL_CALENDAR_NAME, + false, + false, + true + ], + [ + 'principals/users/myuser', + 'calendars/myuser', + false, + CalDavBackend::PERSONAL_CALENDAR_URI, + CalDavBackend::PERSONAL_CALENDAR_NAME, + false + ], + [ + 'principals/users/myuser', + null, + false, + CalDavBackend::PERSONAL_CALENDAR_URI, + CalDavBackend::PERSONAL_CALENDAR_NAME, + true + ], + [ + 'principals/users/myuser', + 'calendars/myuser', + false, + CalDavBackend::PERSONAL_CALENDAR_URI, + CalDavBackend::PERSONAL_CALENDAR_NAME, + true, + false, + false, + false, + ], + [ + 'principals/users/myuser', + 'calendars/myuser', + false, + 'my_other_calendar', + 'My Other Calendar', + true + ], + [ + 'principals/calendar-resources', + 'system-calendars/calendar-resources/myuser', + true, + CalDavBackend::RESOURCE_BOOKING_CALENDAR_URI, + CalDavBackend::RESOURCE_BOOKING_CALENDAR_NAME, + true + ], + [ + 'principals/calendar-resources', + 'system-calendars/calendar-resources/myuser', + true, + CalDavBackend::RESOURCE_BOOKING_CALENDAR_URI, + CalDavBackend::RESOURCE_BOOKING_CALENDAR_NAME, + false + ], + [ + 'principals/something-else', + 'calendars/whatever', + false, + CalDavBackend::PERSONAL_CALENDAR_URI, + CalDavBackend::PERSONAL_CALENDAR_NAME, + true + ], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('propFindDefaultCalendarUrlProvider')] + public function testPropFindDefaultCalendarUrl(string $principalUri, ?string $calendarHome, bool $isResource, string $calendarUri, string $displayName, bool $exists, bool $deleted = false, bool $hasExistingCalendars = false, bool $propertiesForPath = true): void { + $propFind = new PropFind( + $principalUri, + [ + Plugin::SCHEDULE_DEFAULT_CALENDAR_URL + ], + 0 + ); + /** @var IPrincipal&MockObject $node */ + $node = $this->getMockBuilder(IPrincipal::class) + ->disableOriginalConstructor() + ->getMock(); + + $node->expects($this->once()) + ->method('getPrincipalUrl') + ->with() + ->willReturn($principalUri); + + $calDAVPlugin = $this->getMockBuilder(CalDAVPlugin::class) + ->disableOriginalConstructor() + ->getMock(); + + $calDAVPlugin->expects($this->once()) + ->method('getCalendarHomeForPrincipal') + ->willReturn($calendarHome); + + $this->server->expects($this->once()) + ->method('getPlugin') + ->with('caldav') + ->willReturn($calDAVPlugin); + if (!$calendarHome) { + $this->plugin->propFindDefaultCalendarUrl($propFind, $node); + + $this->assertNull($propFind->get(Plugin::SCHEDULE_DEFAULT_CALENDAR_URL)); + return; + } + if ($principalUri === 'principals/something-else') { + $this->plugin->propFindDefaultCalendarUrl($propFind, $node); + + $this->assertNull($propFind->get(Plugin::SCHEDULE_DEFAULT_CALENDAR_URL)); + return; + } + + if (!$isResource) { + $this->config->expects($this->once()) + ->method('getUserValue') + ->with('myuser', 'dav', 'defaultCalendar', CalDavBackend::PERSONAL_CALENDAR_URI) + ->willReturn($calendarUri); + } + + $calendarHomeObject = $this->createMock(CalendarHome::class); + $calendarHomeObject->expects($this->once()) + ->method('childExists') + ->with($calendarUri) + ->willReturn($exists); + + if ($exists) { + $calendar = $this->createMock(Calendar::class); + $calendar->expects($this->once())->method('isDeleted')->willReturn($deleted); + $calendarHomeObject->expects($deleted && !$hasExistingCalendars ? $this->exactly(2) : $this->once())->method('getChild')->with($calendarUri)->willReturn($calendar); + } + + $calendarBackend = $this->createMock(CalDavBackend::class); + $calendarUri = $hasExistingCalendars ? 'custom' : $calendarUri; + $displayName = $hasExistingCalendars ? 'Custom Calendar' : $displayName; + + $existingCalendars = $hasExistingCalendars ? [ + new Calendar( + $calendarBackend, + ['uri' => 'deleted', '{DAV:}displayname' => 'A deleted calendar', TrashbinPlugin::PROPERTY_DELETED_AT => 42], + $this->createMock(IL10N::class), + $this->config, + $this->createMock(LoggerInterface::class) + ), + new Calendar( + $calendarBackend, + ['uri' => $calendarUri, '{DAV:}displayname' => $displayName], + $this->createMock(IL10N::class), + $this->config, + $this->createMock(LoggerInterface::class) + ) + ] : []; + + if (!$exists || $deleted) { + if (!$hasExistingCalendars) { + $calendarBackend->expects($this->once()) + ->method('createCalendar') + ->with($principalUri, $calendarUri, [ + '{DAV:}displayname' => $displayName, + ]); + + $calendarHomeObject->expects($this->exactly($deleted ? 2 : 1)) + ->method('getCalDAVBackend') + ->with() + ->willReturn($calendarBackend); + } + + if (!$isResource) { + $calendarHomeObject->expects($this->once()) + ->method('getChildren') + ->with() + ->willReturn($existingCalendars); + } + } + + /** @var Tree&MockObject $tree */ + $tree = $this->createMock(Tree::class); + $tree->expects($this->once()) + ->method('getNodeForPath') + ->with($calendarHome) + ->willReturn($calendarHomeObject); + $this->server->tree = $tree; + + $properties = $propertiesForPath ? [ + ['href' => '/remote.php/dav/' . $calendarHome . '/' . $calendarUri] + ] : []; + + $this->server->expects($this->once()) + ->method('getPropertiesForPath') + ->with($calendarHome . '/' . $calendarUri, [], 1) + ->willReturn($properties); + + $this->plugin->propFindDefaultCalendarUrl($propFind, $node); + + if (!$propertiesForPath) { + $this->assertNull($propFind->get(Plugin::SCHEDULE_DEFAULT_CALENDAR_URL)); + return; + } + + /** @var LocalHref $result */ + $result = $propFind->get(Plugin::SCHEDULE_DEFAULT_CALENDAR_URL); + $this->assertEquals('/remote.php/dav/' . $calendarHome . '/' . $calendarUri, $result->getHref()); + } + + /** + * Test Calendar Event Creation for Personal Calendar + * + * Should generate 2 messages for attendees User 2 and User External + */ + public function testCalendarObjectChangePersonalCalendarCreate(): void { + + // define place holders + /** @var Message[] $iTipMessages */ + $iTipMessages = []; + // construct calendar node + $calendarNode = new Calendar( + $this->createMock(BackendInterface::class), + [ + 'uri' => 'personal', + 'principaluri' => 'principals/users/user1', + '{DAV:}displayname' => 'Calendar Shared By User1', + ], + $this->createMock(IL10N::class), + $this->config, + $this->logger + ); + // construct server request object + $request = new Request( + 'PUT', + '/remote.php/dav/calendars/user1/personal/B0DC78AE-6DD7-47E3-80BE-89F23E6D5383.ics' + ); + $request->setBaseUrl('/remote.php/dav/'); + // construct server response object + $response = new Response(); + // construct server tree object + $tree = $this->createMock(Tree::class); + $tree->expects($this->once()) + ->method('getNodeForPath') + ->with('calendars/user1/personal') + ->willReturn($calendarNode); + // construct server properties and returns + $this->server->httpRequest = $request; + $this->server->tree = $tree; + $this->server->expects($this->exactly(1))->method('getProperties') + ->willReturnMap([ + [ + 'principals/users/user1', + ['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'], + ['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set' => new LocalHref( + ['mailto:user1@testing.local','/remote.php/dav/principals/users/user1/'] + )] + ] + ]); + $this->server->expects($this->exactly(2))->method('emit')->willReturnCallback( + function (string $eventName, array $arguments = [], ?callable $continueCallBack = null) use (&$iTipMessages) { + $this->assertEquals('schedule', $eventName); + $this->assertCount(1, $arguments); + $iTipMessages[] = $arguments[0]; + return true; + } + ); + // construct calendar with a 1 hour event and same start/end time zones + $vCalendar = new VCalendar(); + $vEvent = $vCalendar->add('VEVENT', []); + $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc'); + $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']); + $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']); + $vEvent->add('SUMMARY', 'Test Recurring Event'); + $vEvent->add('ORGANIZER', 'mailto:user1@testing.local', ['CN' => 'User One']); + $vEvent->add('ATTENDEE', 'mailto:user2@testing.local', [ + 'CN' => 'User Two', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); + $vEvent->add('ATTENDEE', 'mailto:user@external.local', [ + 'CN' => 'User External', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); + // define flags + $newFlag = true; + $modifiedFlag = false; + // execute method + $this->plugin->calendarObjectChange( + $request, + $response, + $vCalendar, + 'calendars/user1/personal', + $modifiedFlag, + $newFlag + ); + // test for correct iTip message count + $this->assertCount(2, $iTipMessages); + // test for Sharer Attendee + $this->assertEquals('mailto:user1@testing.local', $iTipMessages[0]->sender); + $this->assertEquals('mailto:user2@testing.local', $iTipMessages[0]->recipient); + $this->assertTrue($iTipMessages[0]->significantChange); + // test for External Attendee + $this->assertEquals('mailto:user1@testing.local', $iTipMessages[1]->sender); + $this->assertEquals('mailto:user@external.local', $iTipMessages[1]->recipient); + $this->assertTrue($iTipMessages[1]->significantChange); + + } + + /** + * Test Calendar Event Creation for Shared Calendar as Sharer/Owner + * + * Should generate 3 messages for attendees User 2 (Sharee), User 3 (Non-Sharee) and User External + */ + public function testCalendarObjectChangeSharedCalendarSharerCreate(): void { + + // define place holders + /** @var Message[] $iTipMessages */ + $iTipMessages = []; + // construct calendar node + $calendarNode = new Calendar( + $this->createMock(BackendInterface::class), + [ + 'uri' => 'calendar_shared_by_user1', + 'principaluri' => 'principals/users/user1', + '{DAV:}displayname' => 'Calendar Shared By User1', + '{http://owncloud.org/ns}owner-principal' => 'principals/users/user1' + ], + $this->createMock(IL10N::class), + $this->config, + $this->logger + ); + // construct server request object + $request = new Request( + 'PUT', + '/remote.php/dav/calendars/user1/calendar_shared_by_user1/B0DC78AE-6DD7-47E3-80BE-89F23E6D5383.ics' + ); + $request->setBaseUrl('/remote.php/dav/'); + // construct server response object + $response = new Response(); + // construct server tree object + $tree = $this->createMock(Tree::class); + $tree->expects($this->once()) + ->method('getNodeForPath') + ->with('calendars/user1/calendar_shared_by_user1') + ->willReturn($calendarNode); + // construct server properties and returns + $this->server->httpRequest = $request; + $this->server->tree = $tree; + $this->server->expects($this->exactly(1))->method('getProperties') + ->willReturnMap([ + [ + 'principals/users/user1', + ['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'], + ['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set' => new LocalHref( + ['mailto:user1@testing.local','/remote.php/dav/principals/users/user1/'] + )] + ] + ]); + $this->server->expects($this->exactly(3))->method('emit')->willReturnCallback( + function (string $eventName, array $arguments = [], ?callable $continueCallBack = null) use (&$iTipMessages) { + $this->assertEquals('schedule', $eventName); + $this->assertCount(1, $arguments); + $iTipMessages[] = $arguments[0]; + return true; + } + ); + // construct calendar with a 1 hour event and same start/end time zones + $vCalendar = new VCalendar(); + $vEvent = $vCalendar->add('VEVENT', []); + $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc'); + $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']); + $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']); + $vEvent->add('SUMMARY', 'Test Recurring Event'); + $vEvent->add('ORGANIZER', 'mailto:user1@testing.local', ['CN' => 'User One']); + $vEvent->add('ATTENDEE', 'mailto:user2@testing.local', [ + 'CN' => 'User Two', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); + $vEvent->add('ATTENDEE', 'mailto:user3@testing.local', [ + 'CN' => 'User Three', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); + $vEvent->add('ATTENDEE', 'mailto:user@external.local', [ + 'CN' => 'User External', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); + // define flags + $newFlag = true; + $modifiedFlag = false; + // execute method + $this->plugin->calendarObjectChange( + $request, + $response, + $vCalendar, + 'calendars/user1/calendar_shared_by_user1', + $modifiedFlag, + $newFlag + ); + // test for correct iTip message count + $this->assertCount(3, $iTipMessages); + // test for Sharer Attendee + $this->assertEquals('mailto:user1@testing.local', $iTipMessages[0]->sender); + $this->assertEquals('mailto:user2@testing.local', $iTipMessages[0]->recipient); + $this->assertTrue($iTipMessages[0]->significantChange); + // test for Non Shee Attendee + $this->assertEquals('mailto:user1@testing.local', $iTipMessages[1]->sender); + $this->assertEquals('mailto:user3@testing.local', $iTipMessages[1]->recipient); + $this->assertTrue($iTipMessages[1]->significantChange); + // test for External Attendee + $this->assertEquals('mailto:user1@testing.local', $iTipMessages[2]->sender); + $this->assertEquals('mailto:user@external.local', $iTipMessages[2]->recipient); + $this->assertTrue($iTipMessages[2]->significantChange); + + } + + /** + * Test Calendar Event Creation for Shared Calendar as Shree + * + * Should generate 3 messages for attendees User 1 (Sharer/Owner), User 3 (Non-Sharee) and User External + */ + public function testCalendarObjectChangeSharedCalendarShreeCreate(): void { + + // define place holders + /** @var Message[] $iTipMessages */ + $iTipMessages = []; + // construct calendar node + $calendarNode = new Calendar( + $this->createMock(BackendInterface::class), + [ + 'uri' => 'calendar_shared_by_user1', + 'principaluri' => 'principals/users/user2', + '{DAV:}displayname' => 'Calendar Shared By User1', + '{http://owncloud.org/ns}owner-principal' => 'principals/users/user1' + ], + $this->createMock(IL10N::class), + $this->config, + $this->logger + ); + // construct server request object + $request = new Request( + 'PUT', + '/remote.php/dav/calendars/user2/calendar_shared_by_user1/B0DC78AE-6DD7-47E3-80BE-89F23E6D5383.ics' + ); + $request->setBaseUrl('/remote.php/dav/'); + // construct server response object + $response = new Response(); + // construct server tree object + $tree = $this->createMock(Tree::class); + $tree->expects($this->once()) + ->method('getNodeForPath') + ->with('calendars/user2/calendar_shared_by_user1') + ->willReturn($calendarNode); + // construct server properties and returns + $this->server->httpRequest = $request; + $this->server->tree = $tree; + $this->server->expects($this->exactly(2))->method('getProperties') + ->willReturnMap([ + [ + 'principals/users/user1', + ['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'], + ['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set' => new LocalHref( + ['mailto:user1@testing.local','/remote.php/dav/principals/users/user1/'] + )] + ], + [ + 'principals/users/user2', + ['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'], + ['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set' => new LocalHref( + ['mailto:user2@testing.local','/remote.php/dav/principals/users/user2/'] + )] + ] + ]); + $this->server->expects($this->exactly(3))->method('emit')->willReturnCallback( + function (string $eventName, array $arguments = [], ?callable $continueCallBack = null) use (&$iTipMessages) { + $this->assertEquals('schedule', $eventName); + $this->assertCount(1, $arguments); + $iTipMessages[] = $arguments[0]; + return true; + } + ); + // construct calendar with a 1 hour event and same start/end time zones + $vCalendar = new VCalendar(); + $vEvent = $vCalendar->add('VEVENT', []); + $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc'); + $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']); + $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']); + $vEvent->add('SUMMARY', 'Test Recurring Event'); + $vEvent->add('ORGANIZER', 'mailto:user2@testing.local', ['CN' => 'User Two']); + $vEvent->add('ATTENDEE', 'mailto:user1@testing.local', [ + 'CN' => 'User One', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); + $vEvent->add('ATTENDEE', 'mailto:user3@testing.local', [ + 'CN' => 'User Three', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); + $vEvent->add('ATTENDEE', 'mailto:user@external.local', [ + 'CN' => 'User External', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); + // define flags + $newFlag = true; + $modifiedFlag = false; + // execute method + $this->plugin->calendarObjectChange( + $request, + $response, + $vCalendar, + 'calendars/user2/calendar_shared_by_user1', + $modifiedFlag, + $newFlag + ); + // test for correct iTip message count + $this->assertCount(3, $iTipMessages); + // test for Sharer Attendee + $this->assertEquals('mailto:user2@testing.local', $iTipMessages[0]->sender); + $this->assertEquals('mailto:user1@testing.local', $iTipMessages[0]->recipient); + $this->assertTrue($iTipMessages[0]->significantChange); + // test for Non Shee Attendee + $this->assertEquals('mailto:user2@testing.local', $iTipMessages[1]->sender); + $this->assertEquals('mailto:user3@testing.local', $iTipMessages[1]->recipient); + $this->assertTrue($iTipMessages[1]->significantChange); + // test for External Attendee + $this->assertEquals('mailto:user2@testing.local', $iTipMessages[2]->sender); + $this->assertEquals('mailto:user@external.local', $iTipMessages[2]->recipient); + $this->assertTrue($iTipMessages[2]->significantChange); + + } + + /** + * Test Calendar Event Creation with iTip and iMip disabled + * + * Should generate 2 messages for attendees User 2 and User External + */ + public function testCalendarObjectChangeWithSchedulingDisabled(): void { + // construct server request + $request = new Request( + 'PUT', + '/remote.php/dav/calendars/user1/personal/B0DC78AE-6DD7-47E3-80BE-89F23E6D5383.ics', + ['x-nc-scheduling' => 'false'] + ); + $request->setBaseUrl('/remote.php/dav/'); + // construct server response + $response = new Response(); + // construct server tree + $tree = $this->createMock(Tree::class); + $tree->expects($this->never()) + ->method('getNodeForPath'); + // construct server properties and returns + $this->server->httpRequest = $request; + $this->server->tree = $tree; + // construct empty calendar event + $vCalendar = new VCalendar(); + $vEvent = $vCalendar->add('VEVENT', []); + // define flags + $newFlag = true; + $modifiedFlag = false; + // execute method + $this->plugin->calendarObjectChange( + $request, + $response, + $vCalendar, + 'calendars/user1/personal', + $modifiedFlag, + $newFlag + ); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Search/Request/CalendarSearchReportTest.php b/apps/dav/tests/unit/CalDAV/Search/Request/CalendarSearchReportTest.php new file mode 100644 index 00000000000..02ae504bce0 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Search/Request/CalendarSearchReportTest.php @@ -0,0 +1,324 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\Search\Xml\Request; + +use OCA\DAV\CalDAV\Search\Xml\Request\CalendarSearchReport; +use Sabre\Xml\Reader; +use Test\TestCase; + +class CalendarSearchReportTest extends TestCase { + private array $elementMap = [ + '{http://nextcloud.com/ns}calendar-search' + => 'OCA\\DAV\\CalDAV\\Search\\Xml\\Request\\CalendarSearchReport', + ]; + + public function testFoo(): void { + $xml = <<<XML +<?xml version="1.0" encoding="UTF-8"?> +<nc:calendar-search xmlns:nc="http://nextcloud.com/ns" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:"> + <d:prop> + <d:getetag /> + <c:calendar-data /> + </d:prop> + <nc:filter> + <nc:comp-filter name="VEVENT" /> + <nc:comp-filter name="VTODO" /> + <nc:prop-filter name="SUMMARY" /> + <nc:prop-filter name="LOCATION" /> + <nc:prop-filter name="ATTENDEE" /> + <nc:param-filter property="ATTENDEE" name="CN" /> + <nc:search-term>foo</nc:search-term> + </nc:filter> + <nc:limit>10</nc:limit> + <nc:offset>5</nc:offset> +</nc:calendar-search> +XML; + + $result = $this->parse($xml); + + $calendarSearchReport = new CalendarSearchReport(); + $calendarSearchReport->properties = [ + '{DAV:}getetag', + '{urn:ietf:params:xml:ns:caldav}calendar-data', + ]; + $calendarSearchReport->filters = [ + 'comps' => [ + 'VEVENT', + 'VTODO' + ], + 'props' => [ + 'SUMMARY', + 'LOCATION', + 'ATTENDEE' + ], + 'params' => [ + [ + 'property' => 'ATTENDEE', + 'parameter' => 'CN' + ] + ], + 'search-term' => 'foo' + ]; + $calendarSearchReport->limit = 10; + $calendarSearchReport->offset = 5; + + $this->assertEquals( + $calendarSearchReport, + $result['value'] + ); + } + + public function testNoLimitOffset(): void { + $xml = <<<XML +<?xml version="1.0" encoding="UTF-8"?> +<nc:calendar-search xmlns:nc="http://nextcloud.com/ns" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:"> + <d:prop> + <d:getetag /> + <c:calendar-data /> + </d:prop> + <nc:filter> + <nc:comp-filter name="VEVENT" /> + <nc:prop-filter name="SUMMARY" /> + <nc:search-term>foo</nc:search-term> + </nc:filter> +</nc:calendar-search> +XML; + + $result = $this->parse($xml); + + $calendarSearchReport = new CalendarSearchReport(); + $calendarSearchReport->properties = [ + '{DAV:}getetag', + '{urn:ietf:params:xml:ns:caldav}calendar-data', + ]; + $calendarSearchReport->filters = [ + 'comps' => [ + 'VEVENT', + ], + 'props' => [ + 'SUMMARY', + ], + 'search-term' => 'foo' + ]; + $calendarSearchReport->limit = null; + $calendarSearchReport->offset = null; + + $this->assertEquals( + $calendarSearchReport, + $result['value'] + ); + } + + + public function testRequiresCompFilter(): void { + $this->expectException(\Sabre\DAV\Exception\BadRequest::class); + $this->expectExceptionMessage('{http://nextcloud.com/ns}prop-filter or {http://nextcloud.com/ns}param-filter given without any {http://nextcloud.com/ns}comp-filter'); + + $xml = <<<XML +<?xml version="1.0" encoding="UTF-8"?> +<nc:calendar-search xmlns:nc="http://nextcloud.com/ns" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:"> + <d:prop> + <d:getetag /> + <c:calendar-data /> + </d:prop> + <nc:filter> + <nc:prop-filter name="SUMMARY" /> + <nc:prop-filter name="LOCATION" /> + <nc:prop-filter name="ATTENDEE" /> + <nc:param-filter property="ATTENDEE" name="CN" /> + <nc:search-term>foo</nc:search-term> + </nc:filter> + <nc:limit>10</nc:limit> + <nc:offset>5</nc:offset> +</nc:calendar-search> +XML; + + $this->parse($xml); + } + + + public function testRequiresFilter(): void { + $this->expectException(\Sabre\DAV\Exception\BadRequest::class); + $this->expectExceptionMessage('The {http://nextcloud.com/ns}filter element is required for this request'); + + $xml = <<<XML +<?xml version="1.0" encoding="UTF-8"?> +<nc:calendar-search xmlns:nc="http://nextcloud.com/ns" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:"> + <d:prop> + <d:getetag /> + <c:calendar-data /> + </d:prop> +</nc:calendar-search> +XML; + + $this->parse($xml); + } + + + public function testNoSearchTerm(): void { + $this->expectException(\Sabre\DAV\Exception\BadRequest::class); + $this->expectExceptionMessage('{http://nextcloud.com/ns}search-term is required for this request'); + + $xml = <<<XML +<?xml version="1.0" encoding="UTF-8"?> +<nc:calendar-search xmlns:nc="http://nextcloud.com/ns" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:"> + <d:prop> + <d:getetag /> + <c:calendar-data /> + </d:prop> + <nc:filter> + <nc:comp-filter name="VEVENT" /> + <nc:comp-filter name="VTODO" /> + <nc:prop-filter name="SUMMARY" /> + <nc:prop-filter name="LOCATION" /> + <nc:prop-filter name="ATTENDEE" /> + <nc:param-filter property="ATTENDEE" name="CN" /> + </nc:filter> + <nc:limit>10</nc:limit> + <nc:offset>5</nc:offset> +</nc:calendar-search> +XML; + + $this->parse($xml); + } + + + public function testCompOnly(): void { + $this->expectException(\Sabre\DAV\Exception\BadRequest::class); + $this->expectExceptionMessage('At least one{http://nextcloud.com/ns}prop-filter or {http://nextcloud.com/ns}param-filter is required for this request'); + + $xml = <<<XML +<?xml version="1.0" encoding="UTF-8"?> +<nc:calendar-search xmlns:nc="http://nextcloud.com/ns" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:"> + <d:prop> + <d:getetag /> + <c:calendar-data /> + </d:prop> + <nc:filter> + <nc:comp-filter name="VEVENT" /> + <nc:comp-filter name="VTODO" /> + <nc:search-term>foo</nc:search-term> + </nc:filter> +</nc:calendar-search> +XML; + + $result = $this->parse($xml); + + $calendarSearchReport = new CalendarSearchReport(); + $calendarSearchReport->properties = [ + '{DAV:}getetag', + '{urn:ietf:params:xml:ns:caldav}calendar-data', + ]; + $calendarSearchReport->filters = [ + 'comps' => [ + 'VEVENT', + 'VTODO' + ], + 'search-term' => 'foo' + ]; + $calendarSearchReport->limit = null; + $calendarSearchReport->offset = null; + + $this->assertEquals( + $calendarSearchReport, + $result['value'] + ); + } + + public function testPropOnly(): void { + $xml = <<<XML +<?xml version="1.0" encoding="UTF-8"?> +<nc:calendar-search xmlns:nc="http://nextcloud.com/ns" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:"> + <d:prop> + <d:getetag /> + <c:calendar-data /> + </d:prop> + <nc:filter> + <nc:comp-filter name="VEVENT" /> + <nc:prop-filter name="SUMMARY" /> + <nc:search-term>foo</nc:search-term> + </nc:filter> +</nc:calendar-search> +XML; + + $result = $this->parse($xml); + + $calendarSearchReport = new CalendarSearchReport(); + $calendarSearchReport->properties = [ + '{DAV:}getetag', + '{urn:ietf:params:xml:ns:caldav}calendar-data', + ]; + $calendarSearchReport->filters = [ + 'comps' => [ + 'VEVENT', + ], + 'props' => [ + 'SUMMARY', + ], + 'search-term' => 'foo' + ]; + $calendarSearchReport->limit = null; + $calendarSearchReport->offset = null; + + $this->assertEquals( + $calendarSearchReport, + $result['value'] + ); + } + + public function testParamOnly(): void { + $xml = <<<XML +<?xml version="1.0" encoding="UTF-8"?> +<nc:calendar-search xmlns:nc="http://nextcloud.com/ns" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:d="DAV:"> + <d:prop> + <d:getetag /> + <c:calendar-data /> + </d:prop> + <nc:filter> + <nc:comp-filter name="VEVENT" /> + <nc:param-filter property="ATTENDEE" name="CN" /> + <nc:search-term>foo</nc:search-term> + </nc:filter> +</nc:calendar-search> +XML; + + $result = $this->parse($xml); + + $calendarSearchReport = new CalendarSearchReport(); + $calendarSearchReport->properties = [ + '{DAV:}getetag', + '{urn:ietf:params:xml:ns:caldav}calendar-data', + ]; + $calendarSearchReport->filters = [ + 'comps' => [ + 'VEVENT', + ], + 'params' => [ + [ + 'property' => 'ATTENDEE', + 'parameter' => 'CN' + ] + ], + 'search-term' => 'foo' + ]; + $calendarSearchReport->limit = null; + $calendarSearchReport->offset = null; + + $this->assertEquals( + $calendarSearchReport, + $result['value'] + ); + } + + private function parse(string $xml, array $elementMap = []): array { + $reader = new Reader(); + $reader->elementMap = array_merge($this->elementMap, $elementMap); + $reader->xml($xml); + return $reader->parse(); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Search/SearchPluginTest.php b/apps/dav/tests/unit/CalDAV/Search/SearchPluginTest.php new file mode 100644 index 00000000000..e576fbae34c --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Search/SearchPluginTest.php @@ -0,0 +1,117 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\Search; + +use OCA\DAV\CalDAV\CalendarHome; +use OCA\DAV\CalDAV\Search\SearchPlugin; +use OCA\DAV\CalDAV\Search\Xml\Request\CalendarSearchReport; +use Sabre\Xml\Service; +use Test\TestCase; + +class SearchPluginTest extends TestCase { + protected $server; + + /** @var SearchPlugin $plugin */ + protected $plugin; + + protected function setUp(): void { + parent::setUp(); + + $this->server = $this->createMock(\Sabre\DAV\Server::class); + $this->server->tree = $this->createMock(\Sabre\DAV\Tree::class); + $this->server->httpResponse = $this->createMock(\Sabre\HTTP\Response::class); + $this->server->xml = new Service(); + + $this->plugin = new SearchPlugin(); + $this->plugin->initialize($this->server); + } + + public function testGetFeatures(): void { + $this->assertEquals(['nc-calendar-search'], $this->plugin->getFeatures()); + } + + public function testGetName(): void { + $this->assertEquals('nc-calendar-search', $this->plugin->getPluginName()); + } + + public function testInitialize(): void { + $server = $this->createMock(\Sabre\DAV\Server::class); + + $plugin = new SearchPlugin(); + + $server->expects($this->once()) + ->method('on') + ->with('report', [$plugin, 'report']); + $server->xml = new Service(); + + $plugin->initialize($server); + + $this->assertEquals( + $server->xml->elementMap['{http://nextcloud.com/ns}calendar-search'], + 'OCA\\DAV\\CalDAV\\Search\\Xml\\Request\\CalendarSearchReport' + ); + } + + public function testReportUnknown(): void { + $result = $this->plugin->report('{urn:ietf:params:xml:ns:caldav}calendar-query', 'REPORT', null); + $this->assertEquals($result, null); + $this->assertNotEquals($this->server->transactionType, 'report-nc-calendar-search'); + } + + public function testReport(): void { + $report = $this->createMock(CalendarSearchReport::class); + $report->filters = []; + $calendarHome = $this->createMock(CalendarHome::class); + $this->server->expects($this->once()) + ->method('getRequestUri') + ->with() + ->willReturn('/re/quest/u/r/i'); + $this->server->tree->expects($this->once()) + ->method('getNodeForPath') + ->with('/re/quest/u/r/i') + ->willReturn($calendarHome); + $this->server->expects($this->once()) + ->method('getHTTPDepth') + ->with(2) + ->willReturn(2); + $this->server + ->method('getHTTPPrefer') + ->willReturn([ + 'return' => null + ]); + $calendarHome->expects($this->once()) + ->method('calendarSearch') + ->willReturn([]); + + $this->plugin->report('{http://nextcloud.com/ns}calendar-search', $report, ''); + } + + public function testSupportedReportSetNoCalendarHome(): void { + $this->server->tree->expects($this->once()) + ->method('getNodeForPath') + ->with('/foo/bar') + ->willReturn(null); + + $reports = $this->plugin->getSupportedReportSet('/foo/bar'); + $this->assertEquals([], $reports); + } + + public function testSupportedReportSet(): void { + $calendarHome = $this->createMock(CalendarHome::class); + + $this->server->tree->expects($this->once()) + ->method('getNodeForPath') + ->with('/bar/foo') + ->willReturn($calendarHome); + + $reports = $this->plugin->getSupportedReportSet('/bar/foo'); + $this->assertEquals([ + '{http://nextcloud.com/ns}calendar-search' + ], $reports); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Security/RateLimitingPluginTest.php b/apps/dav/tests/unit/CalDAV/Security/RateLimitingPluginTest.php new file mode 100644 index 00000000000..a5cf6a23c66 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Security/RateLimitingPluginTest.php @@ -0,0 +1,188 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Tests\unit\CalDAV\Security; + +use OC\Security\RateLimiting\Exception\RateLimitExceededException; +use OC\Security\RateLimiting\Limiter; +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CalDAV\Security\RateLimitingPlugin; +use OCA\DAV\Connector\Sabre\Exception\TooManyRequests; +use OCP\IAppConfig; +use OCP\IUser; +use OCP\IUserManager; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Sabre\DAV\Exception\Forbidden; +use Test\TestCase; + +class RateLimitingPluginTest extends TestCase { + + private Limiter&MockObject $limiter; + private CalDavBackend&MockObject $caldavBackend; + private IUserManager&MockObject $userManager; + private LoggerInterface&MockObject $logger; + private IAppConfig&MockObject $config; + private string $userId = 'user123'; + private RateLimitingPlugin $plugin; + + protected function setUp(): void { + parent::setUp(); + + $this->limiter = $this->createMock(Limiter::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->caldavBackend = $this->createMock(CalDavBackend::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->config = $this->createMock(IAppConfig::class); + $this->plugin = new RateLimitingPlugin( + $this->limiter, + $this->userManager, + $this->caldavBackend, + $this->logger, + $this->config, + $this->userId, + ); + } + + public function testNoUserObject(): void { + $this->limiter->expects(self::never()) + ->method('registerUserRequest'); + + $this->plugin->beforeBind('calendars/foo/cal'); + } + + public function testUnrelated(): void { + $user = $this->createMock(IUser::class); + $this->userManager->expects(self::once()) + ->method('get') + ->with($this->userId) + ->willReturn($user); + $this->limiter->expects(self::never()) + ->method('registerUserRequest'); + + $this->plugin->beforeBind('foo/bar'); + } + + public function testRegisterCalendarCreation(): void { + $user = $this->createMock(IUser::class); + $this->userManager->expects(self::once()) + ->method('get') + ->with($this->userId) + ->willReturn($user); + $this->config + ->method('getValueInt') + ->with('dav') + ->willReturnArgument(2); + $this->limiter->expects(self::once()) + ->method('registerUserRequest') + ->with( + 'caldav-create-calendar', + 10, + 3600, + $user, + ); + + $this->plugin->beforeBind('calendars/foo/cal'); + } + + public function testCalendarCreationRateLimitExceeded(): void { + $user = $this->createMock(IUser::class); + $this->userManager->expects(self::once()) + ->method('get') + ->with($this->userId) + ->willReturn($user); + $this->config + ->method('getValueInt') + ->with('dav') + ->willReturnArgument(2); + $this->limiter->expects(self::once()) + ->method('registerUserRequest') + ->with( + 'caldav-create-calendar', + 10, + 3600, + $user, + ) + ->willThrowException(new RateLimitExceededException()); + $this->expectException(TooManyRequests::class); + + $this->plugin->beforeBind('calendars/foo/cal'); + } + + public function testCalendarLimitReached(): void { + $user = $this->createMock(IUser::class); + $this->userManager->expects(self::once()) + ->method('get') + ->with($this->userId) + ->willReturn($user); + $user->method('getUID')->willReturn('user123'); + $this->config + ->method('getValueInt') + ->with('dav') + ->willReturnArgument(2); + $this->limiter->expects(self::once()) + ->method('registerUserRequest') + ->with( + 'caldav-create-calendar', + 10, + 3600, + $user, + ); + $this->caldavBackend->expects(self::once()) + ->method('getCalendarsForUserCount') + ->with('principals/users/user123') + ->willReturn(27); + $this->caldavBackend->expects(self::once()) + ->method('getSubscriptionsForUserCount') + ->with('principals/users/user123') + ->willReturn(3); + $this->expectException(Forbidden::class); + + $this->plugin->beforeBind('calendars/foo/cal'); + } + + public function testNoCalendarsSubscriptsLimit(): void { + $user = $this->createMock(IUser::class); + $this->userManager->expects(self::once()) + ->method('get') + ->with($this->userId) + ->willReturn($user); + $user->method('getUID')->willReturn('user123'); + $this->config + ->method('getValueInt') + ->with('dav') + ->willReturnCallback(function ($app, $key, $default) { + switch ($key) { + case 'maximumCalendarsSubscriptions': + return -1; + default: + return $default; + } + }); + $this->limiter->expects(self::once()) + ->method('registerUserRequest') + ->with( + 'caldav-create-calendar', + 10, + 3600, + $user, + ); + $this->caldavBackend->expects(self::never()) + ->method('getCalendarsForUserCount') + ->with('principals/users/user123') + ->willReturn(27); + $this->caldavBackend->expects(self::never()) + ->method('getSubscriptionsForUserCount') + ->with('principals/users/user123') + ->willReturn(3); + + $this->plugin->beforeBind('calendars/foo/cal'); + } + +} diff --git a/apps/dav/tests/unit/CalDAV/Status/StatusServiceTest.php b/apps/dav/tests/unit/CalDAV/Status/StatusServiceTest.php new file mode 100644 index 00000000000..ee0ef2334ec --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Status/StatusServiceTest.php @@ -0,0 +1,445 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\Status; + +use OC\Calendar\CalendarQuery; +use OCA\DAV\CalDAV\CalendarImpl; +use OCA\DAV\CalDAV\Status\StatusService; +use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\Service\StatusService as UserStatusService; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Calendar\IManager; +use OCP\ICache; +use OCP\ICacheFactory; +use OCP\IUser; +use OCP\IUserManager; +use OCP\User\IAvailabilityCoordinator; +use OCP\User\IOutOfOfficeData; +use OCP\UserStatus\IUserStatus; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class StatusServiceTest extends TestCase { + private ITimeFactory&MockObject $timeFactory; + private IManager&MockObject $calendarManager; + private IUserManager&MockObject $userManager; + private UserStatusService&MockObject $userStatusService; + private IAvailabilityCoordinator&MockObject $availabilityCoordinator; + private ICacheFactory&MockObject $cacheFactory; + private LoggerInterface&MockObject $logger; + private ICache&MockObject $cache; + private StatusService $service; + + protected function setUp(): void { + parent::setUp(); + + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->calendarManager = $this->createMock(IManager::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->userStatusService = $this->createMock(UserStatusService::class); + $this->availabilityCoordinator = $this->createMock(IAvailabilityCoordinator::class); + $this->cacheFactory = $this->createMock(ICacheFactory::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->cache = $this->createMock(ICache::class); + $this->cacheFactory->expects(self::once()) + ->method('createLocal') + ->with('CalendarStatusService') + ->willReturn($this->cache); + + $this->service = new StatusService($this->timeFactory, + $this->calendarManager, + $this->userManager, + $this->userStatusService, + $this->availabilityCoordinator, + $this->cacheFactory, + $this->logger, + ); + } + + public function testNoUser(): void { + $this->userManager->expects(self::once()) + ->method('get') + ->willReturn(null); + $this->availabilityCoordinator->expects(self::never()) + ->method('getCurrentOutOfOfficeData'); + $this->availabilityCoordinator->expects(self::never()) + ->method('isInEffect'); + $this->logger->expects(self::never()) + ->method('debug'); + $this->cache->expects(self::never()) + ->method('get'); + $this->cache->expects(self::never()) + ->method('set'); + $this->calendarManager->expects(self::never()) + ->method('getCalendarsForPrincipal'); + $this->calendarManager->expects(self::never()) + ->method('newQuery'); + $this->timeFactory->expects(self::never()) + ->method('getDateTime'); + $this->calendarManager->expects(self::never()) + ->method('searchForPrincipal'); + $this->userStatusService->expects(self::never()) + ->method('revertUserStatus'); + $this->userStatusService->expects(self::never()) + ->method('setUserStatus'); + $this->userStatusService->expects(self::never()) + ->method('findByUserId'); + + $this->service->processCalendarStatus('admin'); + } + + public function testOOOInEffect(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + ]); + + $this->userManager->expects(self::once()) + ->method('get') + ->willReturn($user); + $this->availabilityCoordinator->expects(self::once()) + ->method('getCurrentOutOfOfficeData') + ->willReturn($this->createMock(IOutOfOfficeData::class)); + $this->availabilityCoordinator->expects(self::once()) + ->method('isInEffect') + ->willReturn(true); + $this->logger->expects(self::once()) + ->method('debug'); + $this->cache->expects(self::never()) + ->method('get'); + $this->cache->expects(self::never()) + ->method('set'); + $this->calendarManager->expects(self::never()) + ->method('getCalendarsForPrincipal'); + $this->calendarManager->expects(self::never()) + ->method('newQuery'); + $this->timeFactory->expects(self::never()) + ->method('getDateTime'); + $this->calendarManager->expects(self::never()) + ->method('searchForPrincipal'); + $this->userStatusService->expects(self::never()) + ->method('revertUserStatus'); + $this->userStatusService->expects(self::never()) + ->method('setUserStatus'); + $this->userStatusService->expects(self::never()) + ->method('findByUserId'); + + $this->service->processCalendarStatus('admin'); + } + + public function testNoCalendars(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + ]); + + $this->userManager->expects(self::once()) + ->method('get') + ->willReturn($user); + $this->availabilityCoordinator->expects(self::once()) + ->method('getCurrentOutOfOfficeData') + ->willReturn(null); + $this->availabilityCoordinator->expects(self::never()) + ->method('isInEffect'); + $this->cache->expects(self::once()) + ->method('get') + ->willReturn(null); + $this->cache->expects(self::once()) + ->method('set'); + $this->calendarManager->expects(self::once()) + ->method('getCalendarsForPrincipal') + ->willReturn([]); + $this->calendarManager->expects(self::never()) + ->method('newQuery'); + $this->timeFactory->expects(self::never()) + ->method('getDateTime'); + $this->calendarManager->expects(self::never()) + ->method('searchForPrincipal'); + $this->userStatusService->expects(self::once()) + ->method('revertUserStatus'); + $this->logger->expects(self::once()) + ->method('debug'); + $this->userStatusService->expects(self::never()) + ->method('setUserStatus'); + $this->userStatusService->expects(self::never()) + ->method('findByUserId'); + + $this->service->processCalendarStatus('admin'); + } + + public function testNoCalendarEvents(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + ]); + + $this->userManager->expects(self::once()) + ->method('get') + ->willReturn($user); + $this->availabilityCoordinator->expects(self::once()) + ->method('getCurrentOutOfOfficeData') + ->willReturn(null); + $this->availabilityCoordinator->expects(self::never()) + ->method('isInEffect'); + $this->cache->expects(self::once()) + ->method('get') + ->willReturn(null); + $this->cache->expects(self::once()) + ->method('set'); + $this->calendarManager->expects(self::once()) + ->method('getCalendarsForPrincipal') + ->willReturn([$this->createMock(CalendarImpl::class)]); + $this->calendarManager->expects(self::once()) + ->method('newQuery') + ->willReturn(new CalendarQuery('admin')); + $this->timeFactory->expects(self::exactly(2)) + ->method('getDateTime') + ->willReturn(new \DateTime()); + $this->calendarManager->expects(self::once()) + ->method('searchForPrincipal') + ->willReturn([]); + $this->userStatusService->expects(self::once()) + ->method('revertUserStatus'); + $this->logger->expects(self::once()) + ->method('debug'); + $this->userStatusService->expects(self::never()) + ->method('setUserStatus'); + $this->userStatusService->expects(self::never()) + ->method('findByUserId'); + + $this->service->processCalendarStatus('admin'); + } + + public function testCalendarNoEventObjects(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + ]); + + $this->userManager->expects(self::once()) + ->method('get') + ->willReturn($user); + $this->availabilityCoordinator->expects(self::once()) + ->method('getCurrentOutOfOfficeData') + ->willReturn(null); + $this->availabilityCoordinator->expects(self::never()) + ->method('isInEffect'); + $this->cache->expects(self::once()) + ->method('get') + ->willReturn(null); + $this->cache->expects(self::once()) + ->method('set'); + $this->calendarManager->expects(self::once()) + ->method('getCalendarsForPrincipal') + ->willReturn([$this->createMock(CalendarImpl::class)]); + $this->calendarManager->expects(self::once()) + ->method('newQuery') + ->willReturn(new CalendarQuery('admin')); + $this->timeFactory->expects(self::exactly(2)) + ->method('getDateTime') + ->willReturn(new \DateTime()); + $this->userStatusService->expects(self::once()) + ->method('findByUserId') + ->willThrowException(new DoesNotExistException('')); + $this->calendarManager->expects(self::once()) + ->method('searchForPrincipal') + ->willReturn([['objects' => []]]); + $this->userStatusService->expects(self::once()) + ->method('revertUserStatus'); + $this->logger->expects(self::once()) + ->method('debug'); + $this->userStatusService->expects(self::never()) + ->method('setUserStatus'); + + + $this->service->processCalendarStatus('admin'); + } + + public function testCalendarEvent(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + ]); + + $this->userManager->expects(self::once()) + ->method('get') + ->willReturn($user); + $this->availabilityCoordinator->expects(self::once()) + ->method('getCurrentOutOfOfficeData') + ->willReturn(null); + $this->availabilityCoordinator->expects(self::never()) + ->method('isInEffect'); + $this->cache->expects(self::once()) + ->method('get') + ->willReturn(null); + $this->cache->expects(self::once()) + ->method('set'); + $this->calendarManager->expects(self::once()) + ->method('getCalendarsForPrincipal') + ->willReturn([$this->createMock(CalendarImpl::class)]); + $this->calendarManager->expects(self::once()) + ->method('newQuery') + ->willReturn(new CalendarQuery('admin')); + $this->timeFactory->expects(self::exactly(2)) + ->method('getDateTime') + ->willReturn(new \DateTime()); + $this->userStatusService->expects(self::once()) + ->method('findByUserId') + ->willThrowException(new DoesNotExistException('')); + $this->calendarManager->expects(self::once()) + ->method('searchForPrincipal') + ->willReturn([['objects' => [[]]]]); + $this->userStatusService->expects(self::never()) + ->method('revertUserStatus'); + $this->logger->expects(self::once()) + ->method('debug'); + $this->userStatusService->expects(self::once()) + ->method('setUserStatus'); + + + $this->service->processCalendarStatus('admin'); + } + + public function testCallStatus(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + ]); + + $this->userManager->expects(self::once()) + ->method('get') + ->willReturn($user); + $this->availabilityCoordinator->expects(self::once()) + ->method('getCurrentOutOfOfficeData') + ->willReturn(null); + $this->availabilityCoordinator->expects(self::never()) + ->method('isInEffect'); + $this->cache->expects(self::once()) + ->method('get') + ->willReturn(null); + $this->cache->expects(self::once()) + ->method('set'); + $this->calendarManager->expects(self::once()) + ->method('getCalendarsForPrincipal') + ->willReturn([$this->createMock(CalendarImpl::class)]); + $this->calendarManager->expects(self::once()) + ->method('newQuery') + ->willReturn(new CalendarQuery('admin')); + $this->timeFactory->expects(self::exactly(2)) + ->method('getDateTime') + ->willReturn(new \DateTime()); + $this->calendarManager->expects(self::once()) + ->method('searchForPrincipal') + ->willReturn([['objects' => [[]]]]); + $userStatus = new UserStatus(); + $userStatus->setMessageId(IUserStatus::MESSAGE_CALL); + $userStatus->setStatusTimestamp(123456); + $this->userStatusService->expects(self::once()) + ->method('findByUserId') + ->willReturn($userStatus); + $this->logger->expects(self::once()) + ->method('debug'); + $this->userStatusService->expects(self::never()) + ->method('revertUserStatus'); + $this->userStatusService->expects(self::never()) + ->method('setUserStatus'); + + + $this->service->processCalendarStatus('admin'); + } + + public function testInvisibleStatus(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + ]); + + $this->userManager->expects(self::once()) + ->method('get') + ->willReturn($user); + $this->availabilityCoordinator->expects(self::once()) + ->method('getCurrentOutOfOfficeData') + ->willReturn(null); + $this->availabilityCoordinator->expects(self::never()) + ->method('isInEffect'); + $this->cache->expects(self::once()) + ->method('get') + ->willReturn(null); + $this->cache->expects(self::once()) + ->method('set'); + $this->calendarManager->expects(self::once()) + ->method('getCalendarsForPrincipal') + ->willReturn([$this->createMock(CalendarImpl::class)]); + $this->calendarManager->expects(self::once()) + ->method('newQuery') + ->willReturn(new CalendarQuery('admin')); + $this->timeFactory->expects(self::exactly(2)) + ->method('getDateTime') + ->willReturn(new \DateTime()); + $this->calendarManager->expects(self::once()) + ->method('searchForPrincipal') + ->willReturn([['objects' => [[]]]]); + $userStatus = new UserStatus(); + $userStatus->setStatus(IUserStatus::INVISIBLE); + $userStatus->setStatusTimestamp(123456); + $this->userStatusService->expects(self::once()) + ->method('findByUserId') + ->willReturn($userStatus); + $this->logger->expects(self::once()) + ->method('debug'); + $this->userStatusService->expects(self::never()) + ->method('revertUserStatus'); + $this->userStatusService->expects(self::never()) + ->method('setUserStatus'); + + + $this->service->processCalendarStatus('admin'); + } + + public function testDNDStatus(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'admin', + ]); + + $this->userManager->expects(self::once()) + ->method('get') + ->willReturn($user); + $this->availabilityCoordinator->expects(self::once()) + ->method('getCurrentOutOfOfficeData') + ->willReturn(null); + $this->availabilityCoordinator->expects(self::never()) + ->method('isInEffect'); + $this->cache->expects(self::once()) + ->method('get') + ->willReturn(null); + $this->cache->expects(self::once()) + ->method('set'); + $this->calendarManager->expects(self::once()) + ->method('getCalendarsForPrincipal') + ->willReturn([$this->createMock(CalendarImpl::class)]); + $this->calendarManager->expects(self::once()) + ->method('newQuery') + ->willReturn(new CalendarQuery('admin')); + $this->timeFactory->expects(self::exactly(2)) + ->method('getDateTime') + ->willReturn(new \DateTime()); + $this->calendarManager->expects(self::once()) + ->method('searchForPrincipal') + ->willReturn([['objects' => [[]]]]); + $userStatus = new UserStatus(); + $userStatus->setStatus(IUserStatus::DND); + $userStatus->setStatusTimestamp(123456); + $this->userStatusService->expects(self::once()) + ->method('findByUserId') + ->willReturn($userStatus); + $this->logger->expects(self::once()) + ->method('debug'); + $this->userStatusService->expects(self::never()) + ->method('revertUserStatus'); + $this->userStatusService->expects(self::never()) + ->method('setUserStatus'); + + + $this->service->processCalendarStatus('admin'); + } +} diff --git a/apps/dav/tests/unit/CalDAV/TimeZoneFactoryTest.php b/apps/dav/tests/unit/CalDAV/TimeZoneFactoryTest.php new file mode 100644 index 00000000000..2d6d0e86358 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/TimeZoneFactoryTest.php @@ -0,0 +1,51 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV; + +use DateTimeZone; +use OCA\DAV\CalDAV\TimeZoneFactory; +use Test\TestCase; + +class TimeZoneFactoryTest extends TestCase { + + private TimeZoneFactory $factory; + + protected function setUp(): void { + parent::setUp(); + + $this->factory = new TimeZoneFactory(); + } + + public function testIsMS(): void { + // test Microsoft time zone + $this->assertTrue(TimeZoneFactory::isMS('Eastern Standard Time')); + // test IANA time zone + $this->assertFalse(TimeZoneFactory::isMS('America/Toronto')); + // test Fake time zone + $this->assertFalse(TimeZoneFactory::isMS('Fake Eastern Time')); + } + + public function testToIana(): void { + // test Microsoft time zone + $this->assertEquals('America/Toronto', TimeZoneFactory::toIANA('Eastern Standard Time')); + // test IANA time zone + $this->assertEquals(null, TimeZoneFactory::toIANA('America/Toronto')); + // test Fake time zone + $this->assertEquals(null, TimeZoneFactory::toIANA('Fake Eastern Time')); + } + + public function testFromName(): void { + // test Microsoft time zone + $this->assertEquals(new DateTimeZone('America/Toronto'), $this->factory->fromName('Eastern Standard Time')); + // test IANA time zone + $this->assertEquals(new DateTimeZone('America/Toronto'), $this->factory->fromName('America/Toronto')); + // test Fake time zone + $this->assertEquals(null, $this->factory->fromName('Fake Eastern Time')); + } + +} diff --git a/apps/dav/tests/unit/CalDAV/TimezoneServiceTest.php b/apps/dav/tests/unit/CalDAV/TimezoneServiceTest.php new file mode 100644 index 00000000000..5bb87be67c1 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/TimezoneServiceTest.php @@ -0,0 +1,142 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Tests\unit\CalDAV; + +use DateTimeZone; +use OCA\DAV\CalDAV\CalendarImpl; +use OCA\DAV\CalDAV\TimezoneService; +use OCA\DAV\Db\Property; +use OCA\DAV\Db\PropertyMapper; +use OCP\Calendar\ICalendar; +use OCP\Calendar\IManager; +use OCP\IConfig; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\VObject\Component\VTimeZone; +use Test\TestCase; + +class TimezoneServiceTest extends TestCase { + private IConfig&MockObject $config; + private PropertyMapper&MockObject $propertyMapper; + private IManager&MockObject $calendarManager; + private TimezoneService $service; + + protected function setUp(): void { + parent::setUp(); + + $this->config = $this->createMock(IConfig::class); + $this->propertyMapper = $this->createMock(PropertyMapper::class); + $this->calendarManager = $this->createMock(IManager::class); + + $this->service = new TimezoneService( + $this->config, + $this->propertyMapper, + $this->calendarManager, + ); + } + + public function testGetUserTimezoneFromSettings(): void { + $this->config->expects(self::once()) + ->method('getUserValue') + ->with('test123', 'core', 'timezone', '') + ->willReturn('Europe/Warsaw'); + + $timezone = $this->service->getUserTimezone('test123'); + + self::assertSame('Europe/Warsaw', $timezone); + } + + public function testGetUserTimezoneFromAvailability(): void { + $this->config->expects(self::once()) + ->method('getUserValue') + ->with('test123', 'core', 'timezone', '') + ->willReturn(''); + $property = new Property(); + $property->setPropertyvalue('BEGIN:VCALENDAR +PRODID:Nextcloud DAV app +BEGIN:VTIMEZONE +TZID:Europe/Vienna +END:VTIMEZONE +END:VCALENDAR'); + $this->propertyMapper->expects(self::once()) + ->method('findPropertyByPathAndName') + ->willReturn([ + $property, + ]); + + $timezone = $this->service->getUserTimezone('test123'); + + self::assertNotNull($timezone); + self::assertEquals('Europe/Vienna', $timezone); + } + + public function testGetUserTimezoneFromPersonalCalendar(): void { + $this->config->expects(self::exactly(2)) + ->method('getUserValue') + ->willReturnMap([ + ['test123', 'core', 'timezone', '', ''], + ['test123', 'dav', 'defaultCalendar', '', 'personal-1'], + ]); + $other = $this->createMock(ICalendar::class); + $other->method('getUri')->willReturn('other'); + $personal = $this->createMock(CalendarImpl::class); + $personal->method('getUri')->willReturn('personal-1'); + $tz = new DateTimeZone('Europe/Berlin'); + $vtz = $this->createMock(VTimeZone::class); + $vtz->method('getTimeZone')->willReturn($tz); + $personal->method('getSchedulingTimezone')->willReturn($vtz); + $this->calendarManager->expects(self::once()) + ->method('getCalendarsForPrincipal') + ->with('principals/users/test123') + ->willReturn([ + $other, + $personal, + ]); + + $timezone = $this->service->getUserTimezone('test123'); + + self::assertNotNull($timezone); + self::assertEquals('Europe/Berlin', $timezone); + } + + public function testGetUserTimezoneFromAny(): void { + $this->config->expects(self::exactly(2)) + ->method('getUserValue') + ->willReturnMap([ + ['test123', 'core', 'timezone', '', ''], + ['test123', 'dav', 'defaultCalendar', '', 'personal-1'], + ]); + $other = $this->createMock(ICalendar::class); + $other->method('getUri')->willReturn('other'); + $personal = $this->createMock(CalendarImpl::class); + $personal->method('getUri')->willReturn('personal-2'); + $tz = new DateTimeZone('Europe/Prague'); + $vtz = $this->createMock(VTimeZone::class); + $vtz->method('getTimeZone')->willReturn($tz); + $personal->method('getSchedulingTimezone')->willReturn($vtz); + $this->calendarManager->expects(self::once()) + ->method('getCalendarsForPrincipal') + ->with('principals/users/test123') + ->willReturn([ + $other, + $personal, + ]); + + $timezone = $this->service->getUserTimezone('test123'); + + self::assertNotNull($timezone); + self::assertEquals('Europe/Prague', $timezone); + } + + public function testGetUserTimezoneNoneFound(): void { + $timezone = $this->service->getUserTimezone('test123'); + + self::assertNull($timezone); + } + +} diff --git a/apps/dav/tests/unit/CalDAV/TipBrokerTest.php b/apps/dav/tests/unit/CalDAV/TipBrokerTest.php new file mode 100644 index 00000000000..ddf992767d6 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/TipBrokerTest.php @@ -0,0 +1,180 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV; + +use OCA\DAV\CalDAV\TipBroker; +use Sabre\VObject\Component\VCalendar; +use Test\TestCase; + +class TipBrokerTest extends TestCase { + + private TipBroker $broker; + private VCalendar $vCalendar1a; + + protected function setUp(): void { + parent::setUp(); + + $this->broker = new TipBroker(); + // construct calendar with a 1 hour event and same start/end time zones + $this->vCalendar1a = new VCalendar(); + /** @var VEvent $vEvent */ + $vEvent = $this->vCalendar1a->add('VEVENT', []); + $vEvent->add('UID', '96a0e6b1-d886-4a55-a60d-152b31401dcc'); + $vEvent->add('DTSTAMP', '20240701T000000Z'); + $vEvent->add('CREATED', '20240701T000000Z'); + $vEvent->add('LAST-MODIFIED', '20240701T000000Z'); + $vEvent->add('SEQUENCE', '1'); + $vEvent->add('STATUS', 'CONFIRMED'); + $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']); + $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']); + $vEvent->add('SUMMARY', 'Test Event'); + $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']); + $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [ + 'CN' => 'Attendee One', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); + } + + public function testParseEventForOrganizerOnCreate(): void { + + // construct calendar and generate event info for newly created event with one attendee + $calendar = clone $this->vCalendar1a; + $previousEventInfo = [ + 'organizer' => null, + 'significantChangeHash' => '', + 'attendees' => [], + ]; + $currentEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]); + // test iTip generation + $messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]); + $this->assertCount(1, $messages); + $this->assertEquals('REQUEST', $messages[0]->method); + $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender); + $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient); + + } + + public function testParseEventForOrganizerOnModify(): void { + + // construct calendar and generate event info for modified event with one attendee + $calendar = clone $this->vCalendar1a; + $previousEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]); + $calendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z'); + $calendar->VEVENT->SEQUENCE->setValue(2); + $calendar->VEVENT->SUMMARY->setValue('Test Event Modified'); + $currentEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]); + // test iTip generation + $messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]); + $this->assertCount(1, $messages); + $this->assertEquals('REQUEST', $messages[0]->method); + $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender); + $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient); + + } + + public function testParseEventForOrganizerOnDelete(): void { + + // construct calendar and generate event info for modified event with one attendee + $calendar = clone $this->vCalendar1a; + $previousEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]); + $currentEventInfo = $previousEventInfo; + $currentEventInfo['attendees'] = []; + ++$currentEventInfo['sequence']; + // test iTip generation + $messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]); + $this->assertCount(1, $messages); + $this->assertEquals('CANCEL', $messages[0]->method); + $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender); + $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient); + + } + + public function testParseEventForOrganizerOnStatusCancelled(): void { + + // construct calendar and generate event info for modified event with one attendee + $calendar = clone $this->vCalendar1a; + $previousEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]); + $calendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z'); + $calendar->VEVENT->SEQUENCE->setValue(2); + $calendar->VEVENT->STATUS->setValue('CANCELLED'); + $currentEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]); + // test iTip generation + $messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]); + $this->assertCount(1, $messages); + $this->assertEquals('CANCEL', $messages[0]->method); + $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender); + $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient); + + } + + public function testParseEventForOrganizerOnAddAttendee(): void { + + // construct calendar and generate event info for modified event with two attendees + $calendar = clone $this->vCalendar1a; + $previousEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]); + $calendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z'); + $calendar->VEVENT->SEQUENCE->setValue(2); + $calendar->VEVENT->add('ATTENDEE', 'mailto:attendee2@testing.com', [ + 'CN' => 'Attendee Two', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); + $currentEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]); + // test iTip generation + $messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]); + $this->assertCount(2, $messages); + $this->assertEquals('REQUEST', $messages[0]->method); + $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender); + $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient); + $this->assertEquals('REQUEST', $messages[1]->method); + $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[1]->sender); + $this->assertEquals($calendar->VEVENT->ATTENDEE[1]->getValue(), $messages[1]->recipient); + + } + + public function testParseEventForOrganizerOnRemoveAttendee(): void { + + // construct calendar and generate event info for modified event with two attendees + $calendar = clone $this->vCalendar1a; + $calendar->VEVENT->add('ATTENDEE', 'mailto:attendee2@testing.com', [ + 'CN' => 'Attendee Two', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); + $previousEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]); + $calendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z'); + $calendar->VEVENT->SEQUENCE->setValue(2); + $calendar->VEVENT->remove('ATTENDEE'); + $calendar->VEVENT->add('ATTENDEE', 'mailto:attendee1@testing.com', [ + 'CN' => 'Attendee One', + 'CUTYPE' => 'INDIVIDUAL', + 'PARTSTAT' => 'NEEDS-ACTION', + 'ROLE' => 'REQ-PARTICIPANT', + 'RSVP' => 'TRUE' + ]); + $currentEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]); + // test iTip generation + $messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]); + $this->assertCount(2, $messages); + $this->assertEquals('REQUEST', $messages[0]->method); + $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender); + $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient); + $this->assertEquals('CANCEL', $messages[1]->method); + $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[1]->sender); + $this->assertEquals('mailto:attendee2@testing.com', $messages[1]->recipient); + + } + +} diff --git a/apps/dav/tests/unit/CalDAV/Validation/CalDavValidatePluginTest.php b/apps/dav/tests/unit/CalDAV/Validation/CalDavValidatePluginTest.php new file mode 100644 index 00000000000..74fb4b5e94e --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Validation/CalDavValidatePluginTest.php @@ -0,0 +1,73 @@ +<?php + +declare(strict_types=1); + +/* + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Tests\unit\CalDAV\Validation; + +use OCA\DAV\CalDAV\Validation\CalDavValidatePlugin; +use OCP\IAppConfig; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\Exception\Forbidden; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; +use Test\TestCase; + +class CalDavValidatePluginTest extends TestCase { + private IAppConfig&MockObject $config; + private RequestInterface&MockObject $request; + private ResponseInterface&MockObject $response; + + private CalDavValidatePlugin $plugin; + + protected function setUp(): void { + parent::setUp(); + // construct mock objects + $this->config = $this->createMock(IAppConfig::class); + $this->request = $this->createMock(RequestInterface::class); + $this->response = $this->createMock(ResponseInterface::class); + $this->plugin = new CalDavValidatePlugin( + $this->config, + ); + } + + public function testPutSizeLessThenLimit(): void { + + // construct method responses + $this->config + ->method('getValueInt') + ->with('dav', 'event_size_limit', 10485760) + ->willReturn(10485760); + $this->request + ->method('getRawServerValue') + ->with('CONTENT_LENGTH') + ->willReturn('1024'); + // test condition + $this->assertTrue( + $this->plugin->beforePut($this->request, $this->response) + ); + + } + + public function testPutSizeMoreThenLimit(): void { + + // construct method responses + $this->config + ->method('getValueInt') + ->with('dav', 'event_size_limit', 10485760) + ->willReturn(10485760); + $this->request + ->method('getRawServerValue') + ->with('CONTENT_LENGTH') + ->willReturn('16242880'); + $this->expectException(Forbidden::class); + // test condition + $this->plugin->beforePut($this->request, $this->response); + + } + +} diff --git a/apps/dav/tests/unit/CalDAV/WebcalCaching/ConnectionTest.php b/apps/dav/tests/unit/CalDAV/WebcalCaching/ConnectionTest.php new file mode 100644 index 00000000000..c29415ecef3 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/WebcalCaching/ConnectionTest.php @@ -0,0 +1,176 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\WebcalCaching; + +use OCA\DAV\CalDAV\WebcalCaching\Connection; +use OCP\Http\Client\IClient; +use OCP\Http\Client\IClientService; +use OCP\Http\Client\IResponse; +use OCP\Http\Client\LocalServerException; +use OCP\IAppConfig; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; + +use Test\TestCase; + +class ConnectionTest extends TestCase { + + private IClientService&MockObject $clientService; + private IAppConfig&MockObject $config; + private LoggerInterface&MockObject $logger; + private Connection $connection; + + public function setUp(): void { + $this->clientService = $this->createMock(IClientService::class); + $this->config = $this->createMock(IAppConfig::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->connection = new Connection($this->clientService, $this->config, $this->logger); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('runLocalURLDataProvider')] + public function testLocalUrl($source): void { + $subscription = [ + 'id' => 42, + 'uri' => 'sub123', + 'refreshreate' => 'P1H', + 'striptodos' => 1, + 'stripalarms' => 1, + 'stripattachments' => 1, + 'source' => $source, + 'lastmodified' => 0, + ]; + + $client = $this->createMock(IClient::class); + $this->clientService->expects(self::once()) + ->method('newClient') + ->with() + ->willReturn($client); + + $this->config->expects(self::once()) + ->method('getValueString') + ->with('dav', 'webcalAllowLocalAccess', 'no') + ->willReturn('no'); + + $localServerException = new LocalServerException(); + $client->expects(self::once()) + ->method('get') + ->willThrowException($localServerException); + $this->logger->expects(self::once()) + ->method('warning') + ->with('Subscription 42 was not refreshed because it violates local access rules', ['exception' => $localServerException]); + + $this->connection->queryWebcalFeed($subscription); + } + + public function testInvalidUrl(): void { + $subscription = [ + 'id' => 42, + 'uri' => 'sub123', + 'refreshreate' => 'P1H', + 'striptodos' => 1, + 'stripalarms' => 1, + 'stripattachments' => 1, + 'source' => '!@#$', + 'lastmodified' => 0, + ]; + + $client = $this->createMock(IClient::class); + $this->config->expects(self::never()) + ->method('getValueString'); + $client->expects(self::never()) + ->method('get'); + + $this->connection->queryWebcalFeed($subscription); + + } + + /** + * @param string $result + * @param string $contentType + */ + #[\PHPUnit\Framework\Attributes\DataProvider('urlDataProvider')] + public function testConnection(string $url, string $result, string $contentType): void { + $client = $this->createMock(IClient::class); + $response = $this->createMock(IResponse::class); + $subscription = [ + 'id' => 42, + 'uri' => 'sub123', + 'refreshreate' => 'P1H', + 'striptodos' => 1, + 'stripalarms' => 1, + 'stripattachments' => 1, + 'source' => $url, + 'lastmodified' => 0, + ]; + + $this->clientService->expects($this->once()) + ->method('newClient') + ->with() + ->willReturn($client); + + $this->config->expects($this->once()) + ->method('getValueString') + ->with('dav', 'webcalAllowLocalAccess', 'no') + ->willReturn('no'); + + $client->expects($this->once()) + ->method('get') + ->with('https://foo.bar/bla2') + ->willReturn($response); + + $response->expects($this->once()) + ->method('getBody') + ->with() + ->willReturn($result); + $response->expects($this->once()) + ->method('getHeader') + ->with('Content-Type') + ->willReturn($contentType); + + $this->connection->queryWebcalFeed($subscription); + } + + public static function runLocalURLDataProvider(): array { + return [ + ['localhost/foo.bar'], + ['localHost/foo.bar'], + ['random-host/foo.bar'], + ['[::1]/bla.blub'], + ['[::]/bla.blub'], + ['192.168.0.1'], + ['172.16.42.1'], + ['[fdf8:f53b:82e4::53]/secret.ics'], + ['[fe80::200:5aee:feaa:20a2]/secret.ics'], + ['[0:0:0:0:0:0:10.0.0.1]/secret.ics'], + ['[0:0:0:0:0:ffff:127.0.0.0]/secret.ics'], + ['10.0.0.1'], + ['another-host.local'], + ['service.localhost'], + ]; + } + + public static function urlDataProvider(): array { + return [ + [ + 'https://foo.bar/bla2', + "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", + 'text/calendar;charset=utf8', + ], + [ + 'https://foo.bar/bla2', + '["vcalendar",[["prodid",{},"text","-//Example Corp.//Example Client//EN"],["version",{},"text","2.0"]],[["vtimezone",[["last-modified",{},"date-time","2004-01-10T03:28:45Z"],["tzid",{},"text","US/Eastern"]],[["daylight",[["dtstart",{},"date-time","2000-04-04T02:00:00"],["rrule",{},"recur",{"freq":"YEARLY","byday":"1SU","bymonth":4}],["tzname",{},"text","EDT"],["tzoffsetfrom",{},"utc-offset","-05:00"],["tzoffsetto",{},"utc-offset","-04:00"]],[]],["standard",[["dtstart",{},"date-time","2000-10-26T02:00:00"],["rrule",{},"recur",{"freq":"YEARLY","byday":"1SU","bymonth":10}],["tzname",{},"text","EST"],["tzoffsetfrom",{},"utc-offset","-04:00"],["tzoffsetto",{},"utc-offset","-05:00"]],[]]]],["vevent",[["dtstamp",{},"date-time","2006-02-06T00:11:21Z"],["dtstart",{"tzid":"US/Eastern"},"date-time","2006-01-02T14:00:00"],["duration",{},"duration","PT1H"],["recurrence-id",{"tzid":"US/Eastern"},"date-time","2006-01-04T12:00:00"],["summary",{},"text","Event #2"],["uid",{},"text","12345"]],[]]]]', + 'application/calendar+json', + ], + [ + 'https://foo.bar/bla2', + '<?xml version="1.0" encoding="utf-8" ?><icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"><vcalendar><properties><prodid><text>-//Example Inc.//Example Client//EN</text></prodid><version><text>2.0</text></version></properties><components><vevent><properties><dtstamp><date-time>2006-02-06T00:11:21Z</date-time></dtstamp><dtstart><parameters><tzid><text>US/Eastern</text></tzid></parameters><date-time>2006-01-04T14:00:00</date-time></dtstart><duration><duration>PT1H</duration></duration><recurrence-id><parameters><tzid><text>US/Eastern</text></tzid></parameters><date-time>2006-01-04T12:00:00</date-time></recurrence-id><summary><text>Event #2 bis</text></summary><uid><text>12345</text></uid></properties></vevent></components></vcalendar></icalendar>', + 'application/calendar+xml', + ], + ]; + } +} diff --git a/apps/dav/tests/unit/CalDAV/WebcalCaching/PluginTest.php b/apps/dav/tests/unit/CalDAV/WebcalCaching/PluginTest.php new file mode 100644 index 00000000000..804af021d5a --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/WebcalCaching/PluginTest.php @@ -0,0 +1,152 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\WebcalCaching; + +use OCA\DAV\CalDAV\WebcalCaching\Plugin; +use OCP\IRequest; +use Sabre\DAV\Server; +use Sabre\DAV\Tree; +use Sabre\HTTP\Request; +use Sabre\HTTP\Response; + +class PluginTest extends \Test\TestCase { + public function testDisabled(): void { + $request = $this->createMock(IRequest::class); + $request->expects($this->once()) + ->method('isUserAgent') + ->with(Plugin::ENABLE_FOR_CLIENTS) + ->willReturn(false); + + $request->expects($this->once()) + ->method('getHeader') + ->with('X-NC-CalDAV-Webcal-Caching') + ->willReturn(''); + + $plugin = new Plugin($request); + + $this->assertEquals(false, $plugin->isCachingEnabledForThisRequest()); + } + + public function testEnabledUserAgent(): void { + $request = $this->createMock(IRequest::class); + $request->expects($this->once()) + ->method('isUserAgent') + ->with(Plugin::ENABLE_FOR_CLIENTS) + ->willReturn(true); + $request->expects($this->once()) + ->method('getHeader') + ->with('X-NC-CalDAV-Webcal-Caching') + ->willReturn(''); + $request->expects($this->once()) + ->method('getMethod') + ->willReturn('REPORT'); + $request->expects($this->never()) + ->method('getParams'); + + $plugin = new Plugin($request); + + $this->assertEquals(true, $plugin->isCachingEnabledForThisRequest()); + } + + public function testEnabledWebcalCachingHeader(): void { + $request = $this->createMock(IRequest::class); + $request->expects($this->once()) + ->method('isUserAgent') + ->with(Plugin::ENABLE_FOR_CLIENTS) + ->willReturn(false); + $request->expects($this->once()) + ->method('getHeader') + ->with('X-NC-CalDAV-Webcal-Caching') + ->willReturn('On'); + $request->expects($this->once()) + ->method('getMethod') + ->willReturn('REPORT'); + $request->expects($this->never()) + ->method('getParams'); + + $plugin = new Plugin($request); + + $this->assertEquals(true, $plugin->isCachingEnabledForThisRequest()); + } + + public function testEnabledExportRequest(): void { + $request = $this->createMock(IRequest::class); + $request->expects($this->once()) + ->method('isUserAgent') + ->with(Plugin::ENABLE_FOR_CLIENTS) + ->willReturn(false); + $request->expects($this->once()) + ->method('getHeader') + ->with('X-NC-CalDAV-Webcal-Caching') + ->willReturn(''); + $request->expects($this->once()) + ->method('getMethod') + ->willReturn('GET'); + $request->expects($this->once()) + ->method('getParams') + ->willReturn(['export' => '']); + + $plugin = new Plugin($request); + + $this->assertEquals(true, $plugin->isCachingEnabledForThisRequest()); + } + + public function testSkipNonCalendarRequest(): void { + $request = $this->createMock(IRequest::class); + $request->expects($this->once()) + ->method('isUserAgent') + ->with(Plugin::ENABLE_FOR_CLIENTS) + ->willReturn(false); + + $request->expects($this->once()) + ->method('getHeader') + ->with('X-NC-CalDAV-Webcal-Caching') + ->willReturn('On'); + + $sabreRequest = new Request('REPORT', '/remote.php/dav/principals/users/admin/'); + $sabreRequest->setBaseUrl('/remote.php/dav/'); + + $tree = $this->createMock(Tree::class); + $tree->expects($this->never()) + ->method('getNodeForPath'); + + $server = new Server($tree); + + $plugin = new Plugin($request); + $plugin->initialize($server); + + $plugin->beforeMethod($sabreRequest, new Response()); + } + + public function testProcessCalendarRequest(): void { + $request = $this->createMock(IRequest::class); + $request->expects($this->once()) + ->method('isUserAgent') + ->with(Plugin::ENABLE_FOR_CLIENTS) + ->willReturn(false); + + $request->expects($this->once()) + ->method('getHeader') + ->with('X-NC-CalDAV-Webcal-Caching') + ->willReturn('On'); + + $sabreRequest = new Request('REPORT', '/remote.php/dav/calendars/admin/personal/'); + $sabreRequest->setBaseUrl('/remote.php/dav/'); + + $tree = $this->createMock(Tree::class); + $tree->expects($this->once()) + ->method('getNodeForPath'); + + $server = new Server($tree); + + $plugin = new Plugin($request); + $plugin->initialize($server); + + $plugin->beforeMethod($sabreRequest, new Response()); + } +} diff --git a/apps/dav/tests/unit/CalDAV/WebcalCaching/RefreshWebcalServiceTest.php b/apps/dav/tests/unit/CalDAV/WebcalCaching/RefreshWebcalServiceTest.php new file mode 100644 index 00000000000..d4f4b9e878f --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/WebcalCaching/RefreshWebcalServiceTest.php @@ -0,0 +1,325 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CalDAV\WebcalCaching; + +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CalDAV\WebcalCaching\Connection; +use OCA\DAV\CalDAV\WebcalCaching\RefreshWebcalService; +use OCP\AppFramework\Utility\ITimeFactory; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Sabre\DAV\Exception\BadRequest; +use Sabre\VObject; +use Sabre\VObject\Recur\NoInstancesException; + +use Test\TestCase; + +class RefreshWebcalServiceTest extends TestCase { + private CalDavBackend&MockObject $caldavBackend; + private Connection&MockObject $connection; + private LoggerInterface&MockObject $logger; + private ITimeFactory&MockObject $time; + + protected function setUp(): void { + parent::setUp(); + + $this->caldavBackend = $this->createMock(CalDavBackend::class); + $this->connection = $this->createMock(Connection::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->time = $this->createMock(ITimeFactory::class); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('runDataProvider')] + public function testRun(string $body, string $contentType, string $result): void { + $refreshWebcalService = $this->getMockBuilder(RefreshWebcalService::class) + ->onlyMethods(['getRandomCalendarObjectUri']) + ->setConstructorArgs([$this->caldavBackend, $this->logger, $this->connection, $this->time]) + ->getMock(); + + $refreshWebcalService + ->method('getRandomCalendarObjectUri') + ->willReturn('uri-1.ics'); + + $this->caldavBackend->expects(self::once()) + ->method('getSubscriptionsForUser') + ->with('principals/users/testuser') + ->willReturn([ + [ + 'id' => '99', + 'uri' => 'sub456', + RefreshWebcalService::REFRESH_RATE => 'P1D', + RefreshWebcalService::STRIP_TODOS => '1', + RefreshWebcalService::STRIP_ALARMS => '1', + RefreshWebcalService::STRIP_ATTACHMENTS => '1', + 'source' => 'webcal://foo.bar/bla', + 'lastmodified' => 0, + ], + [ + 'id' => '42', + 'uri' => 'sub123', + RefreshWebcalService::REFRESH_RATE => 'PT1H', + RefreshWebcalService::STRIP_TODOS => '1', + RefreshWebcalService::STRIP_ALARMS => '1', + RefreshWebcalService::STRIP_ATTACHMENTS => '1', + 'source' => 'webcal://foo.bar/bla2', + 'lastmodified' => 0, + ], + ]); + + $this->connection->expects(self::once()) + ->method('queryWebcalFeed') + ->willReturn($result); + $this->caldavBackend->expects(self::once()) + ->method('createCalendarObject') + ->with(42, 'uri-1.ics', $result, 1); + + $refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123'); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('identicalDataProvider')] + public function testRunIdentical(string $uid, array $calendarObject, string $body, string $contentType, string $result): void { + $refreshWebcalService = $this->getMockBuilder(RefreshWebcalService::class) + ->onlyMethods(['getRandomCalendarObjectUri']) + ->setConstructorArgs([$this->caldavBackend, $this->logger, $this->connection, $this->time]) + ->getMock(); + + $refreshWebcalService + ->method('getRandomCalendarObjectUri') + ->willReturn('uri-1.ics'); + + $this->caldavBackend->expects(self::once()) + ->method('getSubscriptionsForUser') + ->with('principals/users/testuser') + ->willReturn([ + [ + 'id' => '99', + 'uri' => 'sub456', + RefreshWebcalService::REFRESH_RATE => 'P1D', + RefreshWebcalService::STRIP_TODOS => '1', + RefreshWebcalService::STRIP_ALARMS => '1', + RefreshWebcalService::STRIP_ATTACHMENTS => '1', + 'source' => 'webcal://foo.bar/bla', + 'lastmodified' => 0, + ], + [ + 'id' => '42', + 'uri' => 'sub123', + RefreshWebcalService::REFRESH_RATE => 'PT1H', + RefreshWebcalService::STRIP_TODOS => '1', + RefreshWebcalService::STRIP_ALARMS => '1', + RefreshWebcalService::STRIP_ATTACHMENTS => '1', + 'source' => 'webcal://foo.bar/bla2', + 'lastmodified' => 0, + ], + ]); + + $this->connection->expects(self::once()) + ->method('queryWebcalFeed') + ->willReturn($result); + + $this->caldavBackend->expects(self::once()) + ->method('getLimitedCalendarObjects') + ->willReturn($calendarObject); + + $denormalised = [ + 'etag' => 100, + 'size' => strlen($calendarObject[$uid]['calendardata']), + 'uid' => 'sub456' + ]; + + $this->caldavBackend->expects(self::once()) + ->method('getDenormalizedData') + ->willReturn($denormalised); + + $this->caldavBackend->expects(self::never()) + ->method('createCalendarObject'); + + $refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub456'); + } + + public function testRunJustUpdated(): void { + $refreshWebcalService = $this->getMockBuilder(RefreshWebcalService::class) + ->onlyMethods(['getRandomCalendarObjectUri']) + ->setConstructorArgs([$this->caldavBackend, $this->logger, $this->connection, $this->time]) + ->getMock(); + + $refreshWebcalService + ->method('getRandomCalendarObjectUri') + ->willReturn('uri-1.ics'); + + $this->caldavBackend->expects(self::once()) + ->method('getSubscriptionsForUser') + ->with('principals/users/testuser') + ->willReturn([ + [ + 'id' => '99', + 'uri' => 'sub456', + RefreshWebcalService::REFRESH_RATE => 'P1D', + RefreshWebcalService::STRIP_TODOS => '1', + RefreshWebcalService::STRIP_ALARMS => '1', + RefreshWebcalService::STRIP_ATTACHMENTS => '1', + 'source' => 'webcal://foo.bar/bla', + 'lastmodified' => time(), + ], + [ + 'id' => '42', + 'uri' => 'sub123', + RefreshWebcalService::REFRESH_RATE => 'PT1H', + RefreshWebcalService::STRIP_TODOS => '1', + RefreshWebcalService::STRIP_ALARMS => '1', + RefreshWebcalService::STRIP_ATTACHMENTS => '1', + 'source' => 'webcal://foo.bar/bla2', + 'lastmodified' => time(), + ], + ]); + + $timeMock = $this->createMock(\DateTime::class); + $this->time->expects(self::once()) + ->method('getDateTime') + ->willReturn($timeMock); + $timeMock->expects(self::once()) + ->method('getTimestamp') + ->willReturn(2101724667); + $this->time->expects(self::once()) + ->method('getTime') + ->willReturn(time()); + $this->connection->expects(self::never()) + ->method('queryWebcalFeed'); + $this->caldavBackend->expects(self::never()) + ->method('createCalendarObject'); + + $refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123'); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('runDataProvider')] + public function testRunCreateCalendarNoException(string $body, string $contentType, string $result): void { + $refreshWebcalService = $this->getMockBuilder(RefreshWebcalService::class) + ->onlyMethods(['getRandomCalendarObjectUri', 'getSubscription',]) + ->setConstructorArgs([$this->caldavBackend, $this->logger, $this->connection, $this->time]) + ->getMock(); + + $refreshWebcalService + ->method('getRandomCalendarObjectUri') + ->willReturn('uri-1.ics'); + + $refreshWebcalService + ->method('getSubscription') + ->willReturn([ + 'id' => '42', + 'uri' => 'sub123', + RefreshWebcalService::REFRESH_RATE => 'PT1H', + RefreshWebcalService::STRIP_TODOS => '1', + RefreshWebcalService::STRIP_ALARMS => '1', + RefreshWebcalService::STRIP_ATTACHMENTS => '1', + 'source' => 'webcal://foo.bar/bla2', + 'lastmodified' => 0, + ]); + + $this->connection->expects(self::once()) + ->method('queryWebcalFeed') + ->willReturn($result); + + $this->caldavBackend->expects(self::once()) + ->method('createCalendarObject') + ->with(42, 'uri-1.ics', $result, 1); + + $noInstanceException = new NoInstancesException("can't add calendar object"); + $this->caldavBackend->expects(self::once()) + ->method('createCalendarObject') + ->willThrowException($noInstanceException); + + $this->logger->expects(self::once()) + ->method('warning') + ->with('Unable to create calendar object from subscription {subscriptionId}', ['exception' => $noInstanceException, 'subscriptionId' => '42', 'source' => 'webcal://foo.bar/bla2']); + + $refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123'); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('runDataProvider')] + public function testRunCreateCalendarBadRequest(string $body, string $contentType, string $result): void { + $refreshWebcalService = $this->getMockBuilder(RefreshWebcalService::class) + ->onlyMethods(['getRandomCalendarObjectUri', 'getSubscription']) + ->setConstructorArgs([$this->caldavBackend, $this->logger, $this->connection, $this->time]) + ->getMock(); + + $refreshWebcalService + ->method('getRandomCalendarObjectUri') + ->willReturn('uri-1.ics'); + + $refreshWebcalService + ->method('getSubscription') + ->willReturn([ + 'id' => '42', + 'uri' => 'sub123', + RefreshWebcalService::REFRESH_RATE => 'PT1H', + RefreshWebcalService::STRIP_TODOS => '1', + RefreshWebcalService::STRIP_ALARMS => '1', + RefreshWebcalService::STRIP_ATTACHMENTS => '1', + 'source' => 'webcal://foo.bar/bla2', + 'lastmodified' => 0, + ]); + + $this->connection->expects(self::once()) + ->method('queryWebcalFeed') + ->willReturn($result); + + $this->caldavBackend->expects(self::once()) + ->method('createCalendarObject') + ->with(42, 'uri-1.ics', $result, 1); + + $badRequestException = new BadRequest("can't add reach calendar url"); + $this->caldavBackend->expects(self::once()) + ->method('createCalendarObject') + ->willThrowException($badRequestException); + + $this->logger->expects(self::once()) + ->method('warning') + ->with('Unable to create calendar object from subscription {subscriptionId}', ['exception' => $badRequestException, 'subscriptionId' => '42', 'source' => 'webcal://foo.bar/bla2']); + + $refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123'); + } + + public static function identicalDataProvider(): array { + return [ + [ + '12345', + [ + '12345' => [ + 'id' => 42, + 'etag' => 100, + 'uri' => 'sub456', + 'calendardata' => "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", + ], + ], + "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", + 'text/calendar;charset=utf8', + "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20180218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", + ], + ]; + } + + public static function runDataProvider(): array { + return [ + [ + "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", + 'text/calendar;charset=utf8', + "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject " . VObject\Version::VERSION . "//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", + ], + [ + '["vcalendar",[["prodid",{},"text","-//Example Corp.//Example Client//EN"],["version",{},"text","2.0"]],[["vtimezone",[["last-modified",{},"date-time","2004-01-10T03:28:45Z"],["tzid",{},"text","US/Eastern"]],[["daylight",[["dtstart",{},"date-time","2000-04-04T02:00:00"],["rrule",{},"recur",{"freq":"YEARLY","byday":"1SU","bymonth":4}],["tzname",{},"text","EDT"],["tzoffsetfrom",{},"utc-offset","-05:00"],["tzoffsetto",{},"utc-offset","-04:00"]],[]],["standard",[["dtstart",{},"date-time","2000-10-26T02:00:00"],["rrule",{},"recur",{"freq":"YEARLY","byday":"1SU","bymonth":10}],["tzname",{},"text","EST"],["tzoffsetfrom",{},"utc-offset","-04:00"],["tzoffsetto",{},"utc-offset","-05:00"]],[]]]],["vevent",[["dtstamp",{},"date-time","2006-02-06T00:11:21Z"],["dtstart",{"tzid":"US/Eastern"},"date-time","2006-01-02T14:00:00"],["duration",{},"duration","PT1H"],["recurrence-id",{"tzid":"US/Eastern"},"date-time","2006-01-04T12:00:00"],["summary",{},"text","Event #2"],["uid",{},"text","12345"]],[]]]]', + 'application/calendar+json', + "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject " . VObject\Version::VERSION . "//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VTIMEZONE\r\nLAST-MODIFIED:20040110T032845Z\r\nTZID:US/Eastern\r\nBEGIN:DAYLIGHT\r\nDTSTART:20000404T020000\r\nRRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4\r\nTZNAME:EDT\r\nTZOFFSETFROM:-0500\r\nTZOFFSETTO:-0400\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nDTSTART:20001026T020000\r\nRRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=10\r\nTZNAME:EST\r\nTZOFFSETFROM:-0400\r\nTZOFFSETTO:-0500\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nBEGIN:VEVENT\r\nDTSTAMP:20060206T001121Z\r\nDTSTART;TZID=US/Eastern:20060102T140000\r\nDURATION:PT1H\r\nRECURRENCE-ID;TZID=US/Eastern:20060104T120000\r\nSUMMARY:Event #2\r\nUID:12345\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n" + ], + [ + '<?xml version="1.0" encoding="utf-8" ?><icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"><vcalendar><properties><prodid><text>-//Example Inc.//Example Client//EN</text></prodid><version><text>2.0</text></version></properties><components><vevent><properties><dtstamp><date-time>2006-02-06T00:11:21Z</date-time></dtstamp><dtstart><parameters><tzid><text>US/Eastern</text></tzid></parameters><date-time>2006-01-04T14:00:00</date-time></dtstart><duration><duration>PT1H</duration></duration><recurrence-id><parameters><tzid><text>US/Eastern</text></tzid></parameters><date-time>2006-01-04T12:00:00</date-time></recurrence-id><summary><text>Event #2 bis</text></summary><uid><text>12345</text></uid></properties></vevent></components></vcalendar></icalendar>', + 'application/calendar+xml', + "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject " . VObject\Version::VERSION . "//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nDTSTAMP:20060206T001121Z\r\nDTSTART;TZID=US/Eastern:20060104T140000\r\nDURATION:PT1H\r\nRECURRENCE-ID;TZID=US/Eastern:20060104T120000\r\nSUMMARY:Event #2 bis\r\nUID:12345\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n" + ] + ]; + } +} diff --git a/apps/dav/tests/unit/CapabilitiesTest.php b/apps/dav/tests/unit/CapabilitiesTest.php new file mode 100644 index 00000000000..ad70d576d48 --- /dev/null +++ b/apps/dav/tests/unit/CapabilitiesTest.php @@ -0,0 +1,81 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit; + +use OCA\DAV\Capabilities; +use OCP\IConfig; +use OCP\User\IAvailabilityCoordinator; +use Test\TestCase; + +/** + * @package OCA\DAV\Tests\unit + */ +class CapabilitiesTest extends TestCase { + public function testGetCapabilities(): void { + $config = $this->createMock(IConfig::class); + $config->expects($this->once()) + ->method('getSystemValueBool') + ->with('bulkupload.enabled', $this->isType('bool')) + ->willReturn(false); + $coordinator = $this->createMock(IAvailabilityCoordinator::class); + $coordinator->expects($this->once()) + ->method('isEnabled') + ->willReturn(false); + $capabilities = new Capabilities($config, $coordinator); + $expected = [ + 'dav' => [ + 'chunking' => '1.0', + 'public_shares_chunking' => true, + ], + ]; + $this->assertSame($expected, $capabilities->getCapabilities()); + } + + public function testGetCapabilitiesWithBulkUpload(): void { + $config = $this->createMock(IConfig::class); + $config->expects($this->once()) + ->method('getSystemValueBool') + ->with('bulkupload.enabled', $this->isType('bool')) + ->willReturn(true); + $coordinator = $this->createMock(IAvailabilityCoordinator::class); + $coordinator->expects($this->once()) + ->method('isEnabled') + ->willReturn(false); + $capabilities = new Capabilities($config, $coordinator); + $expected = [ + 'dav' => [ + 'chunking' => '1.0', + 'public_shares_chunking' => true, + 'bulkupload' => '1.0', + ], + ]; + $this->assertSame($expected, $capabilities->getCapabilities()); + } + + public function testGetCapabilitiesWithAbsence(): void { + $config = $this->createMock(IConfig::class); + $config->expects($this->once()) + ->method('getSystemValueBool') + ->with('bulkupload.enabled', $this->isType('bool')) + ->willReturn(false); + $coordinator = $this->createMock(IAvailabilityCoordinator::class); + $coordinator->expects($this->once()) + ->method('isEnabled') + ->willReturn(true); + $capabilities = new Capabilities($config, $coordinator); + $expected = [ + 'dav' => [ + 'chunking' => '1.0', + 'public_shares_chunking' => true, + 'absence-supported' => true, + 'absence-replacement' => true, + ], + ]; + $this->assertSame($expected, $capabilities->getCapabilities()); + } +} diff --git a/apps/dav/tests/unit/CardDAV/Activity/BackendTest.php b/apps/dav/tests/unit/CardDAV/Activity/BackendTest.php new file mode 100644 index 00000000000..a070a3d7131 --- /dev/null +++ b/apps/dav/tests/unit/CardDAV/Activity/BackendTest.php @@ -0,0 +1,483 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\CardDAV\Activity; + +use OCA\DAV\CardDAV\Activity\Backend; +use OCA\DAV\CardDAV\Activity\Provider\Addressbook; +use OCA\DAV\CardDAV\Activity\Provider\Card; +use OCP\Activity\IEvent; +use OCP\Activity\IManager; +use OCP\App\IAppManager; +use OCP\IGroup; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\IUserManager; +use OCP\IUserSession; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class BackendTest extends TestCase { + protected IManager&MockObject $activityManager; + protected IGroupManager&MockObject $groupManager; + protected IUserSession&MockObject $userSession; + protected IAppManager&MockObject $appManager; + protected IUserManager&MockObject $userManager; + + protected function setUp(): void { + parent::setUp(); + $this->activityManager = $this->createMock(IManager::class); + $this->groupManager = $this->createMock(IGroupManager::class); + $this->userSession = $this->createMock(IUserSession::class); + $this->appManager = $this->createMock(IAppManager::class); + $this->userManager = $this->createMock(IUserManager::class); + } + + /** + * @return Backend|MockObject + */ + protected function getBackend(array $methods = []): Backend { + if (empty($methods)) { + return new Backend( + $this->activityManager, + $this->groupManager, + $this->userSession, + $this->appManager, + $this->userManager + ); + } else { + return $this->getMockBuilder(Backend::class) + ->setConstructorArgs([ + $this->activityManager, + $this->groupManager, + $this->userSession, + $this->appManager, + $this->userManager + ]) + ->onlyMethods($methods) + ->getMock(); + } + } + + public static function dataCallTriggerAddressBookActivity(): array { + return [ + ['onAddressbookCreate', [['data']], Addressbook::SUBJECT_ADD, [['data'], [], []]], + ['onAddressbookUpdate', [['data'], ['shares'], ['changed-properties']], Addressbook::SUBJECT_UPDATE, [['data'], ['shares'], ['changed-properties']]], + ['onAddressbookDelete', [['data'], ['shares']], Addressbook::SUBJECT_DELETE, [['data'], ['shares'], []]], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataCallTriggerAddressBookActivity')] + public function testCallTriggerAddressBookActivity(string $method, array $payload, string $expectedSubject, array $expectedPayload): void { + $backend = $this->getBackend(['triggerAddressbookActivity']); + $backend->expects($this->once()) + ->method('triggerAddressbookActivity') + ->willReturnCallback(function () use ($expectedPayload, $expectedSubject): void { + $arguments = func_get_args(); + $this->assertSame($expectedSubject, array_shift($arguments)); + $this->assertEquals($expectedPayload, $arguments); + }); + + call_user_func_array([$backend, $method], $payload); + } + + public static function dataTriggerAddressBookActivity(): array { + return [ + // Add addressbook + [Addressbook::SUBJECT_ADD, [], [], [], '', '', null, []], + [Addressbook::SUBJECT_ADD, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + 'uri' => 'this-uri', + '{DAV:}displayname' => 'Name of addressbook', + ], [], [], '', 'admin', null, ['admin']], + [Addressbook::SUBJECT_ADD, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + 'uri' => 'this-uri', + '{DAV:}displayname' => 'Name of addressbook', + ], [], [], 'test2', 'test2', null, ['admin']], + + // Update addressbook + [Addressbook::SUBJECT_UPDATE, [], [], [], '', '', null, []], + // No visible change - owner only + [Addressbook::SUBJECT_UPDATE, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + 'uri' => 'this-uri', + '{DAV:}displayname' => 'Name of addressbook', + ], ['shares'], [], '', 'admin', null, ['admin']], + // Visible change + [Addressbook::SUBJECT_UPDATE, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + 'uri' => 'this-uri', + '{DAV:}displayname' => 'Name of addressbook', + ], ['shares'], ['{DAV:}displayname' => 'Name'], '', 'admin', ['user1'], ['user1', 'admin']], + [Addressbook::SUBJECT_UPDATE, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + 'uri' => 'this-uri', + '{DAV:}displayname' => 'Name of addressbook', + ], ['shares'], ['{DAV:}displayname' => 'Name'], 'test2', 'test2', ['user1'], ['user1', 'admin']], + + // Delete addressbook + [Addressbook::SUBJECT_DELETE, [], [], [], '', '', null, []], + [Addressbook::SUBJECT_DELETE, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + 'uri' => 'this-uri', + '{DAV:}displayname' => 'Name of addressbook', + ], ['shares'], [], '', 'admin', [], ['admin']], + [Addressbook::SUBJECT_DELETE, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + 'uri' => 'this-uri', + '{DAV:}displayname' => 'Name of addressbook', + ], ['shares'], [], '', 'admin', ['user1'], ['user1', 'admin']], + [Addressbook::SUBJECT_DELETE, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + 'uri' => 'this-uri', + '{DAV:}displayname' => 'Name of addressbook', + ], ['shares'], [], 'test2', 'test2', ['user1'], ['user1', 'admin']], + ]; + } + + /** + * @param string[]|null $shareUsers + * @param string[] $users + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataTriggerAddressBookActivity')] + public function testTriggerAddressBookActivity(string $action, array $data, array $shares, array $changedProperties, string $currentUser, string $author, ?array $shareUsers, array $users): void { + $backend = $this->getBackend(['getUsersForShares']); + + if ($shareUsers === null) { + $backend->expects($this->never()) + ->method('getUsersForShares'); + } else { + $backend->expects($this->once()) + ->method('getUsersForShares') + ->with($shares) + ->willReturn($shareUsers); + } + + if ($author !== '') { + if ($currentUser !== '') { + $this->userSession->expects($this->once()) + ->method('getUser') + ->willReturn($this->getUserMock($currentUser)); + } else { + $this->userSession->expects($this->once()) + ->method('getUser') + ->willReturn(null); + } + + $event = $this->createMock(IEvent::class); + $this->activityManager->expects($this->once()) + ->method('generateEvent') + ->willReturn($event); + + $event->expects($this->once()) + ->method('setApp') + ->with('dav') + ->willReturnSelf(); + $event->expects($this->once()) + ->method('setObject') + ->with('addressbook', $data['id']) + ->willReturnSelf(); + $event->expects($this->once()) + ->method('setType') + ->with('contacts') + ->willReturnSelf(); + $event->expects($this->once()) + ->method('setAuthor') + ->with($author) + ->willReturnSelf(); + + $this->userManager->expects($action === Addressbook::SUBJECT_DELETE ? $this->exactly(sizeof($users)) : $this->never()) + ->method('userExists') + ->willReturn(true); + + $event->expects($this->exactly(count($users))) + ->method('setAffectedUser') + ->willReturnSelf(); + $event->expects($this->exactly(count($users))) + ->method('setSubject') + ->willReturnSelf(); + $this->activityManager->expects($this->exactly(count($users))) + ->method('publish') + ->with($event); + } else { + $this->activityManager->expects($this->never()) + ->method('generateEvent'); + } + + $this->invokePrivate($backend, 'triggerAddressbookActivity', [$action, $data, $shares, $changedProperties]); + } + + public function testNoAddressbookActivityCreatedForSystemAddressbook(): void { + $backend = $this->getBackend(); + $this->activityManager->expects($this->never()) + ->method('generateEvent'); + $this->assertEmpty($this->invokePrivate($backend, 'triggerAddressbookActivity', [Addressbook::SUBJECT_ADD, ['principaluri' => 'principals/system/system'], [], [], '', '', null, []])); + } + + public function testUserDeletionDoesNotCreateActivity(): void { + $backend = $this->getBackend(); + + $this->userManager->expects($this->once()) + ->method('userExists') + ->willReturn(false); + + $this->activityManager->expects($this->never()) + ->method('publish'); + + $this->invokePrivate($backend, 'triggerAddressbookActivity', [Addressbook::SUBJECT_DELETE, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + 'uri' => 'this-uri', + '{DAV:}displayname' => 'Name of addressbook', + ], [], []]); + } + + public static function dataTriggerCardActivity(): array { + $cardData = "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.4.8//EN\r\nUID:test-user\r\nFN:test-user\r\nN:test-user;;;;\r\nEND:VCARD\r\n\r\n"; + + return [ + // Add card + [Card::SUBJECT_ADD, [], [], [], '', '', null, []], + [Card::SUBJECT_ADD, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + 'uri' => 'this-uri', + '{DAV:}displayname' => 'Name of addressbook', + ], [], [ + 'carddata' => $cardData + ], '', 'admin', [], ['admin']], + [Card::SUBJECT_ADD, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + 'uri' => 'this-uri', + '{DAV:}displayname' => 'Name of addressbook', + ], [], ['carddata' => $cardData], 'test2', 'test2', [], ['admin']], + + // Update card + [Card::SUBJECT_UPDATE, [], [], [], '', '', null, []], + // No visible change - owner only + [Card::SUBJECT_UPDATE, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + 'uri' => 'this-uri', + '{DAV:}displayname' => 'Name of addressbook', + ], ['shares'], ['carddata' => $cardData], '', 'admin', [], ['admin']], + // Visible change + [Card::SUBJECT_UPDATE, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + 'uri' => 'this-uri', + '{DAV:}displayname' => 'Name of addressbook', + ], ['shares'], ['carddata' => $cardData], '', 'admin', ['user1'], ['user1', 'admin']], + [Card::SUBJECT_UPDATE, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + 'uri' => 'this-uri', + '{DAV:}displayname' => 'Name of addressbook', + ], ['shares'], ['carddata' => $cardData], 'test2', 'test2', ['user1'], ['user1', 'admin']], + + // Delete card + [Card::SUBJECT_DELETE, [], [], ['carddata' => $cardData], '', '', null, []], + [Card::SUBJECT_DELETE, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + 'uri' => 'this-uri', + '{DAV:}displayname' => 'Name of addressbook', + ], ['shares'], ['carddata' => $cardData], '', 'admin', [], ['admin']], + [Card::SUBJECT_DELETE, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + 'uri' => 'this-uri', + '{DAV:}displayname' => 'Name of addressbook', + ], ['shares'], ['carddata' => $cardData], '', 'admin', ['user1'], ['user1', 'admin']], + [Card::SUBJECT_DELETE, [ + 'principaluri' => 'principal/user/admin', + 'id' => 42, + 'uri' => 'this-uri', + '{DAV:}displayname' => 'Name of addressbook', + ], ['shares'], ['carddata' => $cardData], 'test2', 'test2', ['user1'], ['user1', 'admin']], + ]; + } + + /** + * @param string[]|null $shareUsers + * @param string[] $users + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataTriggerCardActivity')] + public function testTriggerCardActivity(string $action, array $addressBookData, array $shares, array $cardData, string $currentUser, string $author, ?array $shareUsers, array $users): void { + $backend = $this->getBackend(['getUsersForShares']); + + if ($shareUsers === null) { + $backend->expects($this->never()) + ->method('getUsersForShares'); + } else { + $backend->expects($this->once()) + ->method('getUsersForShares') + ->with($shares) + ->willReturn($shareUsers); + } + + if ($author !== '') { + if ($currentUser !== '') { + $this->userSession->expects($this->once()) + ->method('getUser') + ->willReturn($this->getUserMock($currentUser)); + } else { + $this->userSession->expects($this->once()) + ->method('getUser') + ->willReturn(null); + } + + $event = $this->createMock(IEvent::class); + $this->activityManager->expects($this->once()) + ->method('generateEvent') + ->willReturn($event); + + $event->expects($this->once()) + ->method('setApp') + ->with('dav') + ->willReturnSelf(); + $event->expects($this->once()) + ->method('setObject') + ->with('addressbook', $addressBookData['id']) + ->willReturnSelf(); + $event->expects($this->once()) + ->method('setType') + ->with('contacts') + ->willReturnSelf(); + $event->expects($this->once()) + ->method('setAuthor') + ->with($author) + ->willReturnSelf(); + + $event->expects($this->exactly(count($users))) + ->method('setAffectedUser') + ->willReturnSelf(); + $event->expects($this->exactly(count($users))) + ->method('setSubject') + ->willReturnSelf(); + $this->activityManager->expects($this->exactly(count($users))) + ->method('publish') + ->with($event); + } else { + $this->activityManager->expects($this->never()) + ->method('generateEvent'); + } + + $this->invokePrivate($backend, 'triggerCardActivity', [$action, $addressBookData, $shares, $cardData]); + } + + public function testNoCardActivityCreatedForSystemAddressbook(): void { + $backend = $this->getBackend(); + $this->activityManager->expects($this->never()) + ->method('generateEvent'); + $this->assertEmpty($this->invokePrivate($backend, 'triggerCardActivity', [Card::SUBJECT_UPDATE, ['principaluri' => 'principals/system/system'], [], []])); + } + + public static function dataGetUsersForShares(): array { + return [ + [ + [], + [], + [], + ], + [ + [ + ['{http://owncloud.org/ns}principal' => 'principal/users/user1'], + ['{http://owncloud.org/ns}principal' => 'principal/users/user2'], + ['{http://owncloud.org/ns}principal' => 'principal/users/user2'], + ['{http://owncloud.org/ns}principal' => 'principal/users/user2'], + ['{http://owncloud.org/ns}principal' => 'principal/users/user3'], + ], + [], + ['user1', 'user2', 'user3'], + ], + [ + [ + ['{http://owncloud.org/ns}principal' => 'principal/users/user1'], + ['{http://owncloud.org/ns}principal' => 'principal/users/user2'], + ['{http://owncloud.org/ns}principal' => 'principal/users/user2'], + ['{http://owncloud.org/ns}principal' => 'principal/groups/group2'], + ['{http://owncloud.org/ns}principal' => 'principal/groups/group3'], + ], + ['group2' => null, 'group3' => null], + ['user1', 'user2'], + ], + [ + [ + ['{http://owncloud.org/ns}principal' => 'principal/users/user1'], + ['{http://owncloud.org/ns}principal' => 'principal/users/user2'], + ['{http://owncloud.org/ns}principal' => 'principal/users/user2'], + ['{http://owncloud.org/ns}principal' => 'principal/groups/group2'], + ['{http://owncloud.org/ns}principal' => 'principal/groups/group3'], + ], + ['group2' => ['user1', 'user2', 'user3'], 'group3' => ['user2', 'user3', 'user4']], + ['user1', 'user2', 'user3', 'user4'], + ], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataGetUsersForShares')] + public function testGetUsersForShares(array $shares, array $groups, array $expected): void { + $backend = $this->getBackend(); + + $getGroups = []; + foreach ($groups as $gid => $members) { + if ($members === null) { + $getGroups[] = [$gid, null]; + continue; + } + + $group = $this->createMock(IGroup::class); + $group->expects($this->once()) + ->method('getUsers') + ->willReturn($this->getUsers($members)); + + $getGroups[] = [$gid, $group]; + } + + $this->groupManager->expects($this->exactly(sizeof($getGroups))) + ->method('get') + ->willReturnMap($getGroups); + + $users = $this->invokePrivate($backend, 'getUsersForShares', [$shares]); + sort($users); + $this->assertEquals($expected, $users); + } + + /** + * @param string[] $users + * @return IUser[]|MockObject[] + */ + protected function getUsers(array $users): array { + $list = []; + foreach ($users as $user) { + $list[] = $this->getUserMock($user); + } + return $list; + } + + /** + * @return IUser|MockObject + */ + protected function getUserMock(string $uid): IUser { + $user = $this->createMock(IUser::class); + $user->expects($this->once()) + ->method('getUID') + ->willReturn($uid); + return $user; + } +} diff --git a/apps/dav/tests/unit/CardDAV/AddressBookImplTest.php b/apps/dav/tests/unit/CardDAV/AddressBookImplTest.php new file mode 100644 index 00000000000..74699cf3925 --- /dev/null +++ b/apps/dav/tests/unit/CardDAV/AddressBookImplTest.php @@ -0,0 +1,537 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\CardDAV; + +use OCA\DAV\CardDAV\AddressBook; +use OCA\DAV\CardDAV\AddressBookImpl; +use OCA\DAV\CardDAV\CardDavBackend; +use OCA\DAV\Db\PropertyMapper; +use OCP\IURLGenerator; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\VObject\Component\VCard; +use Sabre\VObject\Property\Text; +//use Sabre\VObject\Property\; +use Test\TestCase; + +class AddressBookImplTest extends TestCase { + private array $addressBookInfo; + private AddressBook&MockObject $addressBook; + private IURLGenerator&MockObject $urlGenerator; + private CardDavBackend&MockObject $backend; + private PropertyMapper&MockObject $propertyMapper; + private VCard&MockObject $vCard; + private AddressBookImpl $addressBookImpl; + + protected function setUp(): void { + parent::setUp(); + + $this->addressBookInfo = [ + 'id' => 42, + 'uri' => 'system', + 'principaluri' => 'principals/system/system', + '{DAV:}displayname' => 'display name', + ]; + $this->addressBook = $this->createMock(AddressBook::class); + $this->backend = $this->createMock(CardDavBackend::class); + $this->vCard = $this->createMock(VCard::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + $this->propertyMapper = $this->createMock(PropertyMapper::class); + + $this->addressBookImpl = new AddressBookImpl( + $this->addressBook, + $this->addressBookInfo, + $this->backend, + $this->urlGenerator, + $this->propertyMapper, + null + ); + } + + public function testGetKey(): void { + $this->assertSame($this->addressBookInfo['id'], + $this->addressBookImpl->getKey()); + } + + public function testGetDisplayName(): void { + $this->assertSame($this->addressBookInfo['{DAV:}displayname'], + $this->addressBookImpl->getDisplayName()); + } + + public function testSearch(): void { + /** @var MockObject&AddressBookImpl $addressBookImpl */ + $addressBookImpl = $this->getMockBuilder(AddressBookImpl::class) + ->setConstructorArgs( + [ + $this->addressBook, + $this->addressBookInfo, + $this->backend, + $this->urlGenerator, + $this->propertyMapper, + null + ] + ) + ->onlyMethods(['vCard2Array', 'readCard']) + ->getMock(); + + $pattern = 'pattern'; + $searchProperties = 'properties'; + + $this->backend->expects($this->once())->method('search') + ->with($this->addressBookInfo['id'], $pattern, $searchProperties) + ->willReturn( + [ + ['uri' => 'foo.vcf', 'carddata' => 'cardData1'], + ['uri' => 'bar.vcf', 'carddata' => 'cardData2'] + ] + ); + + $addressBookImpl->expects($this->exactly(2))->method('readCard') + ->willReturn($this->vCard); + $addressBookImpl->expects($this->exactly(2))->method('vCard2Array') + ->willReturnMap([ + ['foo.vcf', $this->vCard, 'vCard'], + ['bar.vcf', $this->vCard, 'vCard'], + ]); + + $result = $addressBookImpl->search($pattern, $searchProperties, []); + $this->assertTrue((is_array($result))); + $this->assertSame(2, count($result)); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestCreate')] + public function testCreate(array $properties): void { + $uid = 'uid'; + + /** @var MockObject&AddressBookImpl $addressBookImpl */ + $addressBookImpl = $this->getMockBuilder(AddressBookImpl::class) + ->setConstructorArgs( + [ + $this->addressBook, + $this->addressBookInfo, + $this->backend, + $this->urlGenerator, + $this->propertyMapper, + null + ] + ) + ->onlyMethods(['vCard2Array', 'createUid', 'createEmptyVCard']) + ->getMock(); + + $expectedProperties = 0; + foreach ($properties as $data) { + if (is_string($data)) { + $expectedProperties++; + } else { + $expectedProperties += count($data); + } + } + + $addressBookImpl->expects($this->once())->method('createUid') + ->willReturn($uid); + $addressBookImpl->expects($this->once())->method('createEmptyVCard') + ->with($uid)->willReturn($this->vCard); + $this->vCard->expects($this->exactly($expectedProperties)) + ->method('createProperty'); + $this->backend->expects($this->once())->method('createCard'); + $this->backend->expects($this->never())->method('updateCard'); + $this->backend->expects($this->never())->method('getCard'); + $addressBookImpl->expects($this->once())->method('vCard2Array') + ->with('uid.vcf', $this->vCard)->willReturn(true); + + $this->assertTrue($addressBookImpl->createOrUpdate($properties)); + } + + public static function dataTestCreate(): array { + return [ + [[]], + [['FN' => 'John Doe']], + [['FN' => 'John Doe', 'EMAIL' => ['john@doe.cloud', 'john.doe@example.org']]], + ]; + } + + public function testUpdate(): void { + $uid = 'uid'; + $uri = 'bla.vcf'; + $properties = ['URI' => $uri, 'UID' => $uid, 'FN' => 'John Doe']; + + /** @var MockObject&AddressBookImpl $addressBookImpl */ + $addressBookImpl = $this->getMockBuilder(AddressBookImpl::class) + ->setConstructorArgs( + [ + $this->addressBook, + $this->addressBookInfo, + $this->backend, + $this->urlGenerator, + $this->propertyMapper, + null + ] + ) + ->onlyMethods(['vCard2Array', 'createUid', 'createEmptyVCard', 'readCard']) + ->getMock(); + + $addressBookImpl->expects($this->never())->method('createUid'); + $addressBookImpl->expects($this->never())->method('createEmptyVCard'); + $this->backend->expects($this->once())->method('getCard') + ->with($this->addressBookInfo['id'], $uri) + ->willReturn(['carddata' => 'data']); + $addressBookImpl->expects($this->once())->method('readCard') + ->with('data')->willReturn($this->vCard); + $this->vCard->expects($this->exactly(count($properties) - 1)) + ->method('createProperty'); + $this->backend->expects($this->never())->method('createCard'); + $this->backend->expects($this->once())->method('updateCard'); + $addressBookImpl->expects($this->once())->method('vCard2Array') + ->with($uri, $this->vCard)->willReturn(true); + + $this->assertTrue($addressBookImpl->createOrUpdate($properties)); + } + + public function testUpdateWithTypes(): void { + $uid = 'uid'; + $uri = 'bla.vcf'; + $properties = ['URI' => $uri, 'UID' => $uid, 'FN' => 'John Doe', 'ADR' => [['type' => 'HOME', 'value' => ';;street;city;;;country']]]; + $vCard = new vCard; + $textProperty = $vCard->createProperty('KEY', 'value'); + + /** @var MockObject&AddressBookImpl $addressBookImpl */ + $addressBookImpl = $this->getMockBuilder(AddressBookImpl::class) + ->setConstructorArgs( + [ + $this->addressBook, + $this->addressBookInfo, + $this->backend, + $this->urlGenerator, + $this->propertyMapper, + null + ] + ) + ->onlyMethods(['vCard2Array', 'createUid', 'createEmptyVCard', 'readCard']) + ->getMock(); + + $this->backend->expects($this->once())->method('getCard') + ->with($this->addressBookInfo['id'], $uri) + ->willReturn(['carddata' => 'data']); + $addressBookImpl->expects($this->once())->method('readCard') + ->with('data')->willReturn($this->vCard); + $this->vCard->method('createProperty')->willReturn($textProperty); + $this->vCard->expects($this->exactly(count($properties) - 1)) + ->method('createProperty'); + $this->vCard->expects($this->once())->method('remove') + ->with('ADR'); + $this->vCard->expects($this->once())->method('add'); + + $addressBookImpl->createOrUpdate($properties); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestGetPermissions')] + public function testGetPermissions(array $permissions, int $expected): void { + $this->addressBook->expects($this->once())->method('getACL') + ->willReturn($permissions); + + $this->assertSame($expected, + $this->addressBookImpl->getPermissions() + ); + } + + public static function dataTestGetPermissions(): array { + return [ + [[], 0], + [[['privilege' => '{DAV:}read', 'principal' => 'principals/system/system']], 1], + [[['privilege' => '{DAV:}read', 'principal' => 'principals/system/system'], ['privilege' => '{DAV:}write', 'principal' => 'principals/someone/else']], 1], + [[['privilege' => '{DAV:}write', 'principal' => 'principals/system/system']], 6], + [[['privilege' => '{DAV:}all', 'principal' => 'principals/system/system']], 31], + [[['privilege' => '{DAV:}read', 'principal' => 'principals/system/system'],['privilege' => '{DAV:}write', 'principal' => 'principals/system/system']], 7], + [[['privilege' => '{DAV:}read', 'principal' => 'principals/system/system'],['privilege' => '{DAV:}all', 'principal' => 'principals/system/system']], 31], + [[['privilege' => '{DAV:}all', 'principal' => 'principals/system/system'],['privilege' => '{DAV:}write', 'principal' => 'principals/system/system']], 31], + [[['privilege' => '{DAV:}read', 'principal' => 'principals/system/system'],['privilege' => '{DAV:}write', 'principal' => 'principals/system/system'],['privilege' => '{DAV:}all', 'principal' => 'principals/system/system']], 31], + [[['privilege' => '{DAV:}all', 'principal' => 'principals/system/system'],['privilege' => '{DAV:}read', 'principal' => 'principals/system/system'],['privilege' => '{DAV:}write', 'principal' => 'principals/system/system']], 31], + ]; + } + + public function testDelete(): void { + $cardId = 1; + $cardUri = 'cardUri'; + $this->backend->expects($this->once())->method('getCardUri') + ->with($cardId)->willReturn($cardUri); + $this->backend->expects($this->once())->method('deleteCard') + ->with($this->addressBookInfo['id'], $cardUri) + ->willReturn(true); + + $this->assertTrue($this->addressBookImpl->delete($cardId)); + } + + public function testReadCard(): void { + $vCard = new VCard(); + $vCard->add(new Text($vCard, 'UID', 'uid')); + $vCardSerialized = $vCard->serialize(); + + $result = $this->invokePrivate($this->addressBookImpl, 'readCard', [$vCardSerialized]); + $resultSerialized = $result->serialize(); + + $this->assertSame($vCardSerialized, $resultSerialized); + } + + public function testCreateUid(): void { + /** @var MockObject&AddressBookImpl $addressBookImpl */ + $addressBookImpl = $this->getMockBuilder(AddressBookImpl::class) + ->setConstructorArgs( + [ + $this->addressBook, + $this->addressBookInfo, + $this->backend, + $this->urlGenerator, + $this->propertyMapper, + null + ] + ) + ->onlyMethods(['getUid']) + ->getMock(); + + $addressBookImpl->expects($this->exactly(2)) + ->method('getUid') + ->willReturnOnConsecutiveCalls( + 'uid0', + 'uid1', + ); + + // simulate that 'uid0' already exists, so the second uid will be returned + $this->backend->expects($this->exactly(2))->method('getContact') + ->willReturnCallback( + function ($id, $uid) { + return ($uid === 'uid0.vcf'); + } + ); + + $this->assertSame('uid1', + $this->invokePrivate($addressBookImpl, 'createUid', []) + ); + } + + public function testCreateEmptyVCard(): void { + $uid = 'uid'; + $expectedVCard = new VCard(); + $expectedVCard->UID = $uid; + $expectedVCardSerialized = $expectedVCard->serialize(); + + $result = $this->invokePrivate($this->addressBookImpl, 'createEmptyVCard', [$uid]); + $resultSerialized = $result->serialize(); + + $this->assertSame($expectedVCardSerialized, $resultSerialized); + } + + public function testVCard2Array(): void { + $vCard = new VCard(); + + $vCard->add($vCard->createProperty('FN', 'Full Name')); + + // Multi-value properties + $vCard->add($vCard->createProperty('CLOUD', 'cloud-user1@localhost')); + $vCard->add($vCard->createProperty('CLOUD', 'cloud-user2@example.tld')); + $vCard->add($vCard->createProperty('EMAIL', 'email-user1@localhost')); + $vCard->add($vCard->createProperty('EMAIL', 'email-user2@example.tld')); + $vCard->add($vCard->createProperty('IMPP', 'impp-user1@localhost')); + $vCard->add($vCard->createProperty('IMPP', 'impp-user2@example.tld')); + $vCard->add($vCard->createProperty('TEL', '+49 123456789')); + $vCard->add($vCard->createProperty('TEL', '+1 555 123456789')); + $vCard->add($vCard->createProperty('URL', 'https://localhost')); + $vCard->add($vCard->createProperty('URL', 'https://example.tld')); + + // Type depending properties + $property = $vCard->createProperty('X-SOCIALPROFILE', 'tw-example'); + $property->add('TYPE', 'twitter'); + $vCard->add($property); + $property = $vCard->createProperty('X-SOCIALPROFILE', 'tw-example-2'); + $property->add('TYPE', 'twitter'); + $vCard->add($property); + $property = $vCard->createProperty('X-SOCIALPROFILE', 'fb-example'); + $property->add('TYPE', 'facebook'); + $vCard->add($property); + + $array = $this->invokePrivate($this->addressBookImpl, 'vCard2Array', ['uri', $vCard]); + unset($array['PRODID']); + unset($array['UID']); + + $this->assertEquals([ + 'URI' => 'uri', + 'VERSION' => '4.0', + 'FN' => 'Full Name', + 'CLOUD' => [ + 'cloud-user1@localhost', + 'cloud-user2@example.tld', + ], + 'EMAIL' => [ + 'email-user1@localhost', + 'email-user2@example.tld', + ], + 'IMPP' => [ + 'impp-user1@localhost', + 'impp-user2@example.tld', + ], + 'TEL' => [ + '+49 123456789', + '+1 555 123456789', + ], + 'URL' => [ + 'https://localhost', + 'https://example.tld', + ], + + 'X-SOCIALPROFILE' => [ + 'tw-example', + 'tw-example-2', + 'fb-example', + ], + + 'isLocalSystemBook' => true, + ], $array); + } + + public function testVCard2ArrayWithTypes(): void { + $vCard = new VCard(); + + $vCard->add($vCard->createProperty('FN', 'Full Name')); + + // Multi-value properties + $vCard->add($vCard->createProperty('CLOUD', 'cloud-user1@localhost')); + $vCard->add($vCard->createProperty('CLOUD', 'cloud-user2@example.tld')); + + $property = $vCard->createProperty('EMAIL', 'email-user1@localhost'); + $property->add('TYPE', 'HOME'); + $vCard->add($property); + $property = $vCard->createProperty('EMAIL', 'email-user2@example.tld'); + $property->add('TYPE', 'WORK'); + $vCard->add($property); + + $vCard->add($vCard->createProperty('IMPP', 'impp-user1@localhost')); + $vCard->add($vCard->createProperty('IMPP', 'impp-user2@example.tld')); + + $property = $vCard->createProperty('TEL', '+49 123456789'); + $property->add('TYPE', 'HOME,VOICE'); + $vCard->add($property); + $property = $vCard->createProperty('TEL', '+1 555 123456789'); + $property->add('TYPE', 'WORK'); + $vCard->add($property); + + $vCard->add($vCard->createProperty('URL', 'https://localhost')); + $vCard->add($vCard->createProperty('URL', 'https://example.tld')); + + // Type depending properties + $property = $vCard->createProperty('X-SOCIALPROFILE', 'tw-example'); + $property->add('TYPE', 'twitter'); + $vCard->add($property); + $property = $vCard->createProperty('X-SOCIALPROFILE', 'tw-example-2'); + $property->add('TYPE', 'twitter'); + $vCard->add($property); + $property = $vCard->createProperty('X-SOCIALPROFILE', 'fb-example'); + $property->add('TYPE', 'facebook'); + $vCard->add($property); + + $array = $this->invokePrivate($this->addressBookImpl, 'vCard2Array', ['uri', $vCard, true]); + unset($array['PRODID']); + unset($array['UID']); + + $this->assertEquals([ + 'URI' => 'uri', + 'VERSION' => '4.0', + 'FN' => 'Full Name', + 'CLOUD' => [ + ['type' => '', 'value' => 'cloud-user1@localhost'], + ['type' => '', 'value' => 'cloud-user2@example.tld'], + ], + 'EMAIL' => [ + ['type' => 'HOME', 'value' => 'email-user1@localhost'], + ['type' => 'WORK', 'value' => 'email-user2@example.tld'], + ], + 'IMPP' => [ + ['type' => '', 'value' => 'impp-user1@localhost'], + ['type' => '', 'value' => 'impp-user2@example.tld'], + ], + 'TEL' => [ + ['type' => 'HOME,VOICE', 'value' => '+49 123456789'], + ['type' => 'WORK', 'value' => '+1 555 123456789'], + ], + 'URL' => [ + ['type' => '', 'value' => 'https://localhost'], + ['type' => '', 'value' => 'https://example.tld'], + ], + + 'X-SOCIALPROFILE' => [ + ['type' => 'twitter', 'value' => 'tw-example'], + ['type' => 'twitter', 'value' => 'tw-example-2'], + ['type' => 'facebook', 'value' => 'fb-example'], + ], + + 'isLocalSystemBook' => true, + ], $array); + } + + public function testIsSystemAddressBook(): void { + $addressBookInfo = [ + '{http://owncloud.org/ns}owner-principal' => 'principals/system/system', + 'principaluri' => 'principals/system/system', + '{DAV:}displayname' => 'display name', + 'id' => 666, + 'uri' => 'system', + ]; + + $addressBookImpl = new AddressBookImpl( + $this->addressBook, + $addressBookInfo, + $this->backend, + $this->urlGenerator, + $this->propertyMapper, + null + ); + + $this->assertTrue($addressBookImpl->isSystemAddressBook()); + } + + public function testIsShared(): void { + $addressBookInfo = [ + '{http://owncloud.org/ns}owner-principal' => 'user1', + '{DAV:}displayname' => 'Test address book', + 'principaluri' => 'user2', + 'id' => 666, + 'uri' => 'default', + ]; + + $addressBookImpl = new AddressBookImpl( + $this->addressBook, + $addressBookInfo, + $this->backend, + $this->urlGenerator, + $this->propertyMapper, + 'user2' + ); + + $this->assertFalse($addressBookImpl->isSystemAddressBook()); + $this->assertTrue($addressBookImpl->isShared()); + } + + public function testIsNotShared(): void { + $addressBookInfo = [ + '{http://owncloud.org/ns}owner-principal' => 'user1', + '{DAV:}displayname' => 'Test address book', + 'principaluri' => 'user1', + 'id' => 666, + 'uri' => 'default', + ]; + + $addressBookImpl = new AddressBookImpl( + $this->addressBook, + $addressBookInfo, + $this->backend, + $this->urlGenerator, + $this->propertyMapper, + 'user2' + ); + + $this->assertFalse($addressBookImpl->isSystemAddressBook()); + $this->assertFalse($addressBookImpl->isShared()); + } +} diff --git a/apps/dav/tests/unit/CardDAV/AddressBookTest.php b/apps/dav/tests/unit/CardDAV/AddressBookTest.php new file mode 100644 index 00000000000..cf28b7b8a8e --- /dev/null +++ b/apps/dav/tests/unit/CardDAV/AddressBookTest.php @@ -0,0 +1,184 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\CardDAV; + +use OCA\DAV\CardDAV\AddressBook; +use OCA\DAV\CardDAV\Card; +use OCA\DAV\CardDAV\CardDavBackend; +use OCP\IL10N; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\PropPatch; +use Test\TestCase; + +class AddressBookTest extends TestCase { + public function testMove(): void { + $backend = $this->createMock(CardDavBackend::class); + $addressBookInfo = [ + '{http://owncloud.org/ns}owner-principal' => 'user1', + '{DAV:}displayname' => 'Test address book', + 'principaluri' => 'user2', + 'id' => 666, + 'uri' => 'default', + ]; + $l10n = $this->createMock(IL10N::class); + $addressBook = new AddressBook($backend, $addressBookInfo, $l10n); + + $card = new Card($backend, $addressBookInfo, ['id' => 5, 'carddata' => 'RANDOM VCF DATA', 'uri' => 'something', 'addressbookid' => 23]); + + $backend->expects($this->once())->method('moveCard') + ->with(23, 'something', 666, 'new') + ->willReturn(true); + + $addressBook->moveInto('new', 'old', $card); + } + + public function testDelete(): void { + /** @var MockObject | CardDavBackend $backend */ + $backend = $this->createMock(CardDavBackend::class); + $backend->expects($this->once())->method('updateShares'); + $backend->expects($this->any())->method('getShares')->willReturn([ + ['href' => 'principal:user2'] + ]); + $addressBookInfo = [ + '{http://owncloud.org/ns}owner-principal' => 'user1', + '{DAV:}displayname' => 'Test address book', + 'principaluri' => 'user2', + 'id' => 666, + 'uri' => 'default', + ]; + $l10n = $this->createMock(IL10N::class); + $logger = $this->createMock(LoggerInterface::class); + $addressBook = new AddressBook($backend, $addressBookInfo, $l10n); + $addressBook->delete(); + } + + + public function testDeleteFromGroup(): void { + $this->expectException(Forbidden::class); + + /** @var MockObject | CardDavBackend $backend */ + $backend = $this->createMock(CardDavBackend::class); + $backend->expects($this->never())->method('updateShares'); + $backend->expects($this->any())->method('getShares')->willReturn([ + ['href' => 'principal:group2'] + ]); + $addressBookInfo = [ + '{http://owncloud.org/ns}owner-principal' => 'user1', + '{DAV:}displayname' => 'Test address book', + 'principaluri' => 'user2', + 'id' => 666, + 'uri' => 'default', + ]; + $l10n = $this->createMock(IL10N::class); + $logger = $this->createMock(LoggerInterface::class); + $addressBook = new AddressBook($backend, $addressBookInfo, $l10n); + $addressBook->delete(); + } + + + public function testPropPatchShared(): void { + /** @var MockObject | CardDavBackend $backend */ + $backend = $this->createMock(CardDavBackend::class); + $backend->expects($this->never())->method('updateAddressBook'); + $addressBookInfo = [ + '{http://owncloud.org/ns}owner-principal' => 'user1', + '{DAV:}displayname' => 'Test address book', + 'principaluri' => 'user2', + 'id' => 666, + 'uri' => 'default', + ]; + $l10n = $this->createMock(IL10N::class); + $logger = $this->createMock(LoggerInterface::class); + $addressBook = new AddressBook($backend, $addressBookInfo, $l10n); + $addressBook->propPatch(new PropPatch(['{DAV:}displayname' => 'Test address book'])); + } + + public function testPropPatchNotShared(): void { + /** @var MockObject | CardDavBackend $backend */ + $backend = $this->createMock(CardDavBackend::class); + $backend->expects($this->atLeast(1))->method('updateAddressBook'); + $addressBookInfo = [ + '{DAV:}displayname' => 'Test address book', + 'principaluri' => 'user1', + 'id' => 666, + 'uri' => 'default', + ]; + $l10n = $this->createMock(IL10N::class); + $logger = $this->createMock(LoggerInterface::class); + $addressBook = new AddressBook($backend, $addressBookInfo, $l10n); + $addressBook->propPatch(new PropPatch(['{DAV:}displayname' => 'Test address book'])); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providesReadOnlyInfo')] + public function testAcl(bool $expectsWrite, ?bool $readOnlyValue, bool $hasOwnerSet): void { + /** @var MockObject | CardDavBackend $backend */ + $backend = $this->createMock(CardDavBackend::class); + $backend->expects($this->any())->method('applyShareAcl')->willReturnArgument(1); + $addressBookInfo = [ + '{DAV:}displayname' => 'Test address book', + 'principaluri' => 'user2', + 'id' => 666, + 'uri' => 'default' + ]; + if (!is_null($readOnlyValue)) { + $addressBookInfo['{http://owncloud.org/ns}read-only'] = $readOnlyValue; + } + if ($hasOwnerSet) { + $addressBookInfo['{http://owncloud.org/ns}owner-principal'] = 'user1'; + } + $l10n = $this->createMock(IL10N::class); + $logger = $this->createMock(LoggerInterface::class); + $addressBook = new AddressBook($backend, $addressBookInfo, $l10n); + $acl = $addressBook->getACL(); + $childAcl = $addressBook->getChildACL(); + + $expectedAcl = [[ + 'privilege' => '{DAV:}read', + 'principal' => $hasOwnerSet ? 'user1' : 'user2', + 'protected' => true + ], [ + 'privilege' => '{DAV:}write', + 'principal' => $hasOwnerSet ? 'user1' : 'user2', + 'protected' => true + ], [ + 'privilege' => '{DAV:}write-properties', + 'principal' => $hasOwnerSet ? 'user1' : 'user2', + 'protected' => true + ]]; + if ($hasOwnerSet) { + $expectedAcl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => 'user2', + 'protected' => true + ]; + if ($expectsWrite) { + $expectedAcl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => 'user2', + 'protected' => true + ]; + } + } + $this->assertEquals($expectedAcl, $acl); + $this->assertEquals($expectedAcl, $childAcl); + } + + public static function providesReadOnlyInfo(): array { + return [ + 'read-only property not set' => [true, null, true], + 'read-only property is false' => [true, false, true], + 'read-only property is true' => [false, true, true], + 'read-only property not set and no owner' => [true, null, false], + 'read-only property is false and no owner' => [true, false, false], + 'read-only property is true and no owner' => [false, true, false], + ]; + } +} diff --git a/apps/dav/tests/unit/CardDAV/BirthdayServiceTest.php b/apps/dav/tests/unit/CardDAV/BirthdayServiceTest.php new file mode 100644 index 00000000000..6908dfd17bc --- /dev/null +++ b/apps/dav/tests/unit/CardDAV/BirthdayServiceTest.php @@ -0,0 +1,433 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\CardDAV; + +use OCA\DAV\CalDAV\BirthdayService; +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CardDAV\CardDavBackend; +use OCA\DAV\DAV\GroupPrincipalBackend; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IL10N; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Reader; +use Test\TestCase; + +class BirthdayServiceTest extends TestCase { + private CalDavBackend&MockObject $calDav; + private CardDavBackend&MockObject $cardDav; + private GroupPrincipalBackend&MockObject $groupPrincipalBackend; + private IConfig&MockObject $config; + private IDBConnection&MockObject $dbConnection; + private IL10N&MockObject $l10n; + private BirthdayService $service; + + protected function setUp(): void { + parent::setUp(); + + $this->calDav = $this->createMock(CalDavBackend::class); + $this->cardDav = $this->createMock(CardDavBackend::class); + $this->groupPrincipalBackend = $this->createMock(GroupPrincipalBackend::class); + $this->config = $this->createMock(IConfig::class); + $this->dbConnection = $this->createMock(IDBConnection::class); + $this->l10n = $this->createMock(IL10N::class); + + $this->l10n->expects($this->any()) + ->method('t') + ->willReturnCallback(function ($string, $args) { + return vsprintf($string, $args); + }); + + $this->service = new BirthdayService($this->calDav, $this->cardDav, + $this->groupPrincipalBackend, $this->config, + $this->dbConnection, $this->l10n); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providesVCards')] + public function testBuildBirthdayFromContact(?string $expectedSummary, ?string $expectedDTStart, ?string $expectedRrule, ?string $expectedFieldType, ?string $expectedUnknownYear, ?string $expectedOriginalYear, ?string $expectedReminder, ?string $data, string $fieldType, string $prefix, bool $supports4Bytes, ?string $configuredReminder): void { + $this->dbConnection->method('supports4ByteText')->willReturn($supports4Bytes); + $cal = $this->service->buildDateFromContact($data, $fieldType, $prefix, $configuredReminder); + + if ($expectedSummary === null) { + $this->assertNull($cal); + } else { + $this->assertInstanceOf('Sabre\VObject\Component\VCalendar', $cal); + $this->assertEquals('-//IDN nextcloud.com//Birthday calendar//EN', $cal->PRODID->getValue()); + $this->assertTrue(isset($cal->VEVENT)); + $this->assertEquals($expectedRrule, $cal->VEVENT->RRULE->getValue()); + $this->assertEquals($expectedSummary, $cal->VEVENT->SUMMARY->getValue()); + $this->assertEquals($expectedDTStart, $cal->VEVENT->DTSTART->getValue()); + $this->assertEquals($expectedFieldType, $cal->VEVENT->{'X-NEXTCLOUD-BC-FIELD-TYPE'}->getValue()); + $this->assertEquals($expectedUnknownYear, $cal->VEVENT->{'X-NEXTCLOUD-BC-UNKNOWN-YEAR'}->getValue()); + + if ($expectedOriginalYear) { + $this->assertEquals($expectedOriginalYear, $cal->VEVENT->{'X-NEXTCLOUD-BC-YEAR'}->getValue()); + } + + if ($expectedReminder) { + $this->assertEquals($expectedReminder, $cal->VEVENT->VALARM->TRIGGER->getValue()); + $this->assertEquals('DURATION', $cal->VEVENT->VALARM->TRIGGER->getValueType()); + } + + $this->assertEquals('TRANSPARENT', $cal->VEVENT->TRANSP->getValue()); + } + } + + public function testOnCardDeleteGloballyDisabled(): void { + $this->config->expects($this->once()) + ->method('getAppValue') + ->with('dav', 'generateBirthdayCalendar', 'yes') + ->willReturn('no'); + + $this->cardDav->expects($this->never())->method('getAddressBookById'); + + $this->service->onCardDeleted(666, 'gump.vcf'); + } + + public function testOnCardDeleteUserDisabled(): void { + $this->config->expects($this->once()) + ->method('getAppValue') + ->with('dav', 'generateBirthdayCalendar', 'yes') + ->willReturn('yes'); + + $this->config->expects($this->once()) + ->method('getUserValue') + ->with('user01', 'dav', 'generateBirthdayCalendar', 'yes') + ->willReturn('no'); + + $this->cardDav->expects($this->once())->method('getAddressBookById') + ->with(666) + ->willReturn([ + 'principaluri' => 'principals/users/user01', + 'uri' => 'default' + ]); + $this->cardDav->expects($this->once())->method('getShares')->willReturn([]); + $this->calDav->expects($this->never())->method('getCalendarByUri'); + $this->calDav->expects($this->never())->method('deleteCalendarObject'); + + $this->service->onCardDeleted(666, 'gump.vcf'); + } + + public function testOnCardDeleted(): void { + $this->config->expects($this->once()) + ->method('getAppValue') + ->with('dav', 'generateBirthdayCalendar', 'yes') + ->willReturn('yes'); + + $this->config->expects($this->once()) + ->method('getUserValue') + ->with('user01', 'dav', 'generateBirthdayCalendar', 'yes') + ->willReturn('yes'); + + $this->cardDav->expects($this->once())->method('getAddressBookById') + ->with(666) + ->willReturn([ + 'principaluri' => 'principals/users/user01', + 'uri' => 'default' + ]); + $this->calDav->expects($this->once())->method('getCalendarByUri') + ->with('principals/users/user01', 'contact_birthdays') + ->willReturn([ + 'id' => 1234 + ]); + $calls = [ + [1234, 'default-gump.vcf.ics'], + [1234, 'default-gump.vcf-death.ics'], + [1234, 'default-gump.vcf-anniversary.ics'], + ]; + $this->calDav->expects($this->exactly(count($calls))) + ->method('deleteCalendarObject') + ->willReturnCallback(function ($calendarId, $objectUri) use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, [$calendarId, $objectUri]); + }); + $this->cardDav->expects($this->once())->method('getShares')->willReturn([]); + + $this->service->onCardDeleted(666, 'gump.vcf'); + } + + public function testOnCardChangedGloballyDisabled(): void { + $this->config->expects($this->once()) + ->method('getAppValue') + ->with('dav', 'generateBirthdayCalendar', 'yes') + ->willReturn('no'); + + $this->cardDav->expects($this->never())->method('getAddressBookById'); + + $service = $this->getMockBuilder(BirthdayService::class) + ->onlyMethods(['buildDateFromContact', 'birthdayEvenChanged']) + ->setConstructorArgs([$this->calDav, $this->cardDav, $this->groupPrincipalBackend, $this->config, $this->dbConnection, $this->l10n]) + ->getMock(); + + $service->onCardChanged(666, 'gump.vcf', ''); + } + + public function testOnCardChangedUserDisabled(): void { + $this->config->expects($this->once()) + ->method('getAppValue') + ->with('dav', 'generateBirthdayCalendar', 'yes') + ->willReturn('yes'); + + $this->config->expects($this->once()) + ->method('getUserValue') + ->with('user01', 'dav', 'generateBirthdayCalendar', 'yes') + ->willReturn('no'); + + $this->cardDav->expects($this->once())->method('getAddressBookById') + ->with(666) + ->willReturn([ + 'principaluri' => 'principals/users/user01', + 'uri' => 'default' + ]); + $this->cardDav->expects($this->once())->method('getShares')->willReturn([]); + $this->calDav->expects($this->never())->method('getCalendarByUri'); + + /** @var BirthdayService&MockObject $service */ + $service = $this->getMockBuilder(BirthdayService::class) + ->onlyMethods(['buildDateFromContact', 'birthdayEvenChanged']) + ->setConstructorArgs([$this->calDav, $this->cardDav, $this->groupPrincipalBackend, $this->config, $this->dbConnection, $this->l10n]) + ->getMock(); + + $service->onCardChanged(666, 'gump.vcf', ''); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providesCardChanges')] + public function testOnCardChanged(string $expectedOp): void { + $this->config->expects($this->once()) + ->method('getAppValue') + ->with('dav', 'generateBirthdayCalendar', 'yes') + ->willReturn('yes'); + + $this->config->expects($this->exactly(2)) + ->method('getUserValue') + ->willReturnMap([ + ['user01', 'dav', 'generateBirthdayCalendar', 'yes', 'yes'], + ['user01', 'dav', 'birthdayCalendarReminderOffset', 'PT9H', 'PT9H'], + ]); + + $this->cardDav->expects($this->once())->method('getAddressBookById') + ->with(666) + ->willReturn([ + 'principaluri' => 'principals/users/user01', + 'uri' => 'default' + ]); + $this->calDav->expects($this->once())->method('getCalendarByUri') + ->with('principals/users/user01', 'contact_birthdays') + ->willReturn([ + 'id' => 1234 + ]); + $this->cardDav->expects($this->once())->method('getShares')->willReturn([]); + + /** @var BirthdayService&MockObject $service */ + $service = $this->getMockBuilder(BirthdayService::class) + ->onlyMethods(['buildDateFromContact', 'birthdayEvenChanged']) + ->setConstructorArgs([$this->calDav, $this->cardDav, $this->groupPrincipalBackend, $this->config, $this->dbConnection, $this->l10n]) + ->getMock(); + + if ($expectedOp === 'delete') { + $this->calDav->expects($this->exactly(3))->method('getCalendarObject')->willReturn(''); + $service->expects($this->exactly(3))->method('buildDateFromContact')->willReturn(null); + + $calls = [ + [1234, 'default-gump.vcf.ics'], + [1234, 'default-gump.vcf-death.ics'], + [1234, 'default-gump.vcf-anniversary.ics'] + ]; + $this->calDav->expects($this->exactly(count($calls))) + ->method('deleteCalendarObject') + ->willReturnCallback(function ($calendarId, $objectUri) use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, [$calendarId, $objectUri]); + }); + } + if ($expectedOp === 'create') { + $vCal = new VCalendar(); + $vCal->PRODID = '-//Nextcloud testing//mocked object//'; + + $service->expects($this->exactly(3))->method('buildDateFromContact')->willReturn($vCal); + + $createCalendarObjectCalls = [ + [1234, 'default-gump.vcf.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nPRODID:-//Nextcloud testing//mocked object//\r\nEND:VCALENDAR\r\n"], + [1234, 'default-gump.vcf-death.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nPRODID:-//Nextcloud testing//mocked object//\r\nEND:VCALENDAR\r\n"], + [1234, 'default-gump.vcf-anniversary.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nPRODID:-//Nextcloud testing//mocked object//\r\nEND:VCALENDAR\r\n"] + ]; + $this->calDav->expects($this->exactly(count($createCalendarObjectCalls))) + ->method('createCalendarObject') + ->willReturnCallback(function ($calendarId, $objectUri, $calendarData) use (&$createCalendarObjectCalls): void { + $expected = array_shift($createCalendarObjectCalls); + $this->assertEquals($expected, [$calendarId, $objectUri, $calendarData]); + }); + } + if ($expectedOp === 'update') { + $vCal = new VCalendar(); + $vCal->PRODID = '-//Nextcloud testing//mocked object//'; + + $service->expects($this->exactly(3))->method('buildDateFromContact')->willReturn($vCal); + $service->expects($this->exactly(3))->method('birthdayEvenChanged')->willReturn(true); + $this->calDav->expects($this->exactly(3))->method('getCalendarObject')->willReturn(['calendardata' => '']); + + $updateCalendarObjectCalls = [ + [1234, 'default-gump.vcf.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nPRODID:-//Nextcloud testing//mocked object//\r\nEND:VCALENDAR\r\n"], + [1234, 'default-gump.vcf-death.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nPRODID:-//Nextcloud testing//mocked object//\r\nEND:VCALENDAR\r\n"], + [1234, 'default-gump.vcf-anniversary.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nCALSCALE:GREGORIAN\r\nPRODID:-//Nextcloud testing//mocked object//\r\nEND:VCALENDAR\r\n"] + ]; + $this->calDav->expects($this->exactly(count($updateCalendarObjectCalls))) + ->method('updateCalendarObject') + ->willReturnCallback(function ($calendarId, $objectUri, $calendarData) use (&$updateCalendarObjectCalls): void { + $expected = array_shift($updateCalendarObjectCalls); + $this->assertEquals($expected, [$calendarId, $objectUri, $calendarData]); + }); + } + + $service->onCardChanged(666, 'gump.vcf', ''); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providesBirthday')] + public function testBirthdayEvenChanged(bool $expected, string $old, string $new): void { + $new = Reader::read($new); + $this->assertEquals($expected, $this->service->birthdayEvenChanged($old, $new)); + } + + public function testGetAllAffectedPrincipals(): void { + $this->cardDav->expects($this->once())->method('getShares')->willReturn([ + [ + '{http://owncloud.org/ns}group-share' => false, + '{http://owncloud.org/ns}principal' => 'principals/users/user01' + ], + [ + '{http://owncloud.org/ns}group-share' => false, + '{http://owncloud.org/ns}principal' => 'principals/users/user01' + ], + [ + '{http://owncloud.org/ns}group-share' => false, + '{http://owncloud.org/ns}principal' => 'principals/users/user02' + ], + [ + '{http://owncloud.org/ns}group-share' => true, + '{http://owncloud.org/ns}principal' => 'principals/groups/users' + ], + ]); + $this->groupPrincipalBackend->expects($this->once())->method('getGroupMemberSet') + ->willReturn([ + [ + 'uri' => 'principals/users/user01', + ], + [ + 'uri' => 'principals/users/user02', + ], + [ + 'uri' => 'principals/users/user03', + ], + ]); + $users = $this->invokePrivate($this->service, 'getAllAffectedPrincipals', [6666]); + $this->assertEquals([ + 'principals/users/user01', + 'principals/users/user02', + 'principals/users/user03' + ], $users); + } + + public function testBirthdayCalendarHasComponentEvent(): void { + $this->calDav->expects($this->once()) + ->method('createCalendar') + ->with('principal001', 'contact_birthdays', [ + '{DAV:}displayname' => 'Contact birthdays', + '{http://apple.com/ns/ical/}calendar-color' => '#E9D859', + 'components' => 'VEVENT', + ]); + $this->service->ensureCalendarExists('principal001'); + } + + public function testResetForUser(): void { + $this->calDav->expects($this->once()) + ->method('getCalendarByUri') + ->with('principals/users/user123', 'contact_birthdays') + ->willReturn(['id' => 42]); + + $this->calDav->expects($this->once()) + ->method('getCalendarObjects') + ->with(42, 0) + ->willReturn([['uri' => '1.ics'], ['uri' => '2.ics'], ['uri' => '3.ics']]); + + $calls = [ + [42, '1.ics', 0], + [42, '2.ics', 0], + [42, '3.ics', 0], + ]; + $this->calDav->expects($this->exactly(count($calls))) + ->method('deleteCalendarObject') + ->willReturnCallback(function ($calendarId, $objectUri, $calendarType) use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, [$calendarId, $objectUri, $calendarType]); + }); + + $this->service->resetForUser('user123'); + } + + public static function providesBirthday(): array { + return [ + [true, + '', + "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"], + [false, + "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", + "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"], + [true, + "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:4567's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", + "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"], + [true, + "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", + "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000102\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"] + ]; + } + + public static function providesCardChanges(): array { + return[ + ['delete'], + ['create'], + ['update'] + ]; + } + + public static function providesVCards(): array { + return [ + // $expectedSummary, $expectedDTStart, $expectedRrule, $expectedFieldType, $expectedUnknownYear, $expectedOriginalYear, $expectedReminder, $data, $fieldType, $prefix, $supports4Byte, $configuredReminder + [null, null, null, null, null, null, null, 'yasfewf', '', '', true, null], + [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nEND:VCARD\r\n", 'BDAY', '', true, null], + [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:\r\nEND:VCARD\r\n", 'BDAY', '', true, null], + [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:someday\r\nEND:VCARD\r\n", 'BDAY', '', true, null], + ['🎂 12345 (1900)', '19700101', 'FREQ=YEARLY', 'BDAY', '0', '1900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19000101\r\nEND:VCARD\r\n", 'BDAY', '', true, null], + ['🎂 12345 (1900)', '19701231', 'FREQ=YEARLY', 'BDAY', '0', '1900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19001231\r\nEND:VCARD\r\n", 'BDAY', '', true, null], + ['Death of 12345 (1900)', '19701231', 'FREQ=YEARLY', 'DEATHDATE', '0', '1900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nDEATHDATE:19001231\r\nEND:VCARD\r\n", 'DEATHDATE', '-death', true, null], + ['Death of 12345 (1900)', '19701231', 'FREQ=YEARLY', 'DEATHDATE', '0', '1900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nDEATHDATE:19001231\r\nEND:VCARD\r\n", 'DEATHDATE', '-death', false, null], + ['💍 12345 (1900)', '19701231', 'FREQ=YEARLY', 'ANNIVERSARY', '0', '1900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nANNIVERSARY:19001231\r\nEND:VCARD\r\n", 'ANNIVERSARY', '-anniversary', true, null], + ['12345 (⚭1900)', '19701231', 'FREQ=YEARLY', 'ANNIVERSARY', '0', '1900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nANNIVERSARY:19001231\r\nEND:VCARD\r\n", 'ANNIVERSARY', '-anniversary', false, null], + ['🎂 12345', '19701231', 'FREQ=YEARLY', 'BDAY', '1', null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:--1231\r\nEND:VCARD\r\n", 'BDAY', '', true, null], + ['🎂 12345', '19701231', 'FREQ=YEARLY', 'BDAY', '1', null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY;X-APPLE-OMIT-YEAR=1604:16041231\r\nEND:VCARD\r\n", 'BDAY', '', true, null], + [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:;VALUE=text:circa 1800\r\nEND:VCARD\r\n", 'BDAY', '', true, null], + [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nN:12345;;;;\r\nBDAY:20031231\r\nEND:VCARD\r\n", 'BDAY', '', true, null], + ['🎂 12345 (900)', '19701231', 'FREQ=YEARLY', 'BDAY', '0', '900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:09001231\r\nEND:VCARD\r\n", 'BDAY', '', true, null], + ['12345 (*1900)', '19700101', 'FREQ=YEARLY', 'BDAY', '0', '1900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19000101\r\nEND:VCARD\r\n", 'BDAY', '', false, null], + ['12345 (*1900)', '19701231', 'FREQ=YEARLY', 'BDAY', '0', '1900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19001231\r\nEND:VCARD\r\n", 'BDAY', '', false, null], + ['12345 *', '19701231', 'FREQ=YEARLY', 'BDAY', '1', null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:--1231\r\nEND:VCARD\r\n", 'BDAY', '', false, null], + ['12345 *', '19701231', 'FREQ=YEARLY', 'BDAY', '1', null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY;X-APPLE-OMIT-YEAR=1604:16041231\r\nEND:VCARD\r\n", 'BDAY', '', false, null], + [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:;VALUE=text:circa 1800\r\nEND:VCARD\r\n", 'BDAY', '', false, null], + [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nN:12345;;;;\r\nBDAY:20031231\r\nEND:VCARD\r\n", 'BDAY', '', false, null], + ['12345 (*900)', '19701231', 'FREQ=YEARLY', 'BDAY', '0', '900', null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:09001231\r\nEND:VCARD\r\n", 'BDAY', '', false, null], + ['12345 (*1900)', '19701231', 'FREQ=YEARLY', 'BDAY', '0', '1900', 'PT9H', "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19001231\r\nEND:VCARD\r\n", 'BDAY', '', false, 'PT9H'], + ['12345 (*1900)', '19701231', 'FREQ=YEARLY', 'BDAY', '0', '1900', '-PT15H', "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19001231\r\nEND:VCARD\r\n", 'BDAY', '', false, '-PT15H'], + ['12345 (*1900)', '19701231', 'FREQ=YEARLY', 'BDAY', '0', '1900', '-P6DT15H', "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19001231\r\nEND:VCARD\r\n", 'BDAY', '', false, '-P6DT15H'], + [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19000101\r\nX-NC-EXCLUDE-FROM-BIRTHDAY-CALENDAR;TYPE=boolean:true\r\nEND:VCARD\r\n", 'BDAY', '', true, null], + [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nX-NC-EXCLUDE-FROM-BIRTHDAY-CALENDAR;TYPE=boolean:true\r\nDEATHDATE:19001231\r\nEND:VCARD\r\n", 'DEATHDATE', '-death', true, null], + [null, null, null, null, null, null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nANNIVERSARY:19001231\r\nX-NC-EXCLUDE-FROM-BIRTHDAY-CALENDAR;TYPE=boolean:true\r\nEND:VCARD\r\n", 'ANNIVERSARY', '-anniversary', true, null], + ['🎂 12345 (1902)', '19720229', 'FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=-1', 'BDAY', '0', null, null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19020229\r\nEND:VCARD\r\n", 'BDAY', '', true, null], + ]; + } +} diff --git a/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php b/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php new file mode 100644 index 00000000000..c5eafa0764a --- /dev/null +++ b/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php @@ -0,0 +1,918 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\CardDAV; + +use OC\KnownUser\KnownUserService; +use OCA\DAV\CalDAV\Proxy\ProxyMapper; +use OCA\DAV\CardDAV\AddressBook; +use OCA\DAV\CardDAV\CardDavBackend; +use OCA\DAV\CardDAV\Sharing\Backend; +use OCA\DAV\CardDAV\Sharing\Service; +use OCA\DAV\Connector\Sabre\Principal; +use OCA\DAV\DAV\Sharing\SharingMapper; +use OCP\Accounts\IAccountManager; +use OCP\App\IAppManager; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\ICacheFactory; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IGroupManager; +use OCP\IL10N; +use OCP\IUserManager; +use OCP\IUserSession; +use OCP\L10N\IFactory; +use OCP\Server; +use OCP\Share\IManager as ShareManager; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Sabre\DAV\Exception\BadRequest; +use Sabre\DAV\PropPatch; +use Sabre\VObject\Component\VCard; +use Sabre\VObject\Property\Text; +use Test\TestCase; +use function time; + +/** + * Class CardDavBackendTest + * + * @group DB + * + * @package OCA\DAV\Tests\unit\CardDAV + */ +class CardDavBackendTest extends TestCase { + private Principal&MockObject $principal; + private IUserManager&MockObject $userManager; + private IGroupManager&MockObject $groupManager; + private IEventDispatcher&MockObject $dispatcher; + private IConfig&MockObject $config; + private Backend $sharingBackend; + private IDBConnection $db; + private CardDavBackend $backend; + private string $dbCardsTable = 'cards'; + private string $dbCardsPropertiesTable = 'cards_properties'; + + public const UNIT_TEST_USER = 'principals/users/carddav-unit-test'; + public const UNIT_TEST_USER1 = 'principals/users/carddav-unit-test1'; + public const UNIT_TEST_GROUP = 'principals/groups/carddav-unit-test-group'; + + private $vcardTest0 = 'BEGIN:VCARD' . PHP_EOL + . 'VERSION:3.0' . PHP_EOL + . 'PRODID:-//Sabre//Sabre VObject 4.1.2//EN' . PHP_EOL + . 'UID:Test' . PHP_EOL + . 'FN:Test' . PHP_EOL + . 'N:Test;;;;' . PHP_EOL + . 'END:VCARD'; + + private $vcardTest1 = 'BEGIN:VCARD' . PHP_EOL + . 'VERSION:3.0' . PHP_EOL + . 'PRODID:-//Sabre//Sabre VObject 4.1.2//EN' . PHP_EOL + . 'UID:Test2' . PHP_EOL + . 'FN:Test2' . PHP_EOL + . 'N:Test2;;;;' . PHP_EOL + . 'END:VCARD'; + + private $vcardTest2 = 'BEGIN:VCARD' . PHP_EOL + . 'VERSION:3.0' . PHP_EOL + . 'PRODID:-//Sabre//Sabre VObject 4.1.2//EN' . PHP_EOL + . 'UID:Test3' . PHP_EOL + . 'FN:Test3' . PHP_EOL + . 'N:Test3;;;;' . PHP_EOL + . 'END:VCARD'; + + private $vcardTestNoUID = 'BEGIN:VCARD' . PHP_EOL + . 'VERSION:3.0' . PHP_EOL + . 'PRODID:-//Sabre//Sabre VObject 4.1.2//EN' . PHP_EOL + . 'FN:TestNoUID' . PHP_EOL + . 'N:TestNoUID;;;;' . PHP_EOL + . 'END:VCARD'; + + protected function setUp(): void { + parent::setUp(); + + $this->userManager = $this->createMock(IUserManager::class); + $this->groupManager = $this->createMock(IGroupManager::class); + $this->config = $this->createMock(IConfig::class); + $this->principal = $this->getMockBuilder(Principal::class) + ->setConstructorArgs([ + $this->userManager, + $this->groupManager, + $this->createMock(IAccountManager::class), + $this->createMock(ShareManager::class), + $this->createMock(IUserSession::class), + $this->createMock(IAppManager::class), + $this->createMock(ProxyMapper::class), + $this->createMock(KnownUserService::class), + $this->config, + $this->createMock(IFactory::class) + ]) + ->onlyMethods(['getPrincipalByPath', 'getGroupMembership', 'findByUri']) + ->getMock(); + $this->principal->method('getPrincipalByPath') + ->willReturn([ + 'uri' => 'principals/best-friend', + '{DAV:}displayname' => 'User\'s displayname', + ]); + $this->principal->method('getGroupMembership') + ->withAnyParameters() + ->willReturn([self::UNIT_TEST_GROUP]); + $this->dispatcher = $this->createMock(IEventDispatcher::class); + + $this->db = Server::get(IDBConnection::class); + $this->sharingBackend = new Backend($this->userManager, + $this->groupManager, + $this->principal, + $this->createMock(ICacheFactory::class), + new Service(new SharingMapper($this->db)), + $this->createMock(LoggerInterface::class) + ); + + $this->backend = new CardDavBackend($this->db, + $this->principal, + $this->userManager, + $this->dispatcher, + $this->sharingBackend, + $this->config, + ); + // start every test with a empty cards_properties and cards table + $query = $this->db->getQueryBuilder(); + $query->delete('cards_properties')->executeStatement(); + $query = $this->db->getQueryBuilder(); + $query->delete('cards')->executeStatement(); + + $this->principal->method('getGroupMembership') + ->withAnyParameters() + ->willReturn([self::UNIT_TEST_GROUP]); + $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); + foreach ($books as $book) { + $this->backend->deleteAddressBook($book['id']); + } + } + + protected function tearDown(): void { + if (is_null($this->backend)) { + return; + } + + $this->principal->method('getGroupMembership') + ->withAnyParameters() + ->willReturn([self::UNIT_TEST_GROUP]); + $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); + foreach ($books as $book) { + $this->backend->deleteAddressBook($book['id']); + } + + parent::tearDown(); + } + + public function testAddressBookOperations(): void { + // create a new address book + $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); + + $this->assertEquals(1, $this->backend->getAddressBooksForUserCount(self::UNIT_TEST_USER)); + $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); + $this->assertEquals(1, count($books)); + $this->assertEquals('Example', $books[0]['{DAV:}displayname']); + $this->assertEquals('User\'s displayname', $books[0]['{http://nextcloud.com/ns}owner-displayname']); + + // update its display name + $patch = new PropPatch([ + '{DAV:}displayname' => 'Unit test', + '{urn:ietf:params:xml:ns:carddav}addressbook-description' => 'Addressbook used for unit testing' + ]); + $this->backend->updateAddressBook($books[0]['id'], $patch); + $patch->commit(); + $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); + $this->assertEquals(1, count($books)); + $this->assertEquals('Unit test', $books[0]['{DAV:}displayname']); + $this->assertEquals('Addressbook used for unit testing', $books[0]['{urn:ietf:params:xml:ns:carddav}addressbook-description']); + + // delete the address book + $this->backend->deleteAddressBook($books[0]['id']); + $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); + $this->assertEquals(0, count($books)); + } + + public function testAddressBookSharing(): void { + $this->userManager->expects($this->any()) + ->method('userExists') + ->willReturn(true); + $this->groupManager->expects($this->any()) + ->method('groupExists') + ->willReturn(true); + $this->principal->expects(self::atLeastOnce()) + ->method('findByUri') + ->willReturnOnConsecutiveCalls(self::UNIT_TEST_USER1, self::UNIT_TEST_GROUP); + + $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); + $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); + $this->assertEquals(1, count($books)); + $l = $this->createMock(IL10N::class); + $addressBook = new AddressBook($this->backend, $books[0], $l); + $this->backend->updateShares($addressBook, [ + [ + 'href' => 'principal:' . self::UNIT_TEST_USER1, + ], + [ + 'href' => 'principal:' . self::UNIT_TEST_GROUP, + ] + ], []); + $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER1); + $this->assertEquals(1, count($books)); + + // delete the address book + $this->backend->deleteAddressBook($books[0]['id']); + $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); + $this->assertEquals(0, count($books)); + } + + public function testCardOperations(): void { + /** @var CardDavBackend&MockObject $backend */ + $backend = $this->getMockBuilder(CardDavBackend::class) + ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend,$this->config]) + ->onlyMethods(['updateProperties', 'purgeProperties']) + ->getMock(); + + // create a new address book + $backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); + $books = $backend->getAddressBooksForUser(self::UNIT_TEST_USER); + $this->assertEquals(1, count($books)); + $bookId = $books[0]['id']; + + $uri = $this->getUniqueID('card'); + // updateProperties is expected twice, once for createCard and once for updateCard + $calls = [ + [$bookId, $uri, $this->vcardTest0], + [$bookId, $uri, $this->vcardTest1], + ]; + $backend->expects($this->exactly(count($calls))) + ->method('updateProperties') + ->willReturnCallback(function () use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + }); + + // Expect event + $this->dispatcher + ->expects($this->exactly(3)) + ->method('dispatchTyped'); + + // create a card + $backend->createCard($bookId, $uri, $this->vcardTest0); + + // get all the cards + $cards = $backend->getCards($bookId); + $this->assertEquals(1, count($cards)); + $this->assertEquals($this->vcardTest0, $cards[0]['carddata']); + + // get the cards + $card = $backend->getCard($bookId, $uri); + $this->assertNotNull($card); + $this->assertArrayHasKey('id', $card); + $this->assertArrayHasKey('uri', $card); + $this->assertArrayHasKey('lastmodified', $card); + $this->assertArrayHasKey('etag', $card); + $this->assertArrayHasKey('size', $card); + $this->assertEquals($this->vcardTest0, $card['carddata']); + + // update the card + $backend->updateCard($bookId, $uri, $this->vcardTest1); + $card = $backend->getCard($bookId, $uri); + $this->assertEquals($this->vcardTest1, $card['carddata']); + + // delete the card + $backend->expects($this->once())->method('purgeProperties')->with($bookId, $card['id']); + $backend->deleteCard($bookId, $uri); + $cards = $backend->getCards($bookId); + $this->assertEquals(0, count($cards)); + } + + public function testMultiCard(): void { + $this->backend = $this->getMockBuilder(CardDavBackend::class) + ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend,$this->config]) + ->onlyMethods(['updateProperties']) + ->getMock(); + + // create a new address book + $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); + $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); + $this->assertEquals(1, count($books)); + $bookId = $books[0]['id']; + + // create a card + $uri0 = self::getUniqueID('card'); + $this->backend->createCard($bookId, $uri0, $this->vcardTest0); + $uri1 = self::getUniqueID('card'); + $this->backend->createCard($bookId, $uri1, $this->vcardTest1); + $uri2 = self::getUniqueID('card'); + $this->backend->createCard($bookId, $uri2, $this->vcardTest2); + + // get all the cards + $cards = $this->backend->getCards($bookId); + $this->assertEquals(3, count($cards)); + usort($cards, function ($a, $b) { + return $a['id'] < $b['id'] ? -1 : 1; + }); + + $this->assertEquals($this->vcardTest0, $cards[0]['carddata']); + $this->assertEquals($this->vcardTest1, $cards[1]['carddata']); + $this->assertEquals($this->vcardTest2, $cards[2]['carddata']); + + // get the cards 1 & 2 (not 0) + $cards = $this->backend->getMultipleCards($bookId, [$uri1, $uri2]); + $this->assertEquals(2, count($cards)); + usort($cards, function ($a, $b) { + return $a['id'] < $b['id'] ? -1 : 1; + }); + foreach ($cards as $index => $card) { + $this->assertArrayHasKey('id', $card); + $this->assertArrayHasKey('uri', $card); + $this->assertArrayHasKey('lastmodified', $card); + $this->assertArrayHasKey('etag', $card); + $this->assertArrayHasKey('size', $card); + $this->assertEquals($this->{ 'vcardTest' . ($index + 1) }, $card['carddata']); + } + + // delete the card + $this->backend->deleteCard($bookId, $uri0); + $this->backend->deleteCard($bookId, $uri1); + $this->backend->deleteCard($bookId, $uri2); + $cards = $this->backend->getCards($bookId); + $this->assertEquals(0, count($cards)); + } + + public function testMultipleUIDOnDifferentAddressbooks(): void { + $this->backend = $this->getMockBuilder(CardDavBackend::class) + ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend,$this->config]) + ->onlyMethods(['updateProperties']) + ->getMock(); + + // create 2 new address books + $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); + $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example2', []); + $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); + $this->assertEquals(2, count($books)); + $bookId0 = $books[0]['id']; + $bookId1 = $books[1]['id']; + + // create a card + $uri0 = $this->getUniqueID('card'); + $this->backend->createCard($bookId0, $uri0, $this->vcardTest0); + + // create another card with same uid but in second address book + $uri1 = $this->getUniqueID('card'); + $this->backend->createCard($bookId1, $uri1, $this->vcardTest0); + } + + public function testMultipleUIDDenied(): void { + $this->backend = $this->getMockBuilder(CardDavBackend::class) + ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend, $this->config]) + ->onlyMethods(['updateProperties']) + ->getMock(); + + // create a new address book + $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); + $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); + $this->assertEquals(1, count($books)); + $bookId = $books[0]['id']; + + // create a card + $uri0 = $this->getUniqueID('card'); + $this->backend->createCard($bookId, $uri0, $this->vcardTest0); + + // create another card with same uid + $uri1 = $this->getUniqueID('card'); + $this->expectException(BadRequest::class); + $test = $this->backend->createCard($bookId, $uri1, $this->vcardTest0); + } + + public function testNoValidUID(): void { + $this->backend = $this->getMockBuilder(CardDavBackend::class) + ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend, $this->config]) + ->onlyMethods(['updateProperties']) + ->getMock(); + + // create a new address book + $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); + $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); + $this->assertEquals(1, count($books)); + $bookId = $books[0]['id']; + + // create a card without uid + $uri1 = $this->getUniqueID('card'); + $this->expectException(BadRequest::class); + $test = $this->backend->createCard($bookId, $uri1, $this->vcardTestNoUID); + } + + public function testDeleteWithoutCard(): void { + $this->backend = $this->getMockBuilder(CardDavBackend::class) + ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend, $this->config]) + ->onlyMethods([ + 'getCardId', + 'addChange', + 'purgeProperties', + 'updateProperties', + ]) + ->getMock(); + + // create a new address book + $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); + $books = $this->backend->getUsersOwnAddressBooks(self::UNIT_TEST_USER); + $this->assertEquals(1, count($books)); + + $bookId = $books[0]['id']; + $uri = $this->getUniqueID('card'); + + // create a new address book + $this->backend->expects($this->once()) + ->method('getCardId') + ->with($bookId, $uri) + ->willThrowException(new \InvalidArgumentException()); + + $calls = [ + [$bookId, $uri, 1], + [$bookId, $uri, 3], + ]; + $this->backend->expects($this->exactly(count($calls))) + ->method('addChange') + ->willReturnCallback(function () use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + }); + $this->backend->expects($this->never()) + ->method('purgeProperties'); + + // create a card + $this->backend->createCard($bookId, $uri, $this->vcardTest0); + + // delete the card + $this->assertTrue($this->backend->deleteCard($bookId, $uri)); + } + + public function testSyncSupport(): void { + $this->backend = $this->getMockBuilder(CardDavBackend::class) + ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend, $this->config]) + ->onlyMethods(['updateProperties']) + ->getMock(); + + // create a new address book + $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); + $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); + $this->assertEquals(1, count($books)); + $bookId = $books[0]['id']; + + // fist call without synctoken + $changes = $this->backend->getChangesForAddressBook($bookId, '', 1); + $syncToken = $changes['syncToken']; + + // add a change + $uri0 = $this->getUniqueID('card'); + $this->backend->createCard($bookId, $uri0, $this->vcardTest0); + + // look for changes + $changes = $this->backend->getChangesForAddressBook($bookId, $syncToken, 1); + $this->assertEquals($uri0, $changes['added'][0]); + } + + public function testSharing(): void { + $this->userManager->expects($this->any()) + ->method('userExists') + ->willReturn(true); + $this->groupManager->expects($this->any()) + ->method('groupExists') + ->willReturn(true); + $this->principal->expects(self::any()) + ->method('findByUri') + ->willReturn(self::UNIT_TEST_USER1); + + $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); + $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); + $this->assertEquals(1, count($books)); + + $l = $this->createMock(IL10N::class); + $exampleBook = new AddressBook($this->backend, $books[0], $l); + $this->backend->updateShares($exampleBook, [['href' => 'principal:' . self::UNIT_TEST_USER1]], []); + + $shares = $this->backend->getShares($exampleBook->getResourceId()); + $this->assertEquals(1, count($shares)); + + // adding the same sharee again has no effect + $this->backend->updateShares($exampleBook, [['href' => 'principal:' . self::UNIT_TEST_USER1]], []); + + $shares = $this->backend->getShares($exampleBook->getResourceId()); + $this->assertEquals(1, count($shares)); + + $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER1); + $this->assertEquals(1, count($books)); + + $this->backend->updateShares($exampleBook, [], ['principal:' . self::UNIT_TEST_USER1]); + + $shares = $this->backend->getShares($exampleBook->getResourceId()); + $this->assertEquals(0, count($shares)); + + $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER1); + $this->assertEquals(0, count($books)); + } + + public function testUpdateProperties(): void { + $bookId = 42; + $cardUri = 'card-uri'; + $cardId = 2; + + $backend = $this->getMockBuilder(CardDavBackend::class) + ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend, $this->config]) + ->onlyMethods(['getCardId'])->getMock(); + + $backend->expects($this->any())->method('getCardId')->willReturn($cardId); + + // add properties for new vCard + $vCard = new VCard(); + $vCard->UID = $cardUri; + $vCard->FN = 'John Doe'; + $this->invokePrivate($backend, 'updateProperties', [$bookId, $cardUri, $vCard->serialize()]); + + $query = $this->db->getQueryBuilder(); + $query->select('*') + ->from('cards_properties') + ->orderBy('name'); + + $qResult = $query->execute(); + $result = $qResult->fetchAll(); + $qResult->closeCursor(); + + $this->assertSame(2, count($result)); + + $this->assertSame('FN', $result[0]['name']); + $this->assertSame('John Doe', $result[0]['value']); + $this->assertSame($bookId, (int)$result[0]['addressbookid']); + $this->assertSame($cardId, (int)$result[0]['cardid']); + + $this->assertSame('UID', $result[1]['name']); + $this->assertSame($cardUri, $result[1]['value']); + $this->assertSame($bookId, (int)$result[1]['addressbookid']); + $this->assertSame($cardId, (int)$result[1]['cardid']); + + // update properties for existing vCard + $vCard = new VCard(); + $vCard->UID = $cardUri; + $this->invokePrivate($backend, 'updateProperties', [$bookId, $cardUri, $vCard->serialize()]); + + $query = $this->db->getQueryBuilder(); + $query->select('*') + ->from('cards_properties'); + + $qResult = $query->execute(); + $result = $qResult->fetchAll(); + $qResult->closeCursor(); + + $this->assertSame(1, count($result)); + + $this->assertSame('UID', $result[0]['name']); + $this->assertSame($cardUri, $result[0]['value']); + $this->assertSame($bookId, (int)$result[0]['addressbookid']); + $this->assertSame($cardId, (int)$result[0]['cardid']); + } + + public function testPurgeProperties(): void { + $query = $this->db->getQueryBuilder(); + $query->insert('cards_properties') + ->values( + [ + 'addressbookid' => $query->createNamedParameter(1), + 'cardid' => $query->createNamedParameter(1), + 'name' => $query->createNamedParameter('name1'), + 'value' => $query->createNamedParameter('value1'), + 'preferred' => $query->createNamedParameter(0) + ] + ); + $query->execute(); + + $query = $this->db->getQueryBuilder(); + $query->insert('cards_properties') + ->values( + [ + 'addressbookid' => $query->createNamedParameter(1), + 'cardid' => $query->createNamedParameter(2), + 'name' => $query->createNamedParameter('name2'), + 'value' => $query->createNamedParameter('value2'), + 'preferred' => $query->createNamedParameter(0) + ] + ); + $query->execute(); + + $this->invokePrivate($this->backend, 'purgeProperties', [1, 1]); + + $query = $this->db->getQueryBuilder(); + $query->select('*') + ->from('cards_properties'); + + $qResult = $query->execute(); + $result = $qResult->fetchAll(); + $qResult->closeCursor(); + + $this->assertSame(1, count($result)); + $this->assertSame(1, (int)$result[0]['addressbookid']); + $this->assertSame(2, (int)$result[0]['cardid']); + } + + public function testGetCardId(): void { + $query = $this->db->getQueryBuilder(); + + $query->insert('cards') + ->values( + [ + 'addressbookid' => $query->createNamedParameter(1), + 'carddata' => $query->createNamedParameter(''), + 'uri' => $query->createNamedParameter('uri'), + 'lastmodified' => $query->createNamedParameter(4738743), + 'etag' => $query->createNamedParameter('etag'), + 'size' => $query->createNamedParameter(120) + ] + ); + $query->execute(); + $id = $query->getLastInsertId(); + + $this->assertSame($id, + $this->invokePrivate($this->backend, 'getCardId', [1, 'uri'])); + } + + + public function testGetCardIdFailed(): void { + $this->expectException(\InvalidArgumentException::class); + + $this->invokePrivate($this->backend, 'getCardId', [1, 'uri']); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestSearch')] + public function testSearch(string $pattern, array $properties, array $options, array $expected): void { + /** @var VCard $vCards */ + $vCards = []; + $vCards[0] = new VCard(); + $vCards[0]->add(new Text($vCards[0], 'UID', 'uid')); + $vCards[0]->add(new Text($vCards[0], 'FN', 'John Doe')); + $vCards[0]->add(new Text($vCards[0], 'CLOUD', 'john@nextcloud.com')); + $vCards[1] = new VCard(); + $vCards[1]->add(new Text($vCards[1], 'UID', 'uid')); + $vCards[1]->add(new Text($vCards[1], 'FN', 'John M. Doe')); + $vCards[2] = new VCard(); + $vCards[2]->add(new Text($vCards[2], 'UID', 'uid')); + $vCards[2]->add(new Text($vCards[2], 'FN', 'find without options')); + $vCards[2]->add(new Text($vCards[2], 'CLOUD', 'peter_pan@nextcloud.com')); + + $vCardIds = []; + $query = $this->db->getQueryBuilder(); + for ($i = 0; $i < 3; $i++) { + $query->insert($this->dbCardsTable) + ->values( + [ + 'addressbookid' => $query->createNamedParameter(0), + 'carddata' => $query->createNamedParameter($vCards[$i]->serialize(), IQueryBuilder::PARAM_LOB), + 'uri' => $query->createNamedParameter('uri' . $i), + 'lastmodified' => $query->createNamedParameter(time()), + 'etag' => $query->createNamedParameter('etag' . $i), + 'size' => $query->createNamedParameter(120), + ] + ); + $query->execute(); + $vCardIds[] = $query->getLastInsertId(); + } + + $query = $this->db->getQueryBuilder(); + $query->insert($this->dbCardsPropertiesTable) + ->values( + [ + 'addressbookid' => $query->createNamedParameter(0), + 'cardid' => $query->createNamedParameter($vCardIds[0]), + 'name' => $query->createNamedParameter('FN'), + 'value' => $query->createNamedParameter('John Doe'), + 'preferred' => $query->createNamedParameter(0) + ] + ); + $query->execute(); + $query = $this->db->getQueryBuilder(); + $query->insert($this->dbCardsPropertiesTable) + ->values( + [ + 'addressbookid' => $query->createNamedParameter(0), + 'cardid' => $query->createNamedParameter($vCardIds[0]), + 'name' => $query->createNamedParameter('CLOUD'), + 'value' => $query->createNamedParameter('John@nextcloud.com'), + 'preferred' => $query->createNamedParameter(0) + ] + ); + $query->execute(); + $query = $this->db->getQueryBuilder(); + $query->insert($this->dbCardsPropertiesTable) + ->values( + [ + 'addressbookid' => $query->createNamedParameter(0), + 'cardid' => $query->createNamedParameter($vCardIds[1]), + 'name' => $query->createNamedParameter('FN'), + 'value' => $query->createNamedParameter('John M. Doe'), + 'preferred' => $query->createNamedParameter(0) + ] + ); + $query->execute(); + $query = $this->db->getQueryBuilder(); + $query->insert($this->dbCardsPropertiesTable) + ->values( + [ + 'addressbookid' => $query->createNamedParameter(0), + 'cardid' => $query->createNamedParameter($vCardIds[2]), + 'name' => $query->createNamedParameter('FN'), + 'value' => $query->createNamedParameter('find without options'), + 'preferred' => $query->createNamedParameter(0) + ] + ); + $query->execute(); + $query = $this->db->getQueryBuilder(); + $query->insert($this->dbCardsPropertiesTable) + ->values( + [ + 'addressbookid' => $query->createNamedParameter(0), + 'cardid' => $query->createNamedParameter($vCardIds[2]), + 'name' => $query->createNamedParameter('CLOUD'), + 'value' => $query->createNamedParameter('peter_pan@nextcloud.com'), + 'preferred' => $query->createNamedParameter(0) + ] + ); + $query->execute(); + + $result = $this->backend->search(0, $pattern, $properties, $options); + + // check result + $this->assertSame(count($expected), count($result)); + $found = []; + foreach ($result as $r) { + foreach ($expected as $exp) { + if ($r['uri'] === $exp[0] && strpos($r['carddata'], $exp[1]) > 0) { + $found[$exp[1]] = true; + break; + } + } + } + + $this->assertSame(count($expected), count($found)); + } + + public static function dataTestSearch(): array { + return [ + ['John', ['FN'], [], [['uri0', 'John Doe'], ['uri1', 'John M. Doe']]], + ['M. Doe', ['FN'], [], [['uri1', 'John M. Doe']]], + ['Do', ['FN'], [], [['uri0', 'John Doe'], ['uri1', 'John M. Doe']]], + 'check if duplicates are handled correctly' => ['John', ['FN', 'CLOUD'], [], [['uri0', 'John Doe'], ['uri1', 'John M. Doe']]], + 'case insensitive' => ['john', ['FN'], [], [['uri0', 'John Doe'], ['uri1', 'John M. Doe']]], + 'limit' => ['john', ['FN'], ['limit' => 1], [['uri0', 'John Doe']]], + 'limit and offset' => ['john', ['FN'], ['limit' => 1, 'offset' => 1], [['uri1', 'John M. Doe']]], + 'find "_" escaped' => ['_', ['CLOUD'], [], [['uri2', 'find without options']]], + 'find not empty CLOUD' => ['%_%', ['CLOUD'], ['escape_like_param' => false], [['uri0', 'John Doe'], ['uri2', 'find without options']]], + ]; + } + + public function testGetCardUri(): void { + $query = $this->db->getQueryBuilder(); + $query->insert($this->dbCardsTable) + ->values( + [ + 'addressbookid' => $query->createNamedParameter(1), + 'carddata' => $query->createNamedParameter('carddata', IQueryBuilder::PARAM_LOB), + 'uri' => $query->createNamedParameter('uri'), + 'lastmodified' => $query->createNamedParameter(5489543), + 'etag' => $query->createNamedParameter('etag'), + 'size' => $query->createNamedParameter(120), + ] + ); + $query->execute(); + + $id = $query->getLastInsertId(); + + $this->assertSame('uri', $this->backend->getCardUri($id)); + } + + + public function testGetCardUriFailed(): void { + $this->expectException(\InvalidArgumentException::class); + + $this->backend->getCardUri(1); + } + + public function testGetContact(): void { + $query = $this->db->getQueryBuilder(); + for ($i = 0; $i < 2; $i++) { + $query->insert($this->dbCardsTable) + ->values( + [ + 'addressbookid' => $query->createNamedParameter($i), + 'carddata' => $query->createNamedParameter('carddata' . $i, IQueryBuilder::PARAM_LOB), + 'uri' => $query->createNamedParameter('uri' . $i), + 'lastmodified' => $query->createNamedParameter(5489543), + 'etag' => $query->createNamedParameter('etag' . $i), + 'size' => $query->createNamedParameter(120), + ] + ); + $query->execute(); + } + + $result = $this->backend->getContact(0, 'uri0'); + $this->assertSame(8, count($result)); + $this->assertSame(0, (int)$result['addressbookid']); + $this->assertSame('uri0', $result['uri']); + $this->assertSame(5489543, (int)$result['lastmodified']); + $this->assertSame('"etag0"', $result['etag']); + $this->assertSame(120, (int)$result['size']); + + // this shouldn't return any result because 'uri1' is in address book 1 + // see https://github.com/nextcloud/server/issues/229 + $result = $this->backend->getContact(0, 'uri1'); + $this->assertEmpty($result); + } + + public function testGetContactFail(): void { + $this->assertEmpty($this->backend->getContact(0, 'uri')); + } + + public function testCollectCardProperties(): void { + $query = $this->db->getQueryBuilder(); + $query->insert($this->dbCardsPropertiesTable) + ->values( + [ + 'addressbookid' => $query->createNamedParameter(666), + 'cardid' => $query->createNamedParameter(777), + 'name' => $query->createNamedParameter('FN'), + 'value' => $query->createNamedParameter('John Doe'), + 'preferred' => $query->createNamedParameter(0) + ] + ) + ->execute(); + + $result = $this->backend->collectCardProperties(666, 'FN'); + $this->assertEquals(['John Doe'], $result); + } + + /** + * @throws \OCP\DB\Exception + * @throws \Sabre\DAV\Exception\BadRequest + */ + public function testPruneOutdatedSyncTokens(): void { + $addressBookId = $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); + $changes = $this->backend->getChangesForAddressBook($addressBookId, '', 1); + $syncToken = $changes['syncToken']; + + $uri = $this->getUniqueID('card'); + $this->backend->createCard($addressBookId, $uri, $this->vcardTest0); + $this->backend->updateCard($addressBookId, $uri, $this->vcardTest1); + + // Do not delete anything if week data as old as ts=0 + $deleted = $this->backend->pruneOutdatedSyncTokens(0, 0); + self::assertSame(0, $deleted); + + $deleted = $this->backend->pruneOutdatedSyncTokens(0, time()); + // At least one from the object creation and one from the object update + $this->assertGreaterThanOrEqual(2, $deleted); + $changes = $this->backend->getChangesForAddressBook($addressBookId, $syncToken, 1); + $this->assertEmpty($changes['added']); + $this->assertEmpty($changes['modified']); + $this->assertEmpty($changes['deleted']); + + // Test that objects remain + + // Currently changes are empty + $changes = $this->backend->getChangesForAddressBook($addressBookId, $syncToken, 100); + $this->assertEquals(0, count($changes['added'] + $changes['modified'] + $changes['deleted'])); + + // Create card + $uri = $this->getUniqueID('card'); + $this->backend->createCard($addressBookId, $uri, $this->vcardTest0); + // We now have one add + $changes = $this->backend->getChangesForAddressBook($addressBookId, $syncToken, 100); + $this->assertEquals(1, count($changes['added'])); + $this->assertEmpty($changes['modified']); + $this->assertEmpty($changes['deleted']); + + // Update card + $this->backend->updateCard($addressBookId, $uri, $this->vcardTest1); + // One add, one modify, but shortened to modify + $changes = $this->backend->getChangesForAddressBook($addressBookId, $syncToken, 100); + $this->assertEmpty($changes['added']); + $this->assertEquals(1, count($changes['modified'])); + $this->assertEmpty($changes['deleted']); + + // Delete all but last change + $deleted = $this->backend->pruneOutdatedSyncTokens(1, time()); + $this->assertEquals(1, $deleted); // We had two changes before, now one + + // Only update should remain + $changes = $this->backend->getChangesForAddressBook($addressBookId, $syncToken, 100); + $this->assertEmpty($changes['added']); + $this->assertEquals(1, count($changes['modified'])); + $this->assertEmpty($changes['deleted']); + + // Check that no crash occurs when prune is called without current changes + $deleted = $this->backend->pruneOutdatedSyncTokens(1, time()); + } +} diff --git a/apps/dav/tests/unit/CardDAV/ContactsManagerTest.php b/apps/dav/tests/unit/CardDAV/ContactsManagerTest.php new file mode 100644 index 00000000000..bdd826f671b --- /dev/null +++ b/apps/dav/tests/unit/CardDAV/ContactsManagerTest.php @@ -0,0 +1,37 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\CardDAV; + +use OCA\DAV\CardDAV\CardDavBackend; +use OCA\DAV\CardDAV\ContactsManager; +use OCA\DAV\Db\PropertyMapper; +use OCP\Contacts\IManager; +use OCP\IL10N; +use OCP\IURLGenerator; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class ContactsManagerTest extends TestCase { + public function test(): void { + /** @var IManager&MockObject $cm */ + $cm = $this->createMock(IManager::class); + $cm->expects($this->exactly(2))->method('registerAddressBook'); + $urlGenerator = $this->createMock(IURLGenerator::class); + /** @var CardDavBackend&MockObject $backEnd */ + $backEnd = $this->createMock(CardDavBackend::class); + $backEnd->method('getAddressBooksForUser')->willReturn([ + ['{DAV:}displayname' => 'Test address book', 'uri' => 'default'], + ]); + $propertyMapper = $this->createMock(PropertyMapper::class); + + $l = $this->createMock(IL10N::class); + $app = new ContactsManager($backEnd, $l, $propertyMapper); + $app->setupContactsProvider($cm, 'user01', $urlGenerator); + } +} diff --git a/apps/dav/tests/unit/CardDAV/ConverterTest.php b/apps/dav/tests/unit/CardDAV/ConverterTest.php new file mode 100644 index 00000000000..00519b82766 --- /dev/null +++ b/apps/dav/tests/unit/CardDAV/ConverterTest.php @@ -0,0 +1,221 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\CardDAV; + +use OCA\DAV\CardDAV\Converter; +use OCP\Accounts\IAccount; +use OCP\Accounts\IAccountManager; +use OCP\Accounts\IAccountProperty; +use OCP\IImage; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\IUserManager; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class ConverterTest extends TestCase { + private IAccountManager&MockObject $accountManager; + private IUserManager&MockObject $userManager; + private IURLGenerator&MockObject $urlGenerator; + private LoggerInterface&MockObject $logger; + + protected function setUp(): void { + parent::setUp(); + + $this->accountManager = $this->createMock(IAccountManager::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + $this->logger = $this->createMock(LoggerInterface::class); + } + + /** + * @return IAccountProperty&MockObject + */ + protected function getAccountPropertyMock(string $name, ?string $value, string $scope) { + $property = $this->createMock(IAccountProperty::class); + $property->expects($this->any()) + ->method('getName') + ->willReturn($name); + $property->expects($this->any()) + ->method('getValue') + ->willReturn((string)$value); + $property->expects($this->any()) + ->method('getScope') + ->willReturn($scope); + $property->expects($this->any()) + ->method('getVerified') + ->willReturn(IAccountManager::NOT_VERIFIED); + return $property; + } + + public function getAccountManager(IUser $user) { + $account = $this->createMock(IAccount::class); + $account->expects($this->any()) + ->method('getAllProperties') + ->willReturnCallback(function () use ($user) { + yield $this->getAccountPropertyMock(IAccountManager::PROPERTY_DISPLAYNAME, $user->getDisplayName(), IAccountManager::SCOPE_FEDERATED); + yield $this->getAccountPropertyMock(IAccountManager::PROPERTY_ADDRESS, '', IAccountManager::SCOPE_LOCAL); + yield $this->getAccountPropertyMock(IAccountManager::PROPERTY_WEBSITE, '', IAccountManager::SCOPE_LOCAL); + yield $this->getAccountPropertyMock(IAccountManager::PROPERTY_EMAIL, $user->getEMailAddress(), IAccountManager::SCOPE_FEDERATED); + yield $this->getAccountPropertyMock(IAccountManager::PROPERTY_AVATAR, $user->getAvatarImage(-1)->data(), IAccountManager::SCOPE_FEDERATED); + yield $this->getAccountPropertyMock(IAccountManager::PROPERTY_PHONE, '', IAccountManager::SCOPE_LOCAL); + yield $this->getAccountPropertyMock(IAccountManager::PROPERTY_TWITTER, '', IAccountManager::SCOPE_LOCAL); + }); + + $accountManager = $this->createMock(IAccountManager::class); + + $accountManager->expects($this->any()) + ->method('getAccount') + ->willReturn($account); + + return $accountManager; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providesNewUsers')] + public function testCreation($expectedVCard, $displayName = null, $eMailAddress = null, $cloudId = null): void { + $user = $this->getUserMock((string)$displayName, $eMailAddress, $cloudId); + $accountManager = $this->getAccountManager($user); + + $converter = new Converter($accountManager, $this->userManager, $this->urlGenerator, $this->logger); + $vCard = $converter->createCardFromUser($user); + if ($expectedVCard !== null) { + $this->assertInstanceOf('Sabre\VObject\Component\VCard', $vCard); + $cardData = $vCard->jsonSerialize(); + $this->compareData($expectedVCard, $cardData); + } else { + $this->assertSame($expectedVCard, $vCard); + } + } + + public function testManagerProp(): void { + $user = $this->getUserMock('user', 'user@domain.tld', 'user@cloud.domain.tld'); + $user->method('getManagerUids') + ->willReturn(['mgr']); + $this->userManager->expects(self::once()) + ->method('getDisplayName') + ->with('mgr') + ->willReturn('Manager'); + $accountManager = $this->getAccountManager($user); + + $converter = new Converter($accountManager, $this->userManager, $this->urlGenerator, $this->logger); + $vCard = $converter->createCardFromUser($user); + + $this->compareData( + [ + 'cloud' => 'user@cloud.domain.tld', + 'email' => 'user@domain.tld', + 'x-managersname' => 'Manager', + ], + $vCard->jsonSerialize() + ); + } + + protected function compareData(array $expected, array $data): void { + foreach ($expected as $key => $value) { + $found = false; + foreach ($data[1] as $d) { + if ($d[0] === $key && $d[3] === $value) { + $found = true; + break; + } + } + if (!$found) { + $this->assertTrue(false, 'Expected data: ' . $key . ' not found.'); + } + } + } + + public static function providesNewUsers(): array { + return [ + [ + null + ], + [ + null, + null, + 'foo@bar.net' + ], + [ + [ + 'cloud' => 'foo@cloud.net', + 'email' => 'foo@bar.net', + 'photo' => 'MTIzNDU2Nzg5', + ], + null, + 'foo@bar.net', + 'foo@cloud.net' + ], + [ + [ + 'cloud' => 'foo@cloud.net', + 'email' => 'foo@bar.net', + 'fn' => 'Dr. Foo Bar', + 'photo' => 'MTIzNDU2Nzg5', + ], + 'Dr. Foo Bar', + 'foo@bar.net', + 'foo@cloud.net' + ], + [ + [ + 'cloud' => 'foo@cloud.net', + 'fn' => 'Dr. Foo Bar', + 'photo' => 'MTIzNDU2Nzg5', + ], + 'Dr. Foo Bar', + null, + 'foo@cloud.net' + ], + [ + [ + 'cloud' => 'foo@cloud.net', + 'fn' => 'Dr. Foo Bar', + 'photo' => 'MTIzNDU2Nzg5', + ], + 'Dr. Foo Bar', + '', + 'foo@cloud.net' + ], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providesNames')] + public function testNameSplitter(string $expected, string $fullName): void { + $converter = new Converter($this->accountManager, $this->userManager, $this->urlGenerator, $this->logger); + $r = $converter->splitFullName($fullName); + $r = implode(';', $r); + $this->assertEquals($expected, $r); + } + + public static function providesNames(): array { + return [ + ['Sauron;;;;', 'Sauron'], + ['Baggins;Bilbo;;;', 'Bilbo Baggins'], + ['Tolkien;John;Ronald Reuel;;', 'John Ronald Reuel Tolkien'], + ]; + } + + /** + * @return IUser&MockObject + */ + protected function getUserMock(string $displayName, ?string $eMailAddress, ?string $cloudId) { + $image0 = $this->createMock(IImage::class); + $image0->method('mimeType')->willReturn('image/jpeg'); + $image0->method('data')->willReturn('123456789'); + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('12345'); + $user->method('getDisplayName')->willReturn($displayName); + $user->method('getEMailAddress')->willReturn($eMailAddress); + $user->method('getCloudId')->willReturn($cloudId); + $user->method('getAvatarImage')->willReturn($image0); + return $user; + } +} diff --git a/apps/dav/tests/unit/CardDAV/ImageExportPluginTest.php b/apps/dav/tests/unit/CardDAV/ImageExportPluginTest.php new file mode 100644 index 00000000000..d47f53bddcd --- /dev/null +++ b/apps/dav/tests/unit/CardDAV/ImageExportPluginTest.php @@ -0,0 +1,174 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\CardDAV; + +use OCA\DAV\CardDAV\AddressBook; +use OCA\DAV\CardDAV\ImageExportPlugin; +use OCA\DAV\CardDAV\PhotoCache; +use OCP\AppFramework\Http; +use OCP\Files\NotFoundException; +use OCP\Files\SimpleFS\ISimpleFile; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\CardDAV\Card; +use Sabre\DAV\Node; +use Sabre\DAV\Server; +use Sabre\DAV\Tree; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; +use Test\TestCase; + +class ImageExportPluginTest extends TestCase { + private ResponseInterface&MockObject $response; + private RequestInterface&MockObject $request; + private Server&MockObject $server; + private Tree&MockObject $tree; + private PhotoCache&MockObject $cache; + private ImageExportPlugin $plugin; + + protected function setUp(): void { + parent::setUp(); + + $this->request = $this->createMock(RequestInterface::class); + $this->response = $this->createMock(ResponseInterface::class); + $this->server = $this->createMock(Server::class); + $this->tree = $this->createMock(Tree::class); + $this->server->tree = $this->tree; + $this->cache = $this->createMock(PhotoCache::class); + + $this->plugin = new ImageExportPlugin($this->cache); + $this->plugin->initialize($this->server); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providesQueryParams')] + public function testQueryParams(array $param): void { + $this->request->expects($this->once())->method('getQueryParameters')->willReturn($param); + $result = $this->plugin->httpGet($this->request, $this->response); + $this->assertTrue($result); + } + + public static function providesQueryParams(): array { + return [ + [[]], + [['1']], + [['foo' => 'bar']], + ]; + } + + public function testNoCard(): void { + $this->request->method('getQueryParameters') + ->willReturn([ + 'photo' + ]); + $this->request->method('getPath') + ->willReturn('user/book/card'); + + $node = $this->createMock(Node::class); + $this->tree->method('getNodeForPath') + ->with('user/book/card') + ->willReturn($node); + + $result = $this->plugin->httpGet($this->request, $this->response); + $this->assertTrue($result); + } + + public static function dataTestCard(): array { + return [ + [null, false], + [null, true], + [32, false], + [32, true], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestCard')] + public function testCard(?int $size, bool $photo): void { + $query = ['photo' => null]; + if ($size !== null) { + $query['size'] = $size; + } + + $this->request->method('getQueryParameters') + ->willReturn($query); + $this->request->method('getPath') + ->willReturn('user/book/card'); + + $card = $this->createMock(Card::class); + $card->method('getETag') + ->willReturn('"myEtag"'); + $card->method('getName') + ->willReturn('card'); + $book = $this->createMock(AddressBook::class); + $book->method('getResourceId') + ->willReturn(1); + + $this->tree->method('getNodeForPath') + ->willReturnCallback(function ($path) use ($card, $book) { + if ($path === 'user/book/card') { + return $card; + } elseif ($path === 'user/book') { + return $book; + } + $this->fail(); + }); + + $size = $size === null ? -1 : $size; + + if ($photo) { + $file = $this->createMock(ISimpleFile::class); + $file->method('getMimeType') + ->willReturn('image/jpeg'); + $file->method('getContent') + ->willReturn('imgdata'); + + $this->cache->method('get') + ->with(1, 'card', $size, $card) + ->willReturn($file); + + $setHeaderCalls = [ + ['Cache-Control', 'private, max-age=3600, must-revalidate'], + ['Etag', '"myEtag"'], + ['Content-Type', 'image/jpeg'], + ['Content-Disposition', 'attachment; filename=card.jpg'], + ]; + $this->response->expects($this->exactly(count($setHeaderCalls))) + ->method('setHeader') + ->willReturnCallback(function () use (&$setHeaderCalls): void { + $expected = array_shift($setHeaderCalls); + $this->assertEquals($expected, func_get_args()); + }); + + $this->response->expects($this->once()) + ->method('setStatus') + ->with(200); + $this->response->expects($this->once()) + ->method('setBody') + ->with('imgdata'); + } else { + $setHeaderCalls = [ + ['Cache-Control', 'private, max-age=3600, must-revalidate'], + ['Etag', '"myEtag"'], + ]; + $this->response->expects($this->exactly(count($setHeaderCalls))) + ->method('setHeader') + ->willReturnCallback(function () use (&$setHeaderCalls): void { + $expected = array_shift($setHeaderCalls); + $this->assertEquals($expected, func_get_args()); + }); + $this->cache->method('get') + ->with(1, 'card', $size, $card) + ->willThrowException(new NotFoundException()); + $this->response->expects($this->once()) + ->method('setStatus') + ->with(Http::STATUS_NO_CONTENT); + } + + $result = $this->plugin->httpGet($this->request, $this->response); + $this->assertFalse($result); + } +} diff --git a/apps/dav/tests/unit/CardDAV/Security/CardDavRateLimitingPluginTest.php b/apps/dav/tests/unit/CardDAV/Security/CardDavRateLimitingPluginTest.php new file mode 100644 index 00000000000..ee599d5a76c --- /dev/null +++ b/apps/dav/tests/unit/CardDAV/Security/CardDavRateLimitingPluginTest.php @@ -0,0 +1,146 @@ +<?php + +declare(strict_types=1); + +/* + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Tests\unit\CardDAV\Security; + +use OC\Security\RateLimiting\Exception\RateLimitExceededException; +use OC\Security\RateLimiting\Limiter; +use OCA\DAV\CardDAV\CardDavBackend; +use OCA\DAV\CardDAV\Security\CardDavRateLimitingPlugin; +use OCA\DAV\Connector\Sabre\Exception\TooManyRequests; +use OCP\IAppConfig; +use OCP\IUser; +use OCP\IUserManager; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Sabre\DAV\Exception\Forbidden; +use Test\TestCase; + +class CardDavRateLimitingPluginTest extends TestCase { + + private Limiter&MockObject $limiter; + private CardDavBackend&MockObject $cardDavBackend; + private IUserManager&MockObject $userManager; + private LoggerInterface&MockObject $logger; + private IAppConfig&MockObject $config; + private string $userId = 'user123'; + private CardDavRateLimitingPlugin $plugin; + + protected function setUp(): void { + parent::setUp(); + + $this->limiter = $this->createMock(Limiter::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->cardDavBackend = $this->createMock(CardDavBackend::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->config = $this->createMock(IAppConfig::class); + $this->plugin = new CardDavRateLimitingPlugin( + $this->limiter, + $this->userManager, + $this->cardDavBackend, + $this->logger, + $this->config, + $this->userId, + ); + } + + public function testNoUserObject(): void { + $this->limiter->expects(self::never()) + ->method('registerUserRequest'); + + $this->plugin->beforeBind('addressbooks/users/foo/addressbookname'); + } + + public function testUnrelated(): void { + $user = $this->createMock(IUser::class); + $this->userManager->expects(self::once()) + ->method('get') + ->with($this->userId) + ->willReturn($user); + $this->limiter->expects(self::never()) + ->method('registerUserRequest'); + + $this->plugin->beforeBind('foo/bar'); + } + + public function testRegisterAddressBookrCreation(): void { + $user = $this->createMock(IUser::class); + $this->userManager->expects(self::once()) + ->method('get') + ->with($this->userId) + ->willReturn($user); + $this->config + ->method('getValueInt') + ->with('dav') + ->willReturnArgument(2); + $this->limiter->expects(self::once()) + ->method('registerUserRequest') + ->with( + 'carddav-create-address-book', + 10, + 3600, + $user, + ); + + $this->plugin->beforeBind('addressbooks/users/foo/addressbookname'); + } + + public function testAddressBookCreationRateLimitExceeded(): void { + $user = $this->createMock(IUser::class); + $this->userManager->expects(self::once()) + ->method('get') + ->with($this->userId) + ->willReturn($user); + $this->config + ->method('getValueInt') + ->with('dav') + ->willReturnArgument(2); + $this->limiter->expects(self::once()) + ->method('registerUserRequest') + ->with( + 'carddav-create-address-book', + 10, + 3600, + $user, + ) + ->willThrowException(new RateLimitExceededException()); + $this->expectException(TooManyRequests::class); + + $this->plugin->beforeBind('addressbooks/users/foo/addressbookname'); + } + + public function testAddressBookLimitReached(): void { + $user = $this->createMock(IUser::class); + $this->userManager->expects(self::once()) + ->method('get') + ->with($this->userId) + ->willReturn($user); + $user->method('getUID')->willReturn('user123'); + $this->config + ->method('getValueInt') + ->with('dav') + ->willReturnArgument(2); + $this->limiter->expects(self::once()) + ->method('registerUserRequest') + ->with( + 'carddav-create-address-book', + 10, + 3600, + $user, + ); + $this->cardDavBackend->expects(self::once()) + ->method('getAddressBooksForUserCount') + ->with('principals/users/user123') + ->willReturn(11); + $this->expectException(Forbidden::class); + + $this->plugin->beforeBind('addressbooks/users/foo/addressbookname'); + } + +} diff --git a/apps/dav/tests/unit/CardDAV/Sharing/PluginTest.php b/apps/dav/tests/unit/CardDAV/Sharing/PluginTest.php new file mode 100644 index 00000000000..1e934a69a53 --- /dev/null +++ b/apps/dav/tests/unit/CardDAV/Sharing/PluginTest.php @@ -0,0 +1,62 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\CardDAV\Sharing; + +use OCA\DAV\Connector\Sabre\Auth; +use OCA\DAV\DAV\Sharing\IShareable; +use OCA\DAV\DAV\Sharing\Plugin; +use OCP\IConfig; +use OCP\IRequest; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\Server; +use Sabre\DAV\SimpleCollection; +use Sabre\HTTP\Request; +use Sabre\HTTP\Response; +use Test\TestCase; + +class PluginTest extends TestCase { + private Plugin $plugin; + private Server $server; + private IShareable&MockObject $book; + + protected function setUp(): void { + parent::setUp(); + + $authBackend = $this->createMock(Auth::class); + $authBackend->method('isDavAuthenticated') + ->willReturn(true); + $request = $this->createMock(IRequest::class); + $config = $this->createMock(IConfig::class); + $this->plugin = new Plugin($authBackend, $request, $config); + + $root = new SimpleCollection('root'); + $this->server = new \Sabre\DAV\Server($root); + $this->book = $this->createMock(IShareable::class); + $this->book->method('getName') + ->willReturn('addressbook1.vcf'); + $root->addChild($this->book); + $this->plugin->initialize($this->server); + } + + public function testSharing(): void { + $this->book->expects($this->once())->method('updateShares')->with([[ + 'href' => 'principal:principals/admin', + 'commonName' => null, + 'summary' => null, + 'readOnly' => false + ]], ['mailto:wilfredo@example.com']); + + // setup request + $request = new Request('POST', 'addressbook1.vcf'); + $request->addHeader('Content-Type', 'application/xml'); + $request->setBody('<?xml version="1.0" encoding="utf-8" ?><CS:share xmlns:D="DAV:" xmlns:CS="http://owncloud.org/ns"><CS:set><D:href>principal:principals/admin</D:href><CS:read-write/></CS:set> <CS:remove><D:href>mailto:wilfredo@example.com</D:href></CS:remove></CS:share>'); + $response = new Response(); + $this->plugin->httpPost($request, $response); + } +} diff --git a/apps/dav/tests/unit/CardDAV/SyncServiceTest.php b/apps/dav/tests/unit/CardDAV/SyncServiceTest.php new file mode 100644 index 00000000000..77caed336f4 --- /dev/null +++ b/apps/dav/tests/unit/CardDAV/SyncServiceTest.php @@ -0,0 +1,480 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\CardDAV; + +use GuzzleHttp\Exception\ClientException; +use GuzzleHttp\Psr7\Request as PsrRequest; +use GuzzleHttp\Psr7\Response as PsrResponse; +use OC\Http\Client\Response; +use OCA\DAV\CardDAV\CardDavBackend; +use OCA\DAV\CardDAV\Converter; +use OCA\DAV\CardDAV\SyncService; +use OCP\Http\Client\IClient; +use OCP\Http\Client\IClientService; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IUser; +use OCP\IUserManager; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Http\Client\ClientExceptionInterface; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; +use Sabre\VObject\Component\VCard; +use Test\TestCase; + +class SyncServiceTest extends TestCase { + + protected CardDavBackend&MockObject $backend; + protected IUserManager&MockObject $userManager; + protected IDBConnection&MockObject $dbConnection; + protected LoggerInterface $logger; + protected Converter&MockObject $converter; + protected IClient&MockObject $client; + protected IConfig&MockObject $config; + protected SyncService $service; + + public function setUp(): void { + parent::setUp(); + + $addressBook = [ + 'id' => 1, + 'uri' => 'system', + 'principaluri' => 'principals/system/system', + '{DAV:}displayname' => 'system', + // watch out, incomplete address book mock. + ]; + + $this->backend = $this->createMock(CardDavBackend::class); + $this->backend->method('getAddressBooksByUri') + ->with('principals/system/system', 1) + ->willReturn($addressBook); + + $this->userManager = $this->createMock(IUserManager::class); + $this->dbConnection = $this->createMock(IDBConnection::class); + $this->logger = new NullLogger(); + $this->converter = $this->createMock(Converter::class); + $this->client = $this->createMock(IClient::class); + $this->config = $this->createMock(IConfig::class); + + $clientService = $this->createMock(IClientService::class); + $clientService->method('newClient') + ->willReturn($this->client); + + $this->service = new SyncService( + $this->backend, + $this->userManager, + $this->dbConnection, + $this->logger, + $this->converter, + $clientService, + $this->config + ); + } + + public function testEmptySync(): void { + $this->backend->expects($this->exactly(0)) + ->method('createCard'); + $this->backend->expects($this->exactly(0)) + ->method('updateCard'); + $this->backend->expects($this->exactly(0)) + ->method('deleteCard'); + + $body = '<?xml version="1.0"?> +<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:oc="http://owncloud.org/ns"> + <d:sync-token>http://sabre.io/ns/sync/1</d:sync-token> +</d:multistatus>'; + + $requestResponse = new Response(new PsrResponse( + 207, + ['Content-Type' => 'application/xml; charset=utf-8', 'Content-Length' => strlen($body)], + $body + )); + + $this->client + ->method('request') + ->willReturn($requestResponse); + + $token = $this->service->syncRemoteAddressBook( + '', + 'system', + 'system', + '1234567890', + null, + '1', + 'principals/system/system', + [] + )[0]; + + $this->assertEquals('http://sabre.io/ns/sync/1', $token); + } + + public function testSyncWithNewElement(): void { + $this->backend->expects($this->exactly(1)) + ->method('createCard'); + $this->backend->expects($this->exactly(0)) + ->method('updateCard'); + $this->backend->expects($this->exactly(0)) + ->method('deleteCard'); + + $this->backend->method('getCard') + ->willReturn(false); + + + $body = '<?xml version="1.0"?> +<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:oc="http://owncloud.org/ns"> + <d:response> + <d:href>/remote.php/dav/addressbooks/system/system/system/Database:alice.vcf</d:href> + <d:propstat> + <d:prop> + <d:getcontenttype>text/vcard; charset=utf-8</d:getcontenttype> + <d:getetag>"2df155fa5c2a24cd7f750353fc63f037"</d:getetag> + </d:prop> + <d:status>HTTP/1.1 200 OK</d:status> + </d:propstat> + </d:response> + <d:sync-token>http://sabre.io/ns/sync/2</d:sync-token> +</d:multistatus>'; + + $reportResponse = new Response(new PsrResponse( + 207, + ['Content-Type' => 'application/xml; charset=utf-8', 'Content-Length' => strlen($body)], + $body + )); + + $this->client + ->method('request') + ->willReturn($reportResponse); + + $vCard = 'BEGIN:VCARD +VERSION:3.0 +PRODID:-//Sabre//Sabre VObject 4.5.4//EN +UID:alice +FN;X-NC-SCOPE=v2-federated:alice +N;X-NC-SCOPE=v2-federated:alice;;;; +X-SOCIALPROFILE;TYPE=NEXTCLOUD;X-NC-SCOPE=v2-published:https://server2.internal/index.php/u/alice +CLOUD:alice@server2.internal +END:VCARD'; + + $getResponse = new Response(new PsrResponse( + 200, + ['Content-Type' => 'text/vcard; charset=utf-8', 'Content-Length' => strlen($vCard)], + $vCard, + )); + + $this->client + ->method('get') + ->willReturn($getResponse); + + $token = $this->service->syncRemoteAddressBook( + '', + 'system', + 'system', + '1234567890', + null, + '1', + 'principals/system/system', + [] + )[0]; + + $this->assertEquals('http://sabre.io/ns/sync/2', $token); + } + + public function testSyncWithUpdatedElement(): void { + $this->backend->expects($this->exactly(0)) + ->method('createCard'); + $this->backend->expects($this->exactly(1)) + ->method('updateCard'); + $this->backend->expects($this->exactly(0)) + ->method('deleteCard'); + + $this->backend->method('getCard') + ->willReturn(true); + + + $body = '<?xml version="1.0"?> +<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:oc="http://owncloud.org/ns"> + <d:response> + <d:href>/remote.php/dav/addressbooks/system/system/system/Database:alice.vcf</d:href> + <d:propstat> + <d:prop> + <d:getcontenttype>text/vcard; charset=utf-8</d:getcontenttype> + <d:getetag>"2df155fa5c2a24cd7f750353fc63f037"</d:getetag> + </d:prop> + <d:status>HTTP/1.1 200 OK</d:status> + </d:propstat> + </d:response> + <d:sync-token>http://sabre.io/ns/sync/3</d:sync-token> +</d:multistatus>'; + + $reportResponse = new Response(new PsrResponse( + 207, + ['Content-Type' => 'application/xml; charset=utf-8', 'Content-Length' => strlen($body)], + $body + )); + + $this->client + ->method('request') + ->willReturn($reportResponse); + + $vCard = 'BEGIN:VCARD +VERSION:3.0 +PRODID:-//Sabre//Sabre VObject 4.5.4//EN +UID:alice +FN;X-NC-SCOPE=v2-federated:alice +N;X-NC-SCOPE=v2-federated:alice;;;; +X-SOCIALPROFILE;TYPE=NEXTCLOUD;X-NC-SCOPE=v2-published:https://server2.internal/index.php/u/alice +CLOUD:alice@server2.internal +END:VCARD'; + + $getResponse = new Response(new PsrResponse( + 200, + ['Content-Type' => 'text/vcard; charset=utf-8', 'Content-Length' => strlen($vCard)], + $vCard, + )); + + $this->client + ->method('get') + ->willReturn($getResponse); + + $token = $this->service->syncRemoteAddressBook( + '', + 'system', + 'system', + '1234567890', + null, + '1', + 'principals/system/system', + [] + )[0]; + + $this->assertEquals('http://sabre.io/ns/sync/3', $token); + } + + public function testSyncWithDeletedElement(): void { + $this->backend->expects($this->exactly(0)) + ->method('createCard'); + $this->backend->expects($this->exactly(0)) + ->method('updateCard'); + $this->backend->expects($this->exactly(1)) + ->method('deleteCard'); + + $body = '<?xml version="1.0"?> +<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:oc="http://owncloud.org/ns"> +<d:response> + <d:href>/remote.php/dav/addressbooks/system/system/system/Database:alice.vcf</d:href> + <d:status>HTTP/1.1 404 Not Found</d:status> +</d:response> +<d:sync-token>http://sabre.io/ns/sync/4</d:sync-token> +</d:multistatus>'; + + $reportResponse = new Response(new PsrResponse( + 207, + ['Content-Type' => 'application/xml; charset=utf-8', 'Content-Length' => strlen($body)], + $body + )); + + $this->client + ->method('request') + ->willReturn($reportResponse); + + $token = $this->service->syncRemoteAddressBook( + '', + 'system', + 'system', + '1234567890', + null, + '1', + 'principals/system/system', + [] + )[0]; + + $this->assertEquals('http://sabre.io/ns/sync/4', $token); + } + + public function testEnsureSystemAddressBookExists(): void { + /** @var CardDavBackend&MockObject $backend */ + $backend = $this->createMock(CardDavBackend::class); + $backend->expects($this->exactly(1))->method('createAddressBook'); + $backend->expects($this->exactly(2)) + ->method('getAddressBooksByUri') + ->willReturnOnConsecutiveCalls( + null, + [], + ); + + $userManager = $this->createMock(IUserManager::class); + $dbConnection = $this->createMock(IDBConnection::class); + $logger = $this->createMock(LoggerInterface::class); + $converter = $this->createMock(Converter::class); + $clientService = $this->createMock(IClientService::class); + $config = $this->createMock(IConfig::class); + + $ss = new SyncService($backend, $userManager, $dbConnection, $logger, $converter, $clientService, $config); + $ss->ensureSystemAddressBookExists('principals/users/adam', 'contacts', []); + } + + public static function dataActivatedUsers(): array { + return [ + [true, 1, 1, 1], + [false, 0, 0, 3], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataActivatedUsers')] + public function testUpdateAndDeleteUser(bool $activated, int $createCalls, int $updateCalls, int $deleteCalls): void { + /** @var CardDavBackend | MockObject $backend */ + $backend = $this->getMockBuilder(CardDavBackend::class)->disableOriginalConstructor()->getMock(); + $logger = $this->getMockBuilder(LoggerInterface::class)->disableOriginalConstructor()->getMock(); + + $backend->expects($this->exactly($createCalls))->method('createCard'); + $backend->expects($this->exactly($updateCalls))->method('updateCard'); + $backend->expects($this->exactly($deleteCalls))->method('deleteCard'); + + $backend->method('getCard')->willReturnOnConsecutiveCalls(false, [ + 'carddata' => "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.4.8//EN\r\nUID:test-user\r\nFN:test-user\r\nN:test-user;;;;\r\nEND:VCARD\r\n\r\n" + ]); + + $backend->method('getAddressBooksByUri') + ->with('principals/system/system', 'system') + ->willReturn(['id' => -1]); + + $userManager = $this->createMock(IUserManager::class); + $dbConnection = $this->createMock(IDBConnection::class); + $user = $this->createMock(IUser::class); + $user->method('getBackendClassName')->willReturn('unittest'); + $user->method('getUID')->willReturn('test-user'); + $user->method('getCloudId')->willReturn('cloudId'); + $user->method('getDisplayName')->willReturn('test-user'); + $user->method('isEnabled')->willReturn($activated); + $converter = $this->createMock(Converter::class); + $converter->expects($this->any()) + ->method('createCardFromUser') + ->willReturn($this->createMock(VCard::class)); + + $clientService = $this->createMock(IClientService::class); + $config = $this->createMock(IConfig::class); + + $ss = new SyncService($backend, $userManager, $dbConnection, $logger, $converter, $clientService, $config); + $ss->updateUser($user); + + $ss->updateUser($user); + + $ss->deleteUser($user); + } + + public function testDeleteAddressbookWhenAccessRevoked(): void { + $this->expectException(ClientExceptionInterface::class); + + $this->backend->expects($this->exactly(0)) + ->method('createCard'); + $this->backend->expects($this->exactly(0)) + ->method('updateCard'); + $this->backend->expects($this->exactly(0)) + ->method('deleteCard'); + $this->backend->expects($this->exactly(1)) + ->method('deleteAddressBook'); + + $request = new PsrRequest( + 'REPORT', + 'https://server2.internal/remote.php/dav/addressbooks/system/system/system', + ['Content-Type' => 'application/xml'], + ); + + $body = '<?xml version="1.0" encoding="utf-8"?> +<d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns"> + <s:exception>Sabre\DAV\Exception\NotAuthenticated</s:exception> + <s:message>No public access to this resource., Username or password was incorrect, No \'Authorization: Bearer\' header found. Either the client didn\'t send one, or the server is mis-configured, Username or password was incorrect</s:message> +</d:error>'; + + $response = new PsrResponse( + 401, + ['Content-Type' => 'application/xml; charset=utf-8', 'Content-Length' => strlen($body)], + $body + ); + + $message = 'Client error: `REPORT https://server2.internal/cloud/remote.php/dav/addressbooks/system/system/system` resulted in a `401 Unauthorized` response: +<?xml version="1.0" encoding="utf-8"?> +<d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns"> + <s:exception>Sabre\DA (truncated...) +'; + + $reportException = new ClientException( + $message, + $request, + $response + ); + + $this->client + ->method('request') + ->willThrowException($reportException); + + $this->service->syncRemoteAddressBook( + '', + 'system', + 'system', + '1234567890', + null, + '1', + 'principals/system/system', + [] + ); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providerUseAbsoluteUriReport')] + public function testUseAbsoluteUriReport(string $host, string $expected): void { + $body = '<?xml version="1.0"?> +<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:oc="http://owncloud.org/ns"> + <d:sync-token>http://sabre.io/ns/sync/1</d:sync-token> +</d:multistatus>'; + + $requestResponse = new Response(new PsrResponse( + 207, + ['Content-Type' => 'application/xml; charset=utf-8', 'Content-Length' => strlen($body)], + $body + )); + + $this->client + ->method('request') + ->with( + 'REPORT', + $this->callback(function ($uri) use ($expected) { + $this->assertEquals($expected, $uri); + return true; + }), + $this->callback(function ($options) { + $this->assertIsArray($options); + return true; + }), + ) + ->willReturn($requestResponse); + + $this->service->syncRemoteAddressBook( + $host, + 'system', + 'remote.php/dav/addressbooks/system/system/system', + '1234567890', + null, + '1', + 'principals/system/system', + [] + ); + } + + public static function providerUseAbsoluteUriReport(): array { + return [ + ['https://server.internal', 'https://server.internal/remote.php/dav/addressbooks/system/system/system'], + ['https://server.internal/', 'https://server.internal/remote.php/dav/addressbooks/system/system/system'], + ['https://server.internal/nextcloud', 'https://server.internal/nextcloud/remote.php/dav/addressbooks/system/system/system'], + ['https://server.internal/nextcloud/', 'https://server.internal/nextcloud/remote.php/dav/addressbooks/system/system/system'], + ['https://server.internal:8080', 'https://server.internal:8080/remote.php/dav/addressbooks/system/system/system'], + ['https://server.internal:8080/', 'https://server.internal:8080/remote.php/dav/addressbooks/system/system/system'], + ['https://server.internal:8080/nextcloud', 'https://server.internal:8080/nextcloud/remote.php/dav/addressbooks/system/system/system'], + ['https://server.internal:8080/nextcloud/', 'https://server.internal:8080/nextcloud/remote.php/dav/addressbooks/system/system/system'], + ]; + } +} diff --git a/apps/dav/tests/unit/CardDAV/SystemAddressBookTest.php b/apps/dav/tests/unit/CardDAV/SystemAddressBookTest.php new file mode 100644 index 00000000000..4a218fa4616 --- /dev/null +++ b/apps/dav/tests/unit/CardDAV/SystemAddressBookTest.php @@ -0,0 +1,428 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Tests\unit\CardDAV; + +use OC\AppFramework\Http\Request; +use OCA\DAV\CardDAV\SyncService; +use OCA\DAV\CardDAV\SystemAddressbook; +use OCA\Federation\TrustedServers; +use OCP\Accounts\IAccountManager; +use OCP\IConfig; +use OCP\IGroup; +use OCP\IGroupManager; +use OCP\IL10N; +use OCP\IRequest; +use OCP\IUser; +use OCP\IUserSession; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\CardDAV\Backend\BackendInterface; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Exception\NotFound; +use Sabre\VObject\Component\VCard; +use Sabre\VObject\Reader; +use Test\TestCase; + +class SystemAddressBookTest extends TestCase { + private BackendInterface&MockObject $cardDavBackend; + private array $addressBookInfo; + private IL10N&MockObject $l10n; + private IConfig&MockObject $config; + private IUserSession $userSession; + private IRequest&MockObject $request; + private array $server; + private TrustedServers&MockObject $trustedServers; + private IGroupManager&MockObject $groupManager; + private SystemAddressbook $addressBook; + + protected function setUp(): void { + parent::setUp(); + + $this->cardDavBackend = $this->createMock(BackendInterface::class); + $this->addressBookInfo = [ + 'id' => 123, + '{DAV:}displayname' => 'Accounts', + 'principaluri' => 'principals/system/system', + ]; + $this->l10n = $this->createMock(IL10N::class); + $this->config = $this->createMock(IConfig::class); + $this->userSession = $this->createMock(IUserSession::class); + $this->request = $this->createMock(Request::class); + $this->server = [ + 'PHP_AUTH_USER' => 'system', + 'PHP_AUTH_PW' => 'shared123', + ]; + $this->request->method('__get')->with('server')->willReturn($this->server); + $this->trustedServers = $this->createMock(TrustedServers::class); + $this->groupManager = $this->createMock(IGroupManager::class); + + $this->addressBook = new SystemAddressbook( + $this->cardDavBackend, + $this->addressBookInfo, + $this->l10n, + $this->config, + $this->userSession, + $this->request, + $this->trustedServers, + $this->groupManager, + ); + } + + public function testGetChildrenAsGuest(): void { + $this->config->expects(self::exactly(3)) + ->method('getAppValue') + ->willReturnMap([ + ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'], + ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'], + ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'], + ]); + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user'); + $user->method('getBackendClassName')->willReturn('Guests'); + $this->userSession->expects(self::once()) + ->method('getUser') + ->willReturn($user); + $vcfWithScopes = <<<VCF +BEGIN:VCARD +VERSION:3.0 +PRODID:-//Sabre//Sabre VObject 4.4.2//EN +UID:admin +FN;X-NC-SCOPE=v2-federated:admin +N;X-NC-SCOPE=v2-federated:admin;;;; +ADR;TYPE=OTHER;X-NC-SCOPE=v2-local:Testing test test test;;;;;; +EMAIL;TYPE=OTHER;X-NC-SCOPE=v2-federated:miau_lalala@gmx.net +TEL;TYPE=OTHER;X-NC-SCOPE=v2-local:+435454454544 +CLOUD:admin@http://localhost +END:VCARD +VCF; + $originalCard = [ + 'carddata' => $vcfWithScopes, + ]; + $this->cardDavBackend->expects(self::once()) + ->method('getCard') + ->with(123, 'Guests:user.vcf') + ->willReturn($originalCard); + + $children = $this->addressBook->getChildren(); + + self::assertCount(1, $children); + } + + public function testGetFilteredChildForFederation(): void { + $this->config->expects(self::exactly(3)) + ->method('getAppValue') + ->willReturnMap([ + ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'], + ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'], + ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'], + ]); + $this->trustedServers->expects(self::once()) + ->method('getServers') + ->willReturn([ + [ + 'shared_secret' => 'shared123', + ], + ]); + $vcfWithScopes = <<<VCF +BEGIN:VCARD +VERSION:3.0 +PRODID:-//Sabre//Sabre VObject 4.4.2//EN +UID:admin +FN;X-NC-SCOPE=v2-federated:admin +N;X-NC-SCOPE=v2-federated:admin;;;; +ADR;TYPE=OTHER;X-NC-SCOPE=v2-local:Testing test test test;;;;;; +EMAIL;TYPE=OTHER;X-NC-SCOPE=v2-federated:miau_lalala@gmx.net +TEL;TYPE=OTHER;X-NC-SCOPE=v2-local:+435454454544 +CLOUD:admin@http://localhost +END:VCARD +VCF; + $originalCard = [ + 'carddata' => $vcfWithScopes, + ]; + $this->cardDavBackend->expects(self::once()) + ->method('getCard') + ->with(123, 'user.vcf') + ->willReturn($originalCard); + + $card = $this->addressBook->getChild('user.vcf'); + + /** @var VCard $vCard */ + $vCard = Reader::read($card->get()); + foreach ($vCard->children() as $child) { + $scope = $child->offsetGet('X-NC-SCOPE'); + if ($scope !== null) { + self::assertNotEquals(IAccountManager::SCOPE_PRIVATE, $scope->getValue()); + self::assertNotEquals(IAccountManager::SCOPE_LOCAL, $scope->getValue()); + } + } + } + + public function testGetChildNotFound(): void { + $this->config->expects(self::exactly(3)) + ->method('getAppValue') + ->willReturnMap([ + ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'], + ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'], + ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'], + ]); + $this->trustedServers->expects(self::once()) + ->method('getServers') + ->willReturn([ + [ + 'shared_secret' => 'shared123', + ], + ]); + $this->expectException(NotFound::class); + + $this->addressBook->getChild('LDAP:user.vcf'); + } + + public function testGetChildWithoutEnumeration(): void { + $this->config->expects(self::exactly(3)) + ->method('getAppValue') + ->willReturnMap([ + ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'no'], + ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'], + ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'], + ]); + $this->expectException(Forbidden::class); + + $this->addressBook->getChild('LDAP:user.vcf'); + } + + public function testGetChildAsGuest(): void { + $this->config->expects(self::exactly(3)) + ->method('getAppValue') + ->willReturnMap([ + ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'], + ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'], + ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'], + ]); + $user = $this->createMock(IUser::class); + $user->method('getBackendClassName')->willReturn('Guests'); + $this->userSession->expects(self::once()) + ->method('getUser') + ->willReturn($user); + $this->expectException(Forbidden::class); + + $this->addressBook->getChild('LDAP:user.vcf'); + } + + public function testGetChildWithGroupEnumerationRestriction(): void { + $this->config->expects(self::exactly(3)) + ->method('getAppValue') + ->willReturnMap([ + ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'], + ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'yes'], + ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'], + ]); + $user = $this->createMock(IUser::class); + $user->method('getBackendClassName')->willReturn('LDAP'); + $this->userSession->expects(self::once()) + ->method('getUser') + ->willReturn($user); + $otherUser = $this->createMock(IUser::class); + $user->method('getBackendClassName')->willReturn('LDAP'); + $otherUser->method('getUID')->willReturn('other'); + $group = $this->createMock(IGroup::class); + $group->expects(self::once()) + ->method('getUsers') + ->willReturn([$otherUser]); + $this->groupManager->expects(self::once()) + ->method('getUserGroups') + ->with($user) + ->willReturn([$group]); + $cardData = <<<VCF +BEGIN:VCARD +VERSION:3.0 +PRODID:-//Sabre//Sabre VObject 4.4.2//EN +UID:admin +FN;X-NC-SCOPE=v2-federated:other +END:VCARD +VCF; + $this->cardDavBackend->expects(self::once()) + ->method('getCard') + ->with($this->addressBookInfo['id'], "{$otherUser->getBackendClassName()}:{$otherUser->getUID()}.vcf") + ->willReturn([ + 'id' => 123, + 'carddata' => $cardData, + ]); + + $this->addressBook->getChild("{$otherUser->getBackendClassName()}:{$otherUser->getUID()}.vcf"); + } + + public function testGetChildWithPhoneNumberEnumerationRestriction(): void { + $this->config->expects(self::exactly(3)) + ->method('getAppValue') + ->willReturnMap([ + ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'], + ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'], + ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'yes'], + ]); + $user = $this->createMock(IUser::class); + $user->method('getBackendClassName')->willReturn('LDAP'); + $this->userSession->expects(self::once()) + ->method('getUser') + ->willReturn($user); + $this->expectException(Forbidden::class); + + $this->addressBook->getChild('LDAP:user.vcf'); + } + + public function testGetOwnChildWithPhoneNumberEnumerationRestriction(): void { + $this->config->expects(self::exactly(3)) + ->method('getAppValue') + ->willReturnMap([ + ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'], + ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'], + ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'yes'], + ]); + $user = $this->createMock(IUser::class); + $user->method('getBackendClassName')->willReturn('LDAP'); + $user->method('getUID')->willReturn('user'); + $this->userSession->expects(self::once()) + ->method('getUser') + ->willReturn($user); + $cardData = <<<VCF +BEGIN:VCARD +VERSION:3.0 +PRODID:-//Sabre//Sabre VObject 4.4.2//EN +UID:admin +FN;X-NC-SCOPE=v2-federated:user +END:VCARD +VCF; + $this->cardDavBackend->expects(self::once()) + ->method('getCard') + ->with($this->addressBookInfo['id'], 'LDAP:user.vcf') + ->willReturn([ + 'id' => 123, + 'carddata' => $cardData, + ]); + + $this->addressBook->getChild('LDAP:user.vcf'); + } + + public function testGetMultipleChildrenWithGroupEnumerationRestriction(): void { + $this->config + ->method('getAppValue') + ->willReturnMap([ + ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'], + ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'yes'], + ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'], + ]); + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user'); + $user->method('getBackendClassName')->willReturn('LDAP'); + $other1 = $this->createMock(IUser::class); + $other1->method('getUID')->willReturn('other1'); + $other1->method('getBackendClassName')->willReturn('LDAP'); + $other2 = $this->createMock(IUser::class); + $other2->method('getUID')->willReturn('other2'); + $other2->method('getBackendClassName')->willReturn('LDAP'); + $other3 = $this->createMock(IUser::class); + $other3->method('getUID')->willReturn('other3'); + $other3->method('getBackendClassName')->willReturn('LDAP'); + $this->userSession + ->method('getUser') + ->willReturn($user); + $group1 = $this->createMock(IGroup::class); + $group1 + ->method('getUsers') + ->willReturn([$user, $other1]); + $group2 = $this->createMock(IGroup::class); + $group2 + ->method('getUsers') + ->willReturn([$other1, $other2, $user]); + $this->groupManager + ->method('getUserGroups') + ->with($user) + ->willReturn([$group1]); + $this->cardDavBackend->expects(self::once()) + ->method('getMultipleCards') + ->with($this->addressBookInfo['id'], [ + SyncService::getCardUri($user), + SyncService::getCardUri($other1), + ]) + ->willReturn([ + [], + [], + ]); + + $cards = $this->addressBook->getMultipleChildren([ + SyncService::getCardUri($user), + SyncService::getCardUri($other1), + // SyncService::getCardUri($other2), // Omitted to test that it's not returned as stray + SyncService::getCardUri($other3), // No overlapping group with this one + ]); + + self::assertCount(2, $cards); + } + + public function testGetMultipleChildrenAsGuest(): void { + $this->config + ->method('getAppValue') + ->willReturnMap([ + ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'], + ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'], + ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'], + ]); + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user'); + $user->method('getBackendClassName')->willReturn('Guests'); + $this->userSession->expects(self::once()) + ->method('getUser') + ->willReturn($user); + + $cards = $this->addressBook->getMultipleChildren(['Database:user1.vcf', 'LDAP:user2.vcf']); + + self::assertEmpty($cards); + } + + public function testGetMultipleChildren(): void { + $this->config + ->method('getAppValue') + ->willReturnMap([ + ['core', 'shareapi_allow_share_dialog_user_enumeration', 'yes', 'yes'], + ['core', 'shareapi_restrict_user_enumeration_to_group', 'no', 'no'], + ['core', 'shareapi_restrict_user_enumeration_to_phone', 'no', 'no'], + ]); + $this->trustedServers + ->method('getServers') + ->willReturn([ + [ + 'shared_secret' => 'shared123', + ], + ]); + $cardData = <<<VCF +BEGIN:VCARD +VERSION:3.0 +PRODID:-//Sabre//Sabre VObject 4.4.2//EN +UID:admin +FN;X-NC-SCOPE=v2-federated:user +END:VCARD +VCF; + $this->cardDavBackend->expects(self::once()) + ->method('getMultipleCards') + ->with($this->addressBookInfo['id'], ['Database:user1.vcf', 'LDAP:user2.vcf']) + ->willReturn([ + [ + 'id' => 123, + 'carddata' => $cardData, + ], + [ + 'id' => 321, + 'carddata' => $cardData, + ], + ]); + + $cards = $this->addressBook->getMultipleChildren(['Database:user1.vcf', 'LDAP:user2.vcf']); + + self::assertCount(2, $cards); + } +} diff --git a/apps/dav/tests/unit/CardDAV/Validation/CardDavValidatePluginTest.php b/apps/dav/tests/unit/CardDAV/Validation/CardDavValidatePluginTest.php new file mode 100644 index 00000000000..058735ba32a --- /dev/null +++ b/apps/dav/tests/unit/CardDAV/Validation/CardDavValidatePluginTest.php @@ -0,0 +1,73 @@ +<?php + +declare(strict_types=1); + +/* + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Tests\unit\CardDAV\Validation; + +use OCA\DAV\CardDAV\Validation\CardDavValidatePlugin; +use OCP\IAppConfig; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\Exception\Forbidden; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; +use Test\TestCase; + +class CardDavValidatePluginTest extends TestCase { + + private CardDavValidatePlugin $plugin; + private IAppConfig&MockObject $config; + private RequestInterface&MockObject $request; + private ResponseInterface&MockObject $response; + + protected function setUp(): void { + parent::setUp(); + // construct mock objects + $this->config = $this->createMock(IAppConfig::class); + $this->request = $this->createMock(RequestInterface::class); + $this->response = $this->createMock(ResponseInterface::class); + $this->plugin = new CardDavValidatePlugin( + $this->config, + ); + } + + public function testPutSizeLessThenLimit(): void { + + // construct method responses + $this->config + ->method('getValueInt') + ->with('dav', 'card_size_limit', 5242880) + ->willReturn(5242880); + $this->request + ->method('getRawServerValue') + ->with('CONTENT_LENGTH') + ->willReturn('1024'); + // test condition + $this->assertTrue( + $this->plugin->beforePut($this->request, $this->response) + ); + + } + + public function testPutSizeMoreThenLimit(): void { + + // construct method responses + $this->config + ->method('getValueInt') + ->with('dav', 'card_size_limit', 5242880) + ->willReturn(5242880); + $this->request + ->method('getRawServerValue') + ->with('CONTENT_LENGTH') + ->willReturn('6242880'); + $this->expectException(Forbidden::class); + // test condition + $this->plugin->beforePut($this->request, $this->response); + + } + +} diff --git a/apps/dav/tests/unit/Command/DeleteCalendarTest.php b/apps/dav/tests/unit/Command/DeleteCalendarTest.php new file mode 100644 index 00000000000..2bd269de6dc --- /dev/null +++ b/apps/dav/tests/unit/Command/DeleteCalendarTest.php @@ -0,0 +1,231 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Tests\unit\Command; + +use OCA\DAV\CalDAV\BirthdayService; +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\Command\DeleteCalendar; +use OCP\IConfig; +use OCP\IL10N; +use OCP\IUserManager; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Tester\CommandTester; +use Test\TestCase; + +/** + * Class DeleteCalendarTest + * + * @package OCA\DAV\Tests\Command + */ +class DeleteCalendarTest extends TestCase { + public const USER = 'user'; + public const NAME = 'calendar'; + + private CalDavBackend&MockObject $calDav; + private IConfig&MockObject $config; + private IL10N&MockObject $l10n; + private IUserManager&MockObject $userManager; + private LoggerInterface&MockObject $logger; + private DeleteCalendar $command; + + protected function setUp(): void { + parent::setUp(); + + $this->calDav = $this->createMock(CalDavBackend::class); + $this->config = $this->createMock(IConfig::class); + $this->l10n = $this->createMock(IL10N::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->command = new DeleteCalendar( + $this->calDav, + $this->config, + $this->l10n, + $this->userManager, + $this->logger + ); + } + + public function testInvalidUser(): void { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage( + 'User <' . self::USER . '> is unknown.'); + + $this->userManager->expects($this->once()) + ->method('userExists') + ->with(self::USER) + ->willReturn(false); + + $commandTester = new CommandTester($this->command); + $commandTester->execute([ + 'uid' => self::USER, + 'name' => self::NAME, + ]); + } + + public function testNoCalendarName(): void { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage( + 'Please specify a calendar name or --birthday'); + + $this->userManager->expects($this->once()) + ->method('userExists') + ->with(self::USER) + ->willReturn(true); + + $commandTester = new CommandTester($this->command); + $commandTester->execute([ + 'uid' => self::USER, + ]); + } + + public function testInvalidCalendar(): void { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage( + 'User <' . self::USER . '> has no calendar named <' . self::NAME . '>.'); + + $this->userManager->expects($this->once()) + ->method('userExists') + ->with(self::USER) + ->willReturn(true); + $this->calDav->expects($this->once()) + ->method('getCalendarByUri') + ->with( + 'principals/users/' . self::USER, + self::NAME + ) + ->willReturn(null); + + $commandTester = new CommandTester($this->command); + $commandTester->execute([ + 'uid' => self::USER, + 'name' => self::NAME, + ]); + } + + public function testDelete(): void { + $id = 1234; + $calendar = [ + 'id' => $id, + 'principaluri' => 'principals/users/' . self::USER, + 'uri' => self::NAME, + ]; + + $this->userManager->expects($this->once()) + ->method('userExists') + ->with(self::USER) + ->willReturn(true); + $this->calDav->expects($this->once()) + ->method('getCalendarByUri') + ->with( + 'principals/users/' . self::USER, + self::NAME + ) + ->willReturn($calendar); + $this->calDav->expects($this->once()) + ->method('deleteCalendar') + ->with($id, false); + + $commandTester = new CommandTester($this->command); + $commandTester->execute([ + 'uid' => self::USER, + 'name' => self::NAME, + ]); + } + + public function testForceDelete(): void { + $id = 1234; + $calendar = [ + 'id' => $id, + 'principaluri' => 'principals/users/' . self::USER, + 'uri' => self::NAME + ]; + + $this->userManager->expects($this->once()) + ->method('userExists') + ->with(self::USER) + ->willReturn(true); + $this->calDav->expects($this->once()) + ->method('getCalendarByUri') + ->with( + 'principals/users/' . self::USER, + self::NAME + ) + ->willReturn($calendar); + $this->calDav->expects($this->once()) + ->method('deleteCalendar') + ->with($id, true); + + $commandTester = new CommandTester($this->command); + $commandTester->execute([ + 'uid' => self::USER, + 'name' => self::NAME, + '-f' => true + ]); + } + + public function testDeleteBirthday(): void { + $id = 1234; + $calendar = [ + 'id' => $id, + 'principaluri' => 'principals/users/' . self::USER, + 'uri' => BirthdayService::BIRTHDAY_CALENDAR_URI, + '{DAV:}displayname' => 'Test', + ]; + + $this->userManager->expects($this->once()) + ->method('userExists') + ->with(self::USER) + ->willReturn(true); + $this->calDav->expects($this->once()) + ->method('getCalendarByUri') + ->with( + 'principals/users/' . self::USER, + BirthdayService::BIRTHDAY_CALENDAR_URI + ) + ->willReturn($calendar); + $this->calDav->expects($this->once()) + ->method('deleteCalendar') + ->with($id); + + $commandTester = new CommandTester($this->command); + $commandTester->execute([ + 'uid' => self::USER, + '--birthday' => true, + ]); + } + + public function testBirthdayHasPrecedence(): void { + $calendar = [ + 'id' => 1234, + 'principaluri' => 'principals/users/' . self::USER, + 'uri' => BirthdayService::BIRTHDAY_CALENDAR_URI, + '{DAV:}displayname' => 'Test', + ]; + $this->userManager->expects($this->once()) + ->method('userExists') + ->with(self::USER) + ->willReturn(true); + $this->calDav->expects($this->once()) + ->method('getCalendarByUri') + ->with( + 'principals/users/' . self::USER, + BirthdayService::BIRTHDAY_CALENDAR_URI + ) + ->willReturn($calendar); + + $commandTester = new CommandTester($this->command); + $commandTester->execute([ + 'uid' => self::USER, + 'name' => self::NAME, + '--birthday' => true, + ]); + } +} diff --git a/apps/dav/tests/unit/Command/ListAddressbooksTest.php b/apps/dav/tests/unit/Command/ListAddressbooksTest.php new file mode 100644 index 00000000000..2768ed576c3 --- /dev/null +++ b/apps/dav/tests/unit/Command/ListAddressbooksTest.php @@ -0,0 +1,107 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\Command; + +use OCA\DAV\CardDAV\CardDavBackend; +use OCA\DAV\Command\ListAddressbooks; +use OCP\IUserManager; +use PHPUnit\Framework\MockObject\MockObject; +use Symfony\Component\Console\Tester\CommandTester; +use Test\TestCase; + +/** + * Class ListCalendarsTest + * + * @package OCA\DAV\Tests\Command + */ +class ListAddressbooksTest extends TestCase { + private IUserManager&MockObject $userManager; + private CardDavBackend&MockObject $cardDavBackend; + private ListAddressbooks $command; + + public const USERNAME = 'username'; + + protected function setUp(): void { + parent::setUp(); + + $this->userManager = $this->createMock(IUserManager::class); + $this->cardDavBackend = $this->createMock(CardDavBackend::class); + + $this->command = new ListAddressbooks( + $this->userManager, + $this->cardDavBackend + ); + } + + public function testWithBadUser(): void { + $this->expectException(\InvalidArgumentException::class); + + $this->userManager->expects($this->once()) + ->method('userExists') + ->with(self::USERNAME) + ->willReturn(false); + + $commandTester = new CommandTester($this->command); + $commandTester->execute([ + 'uid' => self::USERNAME, + ]); + $this->assertStringContainsString('User <' . self::USERNAME . '> in unknown', $commandTester->getDisplay()); + } + + public function testWithCorrectUserWithNoCalendars(): void { + $this->userManager->expects($this->once()) + ->method('userExists') + ->with(self::USERNAME) + ->willReturn(true); + + $this->cardDavBackend->expects($this->once()) + ->method('getAddressBooksForUser') + ->with('principals/users/' . self::USERNAME) + ->willReturn([]); + + $commandTester = new CommandTester($this->command); + $commandTester->execute([ + 'uid' => self::USERNAME, + ]); + $this->assertStringContainsString('User <' . self::USERNAME . "> has no addressbooks\n", $commandTester->getDisplay()); + } + + public static function dataExecute(): array { + return [ + [false, '✓'], + [true, 'x'] + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataExecute')] + public function testWithCorrectUser(bool $readOnly, string $output): void { + $this->userManager->expects($this->once()) + ->method('userExists') + ->with(self::USERNAME) + ->willReturn(true); + + $this->cardDavBackend->expects($this->once()) + ->method('getAddressBooksForUser') + ->with('principals/users/' . self::USERNAME) + ->willReturn([ + [ + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => $readOnly, + 'uri' => 'test', + '{DAV:}displayname' => 'dp', + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => 'owner-principal', + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname' => 'owner-dp', + ] + ]); + + $commandTester = new CommandTester($this->command); + $commandTester->execute([ + 'uid' => self::USERNAME, + ]); + $this->assertStringContainsString($output, $commandTester->getDisplay()); + } +} diff --git a/apps/dav/tests/unit/Command/ListCalendarSharesTest.php b/apps/dav/tests/unit/Command/ListCalendarSharesTest.php new file mode 100644 index 00000000000..e5d4251cbf9 --- /dev/null +++ b/apps/dav/tests/unit/Command/ListCalendarSharesTest.php @@ -0,0 +1,172 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Tests\unit\Command; + +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\Command\ListCalendarShares; +use OCA\DAV\Connector\Sabre\Principal; +use OCA\DAV\DAV\Sharing\SharingMapper; +use OCP\IUserManager; +use PHPUnit\Framework\MockObject\MockObject; +use Symfony\Component\Console\Tester\CommandTester; +use Test\TestCase; + +class ListCalendarSharesTest extends TestCase { + + private IUserManager&MockObject $userManager; + private Principal&MockObject $principal; + private CalDavBackend&MockObject $caldav; + private SharingMapper $sharingMapper; + private ListCalendarShares $command; + + protected function setUp(): void { + parent::setUp(); + + $this->userManager = $this->createMock(IUserManager::class); + $this->principal = $this->createMock(Principal::class); + $this->caldav = $this->createMock(CalDavBackend::class); + $this->sharingMapper = $this->createMock(SharingMapper::class); + + $this->command = new ListCalendarShares( + $this->userManager, + $this->principal, + $this->caldav, + $this->sharingMapper, + ); + } + + public function testUserUnknown(): void { + $user = 'bob'; + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage("User $user is unknown"); + + $this->userManager->expects($this->once()) + ->method('userExists') + ->with($user) + ->willReturn(false); + + $commandTester = new CommandTester($this->command); + $commandTester->execute([ + 'uid' => $user, + ]); + } + + public function testPrincipalNotFound(): void { + $user = 'bob'; + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage("Unable to fetch principal for user $user"); + + $this->userManager->expects($this->once()) + ->method('userExists') + ->with($user) + ->willReturn(true); + + $this->principal->expects($this->once()) + ->method('getPrincipalByPath') + ->with('principals/users/' . $user) + ->willReturn(null); + + $commandTester = new CommandTester($this->command); + $commandTester->execute([ + 'uid' => $user, + ]); + } + + public function testNoCalendarShares(): void { + $user = 'bob'; + + $this->userManager->expects($this->once()) + ->method('userExists') + ->with($user) + ->willReturn(true); + + $this->principal->expects($this->once()) + ->method('getPrincipalByPath') + ->with('principals/users/' . $user) + ->willReturn([ + 'uri' => 'principals/users/' . $user, + ]); + + $this->principal->expects($this->once()) + ->method('getGroupMembership') + ->willReturn([]); + $this->principal->expects($this->once()) + ->method('getCircleMembership') + ->willReturn([]); + + $this->sharingMapper->expects($this->once()) + ->method('getSharesByPrincipals') + ->willReturn([]); + + $commandTester = new CommandTester($this->command); + $commandTester->execute([ + 'uid' => $user, + ]); + + $this->assertStringContainsString( + "User $user has no calendar shares", + $commandTester->getDisplay() + ); + } + + public function testFilterByCalendarId(): void { + $user = 'bob'; + + $this->userManager->expects($this->once()) + ->method('userExists') + ->with($user) + ->willReturn(true); + + $this->principal->expects($this->once()) + ->method('getPrincipalByPath') + ->with('principals/users/' . $user) + ->willReturn([ + 'uri' => 'principals/users/' . $user, + ]); + + $this->principal->expects($this->once()) + ->method('getGroupMembership') + ->willReturn([]); + $this->principal->expects($this->once()) + ->method('getCircleMembership') + ->willReturn([]); + + $this->sharingMapper->expects($this->once()) + ->method('getSharesByPrincipals') + ->willReturn([ + [ + 'id' => 1000, + 'principaluri' => 'principals/users/bob', + 'type' => 'calendar', + 'access' => 2, + 'resourceid' => 10 + ], + [ + 'id' => 1001, + 'principaluri' => 'principals/users/bob', + 'type' => 'calendar', + 'access' => 3, + 'resourceid' => 11 + ], + ]); + + $commandTester = new CommandTester($this->command); + $commandTester->execute([ + 'uid' => $user, + '--calendar-id' => 10, + ]); + + $this->assertStringNotContainsString( + '1001', + $commandTester->getDisplay() + ); + } +} diff --git a/apps/dav/tests/unit/Command/ListCalendarsTest.php b/apps/dav/tests/unit/Command/ListCalendarsTest.php new file mode 100644 index 00000000000..d398a7c772f --- /dev/null +++ b/apps/dav/tests/unit/Command/ListCalendarsTest.php @@ -0,0 +1,112 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\Command; + +use OCA\DAV\CalDAV\BirthdayService; +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\Command\ListCalendars; +use OCP\IUserManager; +use PHPUnit\Framework\MockObject\MockObject; +use Symfony\Component\Console\Tester\CommandTester; +use Test\TestCase; + +/** + * Class ListCalendarsTest + * + * @package OCA\DAV\Tests\Command + */ +class ListCalendarsTest extends TestCase { + private IUserManager&MockObject $userManager; + private CalDavBackend&MockObject $calDav; + private ListCalendars $command; + + public const USERNAME = 'username'; + + protected function setUp(): void { + parent::setUp(); + + $this->userManager = $this->createMock(IUserManager::class); + $this->calDav = $this->createMock(CalDavBackend::class); + + $this->command = new ListCalendars( + $this->userManager, + $this->calDav + ); + } + + public function testWithBadUser(): void { + $this->expectException(\InvalidArgumentException::class); + + $this->userManager->expects($this->once()) + ->method('userExists') + ->with(self::USERNAME) + ->willReturn(false); + + $commandTester = new CommandTester($this->command); + $commandTester->execute([ + 'uid' => self::USERNAME, + ]); + $this->assertStringContainsString('User <' . self::USERNAME . '> in unknown', $commandTester->getDisplay()); + } + + public function testWithCorrectUserWithNoCalendars(): void { + $this->userManager->expects($this->once()) + ->method('userExists') + ->with(self::USERNAME) + ->willReturn(true); + + $this->calDav->expects($this->once()) + ->method('getCalendarsForUser') + ->with('principals/users/' . self::USERNAME) + ->willReturn([]); + + $commandTester = new CommandTester($this->command); + $commandTester->execute([ + 'uid' => self::USERNAME, + ]); + $this->assertStringContainsString('User <' . self::USERNAME . "> has no calendars\n", $commandTester->getDisplay()); + } + + public static function dataExecute(): array { + return [ + [false, '✓'], + [true, 'x'] + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataExecute')] + public function testWithCorrectUser(bool $readOnly, string $output): void { + $this->userManager->expects($this->once()) + ->method('userExists') + ->with(self::USERNAME) + ->willReturn(true); + + $this->calDav->expects($this->once()) + ->method('getCalendarsForUser') + ->with('principals/users/' . self::USERNAME) + ->willReturn([ + [ + 'uri' => BirthdayService::BIRTHDAY_CALENDAR_URI, + ], + [ + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => $readOnly, + 'uri' => 'test', + '{DAV:}displayname' => 'dp', + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => 'owner-principal', + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname' => 'owner-dp', + ] + ]); + + $commandTester = new CommandTester($this->command); + $commandTester->execute([ + 'uid' => self::USERNAME, + ]); + $this->assertStringContainsString($output, $commandTester->getDisplay()); + $this->assertStringNotContainsString(BirthdayService::BIRTHDAY_CALENDAR_URI, $commandTester->getDisplay()); + } +} diff --git a/apps/dav/tests/unit/Command/MoveCalendarTest.php b/apps/dav/tests/unit/Command/MoveCalendarTest.php new file mode 100644 index 00000000000..e9f016961f2 --- /dev/null +++ b/apps/dav/tests/unit/Command/MoveCalendarTest.php @@ -0,0 +1,354 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\Command; + +use InvalidArgumentException; +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\Command\MoveCalendar; +use OCP\IConfig; +use OCP\IGroupManager; +use OCP\IL10N; +use OCP\IUserManager; +use OCP\Share\IManager; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Tester\CommandTester; +use Test\TestCase; + +/** + * Class MoveCalendarTest + * + * @package OCA\DAV\Tests\Command + */ +class MoveCalendarTest extends TestCase { + private IUserManager&MockObject $userManager; + private IGroupManager&MockObject $groupManager; + private \OCP\Share\IManager&MockObject $shareManager; + private IConfig&MockObject $config; + private IL10N&MockObject $l10n; + private CalDavBackend&MockObject $calDav; + private LoggerInterface&MockObject $logger; + private MoveCalendar $command; + + protected function setUp(): void { + parent::setUp(); + + $this->userManager = $this->createMock(IUserManager::class); + $this->groupManager = $this->createMock(IGroupManager::class); + $this->shareManager = $this->createMock(IManager::class); + $this->config = $this->createMock(IConfig::class); + $this->l10n = $this->createMock(IL10N::class); + $this->calDav = $this->createMock(CalDavBackend::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->command = new MoveCalendar( + $this->userManager, + $this->groupManager, + $this->shareManager, + $this->config, + $this->l10n, + $this->calDav, + $this->logger + ); + } + + public static function dataExecute(): array { + return [ + [false, true], + [true, false] + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataExecute')] + public function testWithBadUserOrigin(bool $userOriginExists, bool $userDestinationExists): void { + $this->expectException(\InvalidArgumentException::class); + + $this->userManager->expects($this->exactly($userOriginExists ? 2 : 1)) + ->method('userExists') + ->willReturnMap([ + ['user', $userOriginExists], + ['user2', $userDestinationExists], + ]); + + $commandTester = new CommandTester($this->command); + $commandTester->execute([ + 'name' => $this->command->getName(), + 'sourceuid' => 'user', + 'destinationuid' => 'user2', + ]); + } + + + public function testMoveWithInexistantCalendar(): void { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('User <user> has no calendar named <personal>. You can run occ dav:list-calendars to list calendars URIs for this user.'); + + $this->userManager->expects($this->exactly(2)) + ->method('userExists') + ->willReturnMap([ + ['user', true], + ['user2', true], + ]); + + $this->calDav->expects($this->once())->method('getCalendarByUri') + ->with('principals/users/user', 'personal') + ->willReturn(null); + + $commandTester = new CommandTester($this->command); + $commandTester->execute([ + 'name' => 'personal', + 'sourceuid' => 'user', + 'destinationuid' => 'user2', + ]); + } + + + public function testMoveWithExistingDestinationCalendar(): void { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('User <user2> already has a calendar named <personal>.'); + + $this->userManager->expects($this->exactly(2)) + ->method('userExists') + ->willReturnMap([ + ['user', true], + ['user2', true], + ]); + + $this->calDav->expects($this->exactly(2)) + ->method('getCalendarByUri') + ->willReturnMap([ + ['principals/users/user', 'personal', [ + 'id' => 1234, + ]], + ['principals/users/user2', 'personal', [ + 'id' => 1234, + ]], + ]); + + $commandTester = new CommandTester($this->command); + $commandTester->execute([ + 'name' => 'personal', + 'sourceuid' => 'user', + 'destinationuid' => 'user2', + ]); + } + + public function testMove(): void { + $this->userManager->expects($this->exactly(2)) + ->method('userExists') + ->willReturnMap([ + ['user', true], + ['user2', true], + ]); + + $this->calDav->expects($this->exactly(2)) + ->method('getCalendarByUri') + ->willReturnMap([ + ['principals/users/user', 'personal', [ + 'id' => 1234, + ]], + ['principals/users/user2', 'personal', null], + ]); + + $this->calDav->expects($this->once())->method('getShares') + ->with(1234) + ->willReturn([]); + + $commandTester = new CommandTester($this->command); + $commandTester->execute([ + 'name' => 'personal', + 'sourceuid' => 'user', + 'destinationuid' => 'user2', + ]); + + $this->assertStringContainsString('[OK] Calendar <personal> was moved from user <user> to <user2>', $commandTester->getDisplay()); + } + + public static function dataTestMoveWithDestinationNotPartOfGroup(): array { + return [ + [true], + [false] + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestMoveWithDestinationNotPartOfGroup')] + public function testMoveWithDestinationNotPartOfGroup(bool $shareWithGroupMembersOnly): void { + $this->userManager->expects($this->exactly(2)) + ->method('userExists') + ->willReturnMap([ + ['user', true], + ['user2', true], + ]); + + $this->calDav->expects($this->exactly(2)) + ->method('getCalendarByUri') + ->willReturnMap([ + ['principals/users/user', 'personal', [ + 'id' => 1234, + 'uri' => 'personal', + ]], + ['principals/users/user2', 'personal', null], + ]); + + $this->shareManager->expects($this->once())->method('shareWithGroupMembersOnly') + ->willReturn($shareWithGroupMembersOnly); + + $this->calDav->expects($this->once())->method('getShares') + ->with(1234) + ->willReturn([ + ['href' => 'principal:principals/groups/nextclouders'] + ]); + if ($shareWithGroupMembersOnly === true) { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('User <user2> is not part of the group <nextclouders> with whom the calendar <personal> was shared. You may use -f to move the calendar while deleting this share.'); + } + + $commandTester = new CommandTester($this->command); + $commandTester->execute([ + 'name' => 'personal', + 'sourceuid' => 'user', + 'destinationuid' => 'user2', + ]); + } + + public function testMoveWithDestinationPartOfGroup(): void { + $this->userManager->expects($this->exactly(2)) + ->method('userExists') + ->willReturnMap([ + ['user', true], + ['user2', true], + ]); + + $this->calDav->expects($this->exactly(2)) + ->method('getCalendarByUri') + ->willReturnMap([ + ['principals/users/user', 'personal', [ + 'id' => 1234, + 'uri' => 'personal', + ]], + ['principals/users/user2', 'personal', null], + ]); + + $this->shareManager->expects($this->once())->method('shareWithGroupMembersOnly') + ->willReturn(true); + + $this->calDav->expects($this->once())->method('getShares') + ->with(1234) + ->willReturn([ + ['href' => 'principal:principals/groups/nextclouders'] + ]); + + $this->groupManager->expects($this->once())->method('isInGroup') + ->with('user2', 'nextclouders') + ->willReturn(true); + + $commandTester = new CommandTester($this->command); + $commandTester->execute([ + 'name' => 'personal', + 'sourceuid' => 'user', + 'destinationuid' => 'user2', + ]); + + $this->assertStringContainsString('[OK] Calendar <personal> was moved from user <user> to <user2>', $commandTester->getDisplay()); + } + + public function testMoveWithDestinationNotPartOfGroupAndForce(): void { + $this->userManager->expects($this->exactly(2)) + ->method('userExists') + ->willReturnMap([ + ['user', true], + ['user2', true], + ]); + + $this->calDav->expects($this->exactly(2)) + ->method('getCalendarByUri') + ->willReturnMap([ + ['principals/users/user', 'personal', [ + 'id' => 1234, + 'uri' => 'personal', + '{DAV:}displayname' => 'Personal' + ]], + ['principals/users/user2', 'personal', null], + ]); + + $this->shareManager->expects($this->once())->method('shareWithGroupMembersOnly') + ->willReturn(true); + + $this->calDav->expects($this->once())->method('getShares') + ->with(1234) + ->willReturn([ + [ + 'href' => 'principal:principals/groups/nextclouders', + '{DAV:}displayname' => 'Personal' + ] + ]); + $this->calDav->expects($this->once())->method('updateShares'); + + $commandTester = new CommandTester($this->command); + $commandTester->execute([ + 'name' => 'personal', + 'sourceuid' => 'user', + 'destinationuid' => 'user2', + '--force' => true + ]); + + $this->assertStringContainsString('[OK] Calendar <personal> was moved from user <user> to <user2>', $commandTester->getDisplay()); + } + + public static function dataTestMoveWithCalendarAlreadySharedToDestination(): array { + return [ + [true], + [false] + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestMoveWithCalendarAlreadySharedToDestination')] + public function testMoveWithCalendarAlreadySharedToDestination(bool $force): void { + $this->userManager->expects($this->exactly(2)) + ->method('userExists') + ->willReturnMap([ + ['user', true], + ['user2', true], + ]); + + $this->calDav->expects($this->exactly(2)) + ->method('getCalendarByUri') + ->willReturnMap([ + ['principals/users/user', 'personal', [ + 'id' => 1234, + 'uri' => 'personal', + '{DAV:}displayname' => 'Personal' + ]], + ['principals/users/user2', 'personal', null], + ]); + + $this->calDav->expects($this->once())->method('getShares') + ->with(1234) + ->willReturn([ + [ + 'href' => 'principal:principals/users/user2', + '{DAV:}displayname' => 'Personal' + ] + ]); + + if ($force === false) { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The calendar <personal> is already shared to user <user2>.You may use -f to move the calendar while deleting this share.'); + } else { + $this->calDav->expects($this->once())->method('updateShares'); + } + + $commandTester = new CommandTester($this->command); + $commandTester->execute([ + 'name' => 'personal', + 'sourceuid' => 'user', + 'destinationuid' => 'user2', + '--force' => $force, + ]); + } +} diff --git a/apps/dav/tests/unit/Command/RemoveInvalidSharesTest.php b/apps/dav/tests/unit/Command/RemoveInvalidSharesTest.php new file mode 100644 index 00000000000..ec56aa64eb2 --- /dev/null +++ b/apps/dav/tests/unit/Command/RemoveInvalidSharesTest.php @@ -0,0 +1,54 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2018 ownCloud GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Command; + +use OCA\DAV\Command\RemoveInvalidShares; +use OCA\DAV\Connector\Sabre\Principal; +use OCP\IDBConnection; +use OCP\Server; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Test\TestCase; + +/** + * Class RemoveInvalidSharesTest + * + * @package OCA\DAV\Tests\Unit\Repair + * @group DB + */ +class RemoveInvalidSharesTest extends TestCase { + protected function setUp(): void { + parent::setUp(); + $db = Server::get(IDBConnection::class); + + $db->insertIfNotExist('*PREFIX*dav_shares', [ + 'principaluri' => 'principal:unknown', + 'type' => 'calendar', + 'access' => 2, + 'resourceid' => 666, + ]); + } + + public function test(): void { + $db = Server::get(IDBConnection::class); + $principal = $this->createMock(Principal::class); + + $repair = new RemoveInvalidShares($db, $principal); + $this->invokePrivate($repair, 'run', [$this->createMock(InputInterface::class), $this->createMock(OutputInterface::class)]); + + $query = $db->getQueryBuilder(); + $query->select('*') + ->from('dav_shares') + ->where($query->expr()->eq('principaluri', $query->createNamedParameter('principal:unknown'))); + $result = $query->executeQuery(); + $data = $result->fetchAll(); + $result->closeCursor(); + $this->assertEquals(0, count($data)); + } +} diff --git a/apps/dav/tests/unit/comments/commentnode.php b/apps/dav/tests/unit/Comments/CommentsNodeTest.php index 8ebc5c2ff2c..9e108b4cf63 100644 --- a/apps/dav/tests/unit/comments/commentnode.php +++ b/apps/dav/tests/unit/Comments/CommentsNodeTest.php @@ -1,47 +1,40 @@ <?php + +declare(strict_types=1); /** - * @author Arthur Schiwon <blizzz@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - -namespace OCA\DAV\Tests\Unit\Comments; +namespace OCA\DAV\Tests\unit\Comments; use OCA\DAV\Comments\CommentNode; use OCP\Comments\IComment; +use OCP\Comments\ICommentsManager; use OCP\Comments\MessageTooLongException; - -class CommentsNode extends \Test\TestCase { - - protected $commentsManager; - protected $comment; - protected $node; - protected $userManager; - protected $logger; - protected $userSession; - - public function setUp() { +use OCP\IUser; +use OCP\IUserManager; +use OCP\IUserSession; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Sabre\DAV\PropPatch; + +class CommentsNodeTest extends \Test\TestCase { + protected ICommentsManager&MockObject $commentsManager; + protected IComment&MockObject $comment; + protected IUserManager&MockObject $userManager; + protected LoggerInterface&MockObject $logger; + protected IUserSession&MockObject $userSession; + protected CommentNode $node; + + protected function setUp(): void { parent::setUp(); - $this->commentsManager = $this->getMock('\OCP\Comments\ICommentsManager'); - $this->comment = $this->getMock('\OCP\Comments\IComment'); - $this->userManager = $this->getMock('\OCP\IUserManager'); - $this->userSession = $this->getMock('\OCP\IUserSession'); - $this->logger = $this->getMock('\OCP\ILogger'); + $this->commentsManager = $this->createMock(ICommentsManager::class); + $this->comment = $this->createMock(IComment::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->userSession = $this->createMock(IUserSession::class); + $this->logger = $this->createMock(LoggerInterface::class); $this->node = new CommentNode( $this->commentsManager, @@ -52,28 +45,27 @@ class CommentsNode extends \Test\TestCase { ); } - public function testDelete() { - $user = $this->getMock('\OCP\IUser'); - + public function testDelete(): void { + $user = $this->createMock(IUser::class); $user->expects($this->once()) ->method('getUID') - ->will($this->returnValue('alice')); + ->willReturn('alice'); $this->userSession->expects($this->once()) ->method('getUser') - ->will($this->returnValue($user)); + ->willReturn($user); $this->comment->expects($this->once()) ->method('getId') - ->will($this->returnValue('19')); + ->willReturn('19'); $this->comment->expects($this->any()) ->method('getActorType') - ->will($this->returnValue('users')); + ->willReturn('users'); $this->comment->expects($this->any()) ->method('getActorId') - ->will($this->returnValue('alice')); + ->willReturn('alice'); $this->commentsManager->expects($this->once()) ->method('delete') @@ -82,30 +74,29 @@ class CommentsNode extends \Test\TestCase { $this->node->delete(); } - /** - * @expectedException \Sabre\DAV\Exception\Forbidden - */ - public function testDeleteForbidden() { - $user = $this->getMock('\OCP\IUser'); + public function testDeleteForbidden(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + $user = $this->createMock(IUser::class); $user->expects($this->once()) ->method('getUID') - ->will($this->returnValue('mallory')); + ->willReturn('mallory'); $this->userSession->expects($this->once()) ->method('getUser') - ->will($this->returnValue($user)); + ->willReturn($user); $this->comment->expects($this->never()) ->method('getId'); $this->comment->expects($this->any()) ->method('getActorType') - ->will($this->returnValue('users')); + ->willReturn('users'); $this->comment->expects($this->any()) ->method('getActorId') - ->will($this->returnValue('alice')); + ->willReturn('alice'); $this->commentsManager->expects($this->never()) ->method('delete'); @@ -113,38 +104,37 @@ class CommentsNode extends \Test\TestCase { $this->node->delete(); } - public function testGetName() { + public function testGetName(): void { $id = '19'; $this->comment->expects($this->once()) ->method('getId') - ->will($this->returnValue($id)); + ->willReturn($id); $this->assertSame($this->node->getName(), $id); } - /** - * @expectedException \Sabre\DAV\Exception\MethodNotAllowed - */ - public function testSetName() { + + public function testSetName(): void { + $this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class); + $this->node->setName('666'); } - public function testGetLastModified() { + public function testGetLastModified(): void { $this->assertSame($this->node->getLastModified(), null); } - public function testUpdateComment() { + public function testUpdateComment(): void { $msg = 'Hello Earth'; - $user = $this->getMock('\OCP\IUser'); - + $user = $this->createMock(IUser::class); $user->expects($this->once()) ->method('getUID') - ->will($this->returnValue('alice')); + ->willReturn('alice'); $this->userSession->expects($this->once()) ->method('getUser') - ->will($this->returnValue($user)); + ->willReturn($user); $this->comment->expects($this->once()) ->method('setMessage') @@ -152,11 +142,11 @@ class CommentsNode extends \Test\TestCase { $this->comment->expects($this->any()) ->method('getActorType') - ->will($this->returnValue('users')); + ->willReturn('users'); $this->comment->expects($this->any()) ->method('getActorId') - ->will($this->returnValue('alice')); + ->willReturn('alice'); $this->commentsManager->expects($this->once()) ->method('save') @@ -165,104 +155,105 @@ class CommentsNode extends \Test\TestCase { $this->assertTrue($this->node->updateComment($msg)); } - public function testUpdateCommentLogException() { - $msg = null; - $user = $this->getMock('\OCP\IUser'); + public function testUpdateCommentLogException(): void { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('buh!'); + + $msg = null; + $user = $this->createMock(IUser::class); $user->expects($this->once()) ->method('getUID') - ->will($this->returnValue('alice')); + ->willReturn('alice'); $this->userSession->expects($this->once()) ->method('getUser') - ->will($this->returnValue($user)); + ->willReturn($user); $this->comment->expects($this->once()) ->method('setMessage') ->with($msg) - ->will($this->throwException(new \Exception('buh!'))); + ->willThrowException(new \Exception('buh!')); $this->comment->expects($this->any()) ->method('getActorType') - ->will($this->returnValue('users')); + ->willReturn('users'); $this->comment->expects($this->any()) ->method('getActorId') - ->will($this->returnValue('alice')); + ->willReturn('alice'); $this->commentsManager->expects($this->never()) ->method('save'); $this->logger->expects($this->once()) - ->method('logException'); + ->method('error'); - $this->assertFalse($this->node->updateComment($msg)); + $this->node->updateComment($msg); } - /** - * @expectedException \Sabre\DAV\Exception\BadRequest - * @expectedExceptionMessage Message exceeds allowed character limit of - */ - public function testUpdateCommentMessageTooLongException() { - $user = $this->getMock('\OCP\IUser'); + public function testUpdateCommentMessageTooLongException(): void { + $this->expectException(\Sabre\DAV\Exception\BadRequest::class); + $this->expectExceptionMessage('Message exceeds allowed character limit of'); + + $user = $this->createMock(IUser::class); $user->expects($this->once()) ->method('getUID') - ->will($this->returnValue('alice')); + ->willReturn('alice'); $this->userSession->expects($this->once()) ->method('getUser') - ->will($this->returnValue($user)); + ->willReturn($user); $this->comment->expects($this->once()) ->method('setMessage') - ->will($this->throwException(new MessageTooLongException())); + ->willThrowException(new MessageTooLongException()); $this->comment->expects($this->any()) ->method('getActorType') - ->will($this->returnValue('users')); + ->willReturn('users'); $this->comment->expects($this->any()) ->method('getActorId') - ->will($this->returnValue('alice')); + ->willReturn('alice'); $this->commentsManager->expects($this->never()) ->method('save'); $this->logger->expects($this->once()) - ->method('logException'); + ->method('error'); // imagine 'foo' has >1k characters. comment is mocked anyway. $this->node->updateComment('foo'); } - /** - * @expectedException \Sabre\DAV\Exception\Forbidden - */ - public function testUpdateForbiddenByUser() { - $msg = 'HaXX0r'; - $user = $this->getMock('\OCP\IUser'); + public function testUpdateForbiddenByUser(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + $msg = 'HaXX0r'; + $user = $this->createMock(IUser::class); $user->expects($this->once()) ->method('getUID') - ->will($this->returnValue('mallory')); + ->willReturn('mallory'); $this->userSession->expects($this->once()) ->method('getUser') - ->will($this->returnValue($user)); + ->willReturn($user); $this->comment->expects($this->never()) ->method('setMessage'); $this->comment->expects($this->any()) ->method('getActorType') - ->will($this->returnValue('users')); + ->willReturn('users'); $this->comment->expects($this->any()) ->method('getActorId') - ->will($this->returnValue('alice')); + ->willReturn('alice'); $this->commentsManager->expects($this->never()) ->method('save'); @@ -270,27 +261,26 @@ class CommentsNode extends \Test\TestCase { $this->node->updateComment($msg); } - /** - * @expectedException \Sabre\DAV\Exception\Forbidden - */ - public function testUpdateForbiddenByType() { - $msg = 'HaXX0r'; - $user = $this->getMock('\OCP\IUser'); + public function testUpdateForbiddenByType(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + $msg = 'HaXX0r'; + + $user = $this->createMock(IUser::class); $user->expects($this->never()) ->method('getUID'); $this->userSession->expects($this->once()) ->method('getUser') - ->will($this->returnValue($user)); + ->willReturn($user); $this->comment->expects($this->never()) ->method('setMessage'); $this->comment->expects($this->any()) ->method('getActorType') - ->will($this->returnValue('bots')); + ->willReturn('bots'); $this->commentsManager->expects($this->never()) ->method('save'); @@ -298,22 +288,22 @@ class CommentsNode extends \Test\TestCase { $this->node->updateComment($msg); } - /** - * @expectedException \Sabre\DAV\Exception\Forbidden - */ - public function testUpdateForbiddenByNotLoggedIn() { + + public function testUpdateForbiddenByNotLoggedIn(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + $msg = 'HaXX0r'; $this->userSession->expects($this->once()) ->method('getUser') - ->will($this->returnValue(null)); + ->willReturn(null); $this->comment->expects($this->never()) ->method('setMessage'); $this->comment->expects($this->any()) ->method('getActorType') - ->will($this->returnValue('users')); + ->willReturn('users'); $this->commentsManager->expects($this->never()) ->method('save'); @@ -321,11 +311,8 @@ class CommentsNode extends \Test\TestCase { $this->node->updateComment($msg); } - public function testPropPatch() { - $propPatch = $this->getMockBuilder('Sabre\DAV\PropPatch') - ->disableOriginalConstructor() - ->getMock(); - + public function testPropPatch(): void { + $propPatch = $this->createMock(PropPatch::class); $propPatch->expects($this->once()) ->method('handle') ->with('{http://owncloud.org/ns}message'); @@ -333,7 +320,7 @@ class CommentsNode extends \Test\TestCase { $this->node->propPatch($propPatch); } - public function testGetProperties() { + public function testGetProperties(): void { $ns = '{http://owncloud.org/ns}'; $expected = [ $ns . 'id' => '123', @@ -341,6 +328,18 @@ class CommentsNode extends \Test\TestCase { $ns . 'topmostParentId' => '2', $ns . 'childrenCount' => 3, $ns . 'message' => 'such a nice file you have…', + $ns . 'mentions' => [ + [ $ns . 'mention' => [ + $ns . 'mentionType' => 'user', + $ns . 'mentionId' => 'alice', + $ns . 'mentionDisplayName' => 'Alice Al-Isson', + ] ], + [ $ns . 'mention' => [ + $ns . 'mentionType' => 'user', + $ns . 'mentionId' => 'bob', + $ns . 'mentionDisplayName' => 'Unknown user', + ] ], + ], $ns . 'verb' => 'comment', $ns . 'actorType' => 'users', $ns . 'actorId' => 'alice', @@ -349,84 +348,120 @@ class CommentsNode extends \Test\TestCase { $ns . 'latestChildDateTime' => new \DateTime('2016-01-12 18:48:00'), $ns . 'objectType' => 'files', $ns . 'objectId' => '1848', + $ns . 'referenceId' => 'ref', $ns . 'isUnread' => null, + $ns . 'reactions' => [], + $ns . 'metaData' => [ + 'last_edited_at' => 1702553770, + 'last_edited_by_id' => 'charly', + 'last_edited_by_type' => 'user', + ], + $ns . 'expireDate' => new \DateTime('2016-01-12 19:00:00'), ]; + $this->commentsManager->expects($this->exactly(2)) + ->method('resolveDisplayName') + ->willReturnMap([ + ['user', 'alice', 'Alice Al-Isson'], + ['user', 'bob', 'Unknown user'] + ]); + $this->comment->expects($this->once()) ->method('getId') - ->will($this->returnValue($expected[$ns . 'id'])); + ->willReturn($expected[$ns . 'id']); $this->comment->expects($this->once()) ->method('getParentId') - ->will($this->returnValue($expected[$ns . 'parentId'])); + ->willReturn($expected[$ns . 'parentId']); $this->comment->expects($this->once()) ->method('getTopmostParentId') - ->will($this->returnValue($expected[$ns . 'topmostParentId'])); + ->willReturn($expected[$ns . 'topmostParentId']); $this->comment->expects($this->once()) ->method('getChildrenCount') - ->will($this->returnValue($expected[$ns . 'childrenCount'])); + ->willReturn($expected[$ns . 'childrenCount']); $this->comment->expects($this->once()) ->method('getMessage') - ->will($this->returnValue($expected[$ns . 'message'])); + ->willReturn($expected[$ns . 'message']); + + $this->comment->expects($this->once()) + ->method('getMentions') + ->willReturn([ + ['type' => 'user', 'id' => 'alice'], + ['type' => 'user', 'id' => 'bob'], + ]); $this->comment->expects($this->once()) ->method('getVerb') - ->will($this->returnValue($expected[$ns . 'verb'])); + ->willReturn($expected[$ns . 'verb']); $this->comment->expects($this->exactly(2)) ->method('getActorType') - ->will($this->returnValue($expected[$ns . 'actorType'])); + ->willReturn($expected[$ns . 'actorType']); $this->comment->expects($this->exactly(2)) ->method('getActorId') - ->will($this->returnValue($expected[$ns . 'actorId'])); + ->willReturn($expected[$ns . 'actorId']); $this->comment->expects($this->once()) ->method('getCreationDateTime') - ->will($this->returnValue($expected[$ns . 'creationDateTime'])); + ->willReturn($expected[$ns . 'creationDateTime']); $this->comment->expects($this->once()) ->method('getLatestChildDateTime') - ->will($this->returnValue($expected[$ns . 'latestChildDateTime'])); + ->willReturn($expected[$ns . 'latestChildDateTime']); $this->comment->expects($this->once()) ->method('getObjectType') - ->will($this->returnValue($expected[$ns . 'objectType'])); + ->willReturn($expected[$ns . 'objectType']); $this->comment->expects($this->once()) ->method('getObjectId') - ->will($this->returnValue($expected[$ns . 'objectId'])); + ->willReturn($expected[$ns . 'objectId']); + + $this->comment->expects($this->once()) + ->method('getReferenceId') + ->willReturn($expected[$ns . 'referenceId']); + + $this->comment->expects($this->once()) + ->method('getMetaData') + ->willReturn($expected[$ns . 'metaData']); - $user = $this->getMockBuilder('\OCP\IUser') + $this->comment->expects($this->once()) + ->method('getExpireDate') + ->willReturn($expected[$ns . 'expireDate']); + + $user = $this->getMockBuilder(IUser::class) ->disableOriginalConstructor() ->getMock(); $user->expects($this->once()) ->method('getDisplayName') - ->will($this->returnValue($expected[$ns . 'actorDisplayName'])); + ->willReturn($expected[$ns . 'actorDisplayName']); $this->userManager->expects($this->once()) ->method('get') ->with('alice') - ->will($this->returnValue($user)); + ->willReturn($user); $properties = $this->node->getProperties(null); - foreach($properties as $name => $value) { - $this->assertTrue(array_key_exists($name, $expected)); + foreach ($properties as $name => $value) { + $this->assertArrayHasKey($name, $expected, 'Key not found in the list of $expected'); $this->assertSame($expected[$name], $value); unset($expected[$name]); } $this->assertTrue(empty($expected)); } - public function readCommentProvider() { + public static function readCommentProvider(): array { $creationDT = new \DateTime('2016-01-19 18:48:00'); $diff = new \DateInterval('PT2H'); - $readDT1 = clone $creationDT; $readDT1->sub($diff); - $readDT2 = clone $creationDT; $readDT2->add($diff); + $readDT1 = clone $creationDT; + $readDT1->sub($diff); + $readDT2 = clone $creationDT; + $readDT2->add($diff); return [ [$creationDT, $readDT1, 'true'], [$creationDT, $readDT2, 'false'], @@ -434,22 +469,27 @@ class CommentsNode extends \Test\TestCase { ]; } - /** - * @dataProvider readCommentProvider - * @param $expected - */ - public function testGetPropertiesUnreadProperty($creationDT, $readDT, $expected) { + #[\PHPUnit\Framework\Attributes\DataProvider('readCommentProvider')] + public function testGetPropertiesUnreadProperty(\DateTime $creationDT, ?\DateTime $readDT, string $expected): void { $this->comment->expects($this->any()) ->method('getCreationDateTime') - ->will($this->returnValue($creationDT)); + ->willReturn($creationDT); + + $this->comment->expects($this->any()) + ->method('getMentions') + ->willReturn([]); $this->commentsManager->expects($this->once()) ->method('getReadMark') - ->will($this->returnValue($readDT)); + ->willReturn($readDT); $this->userSession->expects($this->once()) ->method('getUser') - ->will($this->returnValue($this->getMock('\OCP\IUser'))); + ->willReturn( + $this->getMockBuilder(IUser::class) + ->disableOriginalConstructor() + ->getMock() + ); $properties = $this->node->getProperties(null); diff --git a/apps/dav/tests/unit/comments/commentsplugin.php b/apps/dav/tests/unit/Comments/CommentsPluginTest.php index c7d073d1491..18d32772f7b 100644 --- a/apps/dav/tests/unit/comments/commentsplugin.php +++ b/apps/dav/tests/unit/Comments/CommentsPluginTest.php @@ -1,66 +1,48 @@ <?php + /** - * @author Arthur Schiwon <blizzz@owncloud.com> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - -namespace OCA\DAV\Tests\Unit\Comments; +namespace OCA\DAV\Tests\unit\Comments; use OC\Comments\Comment; use OCA\DAV\Comments\CommentsPlugin as CommentsPluginImplementation; +use OCA\DAV\Comments\EntityCollection; use OCP\Comments\IComment; -use Sabre\DAV\Exception\NotFound; - -class CommentsPlugin extends \Test\TestCase { - /** @var \Sabre\DAV\Server */ - private $server; - - /** @var \Sabre\DAV\Tree */ - private $tree; - - /** @var \OCP\Comments\ICommentsManager */ - private $commentsManager; - - /** @var \OCP\IUserSession */ - private $userSession; - - /** @var CommentsPluginImplementation */ - private $plugin; - - public function setUp() { +use OCP\Comments\ICommentsManager; +use OCP\IUser; +use OCP\IUserSession; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\INode; +use Sabre\DAV\Tree; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +class CommentsPluginTest extends \Test\TestCase { + private \Sabre\DAV\Server&MockObject $server; + private Tree&MockObject $tree; + private ICommentsManager&MockObject $commentsManager; + private IUserSession&MockObject $userSession; + private CommentsPluginImplementation $plugin; + + protected function setUp(): void { parent::setUp(); - $this->tree = $this->getMockBuilder('\Sabre\DAV\Tree') - ->disableOriginalConstructor() - ->getMock(); + $this->tree = $this->createMock(Tree::class); - $this->server = $this->getMockBuilder('\Sabre\DAV\Server') + $this->server = $this->getMockBuilder(\Sabre\DAV\Server::class) ->setConstructorArgs([$this->tree]) - ->setMethods(['getRequestUri']) + ->onlyMethods(['getRequestUri']) ->getMock(); - $this->commentsManager = $this->getMock('\OCP\Comments\ICommentsManager'); - $this->userSession = $this->getMock('\OCP\IUserSession'); + $this->commentsManager = $this->createMock(ICommentsManager::class); + $this->userSession = $this->createMock(IUserSession::class); $this->plugin = new CommentsPluginImplementation($this->commentsManager, $this->userSession); } - public function testCreateComment() { + public function testCreateComment(): void { $commentData = [ 'actorType' => 'users', 'verb' => 'comment', @@ -79,20 +61,22 @@ class CommentsPlugin extends \Test\TestCase { $requestData = json_encode($commentData); - $user = $this->getMock('OCP\IUser'); + $user = $this->getMockBuilder(IUser::class) + ->disableOriginalConstructor() + ->getMock(); $user->expects($this->once()) ->method('getUID') - ->will($this->returnValue('alice')); + ->willReturn('alice'); - $node = $this->getMockBuilder('\OCA\DAV\Comments\EntityCollection') + $node = $this->getMockBuilder(EntityCollection::class) ->disableOriginalConstructor() ->getMock(); $node->expects($this->once()) ->method('getName') - ->will($this->returnValue('files')); + ->willReturn('files'); $node->expects($this->once()) ->method('getId') - ->will($this->returnValue('42')); + ->willReturn('42'); $node->expects($this->once()) ->method('setReadMarker') @@ -101,11 +85,11 @@ class CommentsPlugin extends \Test\TestCase { $this->commentsManager->expects($this->once()) ->method('create') ->with('users', 'alice', 'files', '42') - ->will($this->returnValue($comment)); + ->willReturn($comment); $this->userSession->expects($this->once()) ->method('getUser') - ->will($this->returnValue($user)); + ->willReturn($user); // technically, this is a shortcut. Inbetween EntityTypeCollection would // be returned, but doing it exactly right would not be really @@ -114,32 +98,32 @@ class CommentsPlugin extends \Test\TestCase { $this->tree->expects($this->any()) ->method('getNodeForPath') ->with('/' . $path) - ->will($this->returnValue($node)); + ->willReturn($node); - $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface') + $request = $this->getMockBuilder(RequestInterface::class) ->disableOriginalConstructor() ->getMock(); - $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface') + $response = $this->getMockBuilder(ResponseInterface::class) ->disableOriginalConstructor() ->getMock(); $request->expects($this->once()) ->method('getPath') - ->will($this->returnValue('/' . $path)); + ->willReturn('/' . $path); $request->expects($this->once()) ->method('getBodyAsString') - ->will($this->returnValue($requestData)); + ->willReturn($requestData); $request->expects($this->once()) ->method('getHeader') ->with('Content-Type') - ->will($this->returnValue('application/json')); + ->willReturn('application/json'); $request->expects($this->once()) ->method('getUrl') - ->will($this->returnValue('http://example.com/dav/' . $path)); + ->willReturn('http://example.com/dav/' . $path); $response->expects($this->once()) ->method('setHeader') @@ -147,16 +131,16 @@ class CommentsPlugin extends \Test\TestCase { $this->server->expects($this->any()) ->method('getRequestUri') - ->will($this->returnValue($path)); + ->willReturn($path); $this->plugin->initialize($this->server); $this->plugin->httpPost($request, $response); } - /** - * @expectedException \Sabre\DAV\Exception\NotFound - */ - public function testCreateCommentInvalidObject() { + + public function testCreateCommentInvalidObject(): void { + $this->expectException(\Sabre\DAV\Exception\NotFound::class); + $commentData = [ 'actorType' => 'users', 'verb' => 'comment', @@ -164,20 +148,22 @@ class CommentsPlugin extends \Test\TestCase { ]; $comment = new Comment([ - 'objectType' => 'files', - 'objectId' => '666', - 'actorType' => 'users', - 'actorId' => 'alice' - ] + $commentData); + 'objectType' => 'files', + 'objectId' => '666', + 'actorType' => 'users', + 'actorId' => 'alice' + ] + $commentData); $comment->setId('23'); $path = 'comments/files/666'; - $user = $this->getMock('OCP\IUser'); + $user = $this->getMockBuilder(IUser::class) + ->disableOriginalConstructor() + ->getMock(); $user->expects($this->never()) ->method('getUID'); - $node = $this->getMockBuilder('\OCA\DAV\Comments\EntityCollection') + $node = $this->getMockBuilder(EntityCollection::class) ->disableOriginalConstructor() ->getMock(); $node->expects($this->never()) @@ -198,19 +184,19 @@ class CommentsPlugin extends \Test\TestCase { $this->tree->expects($this->any()) ->method('getNodeForPath') ->with('/' . $path) - ->will($this->throwException(new \Sabre\DAV\Exception\NotFound())); + ->willThrowException(new \Sabre\DAV\Exception\NotFound()); - $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface') + $request = $this->getMockBuilder(RequestInterface::class) ->disableOriginalConstructor() ->getMock(); - $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface') + $response = $this->getMockBuilder(ResponseInterface::class) ->disableOriginalConstructor() ->getMock(); $request->expects($this->once()) ->method('getPath') - ->will($this->returnValue('/' . $path)); + ->willReturn('/' . $path); $request->expects($this->never()) ->method('getBodyAsString'); @@ -227,16 +213,16 @@ class CommentsPlugin extends \Test\TestCase { $this->server->expects($this->any()) ->method('getRequestUri') - ->will($this->returnValue($path)); + ->willReturn($path); $this->plugin->initialize($this->server); $this->plugin->httpPost($request, $response); } - /** - * @expectedException \Sabre\DAV\Exception\BadRequest - */ - public function testCreateCommentInvalidActor() { + + public function testCreateCommentInvalidActor(): void { + $this->expectException(\Sabre\DAV\Exception\BadRequest::class); + $commentData = [ 'actorType' => 'robots', 'verb' => 'comment', @@ -244,30 +230,32 @@ class CommentsPlugin extends \Test\TestCase { ]; $comment = new Comment([ - 'objectType' => 'files', - 'objectId' => '42', - 'actorType' => 'users', - 'actorId' => 'alice' - ] + $commentData); + 'objectType' => 'files', + 'objectId' => '42', + 'actorType' => 'users', + 'actorId' => 'alice' + ] + $commentData); $comment->setId('23'); $path = 'comments/files/42'; $requestData = json_encode($commentData); - $user = $this->getMock('OCP\IUser'); + $user = $this->getMockBuilder(IUser::class) + ->disableOriginalConstructor() + ->getMock(); $user->expects($this->never()) ->method('getUID'); - $node = $this->getMockBuilder('\OCA\DAV\Comments\EntityCollection') + $node = $this->getMockBuilder(EntityCollection::class) ->disableOriginalConstructor() ->getMock(); $node->expects($this->once()) ->method('getName') - ->will($this->returnValue('files')); + ->willReturn('files'); $node->expects($this->once()) ->method('getId') - ->will($this->returnValue('42')); + ->willReturn('42'); $this->commentsManager->expects($this->never()) ->method('create'); @@ -282,28 +270,28 @@ class CommentsPlugin extends \Test\TestCase { $this->tree->expects($this->any()) ->method('getNodeForPath') ->with('/' . $path) - ->will($this->returnValue($node)); + ->willReturn($node); - $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface') + $request = $this->getMockBuilder(RequestInterface::class) ->disableOriginalConstructor() ->getMock(); - $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface') + $response = $this->getMockBuilder(ResponseInterface::class) ->disableOriginalConstructor() ->getMock(); $request->expects($this->once()) ->method('getPath') - ->will($this->returnValue('/' . $path)); + ->willReturn('/' . $path); $request->expects($this->once()) ->method('getBodyAsString') - ->will($this->returnValue($requestData)); + ->willReturn($requestData); $request->expects($this->once()) ->method('getHeader') ->with('Content-Type') - ->will($this->returnValue('application/json')); + ->willReturn('application/json'); $request->expects($this->never()) ->method('getUrl'); @@ -313,16 +301,16 @@ class CommentsPlugin extends \Test\TestCase { $this->server->expects($this->any()) ->method('getRequestUri') - ->will($this->returnValue($path)); + ->willReturn($path); $this->plugin->initialize($this->server); $this->plugin->httpPost($request, $response); } - /** - * @expectedException \Sabre\DAV\Exception\UnsupportedMediaType - */ - public function testCreateCommentUnsupportedMediaType() { + + public function testCreateCommentUnsupportedMediaType(): void { + $this->expectException(\Sabre\DAV\Exception\UnsupportedMediaType::class); + $commentData = [ 'actorType' => 'users', 'verb' => 'comment', @@ -330,30 +318,32 @@ class CommentsPlugin extends \Test\TestCase { ]; $comment = new Comment([ - 'objectType' => 'files', - 'objectId' => '42', - 'actorType' => 'users', - 'actorId' => 'alice' - ] + $commentData); + 'objectType' => 'files', + 'objectId' => '42', + 'actorType' => 'users', + 'actorId' => 'alice' + ] + $commentData); $comment->setId('23'); $path = 'comments/files/42'; $requestData = json_encode($commentData); - $user = $this->getMock('OCP\IUser'); + $user = $this->getMockBuilder(IUser::class) + ->disableOriginalConstructor() + ->getMock(); $user->expects($this->never()) ->method('getUID'); - $node = $this->getMockBuilder('\OCA\DAV\Comments\EntityCollection') + $node = $this->getMockBuilder(EntityCollection::class) ->disableOriginalConstructor() ->getMock(); $node->expects($this->once()) ->method('getName') - ->will($this->returnValue('files')); + ->willReturn('files'); $node->expects($this->once()) ->method('getId') - ->will($this->returnValue('42')); + ->willReturn('42'); $this->commentsManager->expects($this->never()) ->method('create'); @@ -368,28 +358,28 @@ class CommentsPlugin extends \Test\TestCase { $this->tree->expects($this->any()) ->method('getNodeForPath') ->with('/' . $path) - ->will($this->returnValue($node)); + ->willReturn($node); - $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface') + $request = $this->getMockBuilder(RequestInterface::class) ->disableOriginalConstructor() ->getMock(); - $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface') + $response = $this->getMockBuilder(ResponseInterface::class) ->disableOriginalConstructor() ->getMock(); $request->expects($this->once()) ->method('getPath') - ->will($this->returnValue('/' . $path)); + ->willReturn('/' . $path); $request->expects($this->once()) ->method('getBodyAsString') - ->will($this->returnValue($requestData)); + ->willReturn($requestData); $request->expects($this->once()) ->method('getHeader') ->with('Content-Type') - ->will($this->returnValue('application/trumpscript')); + ->willReturn('application/trumpscript'); $request->expects($this->never()) ->method('getUrl'); @@ -399,16 +389,16 @@ class CommentsPlugin extends \Test\TestCase { $this->server->expects($this->any()) ->method('getRequestUri') - ->will($this->returnValue($path)); + ->willReturn($path); $this->plugin->initialize($this->server); $this->plugin->httpPost($request, $response); } - /** - * @expectedException \Sabre\DAV\Exception\BadRequest - */ - public function testCreateCommentInvalidPayload() { + + public function testCreateCommentInvalidPayload(): void { + $this->expectException(\Sabre\DAV\Exception\BadRequest::class); + $commentData = [ 'actorType' => 'users', 'verb' => '', @@ -416,52 +406,44 @@ class CommentsPlugin extends \Test\TestCase { ]; $comment = new Comment([ - 'objectType' => 'files', - 'objectId' => '42', - 'actorType' => 'users', - 'actorId' => 'alice', - 'message' => 'dummy', - 'verb' => 'dummy' - ]); + 'objectType' => 'files', + 'objectId' => '42', + 'actorType' => 'users', + 'actorId' => 'alice', + 'message' => 'dummy', + 'verb' => 'dummy' + ]); $comment->setId('23'); $path = 'comments/files/42'; $requestData = json_encode($commentData); - $user = $this->getMock('OCP\IUser'); + $user = $this->getMockBuilder(IUser::class) + ->disableOriginalConstructor() + ->getMock(); $user->expects($this->once()) ->method('getUID') - ->will($this->returnValue('alice')); + ->willReturn('alice'); - $node = $this->getMockBuilder('\OCA\DAV\Comments\EntityCollection') + $node = $this->getMockBuilder(EntityCollection::class) ->disableOriginalConstructor() ->getMock(); $node->expects($this->once()) ->method('getName') - ->will($this->returnValue('files')); + ->willReturn('files'); $node->expects($this->once()) ->method('getId') - ->will($this->returnValue('42')); + ->willReturn('42'); $this->commentsManager->expects($this->once()) ->method('create') ->with('users', 'alice', 'files', '42') - ->will($this->returnValue($comment)); - - $this->commentsManager->expects($this->any()) - ->method('setMessage') - ->with('') - ->will($this->throwException(new \InvalidArgumentException())); - - $this->commentsManager->expects($this->any()) - ->method('setVerb') - ->with('') - ->will($this->throwException(new \InvalidArgumentException())); + ->willReturn($comment); $this->userSession->expects($this->once()) ->method('getUser') - ->will($this->returnValue($user)); + ->willReturn($user); // technically, this is a shortcut. Inbetween EntityTypeCollection would // be returned, but doing it exactly right would not be really @@ -470,28 +452,28 @@ class CommentsPlugin extends \Test\TestCase { $this->tree->expects($this->any()) ->method('getNodeForPath') ->with('/' . $path) - ->will($this->returnValue($node)); + ->willReturn($node); - $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface') + $request = $this->getMockBuilder(RequestInterface::class) ->disableOriginalConstructor() ->getMock(); - $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface') + $response = $this->getMockBuilder(ResponseInterface::class) ->disableOriginalConstructor() ->getMock(); $request->expects($this->once()) ->method('getPath') - ->will($this->returnValue('/' . $path)); + ->willReturn('/' . $path); $request->expects($this->once()) ->method('getBodyAsString') - ->will($this->returnValue($requestData)); + ->willReturn($requestData); $request->expects($this->once()) ->method('getHeader') ->with('Content-Type') - ->will($this->returnValue('application/json')); + ->willReturn('application/json'); $request->expects($this->never()) ->method('getUrl'); @@ -501,17 +483,17 @@ class CommentsPlugin extends \Test\TestCase { $this->server->expects($this->any()) ->method('getRequestUri') - ->will($this->returnValue($path)); + ->willReturn($path); $this->plugin->initialize($this->server); $this->plugin->httpPost($request, $response); } - /** - * @expectedException \Sabre\DAV\Exception\BadRequest - * @expectedExceptionMessage Message exceeds allowed character limit of - */ - public function testCreateCommentMessageTooLong() { + + public function testCreateCommentMessageTooLong(): void { + $this->expectException(\Sabre\DAV\Exception\BadRequest::class); + $this->expectExceptionMessage('Message exceeds allowed character limit of'); + $commentData = [ 'actorType' => 'users', 'verb' => 'comment', @@ -519,32 +501,34 @@ class CommentsPlugin extends \Test\TestCase { ]; $comment = new Comment([ - 'objectType' => 'files', - 'objectId' => '42', - 'actorType' => 'users', - 'actorId' => 'alice', - 'verb' => 'comment', - ]); + 'objectType' => 'files', + 'objectId' => '42', + 'actorType' => 'users', + 'actorId' => 'alice', + 'verb' => 'comment', + ]); $comment->setId('23'); $path = 'comments/files/42'; $requestData = json_encode($commentData); - $user = $this->getMock('OCP\IUser'); + $user = $this->getMockBuilder(IUser::class) + ->disableOriginalConstructor() + ->getMock(); $user->expects($this->once()) ->method('getUID') - ->will($this->returnValue('alice')); + ->willReturn('alice'); - $node = $this->getMockBuilder('\OCA\DAV\Comments\EntityCollection') + $node = $this->getMockBuilder(EntityCollection::class) ->disableOriginalConstructor() ->getMock(); $node->expects($this->once()) ->method('getName') - ->will($this->returnValue('files')); + ->willReturn('files'); $node->expects($this->once()) ->method('getId') - ->will($this->returnValue('42')); + ->willReturn('42'); $node->expects($this->never()) ->method('setReadMarker'); @@ -552,11 +536,11 @@ class CommentsPlugin extends \Test\TestCase { $this->commentsManager->expects($this->once()) ->method('create') ->with('users', 'alice', 'files', '42') - ->will($this->returnValue($comment)); + ->willReturn($comment); $this->userSession->expects($this->once()) ->method('getUser') - ->will($this->returnValue($user)); + ->willReturn($user); // technically, this is a shortcut. Inbetween EntityTypeCollection would // be returned, but doing it exactly right would not be really @@ -565,88 +549,96 @@ class CommentsPlugin extends \Test\TestCase { $this->tree->expects($this->any()) ->method('getNodeForPath') ->with('/' . $path) - ->will($this->returnValue($node)); + ->willReturn($node); - $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface') + $request = $this->getMockBuilder(RequestInterface::class) ->disableOriginalConstructor() ->getMock(); - $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface') + $response = $this->getMockBuilder(ResponseInterface::class) ->disableOriginalConstructor() ->getMock(); $request->expects($this->once()) ->method('getPath') - ->will($this->returnValue('/' . $path)); + ->willReturn('/' . $path); $request->expects($this->once()) ->method('getBodyAsString') - ->will($this->returnValue($requestData)); + ->willReturn($requestData); $request->expects($this->once()) ->method('getHeader') ->with('Content-Type') - ->will($this->returnValue('application/json')); + ->willReturn('application/json'); $response->expects($this->never()) ->method('setHeader'); $this->server->expects($this->any()) ->method('getRequestUri') - ->will($this->returnValue($path)); + ->willReturn($path); $this->plugin->initialize($this->server); $this->plugin->httpPost($request, $response); } - /** - * @expectedException \Sabre\DAV\Exception\ReportNotSupported - */ - public function testOnReportInvalidNode() { + + public function testOnReportInvalidNode(): void { + $this->expectException(\Sabre\DAV\Exception\ReportNotSupported::class); + $path = 'totally/unrelated/13'; $this->tree->expects($this->any()) ->method('getNodeForPath') ->with('/' . $path) - ->will($this->returnValue($this->getMock('\Sabre\DAV\INode'))); + ->willReturn( + $this->getMockBuilder(INode::class) + ->disableOriginalConstructor() + ->getMock() + ); $this->server->expects($this->any()) ->method('getRequestUri') - ->will($this->returnValue($path)); + ->willReturn($path); $this->plugin->initialize($this->server); $this->plugin->onReport(CommentsPluginImplementation::REPORT_NAME, [], '/' . $path); } - /** - * @expectedException \Sabre\DAV\Exception\ReportNotSupported - */ - public function testOnReportInvalidReportName() { + + public function testOnReportInvalidReportName(): void { + $this->expectException(\Sabre\DAV\Exception\ReportNotSupported::class); + $path = 'comments/files/42'; $this->tree->expects($this->any()) ->method('getNodeForPath') ->with('/' . $path) - ->will($this->returnValue($this->getMock('\Sabre\DAV\INode'))); + ->willReturn( + $this->getMockBuilder(INode::class) + ->disableOriginalConstructor() + ->getMock() + ); $this->server->expects($this->any()) ->method('getRequestUri') - ->will($this->returnValue($path)); + ->willReturn($path); $this->plugin->initialize($this->server); $this->plugin->onReport('{whoever}whatever', [], '/' . $path); } - public function testOnReportDateTimeEmpty() { + public function testOnReportDateTimeEmpty(): void { $path = 'comments/files/42'; $parameters = [ [ - 'name' => '{http://owncloud.org/ns}limit', + 'name' => '{http://owncloud.org/ns}limit', 'value' => 5, ], [ - 'name' => '{http://owncloud.org/ns}offset', + 'name' => '{http://owncloud.org/ns}offset', 'value' => 10, ], [ @@ -655,15 +647,15 @@ class CommentsPlugin extends \Test\TestCase { ] ]; - $node = $this->getMockBuilder('\OCA\DAV\Comments\EntityCollection') + $node = $this->getMockBuilder(EntityCollection::class) ->disableOriginalConstructor() ->getMock(); $node->expects($this->once()) ->method('findChildren') ->with(5, 10, null) - ->will($this->returnValue([])); + ->willReturn([]); - $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface') + $response = $this->getMockBuilder(ResponseInterface::class) ->disableOriginalConstructor() ->getMock(); @@ -681,27 +673,27 @@ class CommentsPlugin extends \Test\TestCase { $this->tree->expects($this->any()) ->method('getNodeForPath') ->with('/' . $path) - ->will($this->returnValue($node)); + ->willReturn($node); $this->server->expects($this->any()) ->method('getRequestUri') - ->will($this->returnValue($path)); + ->willReturn($path); $this->server->httpResponse = $response; $this->plugin->initialize($this->server); $this->plugin->onReport(CommentsPluginImplementation::REPORT_NAME, $parameters, '/' . $path); } - public function testOnReport() { + public function testOnReport(): void { $path = 'comments/files/42'; $parameters = [ [ - 'name' => '{http://owncloud.org/ns}limit', + 'name' => '{http://owncloud.org/ns}limit', 'value' => 5, ], [ - 'name' => '{http://owncloud.org/ns}offset', + 'name' => '{http://owncloud.org/ns}offset', 'value' => 10, ], [ @@ -710,15 +702,15 @@ class CommentsPlugin extends \Test\TestCase { ] ]; - $node = $this->getMockBuilder('\OCA\DAV\Comments\EntityCollection') + $node = $this->getMockBuilder(EntityCollection::class) ->disableOriginalConstructor() ->getMock(); $node->expects($this->once()) ->method('findChildren') ->with(5, 10, new \DateTime($parameters[2]['value'])) - ->will($this->returnValue([])); + ->willReturn([]); - $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface') + $response = $this->getMockBuilder(ResponseInterface::class) ->disableOriginalConstructor() ->getMock(); @@ -736,17 +728,14 @@ class CommentsPlugin extends \Test\TestCase { $this->tree->expects($this->any()) ->method('getNodeForPath') ->with('/' . $path) - ->will($this->returnValue($node)); + ->willReturn($node); $this->server->expects($this->any()) ->method('getRequestUri') - ->will($this->returnValue($path)); + ->willReturn($path); $this->server->httpResponse = $response; $this->plugin->initialize($this->server); $this->plugin->onReport(CommentsPluginImplementation::REPORT_NAME, $parameters, '/' . $path); } - - - } diff --git a/apps/dav/tests/unit/Comments/EntityCollectionTest.php b/apps/dav/tests/unit/Comments/EntityCollectionTest.php new file mode 100644 index 00000000000..29ebde7d602 --- /dev/null +++ b/apps/dav/tests/unit/Comments/EntityCollectionTest.php @@ -0,0 +1,121 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Comments; + +use OCA\DAV\Comments\CommentNode; +use OCA\DAV\Comments\EntityCollection; +use OCP\Comments\IComment; +use OCP\Comments\ICommentsManager; +use OCP\Comments\NotFoundException; +use OCP\IUserManager; +use OCP\IUserSession; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; + +class EntityCollectionTest extends \Test\TestCase { + protected ICommentsManager&MockObject $commentsManager; + protected IUserManager&MockObject $userManager; + protected LoggerInterface&MockObject $logger; + protected IUserSession&MockObject $userSession; + protected EntityCollection $collection; + + protected function setUp(): void { + parent::setUp(); + + $this->commentsManager = $this->createMock(ICommentsManager::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->userSession = $this->createMock(IUserSession::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->collection = new EntityCollection( + '19', + 'files', + $this->commentsManager, + $this->userManager, + $this->userSession, + $this->logger + ); + } + + public function testGetId(): void { + $this->assertSame($this->collection->getId(), '19'); + } + + public function testGetChild(): void { + $this->commentsManager->expects($this->once()) + ->method('get') + ->with('55') + ->willReturn( + $this->getMockBuilder(IComment::class) + ->disableOriginalConstructor() + ->getMock() + ); + + $node = $this->collection->getChild('55'); + $this->assertInstanceOf(CommentNode::class, $node); + } + + + public function testGetChildException(): void { + $this->expectException(\Sabre\DAV\Exception\NotFound::class); + + $this->commentsManager->expects($this->once()) + ->method('get') + ->with('55') + ->willThrowException(new NotFoundException()); + + $this->collection->getChild('55'); + } + + public function testGetChildren(): void { + $this->commentsManager->expects($this->once()) + ->method('getForObject') + ->with('files', '19') + ->willReturn([ + $this->getMockBuilder(IComment::class) + ->disableOriginalConstructor() + ->getMock() + ]); + + $result = $this->collection->getChildren(); + + $this->assertCount(1, $result); + $this->assertInstanceOf(CommentNode::class, $result[0]); + } + + public function testFindChildren(): void { + $dt = new \DateTime('2016-01-10 18:48:00'); + $this->commentsManager->expects($this->once()) + ->method('getForObject') + ->with('files', '19', 5, 15, $dt) + ->willReturn([ + $this->getMockBuilder(IComment::class) + ->disableOriginalConstructor() + ->getMock() + ]); + + $result = $this->collection->findChildren(5, 15, $dt); + + $this->assertCount(1, $result); + $this->assertInstanceOf(CommentNode::class, $result[0]); + } + + public function testChildExistsTrue(): void { + $this->assertTrue($this->collection->childExists('44')); + } + + public function testChildExistsFalse(): void { + $this->commentsManager->expects($this->once()) + ->method('get') + ->with('44') + ->willThrowException(new NotFoundException()); + + $this->assertFalse($this->collection->childExists('44')); + } +} diff --git a/apps/dav/tests/unit/Comments/EntityTypeCollectionTest.php b/apps/dav/tests/unit/Comments/EntityTypeCollectionTest.php new file mode 100644 index 00000000000..e5178a3e786 --- /dev/null +++ b/apps/dav/tests/unit/Comments/EntityTypeCollectionTest.php @@ -0,0 +1,77 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Comments; + +use OCA\DAV\Comments\EntityCollection as EntityCollectionImplemantation; +use OCA\DAV\Comments\EntityTypeCollection; +use OCP\Comments\ICommentsManager; +use OCP\IUserManager; +use OCP\IUserSession; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; + +class EntityTypeCollectionTest extends \Test\TestCase { + protected ICommentsManager&MockObject $commentsManager; + protected IUserManager&MockObject $userManager; + protected LoggerInterface&MockObject $logger; + protected IUserSession&MockObject $userSession; + protected EntityTypeCollection $collection; + + protected $childMap = []; + + protected function setUp(): void { + parent::setUp(); + + $this->commentsManager = $this->createMock(ICommentsManager::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->userSession = $this->createMock(IUserSession::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->collection = new EntityTypeCollection( + 'files', + $this->commentsManager, + $this->userManager, + $this->userSession, + $this->logger, + function ($child) { + return !empty($this->childMap[$child]); + } + ); + } + + public function testChildExistsYes(): void { + $this->childMap[17] = true; + $this->assertTrue($this->collection->childExists('17')); + } + + public function testChildExistsNo(): void { + $this->assertFalse($this->collection->childExists('17')); + } + + public function testGetChild(): void { + $this->childMap[17] = true; + + $ec = $this->collection->getChild('17'); + $this->assertInstanceOf(EntityCollectionImplemantation::class, $ec); + } + + + public function testGetChildException(): void { + $this->expectException(\Sabre\DAV\Exception\NotFound::class); + + $this->collection->getChild('17'); + } + + + public function testGetChildren(): void { + $this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class); + + $this->collection->getChildren(); + } +} diff --git a/apps/dav/tests/unit/Comments/RootCollectionTest.php b/apps/dav/tests/unit/Comments/RootCollectionTest.php new file mode 100644 index 00000000000..9a05d996c8c --- /dev/null +++ b/apps/dav/tests/unit/Comments/RootCollectionTest.php @@ -0,0 +1,161 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Comments; + +use OC\EventDispatcher\EventDispatcher; +use OCA\DAV\Comments\EntityTypeCollection as EntityTypeCollectionImplementation; +use OCA\DAV\Comments\RootCollection; +use OCP\Comments\CommentsEntityEvent; +use OCP\Comments\ICommentsManager; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IUser; +use OCP\IUserManager; +use OCP\IUserSession; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; + +class RootCollectionTest extends \Test\TestCase { + protected ICommentsManager&MockObject $commentsManager; + protected IUserManager&MockObject $userManager; + protected LoggerInterface&MockObject $logger; + protected IUserSession&MockObject $userSession; + protected IEventDispatcher $dispatcher; + protected IUser&MockObject $user; + protected RootCollection $collection; + + protected function setUp(): void { + parent::setUp(); + + $this->user = $this->createMock(IUser::class); + + $this->commentsManager = $this->createMock(ICommentsManager::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->userSession = $this->createMock(IUserSession::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->dispatcher = new EventDispatcher( + new \Symfony\Component\EventDispatcher\EventDispatcher(), + \OC::$server, + $this->logger + ); + + $this->collection = new RootCollection( + $this->commentsManager, + $this->userManager, + $this->userSession, + $this->dispatcher, + $this->logger + ); + } + + protected function prepareForInitCollections(): void { + $this->user->expects($this->any()) + ->method('getUID') + ->willReturn('alice'); + + $this->userSession->expects($this->once()) + ->method('getUser') + ->willReturn($this->user); + + $this->dispatcher->addListener(CommentsEntityEvent::class, function (CommentsEntityEvent $event): void { + $event->addEntityCollection('files', function () { + return true; + }); + }); + } + + + public function testCreateFile(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + $this->collection->createFile('foo'); + } + + + public function testCreateDirectory(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + $this->collection->createDirectory('foo'); + } + + public function testGetChild(): void { + $this->prepareForInitCollections(); + $etc = $this->collection->getChild('files'); + $this->assertInstanceOf(EntityTypeCollectionImplementation::class, $etc); + } + + + public function testGetChildInvalid(): void { + $this->expectException(\Sabre\DAV\Exception\NotFound::class); + + $this->prepareForInitCollections(); + $this->collection->getChild('robots'); + } + + + public function testGetChildNoAuth(): void { + $this->expectException(\Sabre\DAV\Exception\NotAuthenticated::class); + + $this->collection->getChild('files'); + } + + public function testGetChildren(): void { + $this->prepareForInitCollections(); + $children = $this->collection->getChildren(); + $this->assertFalse(empty($children)); + foreach ($children as $child) { + $this->assertInstanceOf(EntityTypeCollectionImplementation::class, $child); + } + } + + + public function testGetChildrenNoAuth(): void { + $this->expectException(\Sabre\DAV\Exception\NotAuthenticated::class); + + $this->collection->getChildren(); + } + + public function testChildExistsYes(): void { + $this->prepareForInitCollections(); + $this->assertTrue($this->collection->childExists('files')); + } + + public function testChildExistsNo(): void { + $this->prepareForInitCollections(); + $this->assertFalse($this->collection->childExists('robots')); + } + + + public function testChildExistsNoAuth(): void { + $this->expectException(\Sabre\DAV\Exception\NotAuthenticated::class); + + $this->collection->childExists('files'); + } + + + public function testDelete(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + $this->collection->delete(); + } + + public function testGetName(): void { + $this->assertSame('comments', $this->collection->getName()); + } + + + public function testSetName(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + $this->collection->setName('foobar'); + } + + public function testGetLastModified(): void { + $this->assertSame(null, $this->collection->getLastModified()); + } +} diff --git a/apps/dav/tests/unit/Connector/LegacyPublicAuthTest.php b/apps/dav/tests/unit/Connector/LegacyPublicAuthTest.php new file mode 100644 index 00000000000..8b8c775c8ec --- /dev/null +++ b/apps/dav/tests/unit/Connector/LegacyPublicAuthTest.php @@ -0,0 +1,230 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector; + +use OCA\DAV\Connector\LegacyPublicAuth; +use OCP\IRequest; +use OCP\ISession; +use OCP\Security\Bruteforce\IThrottler; +use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\IManager; +use OCP\Share\IShare; +use PHPUnit\Framework\MockObject\MockObject; + +/** + * Class LegacyPublicAuthTest + * + * @group DB + * + * @package OCA\DAV\Tests\unit\Connector + */ +class LegacyPublicAuthTest extends \Test\TestCase { + private ISession&MockObject $session; + private IRequest&MockObject $request; + private IManager&MockObject $shareManager; + private IThrottler&MockObject $throttler; + private LegacyPublicAuth $auth; + private string|false $oldUser; + + protected function setUp(): void { + parent::setUp(); + + $this->session = $this->createMock(ISession::class); + $this->request = $this->createMock(IRequest::class); + $this->shareManager = $this->createMock(IManager::class); + $this->throttler = $this->createMock(IThrottler::class); + + $this->auth = new LegacyPublicAuth( + $this->request, + $this->shareManager, + $this->session, + $this->throttler + ); + + // Store current user + $this->oldUser = \OC_User::getUser(); + } + + protected function tearDown(): void { + \OC_User::setIncognitoMode(false); + + // Set old user + \OC_User::setUserId($this->oldUser); + if ($this->oldUser !== false) { + \OC_Util::setupFS($this->oldUser); + } + + parent::tearDown(); + } + + public function testNoShare(): void { + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->willThrowException(new ShareNotFound()); + + $result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']); + + $this->assertFalse($result); + } + + public function testShareNoPassword(): void { + $share = $this->createMock(IShare::class); + $share->method('getPassword')->willReturn(null); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->willReturn($share); + + $result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']); + + $this->assertTrue($result); + } + + public function testSharePasswordFancyShareType(): void { + $share = $this->createMock(IShare::class); + $share->method('getPassword')->willReturn('password'); + $share->method('getShareType')->willReturn(42); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->willReturn($share); + + $result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']); + + $this->assertFalse($result); + } + + + public function testSharePasswordRemote(): void { + $share = $this->createMock(IShare::class); + $share->method('getPassword')->willReturn('password'); + $share->method('getShareType')->willReturn(IShare::TYPE_REMOTE); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->willReturn($share); + + $result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']); + + $this->assertTrue($result); + } + + public function testSharePasswordLinkValidPassword(): void { + $share = $this->createMock(IShare::class); + $share->method('getPassword')->willReturn('password'); + $share->method('getShareType')->willReturn(IShare::TYPE_LINK); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->willReturn($share); + + $this->shareManager->expects($this->once()) + ->method('checkPassword')->with( + $this->equalTo($share), + $this->equalTo('password') + )->willReturn(true); + + $result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']); + + $this->assertTrue($result); + } + + public function testSharePasswordMailValidPassword(): void { + $share = $this->createMock(IShare::class); + $share->method('getPassword')->willReturn('password'); + $share->method('getShareType')->willReturn(IShare::TYPE_EMAIL); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->willReturn($share); + + $this->shareManager->expects($this->once()) + ->method('checkPassword')->with( + $this->equalTo($share), + $this->equalTo('password') + )->willReturn(true); + + $result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']); + + $this->assertTrue($result); + } + + public function testInvalidSharePasswordLinkValidSession(): void { + $share = $this->createMock(IShare::class); + $share->method('getPassword')->willReturn('password'); + $share->method('getShareType')->willReturn(IShare::TYPE_LINK); + $share->method('getId')->willReturn('42'); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->willReturn($share); + + $this->shareManager->method('checkPassword') + ->with( + $this->equalTo($share), + $this->equalTo('password') + )->willReturn(false); + + $this->session->method('exists')->with('public_link_authenticated')->willReturn(true); + $this->session->method('get')->with('public_link_authenticated')->willReturn('42'); + + $result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']); + + $this->assertTrue($result); + } + + public function testSharePasswordLinkInvalidSession(): void { + $share = $this->createMock(IShare::class); + $share->method('getPassword')->willReturn('password'); + $share->method('getShareType')->willReturn(IShare::TYPE_LINK); + $share->method('getId')->willReturn('42'); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->willReturn($share); + + $this->shareManager->method('checkPassword') + ->with( + $this->equalTo($share), + $this->equalTo('password') + )->willReturn(false); + + $this->session->method('exists')->with('public_link_authenticated')->willReturn(true); + $this->session->method('get')->with('public_link_authenticated')->willReturn('43'); + + $result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']); + + $this->assertFalse($result); + } + + + public function testSharePasswordMailInvalidSession(): void { + $share = $this->createMock(IShare::class); + $share->method('getPassword')->willReturn('password'); + $share->method('getShareType')->willReturn(IShare::TYPE_EMAIL); + $share->method('getId')->willReturn('42'); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->willReturn($share); + + $this->shareManager->method('checkPassword') + ->with( + $this->equalTo($share), + $this->equalTo('password') + )->willReturn(false); + + $this->session->method('exists')->with('public_link_authenticated')->willReturn(true); + $this->session->method('get')->with('public_link_authenticated')->willReturn('43'); + + $result = $this->invokePrivate($this->auth, 'validateUserPass', ['username', 'password']); + + $this->assertFalse($result); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/AuthTest.php b/apps/dav/tests/unit/Connector/Sabre/AuthTest.php new file mode 100644 index 00000000000..4b42a815708 --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/AuthTest.php @@ -0,0 +1,608 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre; + +use OC\Authentication\Exceptions\PasswordLoginForbiddenException; +use OC\Authentication\TwoFactorAuth\Manager; +use OC\User\Session; +use OCA\DAV\Connector\Sabre\Auth; +use OCA\DAV\Connector\Sabre\Exception\PasswordLoginForbidden; +use OCP\IRequest; +use OCP\ISession; +use OCP\IUser; +use OCP\Security\Bruteforce\IThrottler; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\Server; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; +use Test\TestCase; + +/** + * Class AuthTest + * + * @package OCA\DAV\Tests\unit\Connector\Sabre + * @group DB + */ +class AuthTest extends TestCase { + private ISession&MockObject $session; + private Session&MockObject $userSession; + private IRequest&MockObject $request; + private Manager&MockObject $twoFactorManager; + private IThrottler&MockObject $throttler; + private Auth $auth; + + protected function setUp(): void { + parent::setUp(); + $this->session = $this->createMock(ISession::class); + $this->userSession = $this->createMock(Session::class); + $this->request = $this->createMock(IRequest::class); + $this->twoFactorManager = $this->createMock(Manager::class); + $this->throttler = $this->createMock(IThrottler::class); + $this->auth = new Auth( + $this->session, + $this->userSession, + $this->request, + $this->twoFactorManager, + $this->throttler + ); + } + + public function testIsDavAuthenticatedWithoutDavSession(): void { + $this->session + ->expects($this->once()) + ->method('get') + ->with('AUTHENTICATED_TO_DAV_BACKEND') + ->willReturn(null); + + $this->assertFalse(self::invokePrivate($this->auth, 'isDavAuthenticated', ['MyTestUser'])); + } + + public function testIsDavAuthenticatedWithWrongDavSession(): void { + $this->session + ->expects($this->exactly(2)) + ->method('get') + ->with('AUTHENTICATED_TO_DAV_BACKEND') + ->willReturn('AnotherUser'); + + $this->assertFalse(self::invokePrivate($this->auth, 'isDavAuthenticated', ['MyTestUser'])); + } + + public function testIsDavAuthenticatedWithCorrectDavSession(): void { + $this->session + ->expects($this->exactly(2)) + ->method('get') + ->with('AUTHENTICATED_TO_DAV_BACKEND') + ->willReturn('MyTestUser'); + + $this->assertTrue(self::invokePrivate($this->auth, 'isDavAuthenticated', ['MyTestUser'])); + } + + public function testValidateUserPassOfAlreadyDAVAuthenticatedUser(): void { + $user = $this->createMock(IUser::class); + $user->expects($this->exactly(1)) + ->method('getUID') + ->willReturn('MyTestUser'); + $this->userSession + ->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(true); + $this->userSession + ->expects($this->exactly(1)) + ->method('getUser') + ->willReturn($user); + $this->session + ->expects($this->exactly(2)) + ->method('get') + ->with('AUTHENTICATED_TO_DAV_BACKEND') + ->willReturn('MyTestUser'); + $this->session + ->expects($this->once()) + ->method('close'); + + $this->assertTrue(self::invokePrivate($this->auth, 'validateUserPass', ['MyTestUser', 'MyTestPassword'])); + } + + public function testValidateUserPassOfInvalidDAVAuthenticatedUser(): void { + $user = $this->createMock(IUser::class); + $user->expects($this->once()) + ->method('getUID') + ->willReturn('MyTestUser'); + $this->userSession + ->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(true); + $this->userSession + ->expects($this->once()) + ->method('getUser') + ->willReturn($user); + $this->session + ->expects($this->exactly(2)) + ->method('get') + ->with('AUTHENTICATED_TO_DAV_BACKEND') + ->willReturn('AnotherUser'); + $this->session + ->expects($this->once()) + ->method('close'); + + $this->assertFalse(self::invokePrivate($this->auth, 'validateUserPass', ['MyTestUser', 'MyTestPassword'])); + } + + public function testValidateUserPassOfInvalidDAVAuthenticatedUserWithValidPassword(): void { + $user = $this->createMock(IUser::class); + $user->expects($this->exactly(2)) + ->method('getUID') + ->willReturn('MyTestUser'); + $this->userSession + ->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(true); + $this->userSession + ->expects($this->exactly(2)) + ->method('getUser') + ->willReturn($user); + $this->session + ->expects($this->exactly(2)) + ->method('get') + ->with('AUTHENTICATED_TO_DAV_BACKEND') + ->willReturn('AnotherUser'); + $this->userSession + ->expects($this->once()) + ->method('logClientIn') + ->with('MyTestUser', 'MyTestPassword', $this->request) + ->willReturn(true); + $this->session + ->expects($this->once()) + ->method('set') + ->with('AUTHENTICATED_TO_DAV_BACKEND', 'MyTestUser'); + $this->session + ->expects($this->once()) + ->method('close'); + + $this->assertTrue(self::invokePrivate($this->auth, 'validateUserPass', ['MyTestUser', 'MyTestPassword'])); + } + + public function testValidateUserPassWithInvalidPassword(): void { + $this->userSession + ->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(false); + $this->userSession + ->expects($this->once()) + ->method('logClientIn') + ->with('MyTestUser', 'MyTestPassword') + ->willReturn(false); + $this->session + ->expects($this->once()) + ->method('close'); + + $this->assertFalse(self::invokePrivate($this->auth, 'validateUserPass', ['MyTestUser', 'MyTestPassword'])); + } + + + public function testValidateUserPassWithPasswordLoginForbidden(): void { + $this->expectException(PasswordLoginForbidden::class); + + $this->userSession + ->expects($this->once()) + ->method('isLoggedIn') + ->willReturn(false); + $this->userSession + ->expects($this->once()) + ->method('logClientIn') + ->with('MyTestUser', 'MyTestPassword') + ->willThrowException(new PasswordLoginForbiddenException()); + $this->session + ->expects($this->once()) + ->method('close'); + + self::invokePrivate($this->auth, 'validateUserPass', ['MyTestUser', 'MyTestPassword']); + } + + public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenForNonGet(): void { + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + $this->userSession + ->expects($this->any()) + ->method('isLoggedIn') + ->willReturn(true); + $this->request + ->expects($this->any()) + ->method('getMethod') + ->willReturn('POST'); + $this->session + ->expects($this->any()) + ->method('get') + ->with('AUTHENTICATED_TO_DAV_BACKEND') + ->willReturn(null); + $user = $this->createMock(IUser::class); + $user->expects($this->any()) + ->method('getUID') + ->willReturn('MyWrongDavUser'); + $this->userSession + ->expects($this->any()) + ->method('getUser') + ->willReturn($user); + $this->request + ->expects($this->once()) + ->method('passesCSRFCheck') + ->willReturn(false); + + $expectedResponse = [ + false, + "No 'Authorization: Basic' header found. Either the client didn't send one, or the server is misconfigured", + ]; + $response = $this->auth->check($request, $response); + $this->assertSame($expectedResponse, $response); + } + + public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenAndCorrectlyDavAuthenticated(): void { + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + $this->userSession + ->expects($this->any()) + ->method('isLoggedIn') + ->willReturn(true); + $this->request + ->expects($this->any()) + ->method('getMethod') + ->willReturn('PROPFIND'); + $this->request + ->expects($this->any()) + ->method('isUserAgent') + ->willReturn(false); + $this->session + ->expects($this->any()) + ->method('get') + ->with('AUTHENTICATED_TO_DAV_BACKEND') + ->willReturn('LoggedInUser'); + $user = $this->createMock(IUser::class); + $user->expects($this->any()) + ->method('getUID') + ->willReturn('LoggedInUser'); + $this->userSession + ->expects($this->any()) + ->method('getUser') + ->willReturn($user); + $this->request + ->expects($this->once()) + ->method('passesCSRFCheck') + ->willReturn(false); + $this->auth->check($request, $response); + } + + + public function testAuthenticateAlreadyLoggedInWithoutTwoFactorChallengePassed(): void { + $this->expectException(\Sabre\DAV\Exception\NotAuthenticated::class); + $this->expectExceptionMessage('2FA challenge not passed.'); + + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + $this->userSession + ->expects($this->any()) + ->method('isLoggedIn') + ->willReturn(true); + $this->request + ->expects($this->any()) + ->method('getMethod') + ->willReturn('PROPFIND'); + $this->request + ->expects($this->any()) + ->method('isUserAgent') + ->willReturn(false); + $this->session + ->expects($this->any()) + ->method('get') + ->with('AUTHENTICATED_TO_DAV_BACKEND') + ->willReturn('LoggedInUser'); + $user = $this->createMock(IUser::class); + $user->expects($this->any()) + ->method('getUID') + ->willReturn('LoggedInUser'); + $this->userSession + ->expects($this->any()) + ->method('getUser') + ->willReturn($user); + $this->request + ->expects($this->once()) + ->method('passesCSRFCheck') + ->willReturn(true); + $this->twoFactorManager->expects($this->once()) + ->method('needsSecondFactor') + ->with($user) + ->willReturn(true); + $this->auth->check($request, $response); + } + + + public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenAndIncorrectlyDavAuthenticated(): void { + $this->expectException(\Sabre\DAV\Exception\NotAuthenticated::class); + $this->expectExceptionMessage('CSRF check not passed.'); + + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + $this->userSession + ->expects($this->any()) + ->method('isLoggedIn') + ->willReturn(true); + $this->request + ->expects($this->any()) + ->method('getMethod') + ->willReturn('PROPFIND'); + $this->request + ->expects($this->any()) + ->method('isUserAgent') + ->willReturn(false); + $this->session + ->expects($this->any()) + ->method('get') + ->with('AUTHENTICATED_TO_DAV_BACKEND') + ->willReturn('AnotherUser'); + $user = $this->createMock(IUser::class); + $user->expects($this->any()) + ->method('getUID') + ->willReturn('LoggedInUser'); + $this->userSession + ->expects($this->any()) + ->method('getUser') + ->willReturn($user); + $this->request + ->expects($this->once()) + ->method('passesCSRFCheck') + ->willReturn(false); + $this->auth->check($request, $response); + } + + public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenForNonGetAndDesktopClient(): void { + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + $this->userSession + ->expects($this->any()) + ->method('isLoggedIn') + ->willReturn(true); + $this->request + ->expects($this->any()) + ->method('getMethod') + ->willReturn('POST'); + $this->request + ->expects($this->any()) + ->method('isUserAgent') + ->willReturn(true); + $this->session + ->expects($this->any()) + ->method('get') + ->with('AUTHENTICATED_TO_DAV_BACKEND') + ->willReturn(null); + $user = $this->createMock(IUser::class); + $user->expects($this->any()) + ->method('getUID') + ->willReturn('MyWrongDavUser'); + $this->userSession + ->expects($this->any()) + ->method('getUser') + ->willReturn($user); + $this->request + ->expects($this->once()) + ->method('passesCSRFCheck') + ->willReturn(false); + + $this->auth->check($request, $response); + } + + public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenForGet(): void { + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + $this->userSession + ->expects($this->any()) + ->method('isLoggedIn') + ->willReturn(true); + $this->session + ->expects($this->any()) + ->method('get') + ->with('AUTHENTICATED_TO_DAV_BACKEND') + ->willReturn(null); + $user = $this->createMock(IUser::class); + $user->expects($this->any()) + ->method('getUID') + ->willReturn('MyWrongDavUser'); + $this->userSession + ->expects($this->any()) + ->method('getUser') + ->willReturn($user); + $this->request + ->expects($this->any()) + ->method('getMethod') + ->willReturn('GET'); + + $response = $this->auth->check($request, $response); + $this->assertEquals([true, 'principals/users/MyWrongDavUser'], $response); + } + + public function testAuthenticateAlreadyLoggedInWithCsrfTokenForGet(): void { + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + $this->userSession + ->expects($this->any()) + ->method('isLoggedIn') + ->willReturn(true); + $this->session + ->expects($this->any()) + ->method('get') + ->with('AUTHENTICATED_TO_DAV_BACKEND') + ->willReturn(null); + $user = $this->createMock(IUser::class); + $user->expects($this->any()) + ->method('getUID') + ->willReturn('MyWrongDavUser'); + $this->userSession + ->expects($this->any()) + ->method('getUser') + ->willReturn($user); + $this->request + ->expects($this->once()) + ->method('passesCSRFCheck') + ->willReturn(true); + + $response = $this->auth->check($request, $response); + $this->assertEquals([true, 'principals/users/MyWrongDavUser'], $response); + } + + public function testAuthenticateNoBasicAuthenticateHeadersProvided(): void { + $server = $this->createMock(Server::class); + $server->httpRequest = $this->createMock(RequestInterface::class); + $server->httpResponse = $this->createMock(ResponseInterface::class); + $response = $this->auth->check($server->httpRequest, $server->httpResponse); + $this->assertEquals([false, 'No \'Authorization: Basic\' header found. Either the client didn\'t send one, or the server is misconfigured'], $response); + } + + + public function testAuthenticateNoBasicAuthenticateHeadersProvidedWithAjax(): void { + $this->expectException(\Sabre\DAV\Exception\NotAuthenticated::class); + $this->expectExceptionMessage('Cannot authenticate over ajax calls'); + + /** @var \Sabre\HTTP\RequestInterface&MockObject $httpRequest */ + $httpRequest = $this->createMock(RequestInterface::class); + /** @var \Sabre\HTTP\ResponseInterface&MockObject $httpResponse */ + $httpResponse = $this->createMock(ResponseInterface::class); + $this->userSession + ->expects($this->any()) + ->method('isLoggedIn') + ->willReturn(false); + $httpRequest + ->expects($this->exactly(2)) + ->method('getHeader') + ->willReturnMap([ + ['X-Requested-With', 'XMLHttpRequest'], + ['Authorization', null], + ]); + + $this->auth->check($httpRequest, $httpResponse); + } + + public function testAuthenticateWithBasicAuthenticateHeadersProvidedWithAjax(): void { + // No CSRF + $this->request + ->expects($this->once()) + ->method('passesCSRFCheck') + ->willReturn(false); + + /** @var \Sabre\HTTP\RequestInterface&MockObject $httpRequest */ + $httpRequest = $this->createMock(RequestInterface::class); + /** @var \Sabre\HTTP\ResponseInterface&MockObject $httpResponse */ + $httpResponse = $this->createMock(ResponseInterface::class); + $httpRequest + ->expects($this->any()) + ->method('getHeader') + ->willReturnMap([ + ['X-Requested-With', 'XMLHttpRequest'], + ['Authorization', 'basic dXNlcm5hbWU6cGFzc3dvcmQ='], + ]); + + $user = $this->createMock(IUser::class); + $user->expects($this->any()) + ->method('getUID') + ->willReturn('MyDavUser'); + $this->userSession + ->expects($this->any()) + ->method('isLoggedIn') + ->willReturn(false); + $this->userSession + ->expects($this->once()) + ->method('logClientIn') + ->with('username', 'password') + ->willReturn(true); + $this->userSession + ->expects($this->any()) + ->method('getUser') + ->willReturn($user); + + $this->auth->check($httpRequest, $httpResponse); + } + + public function testAuthenticateNoBasicAuthenticateHeadersProvidedWithAjaxButUserIsStillLoggedIn(): void { + /** @var \Sabre\HTTP\RequestInterface $httpRequest */ + $httpRequest = $this->createMock(RequestInterface::class); + /** @var \Sabre\HTTP\ResponseInterface $httpResponse */ + $httpResponse = $this->createMock(ResponseInterface::class); + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('MyTestUser'); + $this->userSession + ->expects($this->any()) + ->method('isLoggedIn') + ->willReturn(true); + $this->userSession + ->expects($this->any()) + ->method('getUser') + ->willReturn($user); + $this->session + ->expects($this->atLeastOnce()) + ->method('get') + ->with('AUTHENTICATED_TO_DAV_BACKEND') + ->willReturn('MyTestUser'); + $this->request + ->expects($this->once()) + ->method('getMethod') + ->willReturn('GET'); + $httpRequest + ->expects($this->atLeastOnce()) + ->method('getHeader') + ->with('Authorization') + ->willReturn(null); + $this->assertEquals( + [true, 'principals/users/MyTestUser'], + $this->auth->check($httpRequest, $httpResponse) + ); + } + + public function testAuthenticateValidCredentials(): void { + $server = $this->createMock(Server::class); + $server->httpRequest = $this->createMock(RequestInterface::class); + $server->httpRequest + ->expects($this->once()) + ->method('getHeader') + ->with('Authorization') + ->willReturn('basic dXNlcm5hbWU6cGFzc3dvcmQ='); + + $server->httpResponse = $this->createMock(ResponseInterface::class); + $this->userSession + ->expects($this->once()) + ->method('logClientIn') + ->with('username', 'password') + ->willReturn(true); + $user = $this->createMock(IUser::class); + $user->expects($this->exactly(2)) + ->method('getUID') + ->willReturn('MyTestUser'); + $this->userSession + ->expects($this->exactly(3)) + ->method('getUser') + ->willReturn($user); + $response = $this->auth->check($server->httpRequest, $server->httpResponse); + $this->assertEquals([true, 'principals/users/MyTestUser'], $response); + } + + public function testAuthenticateInvalidCredentials(): void { + $server = $this->createMock(Server::class); + $server->httpRequest = $this->createMock(RequestInterface::class); + $server->httpRequest + ->expects($this->exactly(2)) + ->method('getHeader') + ->willReturnMap([ + ['Authorization', 'basic dXNlcm5hbWU6cGFzc3dvcmQ='], + ['X-Requested-With', null], + ]); + $server->httpResponse = $this->createMock(ResponseInterface::class); + $this->userSession + ->expects($this->once()) + ->method('logClientIn') + ->with('username', 'password') + ->willReturn(false); + $response = $this->auth->check($server->httpRequest, $server->httpResponse); + $this->assertEquals([false, 'Username or password was incorrect'], $response); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/BearerAuthTest.php b/apps/dav/tests/unit/Connector/Sabre/BearerAuthTest.php new file mode 100644 index 00000000000..1e6267d4cbb --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/BearerAuthTest.php @@ -0,0 +1,82 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre; + +use OC\User\Session; +use OCA\DAV\Connector\Sabre\BearerAuth; +use OCP\IConfig; +use OCP\IRequest; +use OCP\ISession; +use OCP\IUser; +use OCP\IUserSession; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; +use Test\TestCase; + +/** + * @group DB + */ +class BearerAuthTest extends TestCase { + private IUserSession&MockObject $userSession; + private ISession&MockObject $session; + private IRequest&MockObject $request; + private BearerAuth $bearerAuth; + + private IConfig&MockObject $config; + + protected function setUp(): void { + parent::setUp(); + + $this->userSession = $this->createMock(Session::class); + $this->session = $this->createMock(ISession::class); + $this->request = $this->createMock(IRequest::class); + $this->config = $this->createMock(IConfig::class); + + $this->bearerAuth = new BearerAuth( + $this->userSession, + $this->session, + $this->request, + $this->config, + ); + } + + public function testValidateBearerTokenNotLoggedIn(): void { + $this->assertFalse($this->bearerAuth->validateBearerToken('Token')); + } + + public function testValidateBearerToken(): void { + $this->userSession + ->expects($this->exactly(2)) + ->method('isLoggedIn') + ->willReturnOnConsecutiveCalls( + false, + true, + ); + $user = $this->createMock(IUser::class); + $user + ->expects($this->once()) + ->method('getUID') + ->willReturn('admin'); + $this->userSession + ->expects($this->once()) + ->method('getUser') + ->willReturn($user); + + $this->assertSame('principals/users/admin', $this->bearerAuth->validateBearerToken('Token')); + } + + public function testChallenge(): void { + /** @var RequestInterface&MockObject $request */ + $request = $this->createMock(RequestInterface::class); + /** @var ResponseInterface&MockObject $response */ + $response = $this->createMock(ResponseInterface::class); + $result = $this->bearerAuth->challenge($request, $response); + $this->assertEmpty($result); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/BlockLegacyClientPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/BlockLegacyClientPluginTest.php new file mode 100644 index 00000000000..366c9475b1b --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/BlockLegacyClientPluginTest.php @@ -0,0 +1,177 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre; + +use OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin; +use OCA\Theming\ThemingDefaults; +use OCP\IConfig; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\HTTP\RequestInterface; +use Test\TestCase; + +enum ERROR_TYPE { + case MIN_ERROR; + case MAX_ERROR; + case NONE; +} + +/** + * Class BlockLegacyClientPluginTest + * + * @package OCA\DAV\Tests\unit\Connector\Sabre + */ +class BlockLegacyClientPluginTest extends TestCase { + + private IConfig&MockObject $config; + private ThemingDefaults&MockObject $themingDefaults; + private BlockLegacyClientPlugin $blockLegacyClientVersionPlugin; + + protected function setUp(): void { + parent::setUp(); + + $this->config = $this->createMock(IConfig::class); + $this->themingDefaults = $this->createMock(ThemingDefaults::class); + $this->blockLegacyClientVersionPlugin = new BlockLegacyClientPlugin( + $this->config, + $this->themingDefaults, + ); + } + + public static function oldDesktopClientProvider(): array { + return [ + ['Mozilla/5.0 (Windows) mirall/1.5.0', ERROR_TYPE::MIN_ERROR], + ['Mozilla/5.0 (Bogus Text) mirall/1.6.9', ERROR_TYPE::MIN_ERROR], + ['Mozilla/5.0 (Windows) mirall/2.5.0', ERROR_TYPE::MAX_ERROR], + ['Mozilla/5.0 (Bogus Text) mirall/2.0.1', ERROR_TYPE::MAX_ERROR], + ['Mozilla/5.0 (Windows) mirall/2.0.0', ERROR_TYPE::NONE], + ['Mozilla/5.0 (Bogus Text) mirall/2.0.0', ERROR_TYPE::NONE], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('oldDesktopClientProvider')] + public function testBeforeHandlerException(string $userAgent, ERROR_TYPE $errorType): void { + $this->themingDefaults + ->expects($this->atMost(1)) + ->method('getSyncClientUrl') + ->willReturn('https://nextcloud.com/install/#install-clients'); + + $this->config + ->expects($this->exactly(2)) + ->method('getSystemValueString') + ->willReturnCallback(function (string $key) { + if ($key === 'minimum.supported.desktop.version') { + return '1.7.0'; + } + return '2.0.0'; + }); + + if ($errorType !== ERROR_TYPE::NONE) { + $errorString = $errorType === ERROR_TYPE::MIN_ERROR + ? 'This version of the client is unsupported. Upgrade to <a href="https://nextcloud.com/install/#install-clients">version 1.7.0 or later</a>.' + : 'This version of the client is unsupported. Downgrade to <a href="https://nextcloud.com/install/#install-clients">version 2.0.0 or earlier</a>.'; + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + $this->expectExceptionMessage($errorString); + } + + /** @var RequestInterface|MockObject $request */ + $request = $this->createMock(RequestInterface::class); + $request + ->expects($this->once()) + ->method('getHeader') + ->with('User-Agent') + ->willReturn($userAgent); + + $this->blockLegacyClientVersionPlugin->beforeHandler($request); + } + + /** + * Ensure that there is no room for XSS attack through configured URL / version + */ + #[\PHPUnit\Framework\Attributes\DataProvider('oldDesktopClientProvider')] + public function testBeforeHandlerExceptionPreventXSSAttack(string $userAgent, ERROR_TYPE $errorType): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + $this->themingDefaults + ->expects($this->atMost(1)) + ->method('getSyncClientUrl') + ->willReturn('https://example.com"><script>alter("hacked");</script>'); + + $this->config + ->expects($this->exactly(2)) + ->method('getSystemValueString') + ->willReturnCallback(function (string $key) { + if ($key === 'minimum.supported.desktop.version') { + return '1.7.0 <script>alert("unsafe")</script>'; + } + return '2.0.0 <script>alert("unsafe")</script>'; + }); + + $errorString = $errorType === ERROR_TYPE::MIN_ERROR + ? 'This version of the client is unsupported. Upgrade to <a href="https://example.com"><script>alter("hacked");</script>">version 1.7.0 <script>alert("unsafe")</script> or later</a>.' + : 'This version of the client is unsupported. Downgrade to <a href="https://example.com"><script>alter("hacked");</script>">version 2.0.0 <script>alert("unsafe")</script> or earlier</a>.'; + $this->expectExceptionMessage($errorString); + + /** @var RequestInterface|MockObject $request */ + $request = $this->createMock('\Sabre\HTTP\RequestInterface'); + $request + ->expects($this->once()) + ->method('getHeader') + ->with('User-Agent') + ->willReturn($userAgent); + + $this->blockLegacyClientVersionPlugin->beforeHandler($request); + } + + public static function newAndAlternateDesktopClientProvider(): array { + return [ + ['Mozilla/5.0 (Windows) mirall/1.7.0'], + ['Mozilla/5.0 (Bogus Text) mirall/1.9.3'], + ['Mozilla/5.0 (Not Our Client But Old Version) LegacySync/1.1.0'], + ['Mozilla/5.0 (Windows) mirall/4.7.0'], + ['Mozilla/5.0 (Bogus Text) mirall/3.9.3'], + ['Mozilla/5.0 (Not Our Client But Old Version) LegacySync/45.0.0'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('newAndAlternateDesktopClientProvider')] + public function testBeforeHandlerSuccess(string $userAgent): void { + /** @var RequestInterface|MockObject $request */ + $request = $this->createMock(RequestInterface::class); + $request + ->expects($this->once()) + ->method('getHeader') + ->with('User-Agent') + ->willReturn($userAgent); + + $this->config + ->expects($this->exactly(2)) + ->method('getSystemValueString') + ->willReturnCallback(function (string $key) { + if ($key === 'minimum.supported.desktop.version') { + return '1.7.0'; + } + return '10.0.0'; + }); + + $this->blockLegacyClientVersionPlugin->beforeHandler($request); + } + + public function testBeforeHandlerNoUserAgent(): void { + /** @var RequestInterface|MockObject $request */ + $request = $this->createMock(RequestInterface::class); + $request + ->expects($this->once()) + ->method('getHeader') + ->with('User-Agent') + ->willReturn(null); + + $this->blockLegacyClientVersionPlugin->beforeHandler($request); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/CommentsPropertiesPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/CommentsPropertiesPluginTest.php new file mode 100644 index 00000000000..a934d6401c2 --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/CommentsPropertiesPluginTest.php @@ -0,0 +1,117 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre; + +use OCA\DAV\Connector\Sabre\CommentPropertiesPlugin as CommentPropertiesPluginImplementation; +use OCA\DAV\Connector\Sabre\Directory; +use OCA\DAV\Connector\Sabre\File; +use OCP\Comments\ICommentsManager; +use OCP\IUser; +use OCP\IUserSession; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\PropFind; +use Sabre\DAV\Server; + +class CommentsPropertiesPluginTest extends \Test\TestCase { + protected CommentPropertiesPluginImplementation $plugin; + protected ICommentsManager&MockObject $commentsManager; + protected IUserSession&MockObject $userSession; + protected Server&MockObject $server; + + protected function setUp(): void { + parent::setUp(); + + $this->commentsManager = $this->createMock(ICommentsManager::class); + $this->userSession = $this->createMock(IUserSession::class); + $this->server = $this->createMock(Server::class); + + $this->plugin = new CommentPropertiesPluginImplementation($this->commentsManager, $this->userSession); + $this->plugin->initialize($this->server); + } + + public static function nodeProvider(): array { + return [ + [File::class, true], + [Directory::class, true], + [\Sabre\DAV\INode::class, false] + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('nodeProvider')] + public function testHandleGetProperties(string $class, bool $expectedSuccessful): void { + $propFind = $this->createMock(PropFind::class); + + if ($expectedSuccessful) { + $propFind->expects($this->exactly(3)) + ->method('handle'); + } else { + $propFind->expects($this->never()) + ->method('handle'); + } + + $node = $this->createMock($class); + $this->plugin->handleGetProperties($propFind, $node); + } + + public static function baseUriProvider(): array { + return [ + ['owncloud/remote.php/webdav/', '4567', 'owncloud/remote.php/dav/comments/files/4567'], + ['owncloud/remote.php/files/', '4567', 'owncloud/remote.php/dav/comments/files/4567'], + ['owncloud/wicked.php/files/', '4567', null] + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('baseUriProvider')] + public function testGetCommentsLink(string $baseUri, string $fid, ?string $expectedHref): void { + $node = $this->createMock(File::class); + $node->expects($this->any()) + ->method('getId') + ->willReturn($fid); + + $this->server->expects($this->once()) + ->method('getBaseUri') + ->willReturn($baseUri); + + $href = $this->plugin->getCommentsLink($node); + $this->assertSame($expectedHref, $href); + } + + public static function userProvider(): array { + return [ + [IUser::class], + [null] + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('userProvider')] + public function testGetUnreadCount(?string $user): void { + $node = $this->createMock(File::class); + $node->expects($this->any()) + ->method('getId') + ->willReturn('4567'); + + if ($user !== null) { + $user = $this->createMock($user); + } + $this->userSession->expects($this->once()) + ->method('getUser') + ->willReturn($user); + + $this->commentsManager->expects($this->any()) + ->method('getNumberOfCommentsForObject') + ->willReturn(42); + + $unread = $this->plugin->getUnreadCount($node); + if (is_null($user)) { + $this->assertNull($unread); + } else { + $this->assertSame($unread, 42); + } + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/CopyEtagHeaderPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/CopyEtagHeaderPluginTest.php new file mode 100644 index 00000000000..7067cf335ed --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/CopyEtagHeaderPluginTest.php @@ -0,0 +1,78 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre; + +use OCA\DAV\Connector\Sabre\CopyEtagHeaderPlugin; +use OCA\DAV\Connector\Sabre\File; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Server; +use Sabre\DAV\Tree; +use Test\TestCase; + +class CopyEtagHeaderPluginTest extends TestCase { + private CopyEtagHeaderPlugin $plugin; + private Server $server; + + protected function setUp(): void { + parent::setUp(); + $this->server = new \Sabre\DAV\Server(); + $this->plugin = new CopyEtagHeaderPlugin(); + $this->plugin->initialize($this->server); + } + + public function testCopyEtag(): void { + $request = new \Sabre\Http\Request('GET', 'dummy.file'); + $response = new \Sabre\Http\Response(); + $response->setHeader('Etag', 'abcd'); + + $this->plugin->afterMethod($request, $response); + + $this->assertEquals('abcd', $response->getHeader('OC-Etag')); + } + + public function testNoopWhenEmpty(): void { + $request = new \Sabre\Http\Request('GET', 'dummy.file'); + $response = new \Sabre\Http\Response(); + + $this->plugin->afterMethod($request, $response); + + $this->assertNull($response->getHeader('OC-Etag')); + } + + public function testAfterMoveNodeNotFound(): void { + $tree = $this->createMock(Tree::class); + $tree->expects(self::once()) + ->method('getNodeForPath') + ->with('test.txt') + ->willThrowException(new NotFound()); + + $this->server->tree = $tree; + $this->plugin->afterMove('', 'test.txt'); + + // Nothing to assert, we are just testing if the exception is handled + } + + public function testAfterMove(): void { + $node = $this->createMock(File::class); + $node->expects($this->once()) + ->method('getETag') + ->willReturn('123456'); + $tree = $this->createMock(Tree::class); + $tree->expects($this->once()) + ->method('getNodeForPath') + ->with('test.txt') + ->willReturn($node); + + $this->server->tree = $tree; + $this->plugin->afterMove('', 'test.txt'); + + $this->assertEquals('123456', $this->server->httpResponse->getHeader('OC-Etag')); + $this->assertEquals('123456', $this->server->httpResponse->getHeader('Etag')); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/CustomPropertiesBackendTest.php b/apps/dav/tests/unit/Connector/Sabre/CustomPropertiesBackendTest.php new file mode 100644 index 00000000000..cafbdd3ca40 --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/CustomPropertiesBackendTest.php @@ -0,0 +1,234 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre; + +use OCA\DAV\CalDAV\DefaultCalendarValidator; +use OCA\DAV\Connector\Sabre\Directory; +use OCA\DAV\Connector\Sabre\File; +use OCA\DAV\DAV\CustomPropertiesBackend; +use OCA\DAV\Db\PropertyMapper; +use OCP\IDBConnection; +use OCP\IUser; +use OCP\Server; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\Tree; + +/** + * Class CustomPropertiesBackend + * + * @group DB + * + * @package OCA\DAV\Tests\unit\Connector\Sabre + */ +class CustomPropertiesBackendTest extends \Test\TestCase { + private \Sabre\DAV\Server $server; + private \Sabre\DAV\Tree&MockObject $tree; + private IUser&MockObject $user; + private DefaultCalendarValidator&MockObject $defaultCalendarValidator; + private CustomPropertiesBackend $plugin; + + protected function setUp(): void { + parent::setUp(); + + $this->server = new \Sabre\DAV\Server(); + $this->tree = $this->createMock(Tree::class); + + $userId = self::getUniqueID('testcustompropertiesuser'); + + $this->user = $this->createMock(IUser::class); + $this->user->expects($this->any()) + ->method('getUID') + ->willReturn($userId); + + $this->defaultCalendarValidator = $this->createMock(DefaultCalendarValidator::class); + + $this->plugin = new CustomPropertiesBackend( + $this->server, + $this->tree, + Server::get(IDBConnection::class), + $this->user, + Server::get(PropertyMapper::class), + $this->defaultCalendarValidator, + ); + } + + protected function tearDown(): void { + $connection = Server::get(IDBConnection::class); + $deleteStatement = $connection->prepare( + 'DELETE FROM `*PREFIX*properties`' + . ' WHERE `userid` = ?' + ); + $deleteStatement->execute( + [ + $this->user->getUID(), + ] + ); + $deleteStatement->closeCursor(); + + parent::tearDown(); + } + + private function createTestNode(string $class) { + $node = $this->createMock($class); + $node->expects($this->any()) + ->method('getId') + ->willReturn(123); + + $node->expects($this->any()) + ->method('getPath') + ->willReturn('/dummypath'); + + return $node; + } + + private function applyDefaultProps($path = '/dummypath'): void { + // properties to set + $propPatch = new \Sabre\DAV\PropPatch([ + 'customprop' => 'value1', + 'customprop2' => 'value2', + ]); + + $this->plugin->propPatch( + $path, + $propPatch + ); + + $propPatch->commit(); + + $this->assertEmpty($propPatch->getRemainingMutations()); + + $result = $propPatch->getResult(); + $this->assertEquals(200, $result['customprop']); + $this->assertEquals(200, $result['customprop2']); + } + + /** + * Test that propFind on a missing file soft fails + */ + public function testPropFindMissingFileSoftFail(): void { + $propFind = new \Sabre\DAV\PropFind( + '/dummypath', + [ + 'customprop', + 'customprop2', + 'unsetprop', + ], + 0 + ); + + $this->plugin->propFind( + '/dummypath', + $propFind + ); + + $this->plugin->propFind( + '/dummypath', + $propFind + ); + + // assert that the above didn't throw exceptions + $this->assertTrue(true); + } + + /** + * Test setting/getting properties + */ + public function testSetGetPropertiesForFile(): void { + $this->applyDefaultProps(); + + $propFind = new \Sabre\DAV\PropFind( + '/dummypath', + [ + 'customprop', + 'customprop2', + 'unsetprop', + ], + 0 + ); + + $this->plugin->propFind( + '/dummypath', + $propFind + ); + + $this->assertEquals('value1', $propFind->get('customprop')); + $this->assertEquals('value2', $propFind->get('customprop2')); + $this->assertEquals(['unsetprop'], $propFind->get404Properties()); + } + + /** + * Test getting properties from directory + */ + public function testGetPropertiesForDirectory(): void { + $this->applyDefaultProps('/dummypath'); + $this->applyDefaultProps('/dummypath/test.txt'); + + $propNames = [ + 'customprop', + 'customprop2', + 'unsetprop', + ]; + + $propFindRoot = new \Sabre\DAV\PropFind( + '/dummypath', + $propNames, + 1 + ); + + $propFindSub = new \Sabre\DAV\PropFind( + '/dummypath/test.txt', + $propNames, + 0 + ); + + $this->plugin->propFind( + '/dummypath', + $propFindRoot + ); + + $this->plugin->propFind( + '/dummypath/test.txt', + $propFindSub + ); + + // TODO: find a way to assert that no additional SQL queries were + // run while doing the second propFind + + $this->assertEquals('value1', $propFindRoot->get('customprop')); + $this->assertEquals('value2', $propFindRoot->get('customprop2')); + $this->assertEquals(['unsetprop'], $propFindRoot->get404Properties()); + + $this->assertEquals('value1', $propFindSub->get('customprop')); + $this->assertEquals('value2', $propFindSub->get('customprop2')); + $this->assertEquals(['unsetprop'], $propFindSub->get404Properties()); + } + + /** + * Test delete property + */ + public function testDeleteProperty(): void { + $this->applyDefaultProps(); + + $propPatch = new \Sabre\DAV\PropPatch([ + 'customprop' => null, + ]); + + $this->plugin->propPatch( + '/dummypath', + $propPatch + ); + + $propPatch->commit(); + + $this->assertEmpty($propPatch->getRemainingMutations()); + + $result = $propPatch->getResult(); + $this->assertEquals(204, $result['customprop']); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/DirectoryTest.php b/apps/dav/tests/unit/Connector/Sabre/DirectoryTest.php new file mode 100644 index 00000000000..421ee1bdc12 --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/DirectoryTest.php @@ -0,0 +1,468 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre; + +use OC\Files\FileInfo; +use OC\Files\Filesystem; +use OC\Files\Node\Node; +use OC\Files\Storage\Wrapper\Quota; +use OC\Files\View; +use OCA\DAV\Connector\Sabre\Directory; +use OCA\DAV\Connector\Sabre\Exception\Forbidden; +use OCA\DAV\Connector\Sabre\Exception\InvalidPath; +use OCA\Files_Sharing\External\Storage; +use OCP\Constants; +use OCP\Files\ForbiddenException; +use OCP\Files\InvalidPathException; +use OCP\Files\Mount\IMountPoint; +use OCP\Files\StorageNotAvailableException; +use PHPUnit\Framework\MockObject\MockObject; +use Test\Traits\UserTrait; + +class TestViewDirectory extends View { + public function __construct( + private $updatables, + private $deletables, + private $canRename = true, + ) { + } + + public function isUpdatable($path) { + return $this->updatables[$path]; + } + + public function isCreatable($path) { + return $this->updatables[$path]; + } + + public function isDeletable($path) { + return $this->deletables[$path]; + } + + public function rename($source, $target, array $options = []) { + return $this->canRename; + } + + public function getRelativePath($path): ?string { + return $path; + } +} + + +/** + * @group DB + */ +class DirectoryTest extends \Test\TestCase { + use UserTrait; + + private View&MockObject $view; + private FileInfo&MockObject $info; + + protected function setUp(): void { + parent::setUp(); + + $this->view = $this->createMock(View::class); + $this->info = $this->createMock(FileInfo::class); + $this->info->method('isReadable') + ->willReturn(true); + $this->info->method('getType') + ->willReturn(Node::TYPE_FOLDER); + $this->info->method('getName') + ->willReturn('folder'); + $this->info->method('getPath') + ->willReturn('/admin/files/folder'); + $this->info->method('getPermissions') + ->willReturn(Constants::PERMISSION_READ); + } + + private function getDir(string $path = '/'): Directory { + $this->view->expects($this->once()) + ->method('getRelativePath') + ->willReturn($path); + + $this->info->expects($this->once()) + ->method('getPath') + ->willReturn($path); + + return new Directory($this->view, $this->info); + } + + + public function testDeleteRootFolderFails(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + $this->info->expects($this->any()) + ->method('isDeletable') + ->willReturn(true); + $this->view->expects($this->never()) + ->method('rmdir'); + $dir = $this->getDir(); + $dir->delete(); + } + + + public function testDeleteForbidden(): void { + $this->expectException(Forbidden::class); + + // deletion allowed + $this->info->expects($this->once()) + ->method('isDeletable') + ->willReturn(true); + + // but fails + $this->view->expects($this->once()) + ->method('rmdir') + ->with('sub') + ->willThrowException(new ForbiddenException('', true)); + + $dir = $this->getDir('sub'); + $dir->delete(); + } + + + public function testDeleteFolderWhenAllowed(): void { + // deletion allowed + $this->info->expects($this->once()) + ->method('isDeletable') + ->willReturn(true); + + // but fails + $this->view->expects($this->once()) + ->method('rmdir') + ->with('sub') + ->willReturn(true); + + $dir = $this->getDir('sub'); + $dir->delete(); + } + + + public function testDeleteFolderFailsWhenNotAllowed(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + $this->info->expects($this->once()) + ->method('isDeletable') + ->willReturn(false); + + $dir = $this->getDir('sub'); + $dir->delete(); + } + + + public function testDeleteFolderThrowsWhenDeletionFailed(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + // deletion allowed + $this->info->expects($this->once()) + ->method('isDeletable') + ->willReturn(true); + + // but fails + $this->view->expects($this->once()) + ->method('rmdir') + ->with('sub') + ->willReturn(false); + + $dir = $this->getDir('sub'); + $dir->delete(); + } + + public function testGetChildren(): void { + $info1 = $this->createMock(FileInfo::class); + $info2 = $this->createMock(FileInfo::class); + $info1->method('getName') + ->willReturn('first'); + $info1->method('getPath') + ->willReturn('folder/first'); + $info1->method('getEtag') + ->willReturn('abc'); + $info2->method('getName') + ->willReturn('second'); + $info2->method('getPath') + ->willReturn('folder/second'); + $info2->method('getEtag') + ->willReturn('def'); + + $this->view->expects($this->once()) + ->method('getDirectoryContent') + ->willReturn([$info1, $info2]); + + $this->view->expects($this->any()) + ->method('getRelativePath') + ->willReturnCallback(function ($path) { + return str_replace('/admin/files/', '', $path); + }); + + $this->view->expects($this->any()) + ->method('getAbsolutePath') + ->willReturnCallback(function ($path) { + return Filesystem::normalizePath('/admin/files' . $path); + }); + + $this->overwriteService(View::class, $this->view); + + $dir = new Directory($this->view, $this->info); + $nodes = $dir->getChildren(); + + $this->assertCount(2, $nodes); + + // calling a second time just returns the cached values, + // does not call getDirectoryContents again + $dir->getChildren(); + } + + + public function testGetChildrenNoPermission(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + $info = $this->createMock(FileInfo::class); + $info->expects($this->any()) + ->method('isReadable') + ->willReturn(false); + + $dir = new Directory($this->view, $info); + $dir->getChildren(); + } + + + public function testGetChildNoPermission(): void { + $this->expectException(\Sabre\DAV\Exception\NotFound::class); + + $this->info->expects($this->any()) + ->method('isReadable') + ->willReturn(false); + + $dir = new Directory($this->view, $this->info); + $dir->getChild('test'); + } + + + public function testGetChildThrowStorageNotAvailableException(): void { + $this->expectException(\Sabre\DAV\Exception\ServiceUnavailable::class); + + $this->view->expects($this->once()) + ->method('getFileInfo') + ->willThrowException(new StorageNotAvailableException()); + + $dir = new Directory($this->view, $this->info); + $dir->getChild('.'); + } + + + public function testGetChildThrowInvalidPath(): void { + $this->expectException(InvalidPath::class); + + $this->view->expects($this->once()) + ->method('verifyPath') + ->willThrowException(new InvalidPathException()); + $this->view->expects($this->never()) + ->method('getFileInfo'); + + $dir = new Directory($this->view, $this->info); + $dir->getChild('.'); + } + + public function testGetQuotaInfoUnlimited(): void { + $this->createUser('user', 'password'); + self::loginAsUser('user'); + $mountPoint = $this->createMock(IMountPoint::class); + $storage = $this->createMock(Quota::class); + $mountPoint->method('getStorage') + ->willReturn($storage); + + $storage->expects($this->any()) + ->method('instanceOfStorage') + ->willReturnMap([ + ['\OCA\Files_Sharing\SharedStorage', false], + ['\OC\Files\Storage\Wrapper\Quota', false], + [Storage::class, false], + ]); + + $storage->expects($this->once()) + ->method('getOwner') + ->willReturn('user'); + + $storage->expects($this->never()) + ->method('getQuota'); + + $storage->expects($this->once()) + ->method('free_space') + ->willReturn(800); + + $this->info->expects($this->any()) + ->method('getPath') + ->willReturn('/admin/files/foo'); + + $this->info->expects($this->once()) + ->method('getSize') + ->willReturn(200); + + $this->info->expects($this->once()) + ->method('getMountPoint') + ->willReturn($mountPoint); + + $this->view->expects($this->any()) + ->method('getRelativePath') + ->willReturn('/foo'); + + $this->info->expects($this->once()) + ->method('getInternalPath') + ->willReturn('/foo'); + + $mountPoint->method('getMountPoint') + ->willReturn('/user/files/mymountpoint'); + + $dir = new Directory($this->view, $this->info); + $this->assertEquals([200, -3], $dir->getQuotaInfo()); //200 used, unlimited + } + + public function testGetQuotaInfoSpecific(): void { + $this->createUser('user', 'password'); + self::loginAsUser('user'); + $mountPoint = $this->createMock(IMountPoint::class); + $storage = $this->createMock(Quota::class); + $mountPoint->method('getStorage') + ->willReturn($storage); + + $storage->expects($this->any()) + ->method('instanceOfStorage') + ->willReturnMap([ + ['\OCA\Files_Sharing\SharedStorage', false], + ['\OC\Files\Storage\Wrapper\Quota', true], + [Storage::class, false], + ]); + + $storage->expects($this->once()) + ->method('getOwner') + ->willReturn('user'); + + $storage->expects($this->once()) + ->method('getQuota') + ->willReturn(1000); + + $storage->expects($this->once()) + ->method('free_space') + ->willReturn(800); + + $this->info->expects($this->once()) + ->method('getSize') + ->willReturn(200); + + $this->info->expects($this->once()) + ->method('getMountPoint') + ->willReturn($mountPoint); + + $this->info->expects($this->once()) + ->method('getInternalPath') + ->willReturn('/foo'); + + $mountPoint->method('getMountPoint') + ->willReturn('/user/files/mymountpoint'); + + $this->view->expects($this->any()) + ->method('getRelativePath') + ->willReturn('/foo'); + + $dir = new Directory($this->view, $this->info); + $this->assertEquals([200, 800], $dir->getQuotaInfo()); //200 used, 800 free + } + + #[\PHPUnit\Framework\Attributes\DataProvider('moveFailedProvider')] + public function testMoveFailed(string $source, string $destination, array $updatables, array $deletables): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + $this->moveTest($source, $destination, $updatables, $deletables); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('moveSuccessProvider')] + public function testMoveSuccess(string $source, string $destination, array $updatables, array $deletables): void { + $this->moveTest($source, $destination, $updatables, $deletables); + $this->addToAssertionCount(1); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('moveFailedInvalidCharsProvider')] + public function testMoveFailedInvalidChars(string $source, string $destination, array $updatables, array $deletables): void { + $this->expectException(InvalidPath::class); + + $this->moveTest($source, $destination, $updatables, $deletables); + } + + public static function moveFailedInvalidCharsProvider(): array { + return [ + ['a/valid', "a/i\nvalid", ['a' => true, 'a/valid' => true, 'a/c*' => false], []], + ]; + } + + public static function moveFailedProvider(): array { + return [ + ['a/b', 'a/c', ['a' => false, 'a/b' => false, 'a/c' => false], []], + ['a/b', 'b/b', ['a' => false, 'a/b' => false, 'b' => false, 'b/b' => false], []], + ['a/b', 'b/b', ['a' => false, 'a/b' => true, 'b' => false, 'b/b' => false], []], + ['a/b', 'b/b', ['a' => true, 'a/b' => true, 'b' => false, 'b/b' => false], []], + ['a/b', 'b/b', ['a' => true, 'a/b' => true, 'b' => true, 'b/b' => false], ['a/b' => false]], + ['a/b', 'a/c', ['a' => false, 'a/b' => true, 'a/c' => false], []], + ]; + } + + public static function moveSuccessProvider(): array { + return [ + ['a/b', 'b/b', ['a' => true, 'a/b' => true, 'b' => true, 'b/b' => false], ['a/b' => true]], + // older files with special chars can still be renamed to valid names + ['a/b*', 'b/b', ['a' => true, 'a/b*' => true, 'b' => true, 'b/b' => false], ['a/b*' => true]], + ]; + } + + private function moveTest(string $source, string $destination, array $updatables, array $deletables): void { + $view = new TestViewDirectory($updatables, $deletables); + + $sourceInfo = new FileInfo($source, null, null, [ + 'type' => FileInfo::TYPE_FOLDER, + ], null); + $targetInfo = new FileInfo(dirname($destination), null, null, [ + 'type' => FileInfo::TYPE_FOLDER, + ], null); + + $sourceNode = new Directory($view, $sourceInfo); + $targetNode = $this->getMockBuilder(Directory::class) + ->onlyMethods(['childExists']) + ->setConstructorArgs([$view, $targetInfo]) + ->getMock(); + $targetNode->expects($this->any())->method('childExists') + ->with(basename($destination)) + ->willReturn(false); + $this->assertTrue($targetNode->moveInto(basename($destination), $source, $sourceNode)); + } + + + public function testFailingMove(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + $this->expectExceptionMessage('Could not copy directory b, target exists'); + + $source = 'a/b'; + $destination = 'c/b'; + $updatables = ['a' => true, 'a/b' => true, 'b' => true, 'c/b' => false]; + $deletables = ['a/b' => true]; + + $view = new TestViewDirectory($updatables, $deletables); + + $sourceInfo = new FileInfo($source, null, null, ['type' => FileInfo::TYPE_FOLDER], null); + $targetInfo = new FileInfo(dirname($destination), null, null, ['type' => FileInfo::TYPE_FOLDER], null); + + $sourceNode = new Directory($view, $sourceInfo); + $targetNode = $this->getMockBuilder(Directory::class) + ->onlyMethods(['childExists']) + ->setConstructorArgs([$view, $targetInfo]) + ->getMock(); + $targetNode->expects($this->once())->method('childExists') + ->with(basename($destination)) + ->willReturn(true); + + $targetNode->moveInto(basename($destination), $source, $sourceNode); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/DummyGetResponsePluginTest.php b/apps/dav/tests/unit/Connector/Sabre/DummyGetResponsePluginTest.php new file mode 100644 index 00000000000..2d688d64600 --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/DummyGetResponsePluginTest.php @@ -0,0 +1,57 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre; + +use OCA\DAV\Connector\Sabre\DummyGetResponsePlugin; +use Sabre\DAV\Server; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; +use Test\TestCase; + +/** + * Class DummyGetResponsePluginTest + * + * @package OCA\DAV\Tests\unit\Connector\Sabre + */ +class DummyGetResponsePluginTest extends TestCase { + private DummyGetResponsePlugin $dummyGetResponsePlugin; + + protected function setUp(): void { + parent::setUp(); + + $this->dummyGetResponsePlugin = new DummyGetResponsePlugin(); + } + + public function testInitialize(): void { + $server = $this->createMock(Server::class); + $server + ->expects($this->once()) + ->method('on') + ->with('method:GET', [$this->dummyGetResponsePlugin, 'httpGet'], 200); + + $this->dummyGetResponsePlugin->initialize($server); + } + + + public function testHttpGet(): void { + /** @var \Sabre\HTTP\RequestInterface $request */ + $request = $this->createMock(RequestInterface::class); + /** @var \Sabre\HTTP\ResponseInterface $response */ + $response = $this->createMock(ResponseInterface::class); + $response + ->expects($this->once()) + ->method('setBody'); + $response + ->expects($this->once()) + ->method('setStatus') + ->with(200); + + $this->assertSame(false, $this->dummyGetResponsePlugin->httpGet($request, $response)); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/Exception/ForbiddenTest.php b/apps/dav/tests/unit/Connector/Sabre/Exception/ForbiddenTest.php new file mode 100644 index 00000000000..2f9e0ae9196 --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/Exception/ForbiddenTest.php @@ -0,0 +1,44 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre\Exception; + +use OCA\DAV\Connector\Sabre\Exception\Forbidden; +use Sabre\DAV\Server; + +class ForbiddenTest extends \Test\TestCase { + public function testSerialization(): void { + + // create xml doc + $DOM = new \DOMDocument('1.0', 'utf-8'); + $DOM->formatOutput = true; + $error = $DOM->createElementNS('DAV:', 'd:error'); + $error->setAttribute('xmlns:s', \Sabre\DAV\Server::NS_SABREDAV); + $DOM->appendChild($error); + + // serialize the exception + $message = '1234567890'; + $retry = false; + $expectedXml = <<<EOD +<?xml version="1.0" encoding="utf-8"?> +<d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:o="http://owncloud.org/ns"> + <o:retry xmlns:o="o:">false</o:retry> + <o:reason xmlns:o="o:">1234567890</o:reason> +</d:error> + +EOD; + + $ex = new Forbidden($message, $retry); + $server = $this->createMock(Server::class); + $ex->serialize($server, $error); + + // assert + $xml = $DOM->saveXML(); + $this->assertEquals($expectedXml, $xml); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/Exception/InvalidPathTest.php b/apps/dav/tests/unit/Connector/Sabre/Exception/InvalidPathTest.php new file mode 100644 index 00000000000..6f62bef86a3 --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/Exception/InvalidPathTest.php @@ -0,0 +1,44 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre\Exception; + +use OCA\DAV\Connector\Sabre\Exception\InvalidPath; +use Sabre\DAV\Server; + +class InvalidPathTest extends \Test\TestCase { + public function testSerialization(): void { + + // create xml doc + $DOM = new \DOMDocument('1.0', 'utf-8'); + $DOM->formatOutput = true; + $error = $DOM->createElementNS('DAV:', 'd:error'); + $error->setAttribute('xmlns:s', \Sabre\DAV\Server::NS_SABREDAV); + $DOM->appendChild($error); + + // serialize the exception + $message = '1234567890'; + $retry = false; + $expectedXml = <<<EOD +<?xml version="1.0" encoding="utf-8"?> +<d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:o="http://owncloud.org/ns"> + <o:retry xmlns:o="o:">false</o:retry> + <o:reason xmlns:o="o:">1234567890</o:reason> +</d:error> + +EOD; + + $ex = new InvalidPath($message, $retry); + $server = $this->createMock(Server::class); + $ex->serialize($server, $error); + + // assert + $xml = $DOM->saveXML(); + $this->assertEquals($expectedXml, $xml); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/ExceptionLoggerPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/ExceptionLoggerPluginTest.php new file mode 100644 index 00000000000..416ac8a75c9 --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/ExceptionLoggerPluginTest.php @@ -0,0 +1,66 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre; + +use OC\SystemConfig; +use OCA\DAV\Connector\Sabre\Exception\InvalidPath; +use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin; +use OCA\DAV\Exception\ServerMaintenanceMode; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Server; +use Test\TestCase; + +class ExceptionLoggerPluginTest extends TestCase { + private Server $server; + private ExceptionLoggerPlugin $plugin; + private LoggerInterface&MockObject $logger; + + private function init(): void { + $config = $this->createMock(SystemConfig::class); + $config->expects($this->any()) + ->method('getValue') + ->willReturnCallback(function ($key, $default) { + switch ($key) { + case 'loglevel': + return 0; + default: + return $default; + } + }); + + $this->server = new Server(); + $this->logger = $this->createMock(LoggerInterface::class); + $this->plugin = new ExceptionLoggerPlugin('unit-test', $this->logger); + $this->plugin->initialize($this->server); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providesExceptions')] + public function testLogging(string $expectedLogLevel, \Throwable $e): void { + $this->init(); + + $this->logger->expects($this->once()) + ->method($expectedLogLevel) + ->with($e->getMessage(), ['app' => 'unit-test','exception' => $e]); + + $this->plugin->logException($e); + } + + public static function providesExceptions(): array { + return [ + ['debug', new NotFound()], + ['debug', new ServerMaintenanceMode('System is in maintenance mode.')], + // Faking a translation + ['debug', new ServerMaintenanceMode('Syst3m 1s 1n m41nt3n4nc3 m0d3.')], + ['debug', new ServerMaintenanceMode('Upgrade needed')], + ['critical', new InvalidPath('This path leads to nowhere')] + ]; + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/FakeLockerPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/FakeLockerPluginTest.php new file mode 100644 index 00000000000..366932137f4 --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/FakeLockerPluginTest.php @@ -0,0 +1,160 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre; + +use OCA\DAV\Connector\Sabre\FakeLockerPlugin; +use Sabre\DAV\INode; +use Sabre\DAV\PropFind; +use Sabre\DAV\Server; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\Response; +use Sabre\HTTP\ResponseInterface; +use Test\TestCase; + +/** + * Class FakeLockerPluginTest + * + * @package OCA\DAV\Tests\unit\Connector\Sabre + */ +class FakeLockerPluginTest extends TestCase { + private FakeLockerPlugin $fakeLockerPlugin; + + protected function setUp(): void { + parent::setUp(); + $this->fakeLockerPlugin = new FakeLockerPlugin(); + } + + public function testInitialize(): void { + /** @var Server $server */ + $server = $this->createMock(Server::class); + $calls = [ + ['method:LOCK', [$this->fakeLockerPlugin, 'fakeLockProvider'], 1], + ['method:UNLOCK', [$this->fakeLockerPlugin, 'fakeUnlockProvider'], 1], + ['propFind', [$this->fakeLockerPlugin, 'propFind'], 100], + ['validateTokens', [$this->fakeLockerPlugin, 'validateTokens'], 100], + ]; + $server->expects($this->exactly(count($calls))) + ->method('on') + ->willReturnCallback(function () use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + }); + + $this->fakeLockerPlugin->initialize($server); + } + + public function testGetHTTPMethods(): void { + $expected = [ + 'LOCK', + 'UNLOCK', + ]; + $this->assertSame($expected, $this->fakeLockerPlugin->getHTTPMethods('Test')); + } + + public function testGetFeatures(): void { + $expected = [ + 2, + ]; + $this->assertSame($expected, $this->fakeLockerPlugin->getFeatures()); + } + + public function testPropFind(): void { + $propFind = $this->createMock(PropFind::class); + $node = $this->createMock(INode::class); + + $calls = [ + '{DAV:}supportedlock', + '{DAV:}lockdiscovery', + ]; + $propFind->expects($this->exactly(count($calls))) + ->method('handle') + ->willReturnCallback(function ($propertyName) use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, $propertyName); + }); + + $this->fakeLockerPlugin->propFind($propFind, $node); + } + + public static function tokenDataProvider(): array { + return [ + [ + [ + [ + 'tokens' => [ + [ + 'token' => 'aToken', + 'validToken' => false, + ], + [], + [ + 'token' => 'opaquelocktoken:asdf', + 'validToken' => false, + ] + ], + ] + ], + [ + [ + 'tokens' => [ + [ + 'token' => 'aToken', + 'validToken' => false, + ], + [], + [ + 'token' => 'opaquelocktoken:asdf', + 'validToken' => true, + ] + ], + ] + ], + ] + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('tokenDataProvider')] + public function testValidateTokens(array $input, array $expected): void { + $request = $this->createMock(RequestInterface::class); + $this->fakeLockerPlugin->validateTokens($request, $input); + $this->assertSame($expected, $input); + } + + public function testFakeLockProvider(): void { + $request = $this->createMock(RequestInterface::class); + $response = new Response(); + $server = $this->getMockBuilder(Server::class) + ->getMock(); + $this->fakeLockerPlugin->initialize($server); + + $request->expects($this->exactly(2)) + ->method('getPath') + ->willReturn('MyPath'); + + $this->assertSame(false, $this->fakeLockerPlugin->fakeLockProvider($request, $response)); + + $expectedXml = '<?xml version="1.0" encoding="utf-8"?><d:prop xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns"><d:lockdiscovery><d:activelock><d:lockscope><d:exclusive/></d:lockscope><d:locktype><d:write/></d:locktype><d:lockroot><d:href>MyPath</d:href></d:lockroot><d:depth>infinity</d:depth><d:timeout>Second-1800</d:timeout><d:locktoken><d:href>opaquelocktoken:fe4f7f2437b151fbcb4e9f5c8118c6b1</d:href></d:locktoken></d:activelock></d:lockdiscovery></d:prop>'; + + $this->assertXmlStringEqualsXmlString($expectedXml, $response->getBody()); + } + + public function testFakeUnlockProvider(): void { + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $response->expects($this->once()) + ->method('setStatus') + ->with('204'); + $response->expects($this->once()) + ->method('setHeader') + ->with('Content-Length', '0'); + + $this->assertSame(false, $this->fakeLockerPlugin->fakeUnlockProvider($request, $response)); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/FileTest.php b/apps/dav/tests/unit/Connector/Sabre/FileTest.php new file mode 100644 index 00000000000..60c8382e131 --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/FileTest.php @@ -0,0 +1,1031 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre; + +use OC\AppFramework\Http\Request; +use OC\Files\Filesystem; +use OC\Files\Storage\Local; +use OC\Files\Storage\Temporary; +use OC\Files\Storage\Wrapper\PermissionsMask; +use OC\Files\View; +use OCA\DAV\Connector\Sabre\Exception\FileLocked; +use OCA\DAV\Connector\Sabre\Exception\Forbidden; +use OCA\DAV\Connector\Sabre\Exception\InvalidPath; +use OCA\DAV\Connector\Sabre\File; +use OCP\Constants; +use OCP\Encryption\Exceptions\GenericEncryptionException; +use OCP\Files\EntityTooLargeException; +use OCP\Files\FileInfo; +use OCP\Files\ForbiddenException; +use OCP\Files\InvalidContentException; +use OCP\Files\InvalidPathException; +use OCP\Files\LockNotAcquiredException; +use OCP\Files\NotPermittedException; +use OCP\Files\Storage\IStorage; +use OCP\Files\StorageNotAvailableException; +use OCP\IConfig; +use OCP\IRequestId; +use OCP\ITempManager; +use OCP\IUserManager; +use OCP\Lock\ILockingProvider; +use OCP\Lock\LockedException; +use OCP\Server; +use OCP\Util; +use PHPUnit\Framework\MockObject\MockObject; +use Test\HookHelper; +use Test\TestCase; +use Test\Traits\MountProviderTrait; +use Test\Traits\UserTrait; + +/** + * Class File + * + * @group DB + * + * @package OCA\DAV\Tests\unit\Connector\Sabre + */ +class FileTest extends TestCase { + use MountProviderTrait; + use UserTrait; + + private string $user; + protected IConfig&MockObject $config; + protected IRequestId&MockObject $requestId; + + protected function setUp(): void { + parent::setUp(); + + \OC_Hook::clear(); + + $this->user = 'test_user'; + $this->createUser($this->user, 'pass'); + + self::loginAsUser($this->user); + + $this->config = $this->createMock(IConfig::class); + $this->requestId = $this->createMock(IRequestId::class); + } + + protected function tearDown(): void { + $userManager = Server::get(IUserManager::class); + $userManager->get($this->user)->delete(); + + parent::tearDown(); + } + + private function getMockStorage(): MockObject&IStorage { + $storage = $this->createMock(IStorage::class); + $storage->method('getId') + ->willReturn('home::someuser'); + return $storage; + } + + private function getStream(string $string) { + $stream = fopen('php://temp', 'r+'); + fwrite($stream, $string); + fseek($stream, 0); + return $stream; + } + + + public static function fopenFailuresProvider(): array { + return [ + [ + // return false + null, + '\Sabre\Dav\Exception', + false + ], + [ + new NotPermittedException(), + 'Sabre\DAV\Exception\Forbidden' + ], + [ + new EntityTooLargeException(), + 'OCA\DAV\Connector\Sabre\Exception\EntityTooLarge' + ], + [ + new InvalidContentException(), + 'OCA\DAV\Connector\Sabre\Exception\UnsupportedMediaType' + ], + [ + new InvalidPathException(), + 'Sabre\DAV\Exception\Forbidden' + ], + [ + new ForbiddenException('', true), + 'OCA\DAV\Connector\Sabre\Exception\Forbidden' + ], + [ + new LockNotAcquiredException('/test.txt', 1), + 'OCA\DAV\Connector\Sabre\Exception\FileLocked' + ], + [ + new LockedException('/test.txt'), + 'OCA\DAV\Connector\Sabre\Exception\FileLocked' + ], + [ + new GenericEncryptionException(), + 'Sabre\DAV\Exception\ServiceUnavailable' + ], + [ + new StorageNotAvailableException(), + 'Sabre\DAV\Exception\ServiceUnavailable' + ], + [ + new \Sabre\DAV\Exception('Generic sabre exception'), + 'Sabre\DAV\Exception', + false + ], + [ + new \Exception('Generic exception'), + 'Sabre\DAV\Exception' + ], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('fopenFailuresProvider')] + public function testSimplePutFails(?\Throwable $thrownException, string $expectedException, bool $checkPreviousClass = true): void { + // setup + $storage = $this->getMockBuilder(Local::class) + ->onlyMethods(['writeStream']) + ->setConstructorArgs([['datadir' => Server::get(ITempManager::class)->getTemporaryFolder()]]) + ->getMock(); + Filesystem::mount($storage, [], $this->user . '/'); + /** @var View&MockObject $view */ + $view = $this->getMockBuilder(View::class) + ->onlyMethods(['getRelativePath', 'resolvePath']) + ->getMock(); + $view->expects($this->atLeastOnce()) + ->method('resolvePath') + ->willReturnCallback( + function ($path) use ($storage) { + return [$storage, $path]; + } + ); + + if ($thrownException !== null) { + $storage->expects($this->once()) + ->method('writeStream') + ->willThrowException($thrownException); + } else { + $storage->expects($this->once()) + ->method('writeStream') + ->willReturn(0); + } + + $view->expects($this->any()) + ->method('getRelativePath') + ->willReturnArgument(0); + + $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [ + 'permissions' => Constants::PERMISSION_ALL, + 'type' => FileInfo::TYPE_FOLDER, + ], null); + + $file = new File($view, $info); + + // action + $caughtException = null; + try { + $file->put('test data'); + } catch (\Exception $e) { + $caughtException = $e; + } + + $this->assertInstanceOf($expectedException, $caughtException); + if ($checkPreviousClass) { + $this->assertInstanceOf(get_class($thrownException), $caughtException->getPrevious()); + } + + $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files'); + } + + /** + * Simulate putting a file to the given path. + * + * @param string $path path to put the file into + * @param ?string $viewRoot root to use for the view + * @param null|Request $request the HTTP request + * + * @return null|string of the PUT operation which is usually the etag + */ + private function doPut(string $path, ?string $viewRoot = null, ?Request $request = null) { + $view = Filesystem::getView(); + if (!is_null($viewRoot)) { + $view = new View($viewRoot); + } else { + $viewRoot = '/' . $this->user . '/files'; + } + + $info = new \OC\Files\FileInfo( + $viewRoot . '/' . ltrim($path, '/'), + $this->getMockStorage(), + null, + [ + 'permissions' => Constants::PERMISSION_ALL, + 'type' => FileInfo::TYPE_FOLDER, + ], + null + ); + + /** @var File&MockObject $file */ + $file = $this->getMockBuilder(File::class) + ->setConstructorArgs([$view, $info, null, $request]) + ->onlyMethods(['header']) + ->getMock(); + + // beforeMethod locks + $view->lockFile($path, ILockingProvider::LOCK_SHARED); + + $result = $file->put($this->getStream('test data')); + + // afterMethod unlocks + $view->unlockFile($path, ILockingProvider::LOCK_SHARED); + + return $result; + } + + /** + * Test putting a single file + */ + public function testPutSingleFile(): void { + $this->assertNotEmpty($this->doPut('/foo.txt')); + } + + public static function legalMtimeProvider(): array { + return [ + 'string' => [ + 'requestMtime' => 'string', + 'resultMtime' => null + ], + 'castable string (int)' => [ + 'requestMtime' => '987654321', + 'resultMtime' => 987654321 + ], + 'castable string (float)' => [ + 'requestMtime' => '123456789.56', + 'resultMtime' => 123456789 + ], + 'float' => [ + 'requestMtime' => 123456789.56, + 'resultMtime' => 123456789 + ], + 'zero' => [ + 'requestMtime' => 0, + 'resultMtime' => null + ], + 'zero string' => [ + 'requestMtime' => '0', + 'resultMtime' => null + ], + 'negative zero string' => [ + 'requestMtime' => '-0', + 'resultMtime' => null + ], + 'string starting with number following by char' => [ + 'requestMtime' => '2345asdf', + 'resultMtime' => null + ], + 'string castable hex int' => [ + 'requestMtime' => '0x45adf', + 'resultMtime' => null + ], + 'string that looks like invalid hex int' => [ + 'requestMtime' => '0x123g', + 'resultMtime' => null + ], + 'negative int' => [ + 'requestMtime' => -34, + 'resultMtime' => null + ], + 'negative float' => [ + 'requestMtime' => -34.43, + 'resultMtime' => null + ], + ]; + } + + /** + * Test putting a file with string Mtime + */ + #[\PHPUnit\Framework\Attributes\DataProvider('legalMtimeProvider')] + public function testPutSingleFileLegalMtime(mixed $requestMtime, ?int $resultMtime): void { + $request = new Request([ + 'server' => [ + 'HTTP_X_OC_MTIME' => (string)$requestMtime, + ] + ], $this->requestId, $this->config, null); + $file = 'foo.txt'; + + if ($resultMtime === null) { + $this->expectException(\InvalidArgumentException::class); + } + + $this->doPut($file, null, $request); + + if ($resultMtime !== null) { + $this->assertEquals($resultMtime, $this->getFileInfos($file)['mtime']); + } + } + + /** + * Test that putting a file triggers create hooks + */ + public function testPutSingleFileTriggersHooks(): void { + HookHelper::setUpHooks(); + + $this->assertNotEmpty($this->doPut('/foo.txt')); + + $this->assertCount(4, HookHelper::$hookCalls); + $this->assertHookCall( + HookHelper::$hookCalls[0], + Filesystem::signal_create, + '/foo.txt' + ); + $this->assertHookCall( + HookHelper::$hookCalls[1], + Filesystem::signal_write, + '/foo.txt' + ); + $this->assertHookCall( + HookHelper::$hookCalls[2], + Filesystem::signal_post_create, + '/foo.txt' + ); + $this->assertHookCall( + HookHelper::$hookCalls[3], + Filesystem::signal_post_write, + '/foo.txt' + ); + } + + /** + * Test that putting a file triggers update hooks + */ + public function testPutOverwriteFileTriggersHooks(): void { + $view = Filesystem::getView(); + $view->file_put_contents('/foo.txt', 'some content that will be replaced'); + + HookHelper::setUpHooks(); + + $this->assertNotEmpty($this->doPut('/foo.txt')); + + $this->assertCount(4, HookHelper::$hookCalls); + $this->assertHookCall( + HookHelper::$hookCalls[0], + Filesystem::signal_update, + '/foo.txt' + ); + $this->assertHookCall( + HookHelper::$hookCalls[1], + Filesystem::signal_write, + '/foo.txt' + ); + $this->assertHookCall( + HookHelper::$hookCalls[2], + Filesystem::signal_post_update, + '/foo.txt' + ); + $this->assertHookCall( + HookHelper::$hookCalls[3], + Filesystem::signal_post_write, + '/foo.txt' + ); + } + + /** + * Test that putting a file triggers hooks with the correct path + * if the passed view was chrooted (can happen with public webdav + * where the root is the share root) + */ + public function testPutSingleFileTriggersHooksDifferentRoot(): void { + $view = Filesystem::getView(); + $view->mkdir('noderoot'); + + HookHelper::setUpHooks(); + + // happens with public webdav where the view root is the share root + $this->assertNotEmpty($this->doPut('/foo.txt', '/' . $this->user . '/files/noderoot')); + + $this->assertCount(4, HookHelper::$hookCalls); + $this->assertHookCall( + HookHelper::$hookCalls[0], + Filesystem::signal_create, + '/noderoot/foo.txt' + ); + $this->assertHookCall( + HookHelper::$hookCalls[1], + Filesystem::signal_write, + '/noderoot/foo.txt' + ); + $this->assertHookCall( + HookHelper::$hookCalls[2], + Filesystem::signal_post_create, + '/noderoot/foo.txt' + ); + $this->assertHookCall( + HookHelper::$hookCalls[3], + Filesystem::signal_post_write, + '/noderoot/foo.txt' + ); + } + + public static function cancellingHook($params): void { + self::$hookCalls[] = [ + 'signal' => Filesystem::signal_post_create, + 'params' => $params + ]; + } + + /** + * Test put file with cancelled hook + */ + public function testPutSingleFileCancelPreHook(): void { + Util::connectHook( + Filesystem::CLASSNAME, + Filesystem::signal_create, + '\Test\HookHelper', + 'cancellingCallback' + ); + + // action + $thrown = false; + try { + $this->doPut('/foo.txt'); + } catch (\Sabre\DAV\Exception $e) { + $thrown = true; + } + + $this->assertTrue($thrown); + $this->assertEmpty($this->listPartFiles(), 'No stray part files'); + } + + /** + * Test exception when the uploaded size did not match + */ + public function testSimplePutFailsSizeCheck(): void { + // setup + /** @var View&MockObject */ + $view = $this->getMockBuilder(View::class) + ->onlyMethods(['rename', 'getRelativePath', 'filesize']) + ->getMock(); + $view->expects($this->any()) + ->method('rename') + ->withAnyParameters() + ->willReturn(false); + $view->expects($this->any()) + ->method('getRelativePath') + ->willReturnArgument(0); + + $view->expects($this->any()) + ->method('filesize') + ->willReturn(123456); + + $request = new Request([ + 'server' => [ + 'CONTENT_LENGTH' => '123456', + ], + 'method' => 'PUT', + ], $this->requestId, $this->config, null); + + $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [ + 'permissions' => Constants::PERMISSION_ALL, + 'type' => FileInfo::TYPE_FOLDER, + ], null); + + $file = new File($view, $info, null, $request); + + // action + $thrown = false; + try { + // beforeMethod locks + $file->acquireLock(ILockingProvider::LOCK_SHARED); + + $file->put($this->getStream('test data')); + + // afterMethod unlocks + $file->releaseLock(ILockingProvider::LOCK_SHARED); + } catch (\Sabre\DAV\Exception\BadRequest $e) { + $thrown = true; + } + + $this->assertTrue($thrown); + $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files'); + } + + /** + * Test exception during final rename in simple upload mode + */ + public function testSimplePutFailsMoveFromStorage(): void { + $view = new View('/' . $this->user . '/files'); + + // simulate situation where the target file is locked + $view->lockFile('/test.txt', ILockingProvider::LOCK_EXCLUSIVE); + + $info = new \OC\Files\FileInfo('/' . $this->user . '/files/test.txt', $this->getMockStorage(), null, [ + 'permissions' => Constants::PERMISSION_ALL, + 'type' => FileInfo::TYPE_FOLDER, + ], null); + + $file = new File($view, $info); + + // action + $thrown = false; + try { + // beforeMethod locks + $view->lockFile($info->getPath(), ILockingProvider::LOCK_SHARED); + + $file->put($this->getStream('test data')); + + // afterMethod unlocks + $view->unlockFile($info->getPath(), ILockingProvider::LOCK_SHARED); + } catch (FileLocked $e) { + $thrown = true; + } + + $this->assertTrue($thrown); + $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files'); + } + + /** + * Test put file with invalid chars + */ + public function testSimplePutInvalidChars(): void { + // setup + /** @var View&MockObject */ + $view = $this->getMockBuilder(View::class) + ->onlyMethods(['getRelativePath']) + ->getMock(); + $view->expects($this->any()) + ->method('getRelativePath') + ->willReturnArgument(0); + + $info = new \OC\Files\FileInfo("/i\nvalid", $this->getMockStorage(), null, [ + 'permissions' => Constants::PERMISSION_ALL, + 'type' => FileInfo::TYPE_FOLDER, + ], null); + $file = new File($view, $info); + + // action + $thrown = false; + try { + // beforeMethod locks + $view->lockFile($info->getPath(), ILockingProvider::LOCK_SHARED); + + $file->put($this->getStream('test data')); + + // afterMethod unlocks + $view->unlockFile($info->getPath(), ILockingProvider::LOCK_SHARED); + } catch (InvalidPath $e) { + $thrown = true; + } + + $this->assertTrue($thrown); + $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files'); + } + + /** + * Test setting name with setName() with invalid chars + * + */ + public function testSetNameInvalidChars(): void { + $this->expectException(InvalidPath::class); + + // setup + /** @var View&MockObject */ + $view = $this->getMockBuilder(View::class) + ->onlyMethods(['getRelativePath']) + ->getMock(); + + $view->expects($this->any()) + ->method('getRelativePath') + ->willReturnArgument(0); + + $info = new \OC\Files\FileInfo('/valid', $this->getMockStorage(), null, [ + 'permissions' => Constants::PERMISSION_ALL, + 'type' => FileInfo::TYPE_FOLDER, + ], null); + $file = new File($view, $info); + + $file->setName("/i\nvalid"); + } + + + public function testUploadAbort(): void { + // setup + /** @var View&MockObject */ + $view = $this->getMockBuilder(View::class) + ->onlyMethods(['rename', 'getRelativePath', 'filesize']) + ->getMock(); + $view->expects($this->any()) + ->method('rename') + ->withAnyParameters() + ->willReturn(false); + $view->expects($this->any()) + ->method('getRelativePath') + ->willReturnArgument(0); + $view->expects($this->any()) + ->method('filesize') + ->willReturn(123456); + + $request = new Request([ + 'server' => [ + 'CONTENT_LENGTH' => '123456', + ], + 'method' => 'PUT', + ], $this->requestId, $this->config, null); + + $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [ + 'permissions' => Constants::PERMISSION_ALL, + 'type' => FileInfo::TYPE_FOLDER, + ], null); + + $file = new File($view, $info, null, $request); + + // action + $thrown = false; + try { + // beforeMethod locks + $view->lockFile($info->getPath(), ILockingProvider::LOCK_SHARED); + + $file->put($this->getStream('test data')); + + // afterMethod unlocks + $view->unlockFile($info->getPath(), ILockingProvider::LOCK_SHARED); + } catch (\Sabre\DAV\Exception\BadRequest $e) { + $thrown = true; + } + + $this->assertTrue($thrown); + $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files'); + } + + + public function testDeleteWhenAllowed(): void { + // setup + /** @var View&MockObject */ + $view = $this->getMockBuilder(View::class) + ->getMock(); + + $view->expects($this->once()) + ->method('unlink') + ->willReturn(true); + + $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [ + 'permissions' => Constants::PERMISSION_ALL, + 'type' => FileInfo::TYPE_FOLDER, + ], null); + + $file = new File($view, $info); + + // action + $file->delete(); + } + + + public function testDeleteThrowsWhenDeletionNotAllowed(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + // setup + /** @var View&MockObject */ + $view = $this->getMockBuilder(View::class) + ->getMock(); + + $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [ + 'permissions' => 0, + 'type' => FileInfo::TYPE_FOLDER, + ], null); + + $file = new File($view, $info); + + // action + $file->delete(); + } + + + public function testDeleteThrowsWhenDeletionFailed(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + // setup + /** @var View&MockObject */ + $view = $this->getMockBuilder(View::class) + ->getMock(); + + // but fails + $view->expects($this->once()) + ->method('unlink') + ->willReturn(false); + + $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [ + 'permissions' => Constants::PERMISSION_ALL, + 'type' => FileInfo::TYPE_FOLDER, + ], null); + + $file = new File($view, $info); + + // action + $file->delete(); + } + + + public function testDeleteThrowsWhenDeletionThrows(): void { + $this->expectException(Forbidden::class); + + // setup + /** @var View&MockObject */ + $view = $this->getMockBuilder(View::class) + ->getMock(); + + // but fails + $view->expects($this->once()) + ->method('unlink') + ->willThrowException(new ForbiddenException('', true)); + + $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [ + 'permissions' => Constants::PERMISSION_ALL, + 'type' => FileInfo::TYPE_FOLDER, + ], null); + + $file = new File($view, $info); + + // action + $file->delete(); + } + + /** + * Asserts hook call + * + * @param array $callData hook call data to check + * @param string $signal signal name + * @param string $hookPath hook path + */ + protected function assertHookCall($callData, $signal, $hookPath) { + $this->assertEquals($signal, $callData['signal']); + $params = $callData['params']; + $this->assertEquals( + $hookPath, + $params[Filesystem::signal_param_path] + ); + } + + /** + * Test whether locks are set before and after the operation + */ + public function testPutLocking(): void { + $view = new View('/' . $this->user . '/files/'); + + $path = 'test-locking.txt'; + $info = new \OC\Files\FileInfo( + '/' . $this->user . '/files/' . $path, + $this->getMockStorage(), + null, + [ + 'permissions' => Constants::PERMISSION_ALL, + 'type' => FileInfo::TYPE_FOLDER, + ], + null + ); + + $file = new File($view, $info); + + $this->assertFalse( + $this->isFileLocked($view, $path, ILockingProvider::LOCK_SHARED), + 'File unlocked before put' + ); + $this->assertFalse( + $this->isFileLocked($view, $path, ILockingProvider::LOCK_EXCLUSIVE), + 'File unlocked before put' + ); + + $wasLockedPre = false; + $wasLockedPost = false; + $eventHandler = $this->getMockBuilder(\stdclass::class) + ->addMethods(['writeCallback', 'postWriteCallback']) + ->getMock(); + + // both pre and post hooks might need access to the file, + // so only shared lock is acceptable + $eventHandler->expects($this->once()) + ->method('writeCallback') + ->willReturnCallback( + function () use ($view, $path, &$wasLockedPre): void { + $wasLockedPre = $this->isFileLocked($view, $path, ILockingProvider::LOCK_SHARED); + $wasLockedPre = $wasLockedPre && !$this->isFileLocked($view, $path, ILockingProvider::LOCK_EXCLUSIVE); + } + ); + $eventHandler->expects($this->once()) + ->method('postWriteCallback') + ->willReturnCallback( + function () use ($view, $path, &$wasLockedPost): void { + $wasLockedPost = $this->isFileLocked($view, $path, ILockingProvider::LOCK_SHARED); + $wasLockedPost = $wasLockedPost && !$this->isFileLocked($view, $path, ILockingProvider::LOCK_EXCLUSIVE); + } + ); + + Util::connectHook( + Filesystem::CLASSNAME, + Filesystem::signal_write, + $eventHandler, + 'writeCallback' + ); + Util::connectHook( + Filesystem::CLASSNAME, + Filesystem::signal_post_write, + $eventHandler, + 'postWriteCallback' + ); + + // beforeMethod locks + $view->lockFile($path, ILockingProvider::LOCK_SHARED); + + $this->assertNotEmpty($file->put($this->getStream('test data'))); + + // afterMethod unlocks + $view->unlockFile($path, ILockingProvider::LOCK_SHARED); + + $this->assertTrue($wasLockedPre, 'File was locked during pre-hooks'); + $this->assertTrue($wasLockedPost, 'File was locked during post-hooks'); + + $this->assertFalse( + $this->isFileLocked($view, $path, ILockingProvider::LOCK_SHARED), + 'File unlocked after put' + ); + $this->assertFalse( + $this->isFileLocked($view, $path, ILockingProvider::LOCK_EXCLUSIVE), + 'File unlocked after put' + ); + } + + /** + * Returns part files in the given path + * + * @param \OC\Files\View view which root is the current user's "files" folder + * @param string $path path for which to list part files + * + * @return array list of part files + */ + private function listPartFiles(?View $userView = null, $path = '') { + if ($userView === null) { + $userView = Filesystem::getView(); + } + $files = []; + [$storage, $internalPath] = $userView->resolvePath($path); + if ($storage instanceof Local) { + $realPath = $storage->getSourcePath($internalPath); + $dh = opendir($realPath); + while (($file = readdir($dh)) !== false) { + if (str_ends_with($file, '.part')) { + $files[] = $file; + } + } + closedir($dh); + } + return $files; + } + + /** + * returns an array of file information filesize, mtime, filetype, mimetype + * + * @param string $path + * @param View $userView + * @return array + */ + private function getFileInfos($path = '', ?View $userView = null) { + if ($userView === null) { + $userView = Filesystem::getView(); + } + return [ + 'filesize' => $userView->filesize($path), + 'mtime' => $userView->filemtime($path), + 'filetype' => $userView->filetype($path), + 'mimetype' => $userView->getMimeType($path) + ]; + } + + + public function testGetFopenFails(): void { + $this->expectException(\Sabre\DAV\Exception\ServiceUnavailable::class); + + /** @var View&MockObject */ + $view = $this->getMockBuilder(View::class) + ->onlyMethods(['fopen']) + ->getMock(); + $view->expects($this->atLeastOnce()) + ->method('fopen') + ->willReturn(false); + + $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [ + 'permissions' => Constants::PERMISSION_ALL, + 'type' => FileInfo::TYPE_FILE, + ], null); + + $file = new File($view, $info); + + $file->get(); + } + + + public function testGetFopenThrows(): void { + $this->expectException(Forbidden::class); + + /** @var View&MockObject */ + $view = $this->getMockBuilder(View::class) + ->onlyMethods(['fopen']) + ->getMock(); + $view->expects($this->atLeastOnce()) + ->method('fopen') + ->willThrowException(new ForbiddenException('', true)); + + $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [ + 'permissions' => Constants::PERMISSION_ALL, + 'type' => FileInfo::TYPE_FILE, + ], null); + + $file = new File($view, $info); + + $file->get(); + } + + + public function testGetThrowsIfNoPermission(): void { + $this->expectException(\Sabre\DAV\Exception\NotFound::class); + + /** @var View&MockObject */ + $view = $this->getMockBuilder(View::class) + ->onlyMethods(['fopen']) + ->getMock(); + $view->expects($this->never()) + ->method('fopen'); + + $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, [ + 'permissions' => Constants::PERMISSION_CREATE, // no read perm + 'type' => FileInfo::TYPE_FOLDER, + ], null); + + $file = new File($view, $info); + + $file->get(); + } + + public function testSimplePutNoCreatePermissions(): void { + $this->logout(); + + $storage = new Temporary([]); + $storage->file_put_contents('file.txt', 'old content'); + $noCreateStorage = new PermissionsMask([ + 'storage' => $storage, + 'mask' => Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE + ]); + + $this->registerMount($this->user, $noCreateStorage, '/' . $this->user . '/files/root'); + + $this->loginAsUser($this->user); + + $view = new View('/' . $this->user . '/files'); + + $info = $view->getFileInfo('root/file.txt'); + + $file = new File($view, $info); + + // beforeMethod locks + $view->lockFile('root/file.txt', ILockingProvider::LOCK_SHARED); + + $file->put($this->getStream('new content')); + + // afterMethod unlocks + $view->unlockFile('root/file.txt', ILockingProvider::LOCK_SHARED); + + $this->assertEquals('new content', $view->file_get_contents('root/file.txt')); + } + + public function testPutLockExpired(): void { + $view = new View('/' . $this->user . '/files/'); + + $path = 'test-locking.txt'; + $info = new \OC\Files\FileInfo( + '/' . $this->user . '/files/' . $path, + $this->getMockStorage(), + null, + [ + 'permissions' => Constants::PERMISSION_ALL, + 'type' => FileInfo::TYPE_FOLDER, + ], + null + ); + + $file = new File($view, $info); + + // don't lock before the PUT to simulate an expired shared lock + $this->assertNotEmpty($file->put($this->getStream('test data'))); + + // afterMethod unlocks + $view->unlockFile($path, ILockingProvider::LOCK_SHARED); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php new file mode 100644 index 00000000000..4df3accfda9 --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php @@ -0,0 +1,720 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre; + +use OC\Accounts\Account; +use OC\Accounts\AccountProperty; +use OC\User\User; +use OCA\DAV\Connector\Sabre\Directory; +use OCA\DAV\Connector\Sabre\Exception\InvalidPath; +use OCA\DAV\Connector\Sabre\File; +use OCA\DAV\Connector\Sabre\FilesPlugin; +use OCA\DAV\Connector\Sabre\Node; +use OCP\Accounts\IAccountManager; +use OCP\Files\FileInfo; +use OCP\Files\IFilenameValidator; +use OCP\Files\InvalidPathException; +use OCP\Files\StorageNotAvailableException; +use OCP\IConfig; +use OCP\IPreview; +use OCP\IRequest; +use OCP\IUserSession; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\PropFind; +use Sabre\DAV\PropPatch; +use Sabre\DAV\Server; +use Sabre\DAV\Tree; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; +use Sabre\Xml\Service; +use Test\TestCase; + +/** + * @group DB + */ +class FilesPluginTest extends TestCase { + + private Tree&MockObject $tree; + private Server&MockObject $server; + private IConfig&MockObject $config; + private IRequest&MockObject $request; + private IPreview&MockObject $previewManager; + private IUserSession&MockObject $userSession; + private IFilenameValidator&MockObject $filenameValidator; + private IAccountManager&MockObject $accountManager; + private FilesPlugin $plugin; + + protected function setUp(): void { + parent::setUp(); + $this->server = $this->createMock(Server::class); + $this->tree = $this->createMock(Tree::class); + $this->config = $this->createMock(IConfig::class); + $this->config->expects($this->any())->method('getSystemValue') + ->with($this->equalTo('data-fingerprint'), $this->equalTo('')) + ->willReturn('my_fingerprint'); + $this->request = $this->createMock(IRequest::class); + $this->previewManager = $this->createMock(IPreview::class); + $this->userSession = $this->createMock(IUserSession::class); + $this->filenameValidator = $this->createMock(IFilenameValidator::class); + $this->accountManager = $this->createMock(IAccountManager::class); + + $this->plugin = new FilesPlugin( + $this->tree, + $this->config, + $this->request, + $this->previewManager, + $this->userSession, + $this->filenameValidator, + $this->accountManager, + ); + + $response = $this->createMock(ResponseInterface::class); + $this->server->httpResponse = $response; + $this->server->xml = new Service(); + + $this->plugin->initialize($this->server); + } + + private function createTestNode(string $class, string $path = '/dummypath'): MockObject { + $node = $this->createMock($class); + + $node->expects($this->any()) + ->method('getId') + ->willReturn(123); + + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->with($path) + ->willReturn($node); + + $node->expects($this->any()) + ->method('getFileId') + ->willReturn('00000123instanceid'); + $node->expects($this->any()) + ->method('getInternalFileId') + ->willReturn('123'); + $node->expects($this->any()) + ->method('getEtag') + ->willReturn('"abc"'); + $node->expects($this->any()) + ->method('getDavPermissions') + ->willReturn('DWCKMSR'); + + $fileInfo = $this->createMock(FileInfo::class); + $fileInfo->expects($this->any()) + ->method('isReadable') + ->willReturn(true); + $fileInfo->expects($this->any()) + ->method('getCreationTime') + ->willReturn(123456789); + + $node->expects($this->any()) + ->method('getFileInfo') + ->willReturn($fileInfo); + + return $node; + } + + public function testGetPropertiesForFile(): void { + /** @var File&MockObject $node */ + $node = $this->createTestNode(File::class); + + $propFind = new PropFind( + '/dummyPath', + [ + FilesPlugin::GETETAG_PROPERTYNAME, + FilesPlugin::FILEID_PROPERTYNAME, + FilesPlugin::INTERNAL_FILEID_PROPERTYNAME, + FilesPlugin::SIZE_PROPERTYNAME, + FilesPlugin::PERMISSIONS_PROPERTYNAME, + FilesPlugin::DOWNLOADURL_PROPERTYNAME, + FilesPlugin::OWNER_ID_PROPERTYNAME, + FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME, + FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME, + FilesPlugin::CREATIONDATE_PROPERTYNAME, + ], + 0 + ); + + $user = $this->createMock(User::class); + $user + ->expects($this->once()) + ->method('getUID') + ->willReturn('foo'); + $user + ->expects($this->once()) + ->method('getDisplayName') + ->willReturn('M. Foo'); + + $owner = $this->createMock(Account::class); + $this->accountManager->expects($this->once()) + ->method('getAccount') + ->with($user) + ->willReturn($owner); + + $node->expects($this->once()) + ->method('getDirectDownload') + ->willReturn(['url' => 'http://example.com/']); + $node->expects($this->exactly(2)) + ->method('getOwner') + ->willReturn($user); + + $displayNameProp = $this->createMock(AccountProperty::class); + $owner + ->expects($this->once()) + ->method('getProperty') + ->with(IAccountManager::PROPERTY_DISPLAYNAME) + ->willReturn($displayNameProp); + $displayNameProp + ->expects($this->once()) + ->method('getScope') + ->willReturn(IAccountManager::SCOPE_PUBLISHED); + + $this->plugin->handleGetProperties( + $propFind, + $node + ); + + $this->assertEquals('"abc"', $propFind->get(FilesPlugin::GETETAG_PROPERTYNAME)); + $this->assertEquals('00000123instanceid', $propFind->get(FilesPlugin::FILEID_PROPERTYNAME)); + $this->assertEquals('123', $propFind->get(FilesPlugin::INTERNAL_FILEID_PROPERTYNAME)); + $this->assertEquals('1973-11-29T21:33:09+00:00', $propFind->get(FilesPlugin::CREATIONDATE_PROPERTYNAME)); + $this->assertEquals(0, $propFind->get(FilesPlugin::SIZE_PROPERTYNAME)); + $this->assertEquals('DWCKMSR', $propFind->get(FilesPlugin::PERMISSIONS_PROPERTYNAME)); + $this->assertEquals('http://example.com/', $propFind->get(FilesPlugin::DOWNLOADURL_PROPERTYNAME)); + $this->assertEquals('foo', $propFind->get(FilesPlugin::OWNER_ID_PROPERTYNAME)); + $this->assertEquals('M. Foo', $propFind->get(FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME)); + $this->assertEquals('my_fingerprint', $propFind->get(FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME)); + $this->assertEquals([], $propFind->get404Properties()); + } + + public function testGetDisplayNamePropertyWhenNotPublished(): void { + $node = $this->createTestNode(File::class); + $propFind = new PropFind( + '/dummyPath', + [ + FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME, + ], + 0 + ); + + $this->userSession->expects($this->once()) + ->method('getUser') + ->willReturn(null); + + $user = $this->createMock(User::class); + + $user->expects($this->never()) + ->method('getDisplayName'); + + $owner = $this->createMock(Account::class); + $this->accountManager->expects($this->once()) + ->method('getAccount') + ->with($user) + ->willReturn($owner); + + $node->expects($this->once()) + ->method('getOwner') + ->willReturn($user); + + $displayNameProp = $this->createMock(AccountProperty::class); + $owner + ->expects($this->once()) + ->method('getProperty') + ->with(IAccountManager::PROPERTY_DISPLAYNAME) + ->willReturn($displayNameProp); + $displayNameProp + ->expects($this->once()) + ->method('getScope') + ->willReturn(IAccountManager::SCOPE_PRIVATE); + + $this->plugin->handleGetProperties( + $propFind, + $node + ); + + $this->assertEquals(null, $propFind->get(FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME)); + } + + public function testGetDisplayNamePropertyWhenNotPublishedButLoggedIn(): void { + $node = $this->createTestNode(File::class); + + $propFind = new PropFind( + '/dummyPath', + [ + FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME, + ], + 0 + ); + + $user = $this->createMock(User::class); + + $node->expects($this->once()) + ->method('getOwner') + ->willReturn($user); + + $loggedInUser = $this->createMock(User::class); + $this->userSession->expects($this->once()) + ->method('getUser') + ->willReturn($loggedInUser); + + $user + ->expects($this->once()) + ->method('getDisplayName') + ->willReturn('M. Foo'); + + $this->accountManager->expects($this->never()) + ->method('getAccount'); + + $this->plugin->handleGetProperties( + $propFind, + $node + ); + + $this->assertEquals('M. Foo', $propFind->get(FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME)); + } + + public function testGetPropertiesStorageNotAvailable(): void { + /** @var File&MockObject $node */ + $node = $this->createTestNode(File::class); + + $propFind = new PropFind( + '/dummyPath', + [ + FilesPlugin::DOWNLOADURL_PROPERTYNAME, + ], + 0 + ); + + $node->expects($this->once()) + ->method('getDirectDownload') + ->willThrowException(new StorageNotAvailableException()); + + $this->plugin->handleGetProperties( + $propFind, + $node + ); + + $this->assertEquals(null, $propFind->get(FilesPlugin::DOWNLOADURL_PROPERTYNAME)); + } + + public function testGetPublicPermissions(): void { + /** @var IRequest&MockObject */ + $request = $this->createMock(IRequest::class); + $this->plugin = new FilesPlugin( + $this->tree, + $this->config, + $request, + $this->previewManager, + $this->userSession, + $this->filenameValidator, + $this->accountManager, + true, + ); + $this->plugin->initialize($this->server); + + $propFind = new PropFind( + '/dummyPath', + [ + FilesPlugin::PERMISSIONS_PROPERTYNAME, + ], + 0 + ); + + /** @var File&MockObject $node */ + $node = $this->createTestNode(File::class); + $node->expects($this->any()) + ->method('getDavPermissions') + ->willReturn('DWCKMSR'); + + $this->plugin->handleGetProperties( + $propFind, + $node + ); + + $this->assertEquals('DWCKR', $propFind->get(FilesPlugin::PERMISSIONS_PROPERTYNAME)); + } + + public function testGetPropertiesForDirectory(): void { + /** @var Directory&MockObject $node */ + $node = $this->createTestNode(Directory::class); + + $propFind = new PropFind( + '/dummyPath', + [ + FilesPlugin::GETETAG_PROPERTYNAME, + FilesPlugin::FILEID_PROPERTYNAME, + FilesPlugin::SIZE_PROPERTYNAME, + FilesPlugin::PERMISSIONS_PROPERTYNAME, + FilesPlugin::DOWNLOADURL_PROPERTYNAME, + FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME, + ], + 0 + ); + + $node->expects($this->once()) + ->method('getSize') + ->willReturn(1025); + + $this->plugin->handleGetProperties( + $propFind, + $node + ); + + $this->assertEquals('"abc"', $propFind->get(FilesPlugin::GETETAG_PROPERTYNAME)); + $this->assertEquals('00000123instanceid', $propFind->get(FilesPlugin::FILEID_PROPERTYNAME)); + $this->assertEquals(1025, $propFind->get(FilesPlugin::SIZE_PROPERTYNAME)); + $this->assertEquals('DWCKMSR', $propFind->get(FilesPlugin::PERMISSIONS_PROPERTYNAME)); + $this->assertEquals(null, $propFind->get(FilesPlugin::DOWNLOADURL_PROPERTYNAME)); + $this->assertEquals('my_fingerprint', $propFind->get(FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME)); + $this->assertEquals([FilesPlugin::DOWNLOADURL_PROPERTYNAME], $propFind->get404Properties()); + } + + public function testGetPropertiesForRootDirectory(): void { + /** @var Directory&MockObject $node */ + $node = $this->createMock(Directory::class); + $node->expects($this->any())->method('getPath')->willReturn('/'); + + $fileInfo = $this->createMock(FileInfo::class); + $fileInfo->expects($this->any()) + ->method('isReadable') + ->willReturn(true); + + $node->expects($this->any()) + ->method('getFileInfo') + ->willReturn($fileInfo); + + $propFind = new PropFind( + '/', + [ + FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME, + ], + 0 + ); + + $this->plugin->handleGetProperties( + $propFind, + $node + ); + + $this->assertEquals('my_fingerprint', $propFind->get(FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME)); + } + + public function testGetPropertiesWhenNoPermission(): void { + // No read permissions can be caused by files access control. + // But we still want to load the directory list, so this is okay for us. + // $this->expectException(\Sabre\DAV\Exception\NotFound::class); + /** @var Directory&MockObject $node */ + $node = $this->createMock(Directory::class); + $node->expects($this->any())->method('getPath')->willReturn('/'); + + $fileInfo = $this->createMock(FileInfo::class); + $fileInfo->expects($this->any()) + ->method('isReadable') + ->willReturn(false); + + $node->expects($this->any()) + ->method('getFileInfo') + ->willReturn($fileInfo); + + $propFind = new PropFind( + '/test', + [ + FilesPlugin::DATA_FINGERPRINT_PROPERTYNAME, + ], + 0 + ); + + $this->plugin->handleGetProperties( + $propFind, + $node + ); + + $this->addToAssertionCount(1); + } + + public function testUpdateProps(): void { + $node = $this->createTestNode(File::class); + + $testDate = 'Fri, 13 Feb 2015 00:01:02 GMT'; + $testCreationDate = '2007-08-31T16:47+00:00'; + + $node->expects($this->once()) + ->method('touch') + ->with($testDate); + + $node->expects($this->once()) + ->method('setEtag') + ->with('newetag') + ->willReturn(true); + + $node->expects($this->once()) + ->method('setCreationTime') + ->with('1188578820'); + + // properties to set + $propPatch = new PropPatch([ + FilesPlugin::GETETAG_PROPERTYNAME => 'newetag', + FilesPlugin::LASTMODIFIED_PROPERTYNAME => $testDate, + FilesPlugin::CREATIONDATE_PROPERTYNAME => $testCreationDate, + ]); + + + $this->plugin->handleUpdateProperties( + '/dummypath', + $propPatch + ); + + $propPatch->commit(); + + $this->assertEmpty($propPatch->getRemainingMutations()); + + $result = $propPatch->getResult(); + $this->assertEquals(200, $result[FilesPlugin::LASTMODIFIED_PROPERTYNAME]); + $this->assertEquals(200, $result[FilesPlugin::GETETAG_PROPERTYNAME]); + $this->assertEquals(200, $result[FilesPlugin::CREATIONDATE_PROPERTYNAME]); + } + + public function testUpdatePropsForbidden(): void { + $propPatch = new PropPatch([ + FilesPlugin::OWNER_ID_PROPERTYNAME => 'user2', + FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME => 'User Two', + FilesPlugin::FILEID_PROPERTYNAME => 12345, + FilesPlugin::PERMISSIONS_PROPERTYNAME => 'C', + FilesPlugin::SIZE_PROPERTYNAME => 123, + FilesPlugin::DOWNLOADURL_PROPERTYNAME => 'http://example.com/', + ]); + + $this->plugin->handleUpdateProperties( + '/dummypath', + $propPatch + ); + + $propPatch->commit(); + + $this->assertEmpty($propPatch->getRemainingMutations()); + + $result = $propPatch->getResult(); + $this->assertEquals(403, $result[FilesPlugin::OWNER_ID_PROPERTYNAME]); + $this->assertEquals(403, $result[FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME]); + $this->assertEquals(403, $result[FilesPlugin::FILEID_PROPERTYNAME]); + $this->assertEquals(403, $result[FilesPlugin::PERMISSIONS_PROPERTYNAME]); + $this->assertEquals(403, $result[FilesPlugin::SIZE_PROPERTYNAME]); + $this->assertEquals(403, $result[FilesPlugin::DOWNLOADURL_PROPERTYNAME]); + } + + /** + * Test case from https://github.com/owncloud/core/issues/5251 + * + * |-FolderA + * |-text.txt + * |-test.txt + * + * FolderA is an incoming shared folder and there are no delete permissions. + * Thus moving /FolderA/test.txt to /test.txt should fail already on that check + * + */ + public function testMoveSrcNotDeletable(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + $this->expectExceptionMessage('FolderA/test.txt cannot be deleted'); + + $fileInfoFolderATestTXT = $this->createMock(FileInfo::class); + $fileInfoFolderATestTXT->expects($this->once()) + ->method('isDeletable') + ->willReturn(false); + + $node = $this->createMock(Node::class); + $node->expects($this->atLeastOnce()) + ->method('getFileInfo') + ->willReturn($fileInfoFolderATestTXT); + + $this->tree->expects($this->atLeastOnce()) + ->method('getNodeForPath') + ->willReturn($node); + + $this->plugin->checkMove('FolderA/test.txt', 'test.txt'); + } + + public function testMoveSrcDeletable(): void { + $fileInfoFolderATestTXT = $this->createMock(FileInfo::class); + $fileInfoFolderATestTXT->expects($this->once()) + ->method('isDeletable') + ->willReturn(true); + + $node = $this->createMock(Node::class); + $node->expects($this->atLeastOnce()) + ->method('getFileInfo') + ->willReturn($fileInfoFolderATestTXT); + + $this->tree->expects($this->atLeastOnce()) + ->method('getNodeForPath') + ->willReturn($node); + + $this->plugin->checkMove('FolderA/test.txt', 'test.txt'); + } + + public function testMoveSrcNotExist(): void { + $this->expectException(\Sabre\DAV\Exception\NotFound::class); + $this->expectExceptionMessage('FolderA/test.txt does not exist'); + + $node = $this->createMock(Node::class); + $node->expects($this->atLeastOnce()) + ->method('getFileInfo') + ->willReturn(null); + + $this->tree->expects($this->atLeastOnce()) + ->method('getNodeForPath') + ->willReturn($node); + + $this->plugin->checkMove('FolderA/test.txt', 'test.txt'); + } + + public function testMoveDestinationInvalid(): void { + $this->expectException(InvalidPath::class); + $this->expectExceptionMessage('Mocked exception'); + + $fileInfoFolderATestTXT = $this->createMock(FileInfo::class); + $fileInfoFolderATestTXT->expects(self::any()) + ->method('isDeletable') + ->willReturn(true); + + $node = $this->createMock(Node::class); + $node->expects($this->atLeastOnce()) + ->method('getFileInfo') + ->willReturn($fileInfoFolderATestTXT); + + $this->tree->expects($this->atLeastOnce()) + ->method('getNodeForPath') + ->willReturn($node); + + $this->filenameValidator->expects(self::once()) + ->method('validateFilename') + ->with('invalid\\path.txt') + ->willThrowException(new InvalidPathException('Mocked exception')); + + $this->plugin->checkMove('FolderA/test.txt', 'invalid\\path.txt'); + } + + public function testCopySrcNotExist(): void { + $this->expectException(\Sabre\DAV\Exception\NotFound::class); + $this->expectExceptionMessage('FolderA/test.txt does not exist'); + + $node = $this->createMock(Node::class); + $node->expects($this->atLeastOnce()) + ->method('getFileInfo') + ->willReturn(null); + + $this->tree->expects($this->atLeastOnce()) + ->method('getNodeForPath') + ->willReturn($node); + + $this->plugin->checkCopy('FolderA/test.txt', 'test.txt'); + } + + public function testCopyDestinationInvalid(): void { + $this->expectException(InvalidPath::class); + $this->expectExceptionMessage('Mocked exception'); + + $fileInfoFolderATestTXT = $this->createMock(FileInfo::class); + $node = $this->createMock(Node::class); + $node->expects($this->atLeastOnce()) + ->method('getFileInfo') + ->willReturn($fileInfoFolderATestTXT); + + $this->tree->expects($this->atLeastOnce()) + ->method('getNodeForPath') + ->willReturn($node); + + $this->filenameValidator->expects(self::once()) + ->method('validateFilename') + ->with('invalid\\path.txt') + ->willThrowException(new InvalidPathException('Mocked exception')); + + $this->plugin->checkCopy('FolderA/test.txt', 'invalid\\path.txt'); + } + + public static function downloadHeadersProvider(): array { + return [ + [ + false, + 'attachment; filename*=UTF-8\'\'somefile.xml; filename="somefile.xml"' + ], + [ + true, + 'attachment; filename="somefile.xml"' + ], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('downloadHeadersProvider')] + public function testDownloadHeaders(bool $isClumsyAgent, string $contentDispositionHeader): void { + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $request + ->expects($this->once()) + ->method('getPath') + ->willReturn('test/somefile.xml'); + + $node = $this->createMock(File::class); + $node + ->expects($this->once()) + ->method('getName') + ->willReturn('somefile.xml'); + + $this->tree + ->expects($this->once()) + ->method('getNodeForPath') + ->with('test/somefile.xml') + ->willReturn($node); + + $this->request + ->expects($this->once()) + ->method('isUserAgent') + ->willReturn($isClumsyAgent); + + $calls = [ + ['Content-Disposition', $contentDispositionHeader], + ['X-Accel-Buffering', 'no'], + ]; + $response + ->expects($this->exactly(count($calls))) + ->method('addHeader') + ->willReturnCallback(function () use (&$calls): void { + $expected = array_shift($calls); + $this->assertSame($expected, func_get_args()); + }); + + $this->plugin->httpGet($request, $response); + } + + public function testHasPreview(): void { + /** @var Directory&MockObject $node */ + $node = $this->createTestNode(Directory::class); + + $propFind = new PropFind( + '/dummyPath', + [ + FilesPlugin::HAS_PREVIEW_PROPERTYNAME + ], + 0 + ); + + $this->previewManager->expects($this->once()) + ->method('isAvailable') + ->willReturn(false); + + $this->plugin->handleGetProperties( + $propFind, + $node + ); + + $this->assertEquals('false', $propFind->get(FilesPlugin::HAS_PREVIEW_PROPERTYNAME)); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/FilesReportPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/FilesReportPluginTest.php new file mode 100644 index 00000000000..176949f999c --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/FilesReportPluginTest.php @@ -0,0 +1,853 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre; + +use OC\Files\View; +use OCA\DAV\Connector\Sabre\Directory; +use OCA\DAV\Connector\Sabre\FilesPlugin; +use OCA\DAV\Connector\Sabre\FilesReportPlugin as FilesReportPluginImplementation; +use OCP\Accounts\IAccountManager; +use OCP\App\IAppManager; +use OCP\Files\File; +use OCP\Files\FileInfo; +use OCP\Files\Folder; +use OCP\Files\IFilenameValidator; +use OCP\IConfig; +use OCP\IGroupManager; +use OCP\IPreview; +use OCP\IRequest; +use OCP\ITagManager; +use OCP\ITags; +use OCP\IUser; +use OCP\IUserSession; +use OCP\SystemTag\ISystemTag; +use OCP\SystemTag\ISystemTagManager; +use OCP\SystemTag\ISystemTagObjectMapper; +use OCP\SystemTag\TagNotFoundException; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\INode; +use Sabre\DAV\Server; +use Sabre\DAV\Tree; +use Sabre\HTTP\ResponseInterface; + +class FilesReportPluginTest extends \Test\TestCase { + + private \Sabre\DAV\Server&MockObject $server; + private Tree&MockObject $tree; + private ISystemTagObjectMapper&MockObject $tagMapper; + private ISystemTagManager&MockObject $tagManager; + private ITags&MockObject $privateTags; + private ITagManager&MockObject $privateTagManager; + private IUserSession&MockObject $userSession; + private FilesReportPluginImplementation $plugin; + private View&MockObject $view; + private IGroupManager&MockObject $groupManager; + private Folder&MockObject $userFolder; + private IPreview&MockObject $previewManager; + private IAppManager&MockObject $appManager; + + protected function setUp(): void { + parent::setUp(); + + $this->tree = $this->createMock(Tree::class); + $this->view = $this->createMock(View::class); + + $this->server = $this->getMockBuilder(Server::class) + ->setConstructorArgs([$this->tree]) + ->onlyMethods(['getRequestUri', 'getBaseUri']) + ->getMock(); + + $this->server->expects($this->any()) + ->method('getBaseUri') + ->willReturn('http://example.com/owncloud/remote.php/dav'); + + $this->groupManager = $this->createMock(IGroupManager::class); + $this->userFolder = $this->createMock(Folder::class); + $this->previewManager = $this->createMock(IPreview::class); + $this->appManager = $this->createMock(IAppManager::class); + $this->tagManager = $this->createMock(ISystemTagManager::class); + $this->tagMapper = $this->createMock(ISystemTagObjectMapper::class); + $this->userSession = $this->createMock(IUserSession::class); + $this->privateTags = $this->createMock(ITags::class); + $this->privateTagManager = $this->createMock(ITagManager::class); + $this->privateTagManager->expects($this->any()) + ->method('load') + ->with('files') + ->willReturn($this->privateTags); + + $user = $this->createMock(IUser::class); + $user->expects($this->any()) + ->method('getUID') + ->willReturn('testuser'); + $this->userSession->expects($this->any()) + ->method('getUser') + ->willReturn($user); + + $this->plugin = new FilesReportPluginImplementation( + $this->tree, + $this->view, + $this->tagManager, + $this->tagMapper, + $this->privateTagManager, + $this->userSession, + $this->groupManager, + $this->userFolder, + $this->appManager + ); + } + + public function testOnReportInvalidNode(): void { + $path = 'totally/unrelated/13'; + + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->with('/' . $path) + ->willReturn($this->createMock(INode::class)); + + $this->server->expects($this->any()) + ->method('getRequestUri') + ->willReturn($path); + $this->plugin->initialize($this->server); + + $this->assertNull($this->plugin->onReport(FilesReportPluginImplementation::REPORT_NAME, [], '/' . $path)); + } + + public function testOnReportInvalidReportName(): void { + $path = 'test'; + + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->with('/' . $path) + ->willReturn( + $this->getMockBuilder(INode::class) + ->disableOriginalConstructor() + ->getMock() + ); + + $this->server->expects($this->any()) + ->method('getRequestUri') + ->willReturn($path); + $this->plugin->initialize($this->server); + + $this->assertNull($this->plugin->onReport('{whoever}whatever', [], '/' . $path)); + } + + public function testOnReport(): void { + $path = 'test'; + + $parameters = [ + [ + 'name' => '{DAV:}prop', + 'value' => [ + ['name' => '{DAV:}getcontentlength', 'value' => ''], + ['name' => '{http://owncloud.org/ns}size', 'value' => ''], + ], + ], + [ + 'name' => '{http://owncloud.org/ns}filter-rules', + 'value' => [ + ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'], + ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'], + ], + ], + ]; + + $this->groupManager->expects($this->any()) + ->method('isAdmin') + ->willReturn(true); + + $reportTargetNode = $this->createMock(Directory::class); + $reportTargetNode->expects($this->any()) + ->method('getPath') + ->willReturn(''); + + $response = $this->createMock(ResponseInterface::class); + + $response->expects($this->once()) + ->method('setHeader') + ->with('Content-Type', 'application/xml; charset=utf-8'); + + $response->expects($this->once()) + ->method('setStatus') + ->with(207); + + $response->expects($this->once()) + ->method('setBody'); + + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->with('/' . $path) + ->willReturn($reportTargetNode); + + $filesNode1 = $this->createMock(File::class); + $filesNode1->expects($this->any()) + ->method('getSize') + ->willReturn(12); + $filesNode2 = $this->createMock(Folder::class); + $filesNode2->expects($this->any()) + ->method('getSize') + ->willReturn(10); + + $tag123 = $this->createMock(ISystemTag::class); + $tag123->expects($this->any()) + ->method('getName') + ->willReturn('OneTwoThree'); + $tag123->expects($this->any()) + ->method('isUserVisible') + ->willReturn(true); + $tag456 = $this->createMock(ISystemTag::class); + $tag456->expects($this->any()) + ->method('getName') + ->willReturn('FourFiveSix'); + $tag456->expects($this->any()) + ->method('isUserVisible') + ->willReturn(true); + + $this->tagManager->expects($this->once()) + ->method('getTagsByIds') + ->with(['123', '456']) + ->willReturn([$tag123, $tag456]); + + $this->userFolder->expects($this->exactly(2)) + ->method('searchBySystemTag') + ->willReturnMap([ + ['OneTwoThree', 'testuser', 0, 0, [$filesNode1]], + ['FourFiveSix', 'testuser', 0, 0, [$filesNode2]], + ]); + + $this->server->expects($this->any()) + ->method('getRequestUri') + ->willReturn($path); + $this->server->httpResponse = $response; + $this->plugin->initialize($this->server); + + $this->assertFalse($this->plugin->onReport(FilesReportPluginImplementation::REPORT_NAME, $parameters, '/' . $path)); + } + + public function testFindNodesByFileIdsRoot(): void { + $filesNode1 = $this->createMock(Folder::class); + $filesNode1->expects($this->once()) + ->method('getName') + ->willReturn('first node'); + + $filesNode2 = $this->createMock(File::class); + $filesNode2->expects($this->once()) + ->method('getName') + ->willReturn('second node'); + + $reportTargetNode = $this->createMock(Directory::class); + $reportTargetNode->expects($this->any()) + ->method('getPath') + ->willReturn('/'); + + $this->userFolder->expects($this->exactly(2)) + ->method('getFirstNodeById') + ->willReturnMap([ + [111, $filesNode1], + [222, $filesNode2], + ]); + + /** @var Directory&MockObject $reportTargetNode */ + $result = $this->plugin->findNodesByFileIds($reportTargetNode, ['111', '222']); + + $this->assertCount(2, $result); + $this->assertInstanceOf(Directory::class, $result[0]); + $this->assertEquals('first node', $result[0]->getName()); + $this->assertInstanceOf(\OCA\DAV\Connector\Sabre\File::class, $result[1]); + $this->assertEquals('second node', $result[1]->getName()); + } + + public function testFindNodesByFileIdsSubDir(): void { + $filesNode1 = $this->createMock(Folder::class); + $filesNode1->expects($this->once()) + ->method('getName') + ->willReturn('first node'); + + $filesNode2 = $this->createMock(File::class); + $filesNode2->expects($this->once()) + ->method('getName') + ->willReturn('second node'); + + $reportTargetNode = $this->createMock(Directory::class); + $reportTargetNode->expects($this->any()) + ->method('getPath') + ->willReturn('/sub1/sub2'); + + + $subNode = $this->createMock(Folder::class); + + $this->userFolder->expects($this->once()) + ->method('get') + ->with('/sub1/sub2') + ->willReturn($subNode); + + $subNode->expects($this->exactly(2)) + ->method('getFirstNodeById') + ->willReturnMap([ + [111, $filesNode1], + [222, $filesNode2], + ]); + + /** @var Directory&MockObject $reportTargetNode */ + $result = $this->plugin->findNodesByFileIds($reportTargetNode, ['111', '222']); + + $this->assertCount(2, $result); + $this->assertInstanceOf(Directory::class, $result[0]); + $this->assertEquals('first node', $result[0]->getName()); + $this->assertInstanceOf(\OCA\DAV\Connector\Sabre\File::class, $result[1]); + $this->assertEquals('second node', $result[1]->getName()); + } + + public function testPrepareResponses(): void { + $requestedProps = ['{DAV:}getcontentlength', '{http://owncloud.org/ns}fileid', '{DAV:}resourcetype']; + + $fileInfo = $this->createMock(FileInfo::class); + $fileInfo->method('isReadable')->willReturn(true); + + $node1 = $this->createMock(Directory::class); + $node2 = $this->createMock(\OCA\DAV\Connector\Sabre\File::class); + + $node1->expects($this->once()) + ->method('getInternalFileId') + ->willReturn('111'); + $node1->expects($this->any()) + ->method('getPath') + ->willReturn('/node1'); + $node1->method('getFileInfo')->willReturn($fileInfo); + $node2->expects($this->once()) + ->method('getInternalFileId') + ->willReturn('222'); + $node2->expects($this->once()) + ->method('getSize') + ->willReturn(1024); + $node2->expects($this->any()) + ->method('getPath') + ->willReturn('/sub/node2'); + $node2->method('getFileInfo')->willReturn($fileInfo); + + $config = $this->createMock(IConfig::class); + $validator = $this->createMock(IFilenameValidator::class); + $accountManager = $this->createMock(IAccountManager::class); + + $this->server->addPlugin( + new FilesPlugin( + $this->tree, + $config, + $this->createMock(IRequest::class), + $this->previewManager, + $this->createMock(IUserSession::class), + $validator, + $accountManager, + ) + ); + $this->plugin->initialize($this->server); + $responses = $this->plugin->prepareResponses('/files/username', $requestedProps, [$node1, $node2]); + + $this->assertCount(2, $responses); + + $this->assertEquals('http://example.com/owncloud/remote.php/dav/files/username/node1', $responses[0]->getHref()); + $this->assertEquals('http://example.com/owncloud/remote.php/dav/files/username/sub/node2', $responses[1]->getHref()); + + $props1 = $responses[0]->getResponseProperties(); + $this->assertEquals('111', $props1[200]['{http://owncloud.org/ns}fileid']); + $this->assertNull($props1[404]['{DAV:}getcontentlength']); + $this->assertInstanceOf('\Sabre\DAV\Xml\Property\ResourceType', $props1[200]['{DAV:}resourcetype']); + $resourceType1 = $props1[200]['{DAV:}resourcetype']->getValue(); + $this->assertEquals('{DAV:}collection', $resourceType1[0]); + + $props2 = $responses[1]->getResponseProperties(); + $this->assertEquals('1024', $props2[200]['{DAV:}getcontentlength']); + $this->assertEquals('222', $props2[200]['{http://owncloud.org/ns}fileid']); + $this->assertInstanceOf('\Sabre\DAV\Xml\Property\ResourceType', $props2[200]['{DAV:}resourcetype']); + $this->assertCount(0, $props2[200]['{DAV:}resourcetype']->getValue()); + } + + public function testProcessFilterRulesSingle(): void { + $this->groupManager->expects($this->any()) + ->method('isAdmin') + ->willReturn(true); + + $rules = [ + ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'], + ]; + + $filesNode1 = $this->createMock(File::class); + $filesNode1->expects($this->any()) + ->method('getSize') + ->willReturn(12); + $filesNode2 = $this->createMock(Folder::class); + $filesNode2->expects($this->any()) + ->method('getSize') + ->willReturn(10); + + $tag123 = $this->createMock(ISystemTag::class); + $tag123->expects($this->any()) + ->method('getName') + ->willReturn('OneTwoThree'); + $tag123->expects($this->any()) + ->method('isUserVisible') + ->willReturn(true); + + $this->tagManager->expects($this->once()) + ->method('getTagsByIds') + ->with(['123']) + ->willReturn([$tag123]); + + $this->userFolder->expects($this->once()) + ->method('searchBySystemTag') + ->with('OneTwoThree') + ->willReturn([$filesNode1, $filesNode2]); + + $this->assertEquals([$filesNode1, $filesNode2], self::invokePrivate($this->plugin, 'processFilterRulesForFileNodes', [$rules, 0, 0])); + } + + public function testProcessFilterRulesAndCondition(): void { + $this->groupManager->expects($this->any()) + ->method('isAdmin') + ->willReturn(true); + + $filesNode1 = $this->createMock(File::class); + $filesNode1->expects($this->any()) + ->method('getSize') + ->willReturn(12); + $filesNode1->expects($this->any()) + ->method('getId') + ->willReturn(111); + $filesNode2 = $this->createMock(Folder::class); + $filesNode2->expects($this->any()) + ->method('getSize') + ->willReturn(10); + $filesNode2->expects($this->any()) + ->method('getId') + ->willReturn(222); + $filesNode3 = $this->createMock(File::class); + $filesNode3->expects($this->any()) + ->method('getSize') + ->willReturn(14); + $filesNode3->expects($this->any()) + ->method('getId') + ->willReturn(333); + + $tag123 = $this->createMock(ISystemTag::class); + $tag123->expects($this->any()) + ->method('getName') + ->willReturn('OneTwoThree'); + $tag123->expects($this->any()) + ->method('isUserVisible') + ->willReturn(true); + $tag456 = $this->createMock(ISystemTag::class); + $tag456->expects($this->any()) + ->method('getName') + ->willReturn('FourFiveSix'); + $tag456->expects($this->any()) + ->method('isUserVisible') + ->willReturn(true); + + $this->tagManager->expects($this->once()) + ->method('getTagsByIds') + ->with(['123', '456']) + ->willReturn([$tag123, $tag456]); + + $this->userFolder->expects($this->exactly(2)) + ->method('searchBySystemTag') + ->willReturnMap([ + ['OneTwoThree', 'testuser', 0, 0, [$filesNode1, $filesNode2]], + ['FourFiveSix', 'testuser', 0, 0, [$filesNode2, $filesNode3]], + ]); + + $rules = [ + ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'], + ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'], + ]; + + $this->assertEquals([$filesNode2], array_values(self::invokePrivate($this->plugin, 'processFilterRulesForFileNodes', [$rules, null, null]))); + } + + public function testProcessFilterRulesAndConditionWithOneEmptyResult(): void { + $this->groupManager->expects($this->any()) + ->method('isAdmin') + ->willReturn(true); + + $filesNode1 = $this->createMock(File::class); + $filesNode1->expects($this->any()) + ->method('getSize') + ->willReturn(12); + $filesNode1->expects($this->any()) + ->method('getId') + ->willReturn(111); + $filesNode2 = $this->createMock(Folder::class); + $filesNode2->expects($this->any()) + ->method('getSize') + ->willReturn(10); + $filesNode2->expects($this->any()) + ->method('getId') + ->willReturn(222); + + $tag123 = $this->createMock(ISystemTag::class); + $tag123->expects($this->any()) + ->method('getName') + ->willReturn('OneTwoThree'); + $tag123->expects($this->any()) + ->method('isUserVisible') + ->willReturn(true); + $tag456 = $this->createMock(ISystemTag::class); + $tag456->expects($this->any()) + ->method('getName') + ->willReturn('FourFiveSix'); + $tag456->expects($this->any()) + ->method('isUserVisible') + ->willReturn(true); + + $this->tagManager->expects($this->once()) + ->method('getTagsByIds') + ->with(['123', '456']) + ->willReturn([$tag123, $tag456]); + + $this->userFolder->expects($this->exactly(2)) + ->method('searchBySystemTag') + ->willReturnMap([ + ['OneTwoThree', 'testuser', 0, 0, [$filesNode1, $filesNode2]], + ['FourFiveSix', 'testuser', 0, 0, []], + ]); + + $rules = [ + ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'], + ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'], + ]; + + $this->assertEquals([], self::invokePrivate($this->plugin, 'processFilterRulesForFileNodes', [$rules, null, null])); + } + + public function testProcessFilterRulesAndConditionWithFirstEmptyResult(): void { + $this->groupManager->expects($this->any()) + ->method('isAdmin') + ->willReturn(true); + + $filesNode1 = $this->createMock(File::class); + $filesNode1->expects($this->any()) + ->method('getSize') + ->willReturn(12); + $filesNode1->expects($this->any()) + ->method('getId') + ->willReturn(111); + $filesNode2 = $this->createMock(Folder::class); + $filesNode2->expects($this->any()) + ->method('getSize') + ->willReturn(10); + $filesNode2->expects($this->any()) + ->method('getId') + ->willReturn(222); + + $tag123 = $this->createMock(ISystemTag::class); + $tag123->expects($this->any()) + ->method('getName') + ->willReturn('OneTwoThree'); + $tag123->expects($this->any()) + ->method('isUserVisible') + ->willReturn(true); + $tag456 = $this->createMock(ISystemTag::class); + $tag456->expects($this->any()) + ->method('getName') + ->willReturn('FourFiveSix'); + $tag456->expects($this->any()) + ->method('isUserVisible') + ->willReturn(true); + + $this->tagManager->expects($this->once()) + ->method('getTagsByIds') + ->with(['123', '456']) + ->willReturn([$tag123, $tag456]); + + $this->userFolder->expects($this->once()) + ->method('searchBySystemTag') + ->willReturnMap([ + ['OneTwoThree', 'testuser', 0, 0, []], + ]); + + $rules = [ + ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'], + ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'], + ]; + + $this->assertEquals([], self::invokePrivate($this->plugin, 'processFilterRulesForFileNodes', [$rules, null, null])); + } + + public function testProcessFilterRulesAndConditionWithEmptyMidResult(): void { + $this->groupManager->expects($this->any()) + ->method('isAdmin') + ->willReturn(true); + + $filesNode1 = $this->createMock(File::class); + $filesNode1->expects($this->any()) + ->method('getSize') + ->willReturn(12); + $filesNode1->expects($this->any()) + ->method('getId') + ->willReturn(111); + $filesNode2 = $this->createMock(Folder::class); + $filesNode2->expects($this->any()) + ->method('getSize') + ->willReturn(10); + $filesNode2->expects($this->any()) + ->method('getId') + ->willReturn(222); + $filesNode3 = $this->createMock(Folder::class); + $filesNode3->expects($this->any()) + ->method('getSize') + ->willReturn(13); + $filesNode3->expects($this->any()) + ->method('getId') + ->willReturn(333); + + $tag123 = $this->createMock(ISystemTag::class); + $tag123->expects($this->any()) + ->method('getName') + ->willReturn('OneTwoThree'); + $tag123->expects($this->any()) + ->method('isUserVisible') + ->willReturn(true); + $tag456 = $this->createMock(ISystemTag::class); + $tag456->expects($this->any()) + ->method('getName') + ->willReturn('FourFiveSix'); + $tag456->expects($this->any()) + ->method('isUserVisible') + ->willReturn(true); + $tag789 = $this->createMock(ISystemTag::class); + $tag789->expects($this->any()) + ->method('getName') + ->willReturn('SevenEightNine'); + $tag789->expects($this->any()) + ->method('isUserVisible') + ->willReturn(true); + + $this->tagManager->expects($this->once()) + ->method('getTagsByIds') + ->with(['123', '456', '789']) + ->willReturn([$tag123, $tag456, $tag789]); + + $this->userFolder->expects($this->exactly(2)) + ->method('searchBySystemTag') + ->willReturnMap([ + ['OneTwoThree', 'testuser', 0, 0, [$filesNode1, $filesNode2]], + ['FourFiveSix', 'testuser', 0, 0, [$filesNode3]], + ]); + + $rules = [ + ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'], + ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'], + ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '789'], + ]; + + $this->assertEquals([], array_values(self::invokePrivate($this->plugin, 'processFilterRulesForFileNodes', [$rules, null, null]))); + } + + public function testProcessFilterRulesInvisibleTagAsAdmin(): void { + $this->groupManager->expects($this->any()) + ->method('isAdmin') + ->willReturn(true); + + $filesNode1 = $this->createMock(File::class); + $filesNode1->expects($this->any()) + ->method('getSize') + ->willReturn(12); + $filesNode1->expects($this->any()) + ->method('getId') + ->willReturn(111); + $filesNode2 = $this->createMock(Folder::class); + $filesNode2->expects($this->any()) + ->method('getSize') + ->willReturn(10); + $filesNode2->expects($this->any()) + ->method('getId') + ->willReturn(222); + $filesNode3 = $this->createMock(Folder::class); + $filesNode3->expects($this->any()) + ->method('getSize') + ->willReturn(13); + $filesNode3->expects($this->any()) + ->method('getId') + ->willReturn(333); + + $tag123 = $this->createMock(ISystemTag::class); + $tag123->expects($this->any()) + ->method('getName') + ->willReturn('OneTwoThree'); + $tag123->expects($this->any()) + ->method('isUserVisible') + ->willReturn(true); + $tag456 = $this->createMock(ISystemTag::class); + $tag456->expects($this->any()) + ->method('getName') + ->willReturn('FourFiveSix'); + $tag456->expects($this->any()) + ->method('isUserVisible') + ->willReturn(false); + + $this->tagManager->expects($this->once()) + ->method('getTagsByIds') + ->with(['123', '456']) + ->willReturn([$tag123, $tag456]); + + $this->userFolder->expects($this->exactly(2)) + ->method('searchBySystemTag') + ->willReturnMap([ + ['OneTwoThree', 'testuser', 0, 0, [$filesNode1, $filesNode2]], + ['FourFiveSix', 'testuser', 0, 0, [$filesNode2, $filesNode3]], + ]); + + $rules = [ + ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'], + ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'], + ]; + + $this->assertEquals([$filesNode2], array_values(self::invokePrivate($this->plugin, 'processFilterRulesForFileNodes', [$rules, null, null]))); + } + + + public function testProcessFilterRulesInvisibleTagAsUser(): void { + $this->expectException(TagNotFoundException::class); + + $this->groupManager->expects($this->any()) + ->method('isAdmin') + ->willReturn(false); + + $tag123 = $this->createMock(ISystemTag::class); + $tag123->expects($this->any()) + ->method('getName') + ->willReturn('OneTwoThree'); + $tag123->expects($this->any()) + ->method('isUserVisible') + ->willReturn(true); + $tag456 = $this->createMock(ISystemTag::class); + $tag456->expects($this->any()) + ->method('getName') + ->willReturn('FourFiveSix'); + $tag456->expects($this->any()) + ->method('isUserVisible') + ->willReturn(false); + + $this->tagManager->expects($this->once()) + ->method('getTagsByIds') + ->with(['123', '456']) + ->willThrowException(new TagNotFoundException()); + + $this->userFolder->expects($this->never()) + ->method('searchBySystemTag'); + + $rules = [ + ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'], + ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'], + ]; + + self::invokePrivate($this->plugin, 'processFilterRulesForFileNodes', [$rules, null, null]); + } + + public function testProcessFilterRulesVisibleTagAsUser(): void { + $this->groupManager->expects($this->any()) + ->method('isAdmin') + ->willReturn(false); + + $tag1 = $this->createMock(ISystemTag::class); + $tag1->expects($this->any()) + ->method('getId') + ->willReturn('123'); + $tag1->expects($this->any()) + ->method('isUserVisible') + ->willReturn(true); + $tag1->expects($this->any()) + ->method('getName') + ->willReturn('OneTwoThree'); + + $tag2 = $this->createMock(ISystemTag::class); + $tag2->expects($this->any()) + ->method('getId') + ->willReturn('123'); + $tag2->expects($this->any()) + ->method('isUserVisible') + ->willReturn(true); + $tag2->expects($this->any()) + ->method('getName') + ->willReturn('FourFiveSix'); + + $this->tagManager->expects($this->once()) + ->method('getTagsByIds') + ->with(['123', '456']) + ->willReturn([$tag1, $tag2]); + + $filesNode1 = $this->createMock(File::class); + $filesNode1->expects($this->any()) + ->method('getId') + ->willReturn(111); + $filesNode1->expects($this->any()) + ->method('getSize') + ->willReturn(12); + $filesNode2 = $this->createMock(Folder::class); + $filesNode2->expects($this->any()) + ->method('getId') + ->willReturn(222); + $filesNode2->expects($this->any()) + ->method('getSize') + ->willReturn(10); + $filesNode3 = $this->createMock(Folder::class); + $filesNode3->expects($this->any()) + ->method('getId') + ->willReturn(333); + $filesNode3->expects($this->any()) + ->method('getSize') + ->willReturn(33); + + $this->tagManager->expects($this->once()) + ->method('getTagsByIds') + ->with(['123', '456']) + ->willReturn([$tag1, $tag2]); + + // main assertion: only user visible tags are being passed through. + $this->userFolder->expects($this->exactly(2)) + ->method('searchBySystemTag') + ->willReturnMap([ + ['OneTwoThree', 'testuser', 0, 0, [$filesNode1, $filesNode2]], + ['FourFiveSix', 'testuser', 0, 0, [$filesNode2, $filesNode3]], + ]); + + $rules = [ + ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'], + ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'], + ]; + + $this->assertEquals([$filesNode2], array_values(self::invokePrivate($this->plugin, 'processFilterRulesForFileNodes', [$rules, null, null]))); + } + + public function testProcessFavoriteFilter(): void { + $rules = [ + ['name' => '{http://owncloud.org/ns}favorite', 'value' => '1'], + ]; + + $this->privateTags->expects($this->once()) + ->method('getFavorites') + ->willReturn(['456', '789']); + + $this->assertEquals(['456', '789'], array_values(self::invokePrivate($this->plugin, 'processFilterRulesForFileIDs', [$rules]))); + } + + public static function filesBaseUriProvider(): array { + return [ + ['', '', ''], + ['files/username', '', '/files/username'], + ['files/username/test', '/test', '/files/username'], + ['files/username/test/sub', '/test/sub', '/files/username'], + ['test', '/test', ''], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('filesBaseUriProvider')] + public function testFilesBaseUri(string $uri, string $reportPath, string $expectedUri): void { + $this->assertEquals($expectedUri, self::invokePrivate($this->plugin, 'getFilesBaseUri', [$uri, $reportPath])); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/MaintenancePluginTest.php b/apps/dav/tests/unit/Connector/Sabre/MaintenancePluginTest.php new file mode 100644 index 00000000000..bc1d50ac41f --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/MaintenancePluginTest.php @@ -0,0 +1,52 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre; + +use OCA\DAV\Connector\Sabre\MaintenancePlugin; +use OCP\IConfig; +use OCP\IL10N; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +/** + * Class MaintenancePluginTest + * + * @package OCA\DAV\Tests\unit\Connector\Sabre + */ +class MaintenancePluginTest extends TestCase { + private IConfig&MockObject $config; + private IL10N&MockObject $l10n; + private MaintenancePlugin $maintenancePlugin; + + protected function setUp(): void { + parent::setUp(); + + $this->config = $this->createMock(IConfig::class); + $this->l10n = $this->createMock(IL10N::class); + $this->maintenancePlugin = new MaintenancePlugin($this->config, $this->l10n); + } + + + public function testMaintenanceMode(): void { + $this->expectException(\Sabre\DAV\Exception\ServiceUnavailable::class); + $this->expectExceptionMessage('System is in maintenance mode.'); + + $this->config + ->expects($this->exactly(1)) + ->method('getSystemValueBool') + ->with('maintenance') + ->willReturn(true); + $this->l10n + ->expects($this->any()) + ->method('t') + ->willReturnArgument(0); + + $this->maintenancePlugin->checkMaintenanceMode(); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/NodeTest.php b/apps/dav/tests/unit/Connector/Sabre/NodeTest.php new file mode 100644 index 00000000000..11970769a1e --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/NodeTest.php @@ -0,0 +1,271 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace OCA\DAV\Tests\unit\Connector\Sabre; + +use OC\Files\FileInfo; +use OC\Files\Mount\MountPoint; +use OC\Files\Node\Folder; +use OC\Files\View; +use OC\Share20\ShareAttributes; +use OCA\DAV\Connector\Sabre\File; +use OCA\Files_Sharing\SharedMount; +use OCA\Files_Sharing\SharedStorage; +use OCP\Constants; +use OCP\Files\Cache\ICacheEntry; +use OCP\Files\Mount\IMountPoint; +use OCP\Files\Storage\IStorage; +use OCP\ICache; +use OCP\Share\IManager; +use OCP\Share\IShare; +use PHPUnit\Framework\MockObject\MockObject; + +/** + * Class NodeTest + * + * @group DB + * @package OCA\DAV\Tests\unit\Connector\Sabre + */ +class NodeTest extends \Test\TestCase { + public static function davPermissionsProvider(): array { + return [ + [Constants::PERMISSION_ALL, 'file', false, Constants::PERMISSION_ALL, false, 'test', 'RGDNVW'], + [Constants::PERMISSION_ALL, 'dir', false, Constants::PERMISSION_ALL, false, 'test', 'RGDNVCK'], + [Constants::PERMISSION_ALL, 'file', true, Constants::PERMISSION_ALL, false, 'test', 'SRGDNVW'], + [Constants::PERMISSION_ALL, 'file', true, Constants::PERMISSION_ALL, true, 'test', 'SRMGDNVW'], + [Constants::PERMISSION_ALL, 'file', true, Constants::PERMISSION_ALL, true, '' , 'SRMGDNVW'], + [Constants::PERMISSION_ALL, 'file', true, Constants::PERMISSION_ALL - Constants::PERMISSION_UPDATE, true, '' , 'SRMGDNV'], + [Constants::PERMISSION_ALL - Constants::PERMISSION_SHARE, 'file', true, Constants::PERMISSION_ALL, false, 'test', 'SGDNVW'], + [Constants::PERMISSION_ALL - Constants::PERMISSION_UPDATE, 'file', false, Constants::PERMISSION_ALL, false, 'test', 'RGD'], + [Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE, 'file', false, Constants::PERMISSION_ALL, false, 'test', 'RGNVW'], + [Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE, 'file', false, Constants::PERMISSION_ALL, false, 'test', 'RGDNVW'], + [Constants::PERMISSION_ALL - Constants::PERMISSION_READ, 'file', false, Constants::PERMISSION_ALL, false, 'test', 'RDNVW'], + [Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE, 'dir', false, Constants::PERMISSION_ALL, false, 'test', 'RGDNV'], + [Constants::PERMISSION_ALL - Constants::PERMISSION_READ, 'dir', false, Constants::PERMISSION_ALL, false, 'test', 'RDNVCK'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('davPermissionsProvider')] + public function testDavPermissions(int $permissions, string $type, bool $shared, int $shareRootPermissions, bool $mounted, string $internalPath, string $expected): void { + $info = $this->getMockBuilder(FileInfo::class) + ->disableOriginalConstructor() + ->onlyMethods(['getPermissions', 'isShared', 'isMounted', 'getType', 'getInternalPath', 'getStorage', 'getMountPoint']) + ->getMock(); + $info->method('getPermissions') + ->willReturn($permissions); + $info->method('isShared') + ->willReturn($shared); + $info->method('isMounted') + ->willReturn($mounted); + $info->method('getType') + ->willReturn($type); + $info->method('getInternalPath') + ->willReturn($internalPath); + $info->method('getMountPoint') + ->willReturnCallback(function () use ($shared) { + if ($shared) { + return $this->createMock(SharedMount::class); + } else { + return $this->createMock(MountPoint::class); + } + }); + $storage = $this->createMock(IStorage::class); + if ($shared) { + $storage->method('instanceOfStorage') + ->willReturn(true); + $cache = $this->createMock(ICache::class); + $storage->method('getCache') + ->willReturn($cache); + $shareRootEntry = $this->createMock(ICacheEntry::class); + $cache->method('get') + ->willReturn($shareRootEntry); + $shareRootEntry->method('getPermissions') + ->willReturn($shareRootPermissions); + } else { + $storage->method('instanceOfStorage') + ->willReturn(false); + } + $info->method('getStorage') + ->willReturn($storage); + $view = $this->createMock(View::class); + + $node = new File($view, $info); + $this->assertEquals($expected, $node->getDavPermissions()); + } + + public static function sharePermissionsProvider(): array { + return [ + [\OCP\Files\FileInfo::TYPE_FILE, null, 1, 1], + [\OCP\Files\FileInfo::TYPE_FILE, null, 3, 3], + [\OCP\Files\FileInfo::TYPE_FILE, null, 5, 1], + [\OCP\Files\FileInfo::TYPE_FILE, null, 7, 3], + [\OCP\Files\FileInfo::TYPE_FILE, null, 9, 1], + [\OCP\Files\FileInfo::TYPE_FILE, null, 11, 3], + [\OCP\Files\FileInfo::TYPE_FILE, null, 13, 1], + [\OCP\Files\FileInfo::TYPE_FILE, null, 15, 3], + [\OCP\Files\FileInfo::TYPE_FILE, null, 17, 17], + [\OCP\Files\FileInfo::TYPE_FILE, null, 19, 19], + [\OCP\Files\FileInfo::TYPE_FILE, null, 21, 17], + [\OCP\Files\FileInfo::TYPE_FILE, null, 23, 19], + [\OCP\Files\FileInfo::TYPE_FILE, null, 25, 17], + [\OCP\Files\FileInfo::TYPE_FILE, null, 27, 19], + [\OCP\Files\FileInfo::TYPE_FILE, null, 29, 17], + [\OCP\Files\FileInfo::TYPE_FILE, null, 30, 18], + [\OCP\Files\FileInfo::TYPE_FILE, null, 31, 19], + [\OCP\Files\FileInfo::TYPE_FOLDER, null, 1, 1], + [\OCP\Files\FileInfo::TYPE_FOLDER, null, 3, 3], + [\OCP\Files\FileInfo::TYPE_FOLDER, null, 5, 5], + [\OCP\Files\FileInfo::TYPE_FOLDER, null, 7, 7], + [\OCP\Files\FileInfo::TYPE_FOLDER, null, 9, 9], + [\OCP\Files\FileInfo::TYPE_FOLDER, null, 11, 11], + [\OCP\Files\FileInfo::TYPE_FOLDER, null, 13, 13], + [\OCP\Files\FileInfo::TYPE_FOLDER, null, 15, 15], + [\OCP\Files\FileInfo::TYPE_FOLDER, null, 17, 17], + [\OCP\Files\FileInfo::TYPE_FOLDER, null, 19, 19], + [\OCP\Files\FileInfo::TYPE_FOLDER, null, 21, 21], + [\OCP\Files\FileInfo::TYPE_FOLDER, null, 23, 23], + [\OCP\Files\FileInfo::TYPE_FOLDER, null, 25, 25], + [\OCP\Files\FileInfo::TYPE_FOLDER, null, 27, 27], + [\OCP\Files\FileInfo::TYPE_FOLDER, null, 29, 29], + [\OCP\Files\FileInfo::TYPE_FOLDER, null, 30, 30], + [\OCP\Files\FileInfo::TYPE_FOLDER, null, 31, 31], + [\OCP\Files\FileInfo::TYPE_FOLDER, 'shareToken', 7, 7], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('sharePermissionsProvider')] + public function testSharePermissions(string $type, ?string $user, int $permissions, int $expected): void { + $storage = $this->createMock(IStorage::class); + $storage->method('getPermissions')->willReturn($permissions); + + $mountpoint = $this->createMock(IMountPoint::class); + $mountpoint->method('getMountPoint')->willReturn('myPath'); + $shareManager = $this->createMock(IManager::class); + $share = $this->createMock(IShare::class); + + if ($user === null) { + $shareManager->expects($this->never())->method('getShareByToken'); + $share->expects($this->never())->method('getPermissions'); + } else { + $shareManager->expects($this->once())->method('getShareByToken')->with($user) + ->willReturn($share); + $share->expects($this->once())->method('getPermissions')->willReturn($permissions); + } + + $info = $this->getMockBuilder(FileInfo::class) + ->disableOriginalConstructor() + ->onlyMethods(['getStorage', 'getType', 'getMountPoint', 'getPermissions']) + ->getMock(); + + $info->method('getStorage')->willReturn($storage); + $info->method('getType')->willReturn($type); + $info->method('getMountPoint')->willReturn($mountpoint); + $info->method('getPermissions')->willReturn($permissions); + + $view = $this->createMock(View::class); + + $node = new File($view, $info); + $this->invokePrivate($node, 'shareManager', [$shareManager]); + $this->assertEquals($expected, $node->getSharePermissions($user)); + } + + public function testShareAttributes(): void { + $storage = $this->getMockBuilder(SharedStorage::class) + ->disableOriginalConstructor() + ->onlyMethods(['getShare']) + ->getMock(); + + $shareManager = $this->createMock(IManager::class); + $share = $this->createMock(IShare::class); + + $storage->expects($this->once()) + ->method('getShare') + ->willReturn($share); + + $attributes = new ShareAttributes(); + $attributes->setAttribute('permissions', 'download', false); + + $share->expects($this->once())->method('getAttributes')->willReturn($attributes); + + /** @var Folder&MockObject $info */ + $info = $this->getMockBuilder(Folder::class) + ->disableOriginalConstructor() + ->onlyMethods(['getStorage', 'getType']) + ->getMock(); + + $info->method('getStorage')->willReturn($storage); + $info->method('getType')->willReturn(FileInfo::TYPE_FOLDER); + + /** @var View&MockObject $view */ + $view = $this->createMock(View::class); + + $node = new File($view, $info); + $this->invokePrivate($node, 'shareManager', [$shareManager]); + $this->assertEquals($attributes->toArray(), $node->getShareAttributes()); + } + + public function testShareAttributesNonShare(): void { + $storage = $this->createMock(IStorage::class); + $shareManager = $this->createMock(IManager::class); + + /** @var Folder&MockObject */ + $info = $this->getMockBuilder(Folder::class) + ->disableOriginalConstructor() + ->onlyMethods(['getStorage', 'getType']) + ->getMock(); + + $info->method('getStorage')->willReturn($storage); + $info->method('getType')->willReturn(FileInfo::TYPE_FOLDER); + + /** @var View&MockObject */ + $view = $this->createMock(View::class); + + $node = new File($view, $info); + $this->invokePrivate($node, 'shareManager', [$shareManager]); + $this->assertEquals([], $node->getShareAttributes()); + } + + public static function sanitizeMtimeProvider(): array { + return [ + [123456789, 123456789], + ['987654321', 987654321], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('sanitizeMtimeProvider')] + public function testSanitizeMtime(string|int $mtime, int $expected): void { + $view = $this->getMockBuilder(View::class) + ->disableOriginalConstructor() + ->getMock(); + $info = $this->getMockBuilder(FileInfo::class) + ->disableOriginalConstructor() + ->getMock(); + + $node = new File($view, $info); + $result = $this->invokePrivate($node, 'sanitizeMtime', [$mtime]); + $this->assertEquals($expected, $result); + } + + public static function invalidSanitizeMtimeProvider(): array { + return [ + [-1337], [0], ['abcdef'], ['-1337'], ['0'], [12321], [24 * 60 * 60 - 1], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('invalidSanitizeMtimeProvider')] + public function testInvalidSanitizeMtime(int|string $mtime): void { + $this->expectException(\InvalidArgumentException::class); + + $view = $this->createMock(View::class); + $info = $this->createMock(FileInfo::class); + + $node = new File($view, $info); + self::invokePrivate($node, 'sanitizeMtime', [$mtime]); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/ObjectTreeTest.php b/apps/dav/tests/unit/Connector/Sabre/ObjectTreeTest.php new file mode 100644 index 00000000000..b07778e4fbd --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/ObjectTreeTest.php @@ -0,0 +1,243 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre; + +use OC\Files\FileInfo; +use OC\Files\Filesystem; +use OC\Files\Mount\Manager; +use OC\Files\Storage\Common; +use OC\Files\Storage\Temporary; +use OC\Files\View; +use OCA\DAV\Connector\Sabre\Directory; +use OCA\DAV\Connector\Sabre\Exception\InvalidPath; +use OCA\DAV\Connector\Sabre\File; +use OCA\DAV\Connector\Sabre\ObjectTree; +use OCP\Files\Mount\IMountManager; + +/** + * Class ObjectTreeTest + * + * @group DB + * + * @package OCA\DAV\Tests\Unit\Connector\Sabre + */ +class ObjectTreeTest extends \Test\TestCase { + public static function copyDataProvider(): array { + return [ + // copy into same dir + ['a', 'b', ''], + // copy into same dir + ['a/a', 'a/b', 'a'], + // copy into another dir + ['a', 'sub/a', 'sub'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('copyDataProvider')] + public function testCopy(string $sourcePath, string $targetPath, string $targetParent): void { + $view = $this->createMock(View::class); + $view->expects($this->once()) + ->method('verifyPath') + ->with($targetParent); + $view->expects($this->once()) + ->method('file_exists') + ->with($targetPath) + ->willReturn(false); + $view->expects($this->once()) + ->method('copy') + ->with($sourcePath, $targetPath) + ->willReturn(true); + + $info = $this->createMock(FileInfo::class); + $info->expects($this->once()) + ->method('isCreatable') + ->willReturn(true); + + $view->expects($this->once()) + ->method('getFileInfo') + ->with($targetParent === '' ? '.' : $targetParent) + ->willReturn($info); + + $rootDir = new Directory($view, $info); + $objectTree = $this->getMockBuilder(ObjectTree::class) + ->onlyMethods(['nodeExists', 'getNodeForPath']) + ->setConstructorArgs([$rootDir, $view]) + ->getMock(); + + $objectTree->expects($this->once()) + ->method('getNodeForPath') + ->with($this->identicalTo($sourcePath)) + ->willReturn(false); + + /** @var ObjectTree $objectTree */ + $mountManager = Filesystem::getMountManager(); + $objectTree->init($rootDir, $view, $mountManager); + $objectTree->copy($sourcePath, $targetPath); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('copyDataProvider')] + public function testCopyFailNotCreatable($sourcePath, $targetPath, $targetParent): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + $view = $this->createMock(View::class); + $view->expects($this->never()) + ->method('verifyPath'); + $view->expects($this->once()) + ->method('file_exists') + ->with($targetPath) + ->willReturn(false); + $view->expects($this->never()) + ->method('copy'); + + $info = $this->createMock(FileInfo::class); + $info->expects($this->once()) + ->method('isCreatable') + ->willReturn(false); + + $view->expects($this->once()) + ->method('getFileInfo') + ->with($targetParent === '' ? '.' : $targetParent) + ->willReturn($info); + + $rootDir = new Directory($view, $info); + $objectTree = $this->getMockBuilder(ObjectTree::class) + ->onlyMethods(['nodeExists', 'getNodeForPath']) + ->setConstructorArgs([$rootDir, $view]) + ->getMock(); + + $objectTree->expects($this->never()) + ->method('getNodeForPath'); + + /** @var ObjectTree $objectTree */ + $mountManager = Filesystem::getMountManager(); + $objectTree->init($rootDir, $view, $mountManager); + $objectTree->copy($sourcePath, $targetPath); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('nodeForPathProvider')] + public function testGetNodeForPath( + string $inputFileName, + string $fileInfoQueryPath, + string $outputFileName, + string $type, + ): void { + $rootNode = $this->createMock(Directory::class); + $mountManager = $this->createMock(Manager::class); + $view = $this->createMock(View::class); + $fileInfo = $this->createMock(FileInfo::class); + $fileInfo->method('getType') + ->willReturn($type); + $fileInfo->method('getName') + ->willReturn($outputFileName); + $fileInfo->method('getStorage') + ->willReturn($this->createMock(Common::class)); + + $view->method('getFileInfo') + ->with($fileInfoQueryPath) + ->willReturn($fileInfo); + + $tree = new ObjectTree(); + $tree->init($rootNode, $view, $mountManager); + + $node = $tree->getNodeForPath($inputFileName); + + $this->assertNotNull($node); + $this->assertEquals($outputFileName, $node->getName()); + + if ($type === 'file') { + $this->assertInstanceOf(File::class, $node); + } else { + $this->assertInstanceOf(Directory::class, $node); + } + } + + public static function nodeForPathProvider(): array { + return [ + // regular file + [ + 'regularfile.txt', + 'regularfile.txt', + 'regularfile.txt', + 'file', + ], + // regular directory + [ + 'regulardir', + 'regulardir', + 'regulardir', + 'dir', + ], + // regular file in subdir + [ + 'subdir/regularfile.txt', + 'subdir/regularfile.txt', + 'regularfile.txt', + 'file', + ], + // regular directory in subdir + [ + 'subdir/regulardir', + 'subdir/regulardir', + 'regulardir', + 'dir', + ], + ]; + } + + + public function testGetNodeForPathInvalidPath(): void { + $this->expectException(InvalidPath::class); + + $path = '/foo\bar'; + + + $storage = new Temporary([]); + + $view = $this->getMockBuilder(View::class) + ->onlyMethods(['resolvePath']) + ->getMock(); + $view->expects($this->once()) + ->method('resolvePath') + ->willReturnCallback(function ($path) use ($storage) { + return [$storage, ltrim($path, '/')]; + }); + + $rootNode = $this->createMock(Directory::class); + $mountManager = $this->createMock(IMountManager::class); + + $tree = new ObjectTree(); + $tree->init($rootNode, $view, $mountManager); + + $tree->getNodeForPath($path); + } + + public function testGetNodeForPathRoot(): void { + $path = '/'; + + + $storage = new Temporary([]); + + $view = $this->getMockBuilder(View::class) + ->onlyMethods(['resolvePath']) + ->getMock(); + $view->expects($this->any()) + ->method('resolvePath') + ->willReturnCallback(function ($path) use ($storage) { + return [$storage, ltrim($path, '/')]; + }); + + $rootNode = $this->createMock(Directory::class); + $mountManager = $this->createMock(IMountManager::class); + + $tree = new ObjectTree(); + $tree->init($rootNode, $view, $mountManager); + + $this->assertInstanceOf('\Sabre\DAV\INode', $tree->getNodeForPath($path)); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/PrincipalTest.php b/apps/dav/tests/unit/Connector/Sabre/PrincipalTest.php new file mode 100644 index 00000000000..e32d2671063 --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/PrincipalTest.php @@ -0,0 +1,937 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre; + +use OC\KnownUser\KnownUserService; +use OC\User\User; +use OCA\DAV\CalDAV\Proxy\Proxy; +use OCA\DAV\CalDAV\Proxy\ProxyMapper; +use OCA\DAV\Connector\Sabre\Principal; +use OCP\Accounts\IAccount; +use OCP\Accounts\IAccountManager; +use OCP\Accounts\IAccountProperty; +use OCP\Accounts\IAccountPropertyCollection; +use OCP\App\IAppManager; +use OCP\IConfig; +use OCP\IGroup; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\IUserManager; +use OCP\IUserSession; +use OCP\L10N\IFactory; +use OCP\Share\IManager; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\Exception; +use Sabre\DAV\PropPatch; +use Test\TestCase; + +class PrincipalTest extends TestCase { + private IUserManager&MockObject $userManager; + private IGroupManager&MockObject $groupManager; + private IAccountManager&MockObject $accountManager; + private IManager&MockObject $shareManager; + private IUserSession&MockObject $userSession; + private IAppManager&MockObject $appManager; + private ProxyMapper&MockObject $proxyMapper; + private KnownUserService&MockObject $knownUserService; + private IConfig&MockObject $config; + private IFactory&MockObject $languageFactory; + private Principal $connector; + + protected function setUp(): void { + parent::setUp(); + + $this->userManager = $this->createMock(IUserManager::class); + $this->groupManager = $this->createMock(IGroupManager::class); + $this->accountManager = $this->createMock(IAccountManager::class); + $this->shareManager = $this->createMock(IManager::class); + $this->userSession = $this->createMock(IUserSession::class); + $this->appManager = $this->createMock(IAppManager::class); + $this->proxyMapper = $this->createMock(ProxyMapper::class); + $this->knownUserService = $this->createMock(KnownUserService::class); + $this->config = $this->createMock(IConfig::class); + $this->languageFactory = $this->createMock(IFactory::class); + + $this->connector = new Principal( + $this->userManager, + $this->groupManager, + $this->accountManager, + $this->shareManager, + $this->userSession, + $this->appManager, + $this->proxyMapper, + $this->knownUserService, + $this->config, + $this->languageFactory + ); + } + + public function testGetPrincipalsByPrefixWithoutPrefix(): void { + $response = $this->connector->getPrincipalsByPrefix(''); + $this->assertSame([], $response); + } + + public function testGetPrincipalsByPrefixWithUsers(): void { + $fooUser = $this->createMock(User::class); + $fooUser + ->expects($this->once()) + ->method('getUID') + ->willReturn('foo'); + $fooUser + ->expects($this->once()) + ->method('getDisplayName') + ->willReturn('Dr. Foo-Bar'); + $fooUser + ->expects($this->once()) + ->method('getSystemEMailAddress') + ->willReturn(''); + $barUser = $this->createMock(User::class); + $barUser + ->expects($this->once()) + ->method('getUID') + ->willReturn('bar'); + $barUser + ->expects($this->once()) + ->method('getSystemEMailAddress') + ->willReturn('bar@nextcloud.com'); + $this->userManager + ->expects($this->once()) + ->method('search') + ->with('') + ->willReturn([$fooUser, $barUser]); + + $this->languageFactory + ->expects($this->exactly(2)) + ->method('getUserLanguage') + ->willReturnMap([ + [$fooUser, 'de'], + [$barUser, 'en'], + ]); + + $fooAccountPropertyCollection = $this->createMock(IAccountPropertyCollection::class); + $fooAccountPropertyCollection->expects($this->once()) + ->method('getProperties') + ->willReturn([]); + $fooAccount = $this->createMock(IAccount::class); + $fooAccount->expects($this->once()) + ->method('getPropertyCollection') + ->with(IAccountManager::COLLECTION_EMAIL) + ->willReturn($fooAccountPropertyCollection); + + $emailPropertyOne = $this->createMock(IAccountProperty::class); + $emailPropertyOne->expects($this->once()) + ->method('getValue') + ->willReturn('alias@nextcloud.com'); + $emailPropertyTwo = $this->createMock(IAccountProperty::class); + $emailPropertyTwo->expects($this->once()) + ->method('getValue') + ->willReturn('alias2@nextcloud.com'); + + $barAccountPropertyCollection = $this->createMock(IAccountPropertyCollection::class); + $barAccountPropertyCollection->expects($this->once()) + ->method('getProperties') + ->willReturn([$emailPropertyOne, $emailPropertyTwo]); + $barAccount = $this->createMock(IAccount::class); + $barAccount->expects($this->once()) + ->method('getPropertyCollection') + ->with(IAccountManager::COLLECTION_EMAIL) + ->willReturn($barAccountPropertyCollection); + + $this->accountManager + ->expects($this->exactly(2)) + ->method('getAccount') + ->willReturnMap([ + [$fooUser, $fooAccount], + [$barUser, $barAccount], + ]); + + $expectedResponse = [ + 0 => [ + 'uri' => 'principals/users/foo', + '{DAV:}displayname' => 'Dr. Foo-Bar', + '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'INDIVIDUAL', + '{http://nextcloud.com/ns}language' => 'de', + ], + 1 => [ + 'uri' => 'principals/users/bar', + '{DAV:}displayname' => 'bar', + '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'INDIVIDUAL', + '{http://nextcloud.com/ns}language' => 'en', + '{http://sabredav.org/ns}email-address' => 'bar@nextcloud.com', + '{DAV:}alternate-URI-set' => ['mailto:alias@nextcloud.com', 'mailto:alias2@nextcloud.com'] + ] + ]; + $response = $this->connector->getPrincipalsByPrefix('principals/users'); + $this->assertSame($expectedResponse, $response); + } + + public function testGetPrincipalsByPrefixEmpty(): void { + $this->userManager + ->expects($this->once()) + ->method('search') + ->with('') + ->willReturn([]); + + $response = $this->connector->getPrincipalsByPrefix('principals/users'); + $this->assertSame([], $response); + } + + public function testGetPrincipalsByPathWithoutMail(): void { + $fooUser = $this->createMock(User::class); + $fooUser + ->expects($this->once()) + ->method('getUID') + ->willReturn('foo'); + $this->userManager + ->expects($this->once()) + ->method('get') + ->with('foo') + ->willReturn($fooUser); + + $this->languageFactory + ->expects($this->once()) + ->method('getUserLanguage') + ->with($fooUser) + ->willReturn('de'); + + $expectedResponse = [ + 'uri' => 'principals/users/foo', + '{DAV:}displayname' => 'foo', + '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'INDIVIDUAL', + '{http://nextcloud.com/ns}language' => 'de' + ]; + $response = $this->connector->getPrincipalByPath('principals/users/foo'); + $this->assertSame($expectedResponse, $response); + } + + public function testGetPrincipalsByPathWithMail(): void { + $fooUser = $this->createMock(User::class); + $fooUser + ->expects($this->once()) + ->method('getSystemEMailAddress') + ->willReturn('foo@nextcloud.com'); + $fooUser + ->expects($this->once()) + ->method('getUID') + ->willReturn('foo'); + $this->userManager + ->expects($this->once()) + ->method('get') + ->with('foo') + ->willReturn($fooUser); + + $this->languageFactory + ->expects($this->once()) + ->method('getUserLanguage') + ->with($fooUser) + ->willReturn('de'); + + $expectedResponse = [ + 'uri' => 'principals/users/foo', + '{DAV:}displayname' => 'foo', + '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'INDIVIDUAL', + '{http://nextcloud.com/ns}language' => 'de', + '{http://sabredav.org/ns}email-address' => 'foo@nextcloud.com', + ]; + $response = $this->connector->getPrincipalByPath('principals/users/foo'); + $this->assertSame($expectedResponse, $response); + } + + public function testGetPrincipalsByPathEmpty(): void { + $this->userManager + ->expects($this->once()) + ->method('get') + ->with('foo') + ->willReturn(null); + + $response = $this->connector->getPrincipalByPath('principals/users/foo'); + $this->assertNull($response); + } + + public function testGetGroupMemberSet(): void { + $response = $this->connector->getGroupMemberSet('principals/users/foo'); + $this->assertSame([], $response); + } + + + public function testGetGroupMemberSetEmpty(): void { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Principal not found'); + + $this->userManager + ->expects($this->once()) + ->method('get') + ->with('foo') + ->willReturn(null); + + $this->connector->getGroupMemberSet('principals/users/foo/calendar-proxy-read'); + } + + public function testGetGroupMemberSetProxyRead(): void { + $fooUser = $this->createMock(User::class); + $fooUser + ->expects($this->once()) + ->method('getUID') + ->willReturn('foo'); + $this->userManager + ->expects($this->once()) + ->method('get') + ->with('foo') + ->willReturn($fooUser); + + $proxy1 = new Proxy(); + $proxy1->setProxyId('proxyId1'); + $proxy1->setPermissions(1); + + $proxy2 = new Proxy(); + $proxy2->setProxyId('proxyId2'); + $proxy2->setPermissions(3); + + $proxy3 = new Proxy(); + $proxy3->setProxyId('proxyId3'); + $proxy3->setPermissions(3); + + $this->proxyMapper->expects($this->once()) + ->method('getProxiesOf') + ->with('principals/users/foo') + ->willReturn([$proxy1, $proxy2, $proxy3]); + + $this->assertEquals(['proxyId1'], $this->connector->getGroupMemberSet('principals/users/foo/calendar-proxy-read')); + } + + public function testGetGroupMemberSetProxyWrite(): void { + $fooUser = $this->createMock(User::class); + $fooUser + ->expects($this->once()) + ->method('getUID') + ->willReturn('foo'); + $this->userManager + ->expects($this->once()) + ->method('get') + ->with('foo') + ->willReturn($fooUser); + + $proxy1 = new Proxy(); + $proxy1->setProxyId('proxyId1'); + $proxy1->setPermissions(1); + + $proxy2 = new Proxy(); + $proxy2->setProxyId('proxyId2'); + $proxy2->setPermissions(3); + + $proxy3 = new Proxy(); + $proxy3->setProxyId('proxyId3'); + $proxy3->setPermissions(3); + + $this->proxyMapper->expects($this->once()) + ->method('getProxiesOf') + ->with('principals/users/foo') + ->willReturn([$proxy1, $proxy2, $proxy3]); + + $this->assertEquals(['proxyId2', 'proxyId3'], $this->connector->getGroupMemberSet('principals/users/foo/calendar-proxy-write')); + } + + public function testGetGroupMembership(): void { + $fooUser = $this->createMock(User::class); + $group1 = $this->createMock(IGroup::class); + $group1->expects($this->once()) + ->method('getGID') + ->willReturn('group1'); + $group2 = $this->createMock(IGroup::class); + $group2->expects($this->once()) + ->method('getGID') + ->willReturn('foo/bar'); + $this->userManager + ->expects($this->exactly(2)) + ->method('get') + ->with('foo') + ->willReturn($fooUser); + $this->groupManager + ->expects($this->once()) + ->method('getUserGroups') + ->with($fooUser) + ->willReturn([ + $group1, + $group2, + ]); + + $proxy1 = new Proxy(); + $proxy1->setOwnerId('proxyId1'); + $proxy1->setPermissions(1); + + $proxy2 = new Proxy(); + $proxy2->setOwnerId('proxyId2'); + $proxy2->setPermissions(3); + + $this->proxyMapper->expects($this->once()) + ->method('getProxiesFor') + ->with('principals/users/foo') + ->willReturn([$proxy1, $proxy2]); + + $expectedResponse = [ + 'principals/groups/group1', + 'principals/groups/foo%2Fbar', + 'proxyId1/calendar-proxy-read', + 'proxyId2/calendar-proxy-write', + ]; + $response = $this->connector->getGroupMembership('principals/users/foo'); + $this->assertSame($expectedResponse, $response); + } + + + public function testGetGroupMembershipEmpty(): void { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Principal not found'); + + $this->userManager + ->expects($this->once()) + ->method('get') + ->with('foo') + ->willReturn(null); + + $this->connector->getGroupMembership('principals/users/foo'); + } + + + public function testSetGroupMembership(): void { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Setting members of the group is not supported yet'); + + $this->connector->setGroupMemberSet('principals/users/foo', ['foo']); + } + + public function testSetGroupMembershipProxy(): void { + $fooUser = $this->createMock(User::class); + $fooUser + ->expects($this->once()) + ->method('getUID') + ->willReturn('foo'); + $barUser = $this->createMock(User::class); + $barUser + ->expects($this->once()) + ->method('getUID') + ->willReturn('bar'); + $this->userManager + ->expects($this->exactly(2)) + ->method('get') + ->willReturnMap([ + ['foo', $fooUser], + ['bar', $barUser], + ]); + + $this->proxyMapper->expects($this->once()) + ->method('getProxiesOf') + ->with('principals/users/foo') + ->willReturn([]); + + $this->proxyMapper->expects($this->once()) + ->method('insert') + ->with($this->callback(function ($proxy) { + /** @var Proxy $proxy */ + if ($proxy->getOwnerId() !== 'principals/users/foo') { + return false; + } + if ($proxy->getProxyId() !== 'principals/users/bar') { + return false; + } + if ($proxy->getPermissions() !== 3) { + return false; + } + + return true; + })); + + $this->connector->setGroupMemberSet('principals/users/foo/calendar-proxy-write', ['principals/users/bar']); + } + + public function testUpdatePrincipal(): void { + $this->assertSame(0, $this->connector->updatePrincipal('foo', new PropPatch([]))); + } + + public function testSearchPrincipalsWithEmptySearchProperties(): void { + $this->assertSame([], $this->connector->searchPrincipals('principals/users', [])); + } + + public function testSearchPrincipalsWithWrongPrefixPath(): void { + $this->assertSame([], $this->connector->searchPrincipals('principals/groups', + ['{http://sabredav.org/ns}email-address' => 'foo'])); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('searchPrincipalsDataProvider')] + public function testSearchPrincipals(bool $sharingEnabled, bool $groupsOnly, string $test, array $result): void { + $this->shareManager->expects($this->once()) + ->method('shareAPIEnabled') + ->willReturn($sharingEnabled); + + $getUserGroupIdsReturnMap = []; + + if ($sharingEnabled) { + $this->shareManager->expects($this->once()) + ->method('allowEnumeration') + ->willReturn(true); + + $this->shareManager->expects($this->once()) + ->method('shareWithGroupMembersOnly') + ->willReturn($groupsOnly); + + if ($groupsOnly) { + $user = $this->createMock(IUser::class); + $this->userSession->expects($this->atLeastOnce()) + ->method('getUser') + ->willReturn($user); + + $getUserGroupIdsReturnMap[] = [$user, ['group1', 'group2', 'group5']]; + } + } else { + $this->config->expects($this->never()) + ->method('getAppValue'); + $this->shareManager->expects($this->never()) + ->method('shareWithGroupMembersOnly'); + $this->groupManager->expects($this->never()) + ->method($this->anything()); + } + + $user2 = $this->createMock(IUser::class); + $user2->method('getUID')->willReturn('user2'); + $user3 = $this->createMock(IUser::class); + $user3->method('getUID')->willReturn('user3'); + $user4 = $this->createMock(IUser::class); + $user4->method('getUID')->willReturn('user4'); + + if ($sharingEnabled) { + $this->userManager->expects($this->once()) + ->method('getByEmail') + ->with('user@example.com') + ->willReturn([$user2, $user3]); + + $this->userManager->expects($this->once()) + ->method('searchDisplayName') + ->with('User 12') + ->willReturn([$user3, $user4]); + } else { + $this->userManager->expects($this->never()) + ->method('getByEmail'); + + $this->userManager->expects($this->never()) + ->method('searchDisplayName'); + } + + if ($sharingEnabled && $groupsOnly) { + $getUserGroupIdsReturnMap[] = [$user2, ['group1', 'group3']]; + $getUserGroupIdsReturnMap[] = [$user3, ['group3', 'group4']]; + $getUserGroupIdsReturnMap[] = [$user4, ['group4', 'group5']]; + } + + $this->groupManager->expects($this->any()) + ->method('getUserGroupIds') + ->willReturnMap($getUserGroupIdsReturnMap); + + + $this->assertEquals($result, $this->connector->searchPrincipals('principals/users', + ['{http://sabredav.org/ns}email-address' => 'user@example.com', + '{DAV:}displayname' => 'User 12'], $test)); + } + + public static function searchPrincipalsDataProvider(): array { + return [ + [true, false, 'allof', ['principals/users/user3']], + [true, false, 'anyof', ['principals/users/user2', 'principals/users/user3', 'principals/users/user4']], + [true, true, 'allof', []], + [true, true, 'anyof', ['principals/users/user2', 'principals/users/user4']], + [false, false, 'allof', []], + [false, false, 'anyof', []], + ]; + } + + public function testSearchPrincipalByCalendarUserAddressSet(): void { + $this->shareManager->expects($this->exactly(2)) + ->method('shareAPIEnabled') + ->willReturn(true); + + $this->shareManager->expects($this->exactly(2)) + ->method('allowEnumeration') + ->willReturn(true); + + $this->shareManager->expects($this->exactly(2)) + ->method('shareWithGroupMembersOnly') + ->willReturn(false); + + $user2 = $this->createMock(IUser::class); + $user2->method('getUID')->willReturn('user2'); + $user3 = $this->createMock(IUser::class); + $user3->method('getUID')->willReturn('user3'); + + $this->userManager->expects($this->once()) + ->method('getByEmail') + ->with('user@example.com') + ->willReturn([$user2, $user3]); + + $this->assertEquals([ + 'principals/users/user2', + 'principals/users/user3', + ], $this->connector->searchPrincipals('principals/users', + ['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set' => 'user@example.com'])); + } + + public function testSearchPrincipalWithEnumerationDisabledDisplayname(): void { + $this->shareManager->expects($this->once()) + ->method('shareAPIEnabled') + ->willReturn(true); + + $this->shareManager->expects($this->once()) + ->method('allowEnumeration') + ->willReturn(false); + + $this->shareManager->expects($this->once()) + ->method('shareWithGroupMembersOnly') + ->willReturn(false); + + $this->shareManager->expects($this->once()) + ->method('allowEnumerationFullMatch') + ->willReturn(true); + + $user2 = $this->createMock(IUser::class); + $user2->method('getUID')->willReturn('user2'); + $user2->method('getDisplayName')->willReturn('User 2'); + $user2->method('getSystemEMailAddress')->willReturn('user2@foo.bar'); + $user3 = $this->createMock(IUser::class); + $user3->method('getUID')->willReturn('user3'); + $user3->method('getDisplayName')->willReturn('User 22'); + $user3->method('getSystemEMailAddress')->willReturn('user2@foo.bar123'); + $user4 = $this->createMock(IUser::class); + $user4->method('getUID')->willReturn('user4'); + $user4->method('getDisplayName')->willReturn('User 222'); + $user4->method('getSystemEMailAddress')->willReturn('user2@foo.bar456'); + + $this->userManager->expects($this->once()) + ->method('searchDisplayName') + ->with('User 2') + ->willReturn([$user2, $user3, $user4]); + + $this->assertEquals(['principals/users/user2'], $this->connector->searchPrincipals('principals/users', + ['{DAV:}displayname' => 'User 2'])); + } + + public function testSearchPrincipalWithEnumerationDisabledDisplaynameOnFullMatch(): void { + $this->shareManager->expects($this->once()) + ->method('shareAPIEnabled') + ->willReturn(true); + + $this->shareManager->expects($this->once()) + ->method('allowEnumeration') + ->willReturn(false); + + $this->shareManager->expects($this->once()) + ->method('shareWithGroupMembersOnly') + ->willReturn(false); + + $this->shareManager->expects($this->once()) + ->method('allowEnumerationFullMatch') + ->willReturn(false); + + $this->assertEquals([], $this->connector->searchPrincipals('principals/users', + ['{DAV:}displayname' => 'User 2'])); + } + + public function testSearchPrincipalWithEnumerationDisabledEmail(): void { + $this->shareManager->expects($this->once()) + ->method('shareAPIEnabled') + ->willReturn(true); + + $this->shareManager->expects($this->once()) + ->method('allowEnumeration') + ->willReturn(false); + + $this->shareManager->expects($this->once()) + ->method('shareWithGroupMembersOnly') + ->willReturn(false); + + $this->shareManager->expects($this->once()) + ->method('allowEnumerationFullMatch') + ->willReturn(true); + + $this->shareManager->expects($this->once()) + ->method('matchEmail') + ->willReturn(true); + + $user2 = $this->createMock(IUser::class); + $user2->method('getUID')->willReturn('user2'); + $user2->method('getDisplayName')->willReturn('User 2'); + $user2->method('getSystemEMailAddress')->willReturn('user2@foo.bar'); + $user3 = $this->createMock(IUser::class); + $user3->method('getUID')->willReturn('user3'); + $user2->method('getDisplayName')->willReturn('User 22'); + $user2->method('getSystemEMailAddress')->willReturn('user2@foo.bar123'); + $user4 = $this->createMock(IUser::class); + $user4->method('getUID')->willReturn('user4'); + $user2->method('getDisplayName')->willReturn('User 222'); + $user2->method('getSystemEMailAddress')->willReturn('user2@foo.bar456'); + + $this->userManager->expects($this->once()) + ->method('getByEmail') + ->with('user2@foo.bar') + ->willReturn([$user2]); + + $this->assertEquals(['principals/users/user2'], $this->connector->searchPrincipals('principals/users', + ['{http://sabredav.org/ns}email-address' => 'user2@foo.bar'])); + } + + public function testSearchPrincipalWithEnumerationDisabledEmailOnFullMatch(): void { + $this->shareManager->expects($this->once()) + ->method('shareAPIEnabled') + ->willReturn(true); + + $this->shareManager->expects($this->once()) + ->method('allowEnumeration') + ->willReturn(false); + + $this->shareManager->expects($this->once()) + ->method('shareWithGroupMembersOnly') + ->willReturn(false); + + $this->shareManager->expects($this->once()) + ->method('allowEnumerationFullMatch') + ->willReturn(false); + + + $this->assertEquals([], $this->connector->searchPrincipals('principals/users', + ['{http://sabredav.org/ns}email-address' => 'user2@foo.bar'])); + } + + public function testSearchPrincipalWithEnumerationLimitedDisplayname(): void { + $this->shareManager->expects($this->once()) + ->method('shareAPIEnabled') + ->willReturn(true); + + $this->shareManager->expects($this->once()) + ->method('allowEnumeration') + ->willReturn(true); + + $this->shareManager->expects($this->once()) + ->method('limitEnumerationToGroups') + ->willReturn(true); + + $this->shareManager->expects($this->once()) + ->method('shareWithGroupMembersOnly') + ->willReturn(false); + + $user2 = $this->createMock(IUser::class); + $user2->method('getUID')->willReturn('user2'); + $user2->method('getDisplayName')->willReturn('User 2'); + $user2->method('getSystemEMailAddress')->willReturn('user2@foo.bar'); + $user3 = $this->createMock(IUser::class); + $user3->method('getUID')->willReturn('user3'); + $user3->method('getDisplayName')->willReturn('User 22'); + $user3->method('getSystemEMailAddress')->willReturn('user2@foo.bar123'); + $user4 = $this->createMock(IUser::class); + $user4->method('getUID')->willReturn('user4'); + $user4->method('getDisplayName')->willReturn('User 222'); + $user4->method('getSystemEMailAddress')->willReturn('user2@foo.bar456'); + + + $this->userSession->expects($this->once()) + ->method('getUser') + ->willReturn($user2); + + $this->groupManager->expects($this->exactly(4)) + ->method('getUserGroupIds') + ->willReturnMap([ + [$user2, ['group1']], + [$user3, ['group1']], + [$user4, ['group2']], + ]); + + $this->userManager->expects($this->once()) + ->method('searchDisplayName') + ->with('User') + ->willReturn([$user2, $user3, $user4]); + + + $this->assertEquals([ + 'principals/users/user2', + 'principals/users/user3', + ], $this->connector->searchPrincipals('principals/users', + ['{DAV:}displayname' => 'User'])); + } + + public function testSearchPrincipalWithEnumerationLimitedMail(): void { + $this->shareManager->expects($this->once()) + ->method('shareAPIEnabled') + ->willReturn(true); + + $this->shareManager->expects($this->once()) + ->method('allowEnumeration') + ->willReturn(true); + + $this->shareManager->expects($this->once()) + ->method('limitEnumerationToGroups') + ->willReturn(true); + + $this->shareManager->expects($this->once()) + ->method('shareWithGroupMembersOnly') + ->willReturn(false); + + $user2 = $this->createMock(IUser::class); + $user2->method('getUID')->willReturn('user2'); + $user2->method('getDisplayName')->willReturn('User 2'); + $user2->method('getSystemEMailAddress')->willReturn('user2@foo.bar'); + $user3 = $this->createMock(IUser::class); + $user3->method('getUID')->willReturn('user3'); + $user3->method('getDisplayName')->willReturn('User 22'); + $user3->method('getSystemEMailAddress')->willReturn('user2@foo.bar123'); + $user4 = $this->createMock(IUser::class); + $user4->method('getUID')->willReturn('user4'); + $user4->method('getDisplayName')->willReturn('User 222'); + $user4->method('getSystemEMailAddress')->willReturn('user2@foo.bar456'); + + + $this->userSession->expects($this->once()) + ->method('getUser') + ->willReturn($user2); + + $this->groupManager->expects($this->exactly(4)) + ->method('getUserGroupIds') + ->willReturnMap([ + [$user2, ['group1']], + [$user3, ['group1']], + [$user4, ['group2']], + ]); + + $this->userManager->expects($this->once()) + ->method('getByEmail') + ->with('user') + ->willReturn([$user2, $user3, $user4]); + + + $this->assertEquals([ + 'principals/users/user2', + 'principals/users/user3' + ], $this->connector->searchPrincipals('principals/users', + ['{http://sabredav.org/ns}email-address' => 'user'])); + } + + public function testFindByUriSharingApiDisabled(): void { + $this->shareManager->expects($this->once()) + ->method('shareApiEnabled') + ->willReturn(false); + + $this->assertEquals(null, $this->connector->findByUri('mailto:user@foo.com', 'principals/users')); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('findByUriWithGroupRestrictionDataProvider')] + public function testFindByUriWithGroupRestriction(string $uri, string $email, ?string $expects): void { + $this->shareManager->expects($this->once()) + ->method('shareApiEnabled') + ->willReturn(true); + + $this->shareManager->expects($this->once()) + ->method('shareWithGroupMembersOnly') + ->willReturn(true); + + $user = $this->createMock(IUser::class); + $this->userSession->expects($this->once()) + ->method('getUser') + ->willReturn($user); + + $user2 = $this->createMock(IUser::class); + $user2->method('getUID')->willReturn('user2'); + $user3 = $this->createMock(IUser::class); + $user3->method('getUID')->willReturn('user3'); + + $this->userManager->expects($this->once()) + ->method('getByEmail') + ->with($email) + ->willReturn([$email === 'user2@foo.bar' ? $user2 : $user3]); + + if ($email === 'user2@foo.bar') { + $this->groupManager->expects($this->exactly(2)) + ->method('getUserGroupIds') + ->willReturnMap([ + [$user, ['group1', 'group2']], + [$user2, ['group1', 'group3']], + ]); + } else { + $this->groupManager->expects($this->exactly(2)) + ->method('getUserGroupIds') + ->willReturnMap([ + [$user, ['group1', 'group2']], + [$user3, ['group3', 'group3']], + ]); + } + + $this->assertEquals($expects, $this->connector->findByUri($uri, 'principals/users')); + } + + public static function findByUriWithGroupRestrictionDataProvider(): array { + return [ + ['mailto:user2@foo.bar', 'user2@foo.bar', 'principals/users/user2'], + ['mailto:user3@foo.bar', 'user3@foo.bar', null], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('findByUriWithoutGroupRestrictionDataProvider')] + public function testFindByUriWithoutGroupRestriction(string $uri, string $email, string $expects): void { + $this->shareManager->expects($this->once()) + ->method('shareApiEnabled') + ->willReturn(true); + + $this->shareManager->expects($this->once()) + ->method('shareWithGroupMembersOnly') + ->willReturn(false); + + $user2 = $this->createMock(IUser::class); + $user2->method('getUID')->willReturn('user2'); + $user3 = $this->createMock(IUser::class); + $user3->method('getUID')->willReturn('user3'); + + $this->userManager->expects($this->once()) + ->method('getByEmail') + ->with($email) + ->willReturn([$email === 'user2@foo.bar' ? $user2 : $user3]); + + $this->assertEquals($expects, $this->connector->findByUri($uri, 'principals/users')); + } + + public static function findByUriWithoutGroupRestrictionDataProvider(): array { + return [ + ['mailto:user2@foo.bar', 'user2@foo.bar', 'principals/users/user2'], + ['mailto:user3@foo.bar', 'user3@foo.bar', 'principals/users/user3'], + ]; + } + + public function testGetEmailAddressesOfPrincipal(): void { + $principal = [ + '{http://sabredav.org/ns}email-address' => 'bar@company.org', + '{DAV:}alternate-URI-set' => [ + '/some/url', + 'mailto:foo@bar.com', + 'mailto:duplicate@example.com', + ], + '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set' => [ + 'mailto:bernard@example.com', + 'mailto:bernard.desruisseaux@example.com', + ], + '{http://calendarserver.org/ns/}email-address-set' => [ + 'mailto:duplicate@example.com', + 'mailto:user@some.org', + ], + ]; + + $expected = [ + 'bar@company.org', + 'foo@bar.com', + 'duplicate@example.com', + 'bernard@example.com', + 'bernard.desruisseaux@example.com', + 'user@some.org', + ]; + $actual = $this->connector->getEmailAddressesOfPrincipal($principal); + $this->assertEquals($expected, $actual); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/PropFindMonitorPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/PropFindMonitorPluginTest.php new file mode 100644 index 00000000000..9d22befa201 --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/PropFindMonitorPluginTest.php @@ -0,0 +1,133 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace unit\Connector\Sabre; + +use OCA\DAV\Connector\Sabre\PropFindMonitorPlugin; +use OCA\DAV\Connector\Sabre\Server; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Sabre\HTTP\Request; +use Sabre\HTTP\Response; +use Test\TestCase; + +class PropFindMonitorPluginTest extends TestCase { + + private PropFindMonitorPlugin $plugin; + private Server&MockObject $server; + private LoggerInterface&MockObject $logger; + private Request&MockObject $request; + private Response&MockObject $response; + + public static function dataTest(): array { + $minQueriesTrigger = PropFindMonitorPlugin::THRESHOLD_QUERY_FACTOR + * PropFindMonitorPlugin::THRESHOLD_NODES; + return [ + 'No queries logged' => [[], 0], + 'Plugins with queries in less than threshold nodes should not be logged' => [ + [ + 'propFind' => [ + [ + 'PluginName' => [ + 'queries' => 100, + 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES - 1] + ], + [], + ] + ], + 0 + ], + 'Plugins with query-to-node ratio less than threshold should not be logged' => [ + [ + 'propFind' => [ + [ + 'PluginName' => [ + 'queries' => $minQueriesTrigger - 1, + 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES ], + ], + [], + ] + ], + 0 + ], + 'Plugins with more nodes scanned than queries executed should not be logged' => [ + [ + 'propFind' => [ + [ + 'PluginName' => [ + 'queries' => $minQueriesTrigger, + 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES * 2], + ], + [],] + ], + 0 + ], + 'Plugins with queries only in highest depth level should not be logged' => [ + [ + 'propFind' => [ + [ + 'PluginName' => [ + 'queries' => $minQueriesTrigger, + 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES - 1 + ] + ], + [ + 'PluginName' => [ + 'queries' => $minQueriesTrigger * 2, + 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES + ] + ], + ] + ], + 0 + ], + 'Plugins with too many queries should be logged' => [ + [ + 'propFind' => [ + [ + 'FirstPlugin' => [ + 'queries' => $minQueriesTrigger, + 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES, + ], + 'SecondPlugin' => [ + 'queries' => $minQueriesTrigger, + 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES, + ] + ], + [], + ] + ], + 2 + ] + ]; + } + + /** + * @dataProvider dataTest + */ + public function test(array $queries, $expectedLogCalls): void { + $this->plugin->initialize($this->server); + $this->server->expects($this->once())->method('getPluginQueries') + ->willReturn($queries); + + $this->server->expects(empty($queries) ? $this->never() : $this->once()) + ->method('getLogger') + ->willReturn($this->logger); + + $this->logger->expects($this->exactly($expectedLogCalls))->method('error'); + $this->plugin->afterResponse($this->request, $this->response); + } + + protected function setUp(): void { + parent::setUp(); + + $this->plugin = new PropFindMonitorPlugin(); + $this->server = $this->createMock(Server::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->request = $this->createMock(Request::class); + $this->response = $this->createMock(Response::class); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/PropFindPreloadNotifyPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/PropFindPreloadNotifyPluginTest.php new file mode 100644 index 00000000000..52fe3eba5bf --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/PropFindPreloadNotifyPluginTest.php @@ -0,0 +1,92 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre; + +use OCA\DAV\Connector\Sabre\PropFindPreloadNotifyPlugin; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\ICollection; +use Sabre\DAV\IFile; +use Sabre\DAV\PropFind; +use Sabre\DAV\Server; +use Test\TestCase; + +class PropFindPreloadNotifyPluginTest extends TestCase { + + private Server&MockObject $server; + private PropFindPreloadNotifyPlugin $plugin; + + protected function setUp(): void { + parent::setUp(); + + $this->server = $this->createMock(Server::class); + $this->plugin = new PropFindPreloadNotifyPlugin(); + } + + public function testInitialize(): void { + $this->server + ->expects(self::once()) + ->method('on') + ->with('propFind', + $this->anything(), 1); + $this->plugin->initialize($this->server); + } + + public static function dataTestCollectionPreloadNotifier(): array { + return [ + 'When node is not a collection, should not emit' => [ + IFile::class, + 1, + false, + true + ], + 'When node is a collection but depth is zero, should not emit' => [ + ICollection::class, + 0, + false, + true + ], + 'When node is a collection, and depth > 0, should emit' => [ + ICollection::class, + 1, + true, + true + ], + 'When node is a collection, and depth is infinite, should emit' + => [ + ICollection::class, + Server::DEPTH_INFINITY, + true, + true + ], + 'When called called handler returns false, it should be returned' + => [ + ICollection::class, + 1, + true, + false + ] + ]; + } + + #[DataProvider(methodName: 'dataTestCollectionPreloadNotifier')] + public function testCollectionPreloadNotifier(string $nodeType, int $depth, bool $shouldEmit, bool $emitReturns): + void { + $this->plugin->initialize($this->server); + $propFind = $this->createMock(PropFind::class); + $propFind->expects(self::any())->method('getDepth')->willReturn($depth); + $node = $this->createMock($nodeType); + + $expectation = $shouldEmit ? self::once() : self::never(); + $this->server->expects($expectation)->method('emit')->with('preloadCollection', + [$propFind, $node])->willReturn($emitReturns); + $return = $this->plugin->collectionPreloadNotifier($propFind, $node); + $this->assertEquals($emitReturns, $return); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/PropfindCompressionPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/PropfindCompressionPluginTest.php new file mode 100644 index 00000000000..e6f696ed160 --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/PropfindCompressionPluginTest.php @@ -0,0 +1,98 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre; + +use OCA\DAV\Connector\Sabre\PropfindCompressionPlugin; +use Sabre\HTTP\Request; +use Sabre\HTTP\Response; +use Test\TestCase; + +class PropfindCompressionPluginTest extends TestCase { + private PropfindCompressionPlugin $plugin; + + protected function setUp(): void { + parent::setUp(); + + $this->plugin = new PropfindCompressionPlugin(); + } + + public function testNoHeader(): void { + $request = $this->createMock(Request::class); + $response = $this->createMock(Response::class); + + $request->method('getHeader') + ->with('Accept-Encoding') + ->willReturn(null); + + $response->expects($this->never()) + ->method($this->anything()); + + $result = $this->plugin->compressResponse($request, $response); + $this->assertSame($response, $result); + } + + public function testHeaderButNoGzip(): void { + $request = $this->createMock(Request::class); + $response = $this->createMock(Response::class); + + $request->method('getHeader') + ->with('Accept-Encoding') + ->willReturn('deflate'); + + $response->expects($this->never()) + ->method($this->anything()); + + $result = $this->plugin->compressResponse($request, $response); + $this->assertSame($response, $result); + } + + public function testHeaderGzipButNoStringBody(): void { + $request = $this->createMock(Request::class); + $response = $this->createMock(Response::class); + + $request->method('getHeader') + ->with('Accept-Encoding') + ->willReturn('deflate'); + + $response->method('getBody') + ->willReturn(5); + + $result = $this->plugin->compressResponse($request, $response); + $this->assertSame($response, $result); + } + + + public function testProperGzip(): void { + $request = $this->createMock(Request::class); + $response = $this->createMock(Response::class); + + $request->method('getHeader') + ->with('Accept-Encoding') + ->willReturn('gzip, deflate'); + + $response->method('getBody') + ->willReturn('my gzip test'); + + $response->expects($this->once()) + ->method('setHeader') + ->with( + $this->equalTo('Content-Encoding'), + $this->equalTo('gzip') + ); + $response->expects($this->once()) + ->method('setBody') + ->with($this->callback(function ($data) { + $orig = gzdecode($data); + return $orig === 'my gzip test'; + })); + + $result = $this->plugin->compressResponse($request, $response); + $this->assertSame($response, $result); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/PublicAuthTest.php b/apps/dav/tests/unit/Connector/Sabre/PublicAuthTest.php new file mode 100644 index 00000000000..fef62b51c67 --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/PublicAuthTest.php @@ -0,0 +1,384 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector; + +use OCA\DAV\Connector\Sabre\PublicAuth; +use OCP\IRequest; +use OCP\ISession; +use OCP\IURLGenerator; +use OCP\Security\Bruteforce\IThrottler; +use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\IManager; +use OCP\Share\IShare; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; + +/** + * Class PublicAuthTest + * + * @group DB + * + * @package OCA\DAV\Tests\unit\Connector + */ +class PublicAuthTest extends \Test\TestCase { + + private ISession&MockObject $session; + private IRequest&MockObject $request; + private IManager&MockObject $shareManager; + private IThrottler&MockObject $throttler; + private LoggerInterface&MockObject $logger; + private IURLGenerator&MockObject $urlGenerator; + private PublicAuth $auth; + + private bool|string $oldUser; + + protected function setUp(): void { + parent::setUp(); + + $this->session = $this->createMock(ISession::class); + $this->request = $this->createMock(IRequest::class); + $this->shareManager = $this->createMock(IManager::class); + $this->throttler = $this->createMock(IThrottler::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + + $this->auth = new PublicAuth( + $this->request, + $this->shareManager, + $this->session, + $this->throttler, + $this->logger, + $this->urlGenerator, + ); + + // Store current user + $this->oldUser = \OC_User::getUser(); + } + + protected function tearDown(): void { + \OC_User::setIncognitoMode(false); + + // Set old user + \OC_User::setUserId($this->oldUser); + if ($this->oldUser !== false) { + \OC_Util::setupFS($this->oldUser); + } + + parent::tearDown(); + } + + public function testGetToken(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $result = self::invokePrivate($this->auth, 'getToken'); + + $this->assertSame('GX9HSGQrGE', $result); + } + + public function testGetTokenInvalid(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files'); + + $this->expectException(\Sabre\DAV\Exception\NotFound::class); + self::invokePrivate($this->auth, 'getToken'); + } + + public function testCheckTokenValidShare(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $share = $this->createMock(IShare::class); + $share->method('getPassword')->willReturn(null); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->with('GX9HSGQrGE') + ->willReturn($share); + + $result = self::invokePrivate($this->auth, 'checkToken'); + $this->assertSame([true, 'principals/GX9HSGQrGE'], $result); + } + + public function testCheckTokenInvalidShare(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $this->shareManager + ->expects($this->once()) + ->method('getShareByToken') + ->with('GX9HSGQrGE') + ->willThrowException(new ShareNotFound()); + + $this->expectException(\Sabre\DAV\Exception\NotFound::class); + self::invokePrivate($this->auth, 'checkToken'); + } + + public function testCheckTokenAlreadyAuthenticated(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $share = $this->createMock(IShare::class); + $share->method('getShareType')->willReturn(42); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->with('GX9HSGQrGE') + ->willReturn($share); + + $this->session->method('exists')->with('public_link_authenticated')->willReturn(true); + $this->session->method('get')->with('public_link_authenticated')->willReturn('42'); + + $result = self::invokePrivate($this->auth, 'checkToken'); + $this->assertSame([true, 'principals/GX9HSGQrGE'], $result); + } + + public function testCheckTokenPasswordNotAuthenticated(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $share = $this->createMock(IShare::class); + $share->method('getPassword')->willReturn('password'); + $share->method('getShareType')->willReturn(42); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->with('GX9HSGQrGE') + ->willReturn($share); + + $this->session->method('exists')->with('public_link_authenticated')->willReturn(false); + + $this->expectException(\Sabre\DAV\Exception\NotAuthenticated::class); + self::invokePrivate($this->auth, 'checkToken'); + } + + public function testCheckTokenPasswordAuthenticatedWrongShare(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $share = $this->createMock(IShare::class); + $share->method('getPassword')->willReturn('password'); + $share->method('getShareType')->willReturn(42); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->with('GX9HSGQrGE') + ->willReturn($share); + + $this->session->method('exists')->with('public_link_authenticated')->willReturn(false); + $this->session->method('get')->with('public_link_authenticated')->willReturn('43'); + + $this->expectException(\Sabre\DAV\Exception\NotAuthenticated::class); + self::invokePrivate($this->auth, 'checkToken'); + } + + public function testNoShare(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->with('GX9HSGQrGE') + ->willThrowException(new ShareNotFound()); + + $result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']); + + $this->assertFalse($result); + } + + public function testShareNoPassword(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $share = $this->createMock(IShare::class); + $share->method('getPassword')->willReturn(null); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->with('GX9HSGQrGE') + ->willReturn($share); + + $result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']); + + $this->assertTrue($result); + } + + public function testSharePasswordFancyShareType(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $share = $this->createMock(IShare::class); + $share->method('getPassword')->willReturn('password'); + $share->method('getShareType')->willReturn(42); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->with('GX9HSGQrGE') + ->willReturn($share); + + $result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']); + + $this->assertFalse($result); + } + + + public function testSharePasswordRemote(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $share = $this->createMock(IShare::class); + $share->method('getPassword')->willReturn('password'); + $share->method('getShareType')->willReturn(IShare::TYPE_REMOTE); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->with('GX9HSGQrGE') + ->willReturn($share); + + $result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']); + + $this->assertTrue($result); + } + + public function testSharePasswordLinkValidPassword(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $share = $this->createMock(IShare::class); + $share->method('getPassword')->willReturn('password'); + $share->method('getShareType')->willReturn(IShare::TYPE_LINK); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->with('GX9HSGQrGE') + ->willReturn($share); + + $this->shareManager->expects($this->once()) + ->method('checkPassword')->with( + $this->equalTo($share), + $this->equalTo('password') + )->willReturn(true); + + $result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']); + + $this->assertTrue($result); + } + + public function testSharePasswordMailValidPassword(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $share = $this->createMock(IShare::class); + $share->method('getPassword')->willReturn('password'); + $share->method('getShareType')->willReturn(IShare::TYPE_EMAIL); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->with('GX9HSGQrGE') + ->willReturn($share); + + $this->shareManager->expects($this->once()) + ->method('checkPassword')->with( + $this->equalTo($share), + $this->equalTo('password') + )->willReturn(true); + + $result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']); + + $this->assertTrue($result); + } + + public function testInvalidSharePasswordLinkValidSession(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $share = $this->createMock(IShare::class); + $share->method('getPassword')->willReturn('password'); + $share->method('getShareType')->willReturn(IShare::TYPE_LINK); + $share->method('getId')->willReturn('42'); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->with('GX9HSGQrGE') + ->willReturn($share); + + $this->shareManager->expects($this->once()) + ->method('checkPassword') + ->with( + $this->equalTo($share), + $this->equalTo('password') + )->willReturn(false); + + $this->session->method('exists')->with('public_link_authenticated')->willReturn(true); + $this->session->method('get')->with('public_link_authenticated')->willReturn('42'); + + $result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']); + + $this->assertTrue($result); + } + + public function testSharePasswordLinkInvalidSession(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $share = $this->createMock(IShare::class); + $share->method('getPassword')->willReturn('password'); + $share->method('getShareType')->willReturn(IShare::TYPE_LINK); + $share->method('getId')->willReturn('42'); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->with('GX9HSGQrGE') + ->willReturn($share); + + $this->shareManager->expects($this->once()) + ->method('checkPassword') + ->with( + $this->equalTo($share), + $this->equalTo('password') + )->willReturn(false); + + $this->session->method('exists')->with('public_link_authenticated')->willReturn(true); + $this->session->method('get')->with('public_link_authenticated')->willReturn('43'); + + $result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']); + + $this->assertFalse($result); + } + + + public function testSharePasswordMailInvalidSession(): void { + $this->request->method('getPathInfo') + ->willReturn('/dav/files/GX9HSGQrGE'); + + $share = $this->createMock(IShare::class); + $share->method('getPassword')->willReturn('password'); + $share->method('getShareType')->willReturn(IShare::TYPE_EMAIL); + $share->method('getId')->willReturn('42'); + + $this->shareManager->expects($this->once()) + ->method('getShareByToken') + ->with('GX9HSGQrGE') + ->willReturn($share); + + $this->shareManager->expects($this->once()) + ->method('checkPassword') + ->with( + $this->equalTo($share), + $this->equalTo('password') + )->willReturn(false); + + $this->session->method('exists')->with('public_link_authenticated')->willReturn(true); + $this->session->method('get')->with('public_link_authenticated')->willReturn('43'); + + $result = self::invokePrivate($this->auth, 'validateUserPass', ['username', 'password']); + + $this->assertFalse($result); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/QuotaPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/QuotaPluginTest.php new file mode 100644 index 00000000000..6fe2d6ccabe --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/QuotaPluginTest.php @@ -0,0 +1,152 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2013-2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre; + +use OC\Files\View; +use OCA\DAV\Connector\Sabre\QuotaPlugin; +use OCP\Files\FileInfo; +use Test\TestCase; + +class QuotaPluginTest extends TestCase { + private \Sabre\DAV\Server $server; + + private QuotaPlugin $plugin; + + private function init(int $quota, string $checkedPath = ''): void { + $view = $this->buildFileViewMock((string)$quota, $checkedPath); + $this->server = new \Sabre\DAV\Server(); + $this->plugin = new QuotaPlugin($view); + $this->plugin->initialize($this->server); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('lengthProvider')] + public function testLength(?int $expected, array $headers): void { + $this->init(0); + + $this->server->httpRequest = new \Sabre\HTTP\Request('POST', 'dummy.file', $headers); + $length = $this->plugin->getLength(); + $this->assertEquals($expected, $length); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('quotaOkayProvider')] + public function testCheckQuota(int $quota, array $headers): void { + $this->init($quota); + + $this->server->httpRequest = new \Sabre\HTTP\Request('POST', 'dummy.file', $headers); + $result = $this->plugin->checkQuota(''); + $this->assertTrue($result); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('quotaExceededProvider')] + public function testCheckExceededQuota(int $quota, array $headers): void { + $this->expectException(\Sabre\DAV\Exception\InsufficientStorage::class); + + $this->init($quota); + + $this->server->httpRequest = new \Sabre\HTTP\Request('POST', 'dummy.file', $headers); + $this->plugin->checkQuota(''); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('quotaOkayProvider')] + public function testCheckQuotaOnPath(int $quota, array $headers): void { + $this->init($quota, 'sub/test.txt'); + + $this->server->httpRequest = new \Sabre\HTTP\Request('POST', 'dummy.file', $headers); + $result = $this->plugin->checkQuota('/sub/test.txt'); + $this->assertTrue($result); + } + + public static function quotaOkayProvider(): array { + return [ + [1024, []], + [1024, ['X-EXPECTED-ENTITY-LENGTH' => '1024']], + [1024, ['CONTENT-LENGTH' => '512']], + [1024, ['OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512']], + + [FileInfo::SPACE_UNKNOWN, []], + [FileInfo::SPACE_UNKNOWN, ['X-EXPECTED-ENTITY-LENGTH' => '1024']], + [FileInfo::SPACE_UNKNOWN, ['CONTENT-LENGTH' => '512']], + [FileInfo::SPACE_UNKNOWN, ['OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512']], + + [FileInfo::SPACE_UNLIMITED, []], + [FileInfo::SPACE_UNLIMITED, ['X-EXPECTED-ENTITY-LENGTH' => '1024']], + [FileInfo::SPACE_UNLIMITED, ['CONTENT-LENGTH' => '512']], + [FileInfo::SPACE_UNLIMITED, ['OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512']], + ]; + } + + public static function quotaExceededProvider(): array { + return [ + [1023, ['X-EXPECTED-ENTITY-LENGTH' => '1024']], + [511, ['CONTENT-LENGTH' => '512']], + [2047, ['OC-TOTAL-LENGTH' => '2048', 'CONTENT-LENGTH' => '1024']], + ]; + } + + public static function lengthProvider(): array { + return [ + [null, []], + [1024, ['X-EXPECTED-ENTITY-LENGTH' => '1024']], + [512, ['CONTENT-LENGTH' => '512']], + [2048, ['OC-TOTAL-LENGTH' => '2048', 'CONTENT-LENGTH' => '1024']], + [4096, ['OC-TOTAL-LENGTH' => '2048', 'X-EXPECTED-ENTITY-LENGTH' => '4096']], + [null, ['X-EXPECTED-ENTITY-LENGTH' => 'A']], + [null, ['CONTENT-LENGTH' => 'A']], + [1024, ['OC-TOTAL-LENGTH' => 'A', 'CONTENT-LENGTH' => '1024']], + [1024, ['OC-TOTAL-LENGTH' => 'A', 'X-EXPECTED-ENTITY-LENGTH' => '1024']], + [2048, ['OC-TOTAL-LENGTH' => '2048', 'X-EXPECTED-ENTITY-LENGTH' => 'A']], + [2048, ['OC-TOTAL-LENGTH' => '2048', 'CONTENT-LENGTH' => 'A']], + ]; + } + + public static function quotaChunkedOkProvider(): array { + return [ + [1024, 0, ['X-EXPECTED-ENTITY-LENGTH' => '1024']], + [1024, 0, ['CONTENT-LENGTH' => '512']], + [1024, 0, ['OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512']], + // with existing chunks (allowed size = total length - chunk total size) + [400, 128, ['X-EXPECTED-ENTITY-LENGTH' => '512']], + [400, 128, ['CONTENT-LENGTH' => '512']], + [400, 128, ['OC-TOTAL-LENGTH' => '512', 'CONTENT-LENGTH' => '500']], + // \OCP\Files\FileInfo::SPACE-UNKNOWN = -2 + [-2, 0, ['X-EXPECTED-ENTITY-LENGTH' => '1024']], + [-2, 0, ['CONTENT-LENGTH' => '512']], + [-2, 0, ['OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512']], + [-2, 128, ['X-EXPECTED-ENTITY-LENGTH' => '1024']], + [-2, 128, ['CONTENT-LENGTH' => '512']], + [-2, 128, ['OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512']], + ]; + } + + public static function quotaChunkedFailProvider(): array { + return [ + [400, 0, ['X-EXPECTED-ENTITY-LENGTH' => '1024']], + [400, 0, ['CONTENT-LENGTH' => '512']], + [400, 0, ['OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512']], + // with existing chunks (allowed size = total length - chunk total size) + [380, 128, ['X-EXPECTED-ENTITY-LENGTH' => '512']], + [380, 128, ['CONTENT-LENGTH' => '512']], + [380, 128, ['OC-TOTAL-LENGTH' => '512', 'CONTENT-LENGTH' => '500']], + ]; + } + + private function buildFileViewMock(string $quota, string $checkedPath): View { + // mock filesystem + $view = $this->getMockBuilder(View::class) + ->onlyMethods(['free_space']) + ->disableOriginalConstructor() + ->getMock(); + $view->expects($this->any()) + ->method('free_space') + ->with($checkedPath) + ->willReturn($quota); + + return $view; + } +} diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/auth.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/Auth.php index b728a8f3bd8..b01807d5bbb 100644 --- a/apps/dav/tests/unit/connector/sabre/requesttest/auth.php +++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/Auth.php @@ -1,51 +1,30 @@ <?php + +declare(strict_types=1); /** - * @author Robin Appelman <icewind@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ +namespace OCA\DAV\Tests\unit\Connector\Sabre\RequestTest; -namespace OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest; - +use OCP\IUserSession; +use OCP\Server; use Sabre\DAV\Auth\Backend\BackendInterface; use Sabre\HTTP\RequestInterface; use Sabre\HTTP\ResponseInterface; class Auth implements BackendInterface { /** - * @var string - */ - private $user; - - /** - * @var string - */ - private $password; - - /** * Auth constructor. * * @param string $user * @param string $password */ - public function __construct($user, $password) { - $this->user = $user; - $this->password = $password; + public function __construct( + private $user, + private $password, + ) { } /** @@ -76,8 +55,8 @@ class Auth implements BackendInterface { * @param ResponseInterface $response * @return array */ - function check(RequestInterface $request, ResponseInterface $response) { - $userSession = \OC::$server->getUserSession(); + public function check(RequestInterface $request, ResponseInterface $response) { + $userSession = Server::get(IUserSession::class); $result = $userSession->login($this->user, $this->password); if ($result) { //we need to pass the user name, which may differ from login name @@ -87,7 +66,7 @@ class Auth implements BackendInterface { \OC::$server->getUserFolder($user); return [true, "principals/$user"]; } - return [false, "login failed"]; + return [false, 'login failed']; } /** @@ -111,7 +90,7 @@ class Auth implements BackendInterface { * @param ResponseInterface $response * @return void */ - function challenge(RequestInterface $request, ResponseInterface $response) { + public function challenge(RequestInterface $request, ResponseInterface $response): void { // TODO: Implement challenge() method. } } diff --git a/apps/dav/tests/unit/Connector/Sabre/RequestTest/DeleteTest.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/DeleteTest.php new file mode 100644 index 00000000000..7d3488e6b5a --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/DeleteTest.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre\RequestTest; + +use OCP\AppFramework\Http; +use OCP\Files\FileInfo; + +/** + * Class DeleteTest + * + * @group DB + * + * @package OCA\DAV\Tests\unit\Connector\Sabre\RequestTest + */ +class DeleteTest extends RequestTestCase { + public function testBasicUpload(): void { + $user = self::getUniqueID(); + $view = $this->setupUser($user, 'pass'); + + $view->file_put_contents('foo.txt', 'asd'); + $mount = $view->getMount('foo.txt'); + $internalPath = $view->getAbsolutePath(); + + // create a ghost file + $mount->getStorage()->unlink($mount->getInternalPath($internalPath)); + + // cache entry still exists + $this->assertInstanceOf(FileInfo::class, $view->getFileInfo('foo.txt')); + + $response = $this->request($view, $user, 'pass', 'DELETE', '/foo.txt'); + + $this->assertEquals(Http::STATUS_NO_CONTENT, $response->getStatus()); + + // no longer in the cache + $this->assertFalse($view->getFileInfo('foo.txt')); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/RequestTest/DownloadTest.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/DownloadTest.php new file mode 100644 index 00000000000..34171963ef0 --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/DownloadTest.php @@ -0,0 +1,57 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre\RequestTest; + +use OCP\AppFramework\Http; +use OCP\Lock\ILockingProvider; + +/** + * Class DownloadTest + * + * @group DB + * + * @package OCA\DAV\Tests\unit\Connector\Sabre\RequestTest + */ +class DownloadTest extends RequestTestCase { + public function testDownload(): void { + $user = self::getUniqueID(); + $view = $this->setupUser($user, 'pass'); + + $view->file_put_contents('foo.txt', 'bar'); + + $response = $this->request($view, $user, 'pass', 'GET', '/foo.txt'); + $this->assertEquals(Http::STATUS_OK, $response->getStatus()); + $this->assertEquals(stream_get_contents($response->getBody()), 'bar'); + } + + public function testDownloadWriteLocked(): void { + $user = self::getUniqueID(); + $view = $this->setupUser($user, 'pass'); + + $view->file_put_contents('foo.txt', 'bar'); + + $view->lockFile('/foo.txt', ILockingProvider::LOCK_EXCLUSIVE); + + $result = $this->request($view, $user, 'pass', 'GET', '/foo.txt', 'asd'); + $this->assertEquals(Http::STATUS_LOCKED, $result->getStatus()); + } + + public function testDownloadReadLocked(): void { + $user = self::getUniqueID(); + $view = $this->setupUser($user, 'pass'); + + $view->file_put_contents('foo.txt', 'bar'); + + $view->lockFile('/foo.txt', ILockingProvider::LOCK_SHARED); + + $response = $this->request($view, $user, 'pass', 'GET', '/foo.txt', 'asd'); + $this->assertEquals(Http::STATUS_OK, $response->getStatus()); + $this->assertEquals(stream_get_contents($response->getBody()), 'bar'); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/RequestTest/EncryptionMasterKeyUploadTest.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/EncryptionMasterKeyUploadTest.php new file mode 100644 index 00000000000..615490ddc92 --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/EncryptionMasterKeyUploadTest.php @@ -0,0 +1,37 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre\RequestTest; + +use OC\Files\View; +use OCP\IConfig; +use OCP\ITempManager; +use OCP\Server; +use Test\Traits\EncryptionTrait; + +/** + * Class EncryptionMasterKeyUploadTest + * + * @group DB + * + * @package OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest + */ +class EncryptionMasterKeyUploadTest extends UploadTest { + use EncryptionTrait; + + protected function setupUser($name, $password): View { + $this->createUser($name, $password); + $tmpFolder = Server::get(ITempManager::class)->getTemporaryFolder(); + $this->registerMount($name, '\OC\Files\Storage\Local', '/' . $name, ['datadir' => $tmpFolder]); + // we use the master key + Server::get(IConfig::class)->setAppValue('encryption', 'useMasterKey', '1'); + $this->setupForUser($name, $password); + $this->loginWithEncryption($name); + return new View('/' . $name . '/files'); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/RequestTest/EncryptionUploadTest.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/EncryptionUploadTest.php new file mode 100644 index 00000000000..efa7bb54cf8 --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/EncryptionUploadTest.php @@ -0,0 +1,37 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre\RequestTest; + +use OC\Files\View; +use OCP\IConfig; +use OCP\ITempManager; +use OCP\Server; +use Test\Traits\EncryptionTrait; + +/** + * Class EncryptionUploadTest + * + * @group DB + * + * @package OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest + */ +class EncryptionUploadTest extends UploadTest { + use EncryptionTrait; + + protected function setupUser($name, $password): View { + $this->createUser($name, $password); + $tmpFolder = Server::get(ITempManager::class)->getTemporaryFolder(); + $this->registerMount($name, '\OC\Files\Storage\Local', '/' . $name, ['datadir' => $tmpFolder]); + // we use per-user keys + Server::get(IConfig::class)->setAppValue('encryption', 'useMasterKey', '0'); + $this->setupForUser($name, $password); + $this->loginWithEncryption($name); + return new View('/' . $name . '/files'); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/RequestTest/ExceptionPlugin.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/ExceptionPlugin.php new file mode 100644 index 00000000000..0c53e4b1009 --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/ExceptionPlugin.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre\RequestTest; + +use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin; + +class ExceptionPlugin extends ExceptionLoggerPlugin { + /** + * @var \Throwable[] + */ + protected $exceptions = []; + + public function logException(\Throwable $ex): void { + $exceptionClass = get_class($ex); + if (!isset($this->nonFatalExceptions[$exceptionClass])) { + $this->exceptions[] = $ex; + } + } + + /** + * @return \Throwable[] + */ + public function getExceptions() { + return $this->exceptions; + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/RequestTest/PartFileInRootUploadTest.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/PartFileInRootUploadTest.php new file mode 100644 index 00000000000..e6fa489fb24 --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/PartFileInRootUploadTest.php @@ -0,0 +1,42 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre\RequestTest; + +use OC\AllConfig; +use OCP\IConfig; +use OCP\Server; + +/** + * Class PartFileInRootUploadTest + * + * @group DB + * + * @package OCA\DAV\Tests\unit\Connector\Sabre\RequestTest + */ +class PartFileInRootUploadTest extends UploadTest { + protected function setUp(): void { + $config = Server::get(IConfig::class); + $mockConfig = $this->createMock(IConfig::class); + $mockConfig->expects($this->any()) + ->method('getSystemValue') + ->willReturnCallback(function ($key, $default) use ($config) { + if ($key === 'part_file_in_storage') { + return false; + } else { + return $config->getSystemValue($key, $default); + } + }); + $this->overwriteService(AllConfig::class, $mockConfig); + parent::setUp(); + } + + protected function tearDown(): void { + $this->restoreService('AllConfig'); + parent::tearDown(); + } +} diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/requesttest.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/RequestTestCase.php index e3cdca5abfa..404dc7fa5d7 100644 --- a/apps/dav/tests/unit/connector/sabre/requesttest/requesttest.php +++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/RequestTestCase.php @@ -1,49 +1,37 @@ <?php + +declare(strict_types=1); /** - * @author Joas Schilling <nickvergessen@owncloud.com> - * @author Lukas Reschke <lukas@owncloud.com> - * @author Robin Appelman <icewind@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ +namespace OCA\DAV\Tests\unit\Connector\Sabre\RequestTest; -namespace OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest; - +use OC\Files\View; use OCA\DAV\Connector\Sabre\Server; use OCA\DAV\Connector\Sabre\ServerFactory; -use OC\Files\Mount\MountPoint; -use OC\Files\Storage\StorageFactory; -use OC\Files\Storage\Temporary; -use OC\Files\View; -use OCP\IUser; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\Mount\IMountManager; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IPreview; +use OCP\IRequest; +use OCP\IRequestId; +use OCP\ITagManager; +use OCP\ITempManager; +use OCP\IUserSession; +use OCP\L10N\IFactory; +use Psr\Log\LoggerInterface; use Sabre\HTTP\Request; use Test\TestCase; use Test\Traits\MountProviderTrait; use Test\Traits\UserTrait; -abstract class RequestTest extends TestCase { +abstract class RequestTestCase extends TestCase { use UserTrait; use MountProviderTrait; - - /** - * @var \OCA\DAV\Connector\Sabre\ServerFactory - */ - protected $serverFactory; + protected ServerFactory $serverFactory; protected function getStream($string) { $stream = fopen('php://temp', 'r+'); @@ -52,30 +40,33 @@ abstract class RequestTest extends TestCase { return $stream; } - protected function setUp() { + protected function setUp(): void { parent::setUp(); $this->serverFactory = new ServerFactory( - \OC::$server->getConfig(), - \OC::$server->getLogger(), - \OC::$server->getDatabaseConnection(), - \OC::$server->getUserSession(), - \OC::$server->getMountManager(), - \OC::$server->getTagManager(), - $this->getMock('\OCP\IRequest') + \OCP\Server::get(IConfig::class), + \OCP\Server::get(LoggerInterface::class), + \OCP\Server::get(IDBConnection::class), + \OCP\Server::get(IUserSession::class), + \OCP\Server::get(IMountManager::class), + \OCP\Server::get(ITagManager::class), + $this->createMock(IRequest::class), + \OCP\Server::get(IPreview::class), + \OCP\Server::get(IEventDispatcher::class), + \OCP\Server::get(IFactory::class)->get('dav'), ); } - protected function setupUser($name, $password) { + protected function setupUser($name, $password): View { $this->createUser($name, $password); - $tmpFolder = \OC::$server->getTempManager()->getTemporaryFolder(); + $tmpFolder = \OCP\Server::get(ITempManager::class)->getTemporaryFolder(); $this->registerMount($name, '\OC\Files\Storage\Local', '/' . $name, ['datadir' => $tmpFolder]); - $this->loginAsUser($name); + self::loginAsUser($name); return new View('/' . $name . '/files'); } /** - * @param \OC\Files\View $view the view to run the webdav server against + * @param View $view the view to run the webdav server against * @param string $user * @param string $password * @param string $method @@ -85,31 +76,36 @@ abstract class RequestTest extends TestCase { * @return \Sabre\HTTP\Response * @throws \Exception */ - protected function request($view, $user, $password, $method, $url, $body = null, $headers = null) { + protected function request($view, $user, $password, $method, $url, $body = null, $headers = []) { if (is_string($body)) { $body = $this->getStream($body); } $this->logout(); - $exceptionPlugin = new ExceptionPlugin('webdav', null); + $exceptionPlugin = new ExceptionPlugin('webdav', \OCP\Server::get(LoggerInterface::class)); $server = $this->getSabreServer($view, $user, $password, $exceptionPlugin); $request = new Request($method, $url, $headers, $body); // since sabre catches all exceptions we need to save them and throw them from outside the sabre server - $originalServer = $_SERVER; - + $serverParams = []; if (is_array($headers)) { foreach ($headers as $header => $value) { - $_SERVER['HTTP_' . strtoupper(str_replace('-', '_', $header))] = $value; + $serverParams['HTTP_' . strtoupper(str_replace('-', '_', $header))] = $value; } } + $ncRequest = new \OC\AppFramework\Http\Request([ + 'server' => $serverParams + ], $this->createMock(IRequestId::class), $this->createMock(IConfig::class), null); + + $this->overwriteService(IRequest::class, $ncRequest); $result = $this->makeRequest($server, $request); + $this->restoreService(IRequest::class); + foreach ($exceptionPlugin->getExceptions() as $exception) { throw $exception; } - $_SERVER = $originalServer; return $result; } @@ -135,8 +131,9 @@ abstract class RequestTest extends TestCase { */ protected function getSabreServer(View $view, $user, $password, ExceptionPlugin $exceptionPlugin) { $authBackend = new Auth($user, $password); + $authPlugin = new \Sabre\DAV\Auth\Plugin($authBackend); - $server = $this->serverFactory->createServer('/', 'dummy', $authBackend, function () use ($view) { + $server = $this->serverFactory->createServer(false, '/', 'dummy', $authPlugin, function () use ($view) { return $view; }); $server->addPlugin($exceptionPlugin); diff --git a/apps/dav/tests/unit/Connector/Sabre/RequestTest/Sapi.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/Sapi.php new file mode 100644 index 00000000000..08d774e56b8 --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/Sapi.php @@ -0,0 +1,57 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre\RequestTest; + +use Sabre\HTTP\Request; +use Sabre\HTTP\Response; + +class Sapi { + /** + * @var \Sabre\HTTP\Response + */ + private $response; + + /** + * This static method will create a new Request object, based on the + * current PHP request. + * + * @return \Sabre\HTTP\Request + */ + public function getRequest() { + return $this->request; + } + + public function __construct( + private Request $request, + ) { + } + + /** + * @param \Sabre\HTTP\Response $response + * @return void + */ + public function sendResponse(Response $response): void { + // we need to copy the body since we close the source stream + $copyStream = fopen('php://temp', 'r+'); + if (is_string($response->getBody())) { + fwrite($copyStream, $response->getBody()); + } elseif (is_resource($response->getBody())) { + stream_copy_to_stream($response->getBody(), $copyStream); + } + rewind($copyStream); + $this->response = new Response($response->getStatus(), $response->getHeaders(), $copyStream); + } + + /** + * @return \Sabre\HTTP\Response + */ + public function getResponse() { + return $this->response; + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/RequestTest/UploadTest.php b/apps/dav/tests/unit/Connector/Sabre/RequestTest/UploadTest.php new file mode 100644 index 00000000000..5c6d0f03334 --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/RequestTest/UploadTest.php @@ -0,0 +1,78 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre\RequestTest; + +use OCP\AppFramework\Http; +use OCP\Lock\ILockingProvider; + +/** + * Class UploadTest + * + * @group DB + * + * @package OCA\DAV\Tests\unit\Connector\Sabre\RequestTest + */ +class UploadTest extends RequestTestCase { + public function testBasicUpload(): void { + $user = self::getUniqueID(); + $view = $this->setupUser($user, 'pass'); + + $this->assertFalse($view->file_exists('foo.txt')); + $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt', 'asd'); + + $this->assertEquals(Http::STATUS_CREATED, $response->getStatus()); + $this->assertTrue($view->file_exists('foo.txt')); + $this->assertEquals('asd', $view->file_get_contents('foo.txt')); + + $info = $view->getFileInfo('foo.txt'); + $this->assertInstanceOf('\OC\Files\FileInfo', $info); + $this->assertEquals(3, $info->getSize()); + } + + public function testUploadOverWrite(): void { + $user = self::getUniqueID(); + $view = $this->setupUser($user, 'pass'); + + $view->file_put_contents('foo.txt', 'foobar'); + + $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt', 'asd'); + + $this->assertEquals(Http::STATUS_NO_CONTENT, $response->getStatus()); + $this->assertEquals('asd', $view->file_get_contents('foo.txt')); + + $info = $view->getFileInfo('foo.txt'); + $this->assertInstanceOf('\OC\Files\FileInfo', $info); + $this->assertEquals(3, $info->getSize()); + } + + public function testUploadOverWriteReadLocked(): void { + $user = self::getUniqueID(); + $view = $this->setupUser($user, 'pass'); + + $view->file_put_contents('foo.txt', 'bar'); + + $view->lockFile('/foo.txt', ILockingProvider::LOCK_SHARED); + + $result = $this->request($view, $user, 'pass', 'PUT', '/foo.txt', 'asd'); + $this->assertEquals(Http::STATUS_LOCKED, $result->getStatus()); + } + + public function testUploadOverWriteWriteLocked(): void { + $user = self::getUniqueID(); + $view = $this->setupUser($user, 'pass'); + $this->loginAsUser($user); + + $view->file_put_contents('foo.txt', 'bar'); + + $view->lockFile('/foo.txt', ILockingProvider::LOCK_EXCLUSIVE); + + $result = $this->request($view, $user, 'pass', 'PUT', '/foo.txt', 'asd'); + $this->assertEquals(Http::STATUS_LOCKED, $result->getStatus()); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/SharesPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/SharesPluginTest.php new file mode 100644 index 00000000000..33f579eb913 --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/SharesPluginTest.php @@ -0,0 +1,282 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre; + +use OCA\DAV\Connector\Sabre\Directory; +use OCA\DAV\Connector\Sabre\File; +use OCA\DAV\Connector\Sabre\Node; +use OCA\DAV\Connector\Sabre\SharesPlugin; +use OCA\DAV\Upload\UploadFile; +use OCP\Files\Folder; +use OCP\IUser; +use OCP\IUserSession; +use OCP\Share\IManager; +use OCP\Share\IShare; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\Tree; + +class SharesPluginTest extends \Test\TestCase { + public const SHARETYPES_PROPERTYNAME = SharesPlugin::SHARETYPES_PROPERTYNAME; + + private \Sabre\DAV\Server $server; + private \Sabre\DAV\Tree&MockObject $tree; + private \OCP\Share\IManager&MockObject $shareManager; + private Folder&MockObject $userFolder; + private SharesPlugin $plugin; + + protected function setUp(): void { + parent::setUp(); + $this->server = new \Sabre\DAV\Server(); + $this->tree = $this->createMock(Tree::class); + $this->shareManager = $this->createMock(IManager::class); + $user = $this->createMock(IUser::class); + $user->expects($this->once()) + ->method('getUID') + ->willReturn('user1'); + $userSession = $this->createMock(IUserSession::class); + $userSession->expects($this->once()) + ->method('getUser') + ->willReturn($user); + $this->userFolder = $this->createMock(Folder::class); + + $this->plugin = new SharesPlugin( + $this->tree, + $userSession, + $this->userFolder, + $this->shareManager + ); + $this->plugin->initialize($this->server); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('sharesGetPropertiesDataProvider')] + public function testGetProperties(array $shareTypes): void { + $sabreNode = $this->createMock(Node::class); + $sabreNode->expects($this->any()) + ->method('getId') + ->willReturn(123); + $sabreNode->expects($this->any()) + ->method('getPath') + ->willReturn('/subdir'); + + // node API nodes + $node = $this->createMock(Folder::class); + + $sabreNode->method('getNode') + ->willReturn($node); + + $this->shareManager->expects($this->any()) + ->method('getSharesBy') + ->with( + $this->equalTo('user1'), + $this->anything(), + $this->equalTo($node), + $this->equalTo(false), + $this->equalTo(-1) + ) + ->willReturnCallback(function ($userId, $requestedShareType, $node, $flag, $limit) use ($shareTypes) { + if (in_array($requestedShareType, $shareTypes)) { + $share = $this->createMock(IShare::class); + $share->method('getShareType') + ->willReturn($requestedShareType); + return [$share]; + } + return []; + }); + + $this->shareManager->expects($this->any()) + ->method('getSharedWith') + ->with( + $this->equalTo('user1'), + $this->anything(), + $this->equalTo($node), + $this->equalTo(-1) + ) + ->willReturn([]); + + $propFind = new \Sabre\DAV\PropFind( + '/dummyPath', + [self::SHARETYPES_PROPERTYNAME], + 0 + ); + + $this->plugin->handleGetProperties( + $propFind, + $sabreNode + ); + + $result = $propFind->getResultForMultiStatus(); + + $this->assertEmpty($result[404]); + unset($result[404]); + $this->assertEquals($shareTypes, $result[200][self::SHARETYPES_PROPERTYNAME]->getShareTypes()); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('sharesGetPropertiesDataProvider')] + public function testPreloadThenGetProperties(array $shareTypes): void { + $sabreNode1 = $this->createMock(File::class); + $sabreNode1->method('getId') + ->willReturn(111); + $sabreNode2 = $this->createMock(File::class); + $sabreNode2->method('getId') + ->willReturn(222); + $sabreNode2->method('getPath') + ->willReturn('/subdir/foo'); + + $sabreNode = $this->createMock(Directory::class); + $sabreNode->method('getId') + ->willReturn(123); + // never, because we use getDirectoryListing from the Node API instead + $sabreNode->expects($this->never()) + ->method('getChildren'); + $sabreNode->expects($this->any()) + ->method('getPath') + ->willReturn('/subdir'); + + // node API nodes + $node = $this->createMock(Folder::class); + $node->method('getId') + ->willReturn(123); + $node1 = $this->createMock(\OC\Files\Node\File::class); + $node1->method('getId') + ->willReturn(111); + $node2 = $this->createMock(\OC\Files\Node\File::class); + $node2->method('getId') + ->willReturn(222); + + $sabreNode->method('getNode') + ->willReturn($node); + $sabreNode1->method('getNode') + ->willReturn($node1); + $sabreNode2->method('getNode') + ->willReturn($node2); + + $dummyShares = array_map(function ($type) { + $share = $this->createMock(IShare::class); + $share->expects($this->any()) + ->method('getShareType') + ->willReturn($type); + return $share; + }, $shareTypes); + + $this->shareManager->expects($this->any()) + ->method('getSharesBy') + ->with( + $this->equalTo('user1'), + $this->anything(), + $this->anything(), + $this->equalTo(false), + $this->equalTo(-1) + ) + ->willReturnCallback(function ($userId, $requestedShareType, $node, $flag, $limit) use ($shareTypes, $dummyShares) { + if ($node->getId() === 111 && in_array($requestedShareType, $shareTypes)) { + foreach ($dummyShares as $dummyShare) { + if ($dummyShare->getShareType() === $requestedShareType) { + return [$dummyShare]; + } + } + } + + return []; + }); + + $this->shareManager->expects($this->any()) + ->method('getSharedWith') + ->with( + $this->equalTo('user1'), + $this->anything(), + $this->equalTo($node), + $this->equalTo(-1) + ) + ->willReturn([]); + + $this->shareManager->expects($this->any()) + ->method('getSharesInFolder') + ->with( + $this->equalTo('user1'), + $this->anything(), + $this->equalTo(true) + ) + ->willReturnCallback(function ($userId, $node, $flag) use ($shareTypes, $dummyShares) { + return [111 => $dummyShares]; + }); + + // simulate sabre recursive PROPFIND traversal + $propFindRoot = new \Sabre\DAV\PropFind( + '/subdir', + [self::SHARETYPES_PROPERTYNAME], + 1 + ); + $propFind1 = new \Sabre\DAV\PropFind( + '/subdir/test.txt', + [self::SHARETYPES_PROPERTYNAME], + 0 + ); + $propFind2 = new \Sabre\DAV\PropFind( + '/subdir/test2.txt', + [self::SHARETYPES_PROPERTYNAME], + 0 + ); + + $this->server->emit('preloadCollection', [$propFindRoot, $sabreNode]); + $this->plugin->handleGetProperties( + $propFindRoot, + $sabreNode + ); + $this->plugin->handleGetProperties( + $propFind1, + $sabreNode1 + ); + $this->plugin->handleGetProperties( + $propFind2, + $sabreNode2 + ); + + $result = $propFind1->getResultForMultiStatus(); + + $this->assertEmpty($result[404]); + unset($result[404]); + $this->assertEquals($shareTypes, $result[200][self::SHARETYPES_PROPERTYNAME]->getShareTypes()); + } + + public static function sharesGetPropertiesDataProvider(): array { + return [ + [[]], + [[IShare::TYPE_USER]], + [[IShare::TYPE_GROUP]], + [[IShare::TYPE_LINK]], + [[IShare::TYPE_REMOTE]], + [[IShare::TYPE_ROOM]], + [[IShare::TYPE_DECK]], + [[IShare::TYPE_SCIENCEMESH]], + [[IShare::TYPE_USER, IShare::TYPE_GROUP]], + [[IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_LINK]], + [[IShare::TYPE_USER, IShare::TYPE_LINK]], + [[IShare::TYPE_GROUP, IShare::TYPE_LINK]], + [[IShare::TYPE_USER, IShare::TYPE_REMOTE]], + ]; + } + + public function testGetPropertiesSkipChunks(): void { + $sabreNode = $this->createMock(UploadFile::class); + + $propFind = new \Sabre\DAV\PropFind( + '/dummyPath', + [self::SHARETYPES_PROPERTYNAME], + 0 + ); + + $this->plugin->handleGetProperties( + $propFind, + $sabreNode + ); + + $result = $propFind->getResultForMultiStatus(); + $this->assertCount(1, $result[404]); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/TagsPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/TagsPluginTest.php new file mode 100644 index 00000000000..554a4a1424e --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/TagsPluginTest.php @@ -0,0 +1,410 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2014-2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre; + +use OCA\DAV\Connector\Sabre\Directory; +use OCA\DAV\Connector\Sabre\File; +use OCA\DAV\Connector\Sabre\Node; +use OCA\DAV\Connector\Sabre\TagList; +use OCA\DAV\Connector\Sabre\TagsPlugin; +use OCA\DAV\Upload\UploadFile; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\ITagManager; +use OCP\ITags; +use OCP\IUser; +use OCP\IUserSession; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\Tree; + +class TagsPluginTest extends \Test\TestCase { + public const TAGS_PROPERTYNAME = TagsPlugin::TAGS_PROPERTYNAME; + public const FAVORITE_PROPERTYNAME = TagsPlugin::FAVORITE_PROPERTYNAME; + public const TAG_FAVORITE = TagsPlugin::TAG_FAVORITE; + + private \Sabre\DAV\Server $server; + private Tree&MockObject $tree; + private ITagManager&MockObject $tagManager; + private ITags&MockObject $tagger; + private IEventDispatcher&MockObject $eventDispatcher; + private IUserSession&MockObject $userSession; + private TagsPlugin $plugin; + + protected function setUp(): void { + parent::setUp(); + + $this->server = new \Sabre\DAV\Server(); + $this->tree = $this->createMock(Tree::class); + $this->tagger = $this->createMock(ITags::class); + $this->tagManager = $this->createMock(ITagManager::class); + $this->eventDispatcher = $this->createMock(IEventDispatcher::class); + $user = $this->createMock(IUser::class); + + $this->userSession = $this->createMock(IUserSession::class); + $this->userSession->expects($this->any()) + ->method('getUser') + ->withAnyParameters() + ->willReturn($user); + $this->tagManager->expects($this->any()) + ->method('load') + ->with('files') + ->willReturn($this->tagger); + $this->plugin = new TagsPlugin($this->tree, $this->tagManager, $this->eventDispatcher, $this->userSession); + $this->plugin->initialize($this->server); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('tagsGetPropertiesDataProvider')] + public function testGetProperties(array $tags, array $requestedProperties, array $expectedProperties): void { + $node = $this->createMock(Node::class); + $node->expects($this->any()) + ->method('getId') + ->willReturn(123); + + $expectedCallCount = 0; + if (count($requestedProperties) > 0) { + $expectedCallCount = 1; + } + + $this->tagger->expects($this->exactly($expectedCallCount)) + ->method('getTagsForObjects') + ->with($this->equalTo([123])) + ->willReturn([123 => $tags]); + + $propFind = new \Sabre\DAV\PropFind( + '/dummyPath', + $requestedProperties, + 0 + ); + + $this->plugin->handleGetProperties( + $propFind, + $node + ); + + $result = $propFind->getResultForMultiStatus(); + + $this->assertEmpty($result[404]); + unset($result[404]); + $this->assertEquals($expectedProperties, $result); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('tagsGetPropertiesDataProvider')] + public function testPreloadThenGetProperties(array $tags, array $requestedProperties, array $expectedProperties): void { + $node1 = $this->createMock(File::class); + $node1->expects($this->any()) + ->method('getId') + ->willReturn(111); + $node2 = $this->createMock(File::class); + $node2->expects($this->any()) + ->method('getId') + ->willReturn(222); + + $expectedCallCount = 0; + if (count($requestedProperties) > 0) { + // this guarantees that getTagsForObjects + // is only called once and then the tags + // are cached + $expectedCallCount = 1; + } + + $node = $this->createMock(Directory::class); + $node->expects($this->any()) + ->method('getId') + ->willReturn(123); + $node->expects($this->exactly($expectedCallCount)) + ->method('getChildren') + ->willReturn([$node1, $node2]); + + $this->tagger->expects($this->exactly($expectedCallCount)) + ->method('getTagsForObjects') + ->with($this->equalTo([123, 111, 222])) + ->willReturn( + [ + 111 => $tags, + 123 => $tags + ] + ); + + // simulate sabre recursive PROPFIND traversal + $propFindRoot = new \Sabre\DAV\PropFind( + '/subdir', + $requestedProperties, + 1 + ); + $propFind1 = new \Sabre\DAV\PropFind( + '/subdir/test.txt', + $requestedProperties, + 0 + ); + $propFind2 = new \Sabre\DAV\PropFind( + '/subdir/test2.txt', + $requestedProperties, + 0 + ); + + $this->server->emit('preloadCollection', [$propFindRoot, $node]); + + $this->plugin->handleGetProperties( + $propFindRoot, + $node + ); + $this->plugin->handleGetProperties( + $propFind1, + $node1 + ); + $this->plugin->handleGetProperties( + $propFind2, + $node2 + ); + + $result = $propFind1->getResultForMultiStatus(); + + $this->assertEmpty($result[404]); + unset($result[404]); + $this->assertEquals($expectedProperties, $result); + } + + public static function tagsGetPropertiesDataProvider(): array { + return [ + // request both, receive both + [ + ['tag1', 'tag2', self::TAG_FAVORITE], + [self::TAGS_PROPERTYNAME, self::FAVORITE_PROPERTYNAME], + [ + 200 => [ + self::TAGS_PROPERTYNAME => new TagList(['tag1', 'tag2']), + self::FAVORITE_PROPERTYNAME => true, + ] + ] + ], + // request tags alone + [ + ['tag1', 'tag2', self::TAG_FAVORITE], + [self::TAGS_PROPERTYNAME], + [ + 200 => [ + self::TAGS_PROPERTYNAME => new TagList(['tag1', 'tag2']), + ] + ] + ], + // request fav alone + [ + ['tag1', 'tag2', self::TAG_FAVORITE], + [self::FAVORITE_PROPERTYNAME], + [ + 200 => [ + self::FAVORITE_PROPERTYNAME => true, + ] + ] + ], + // request none + [ + ['tag1', 'tag2', self::TAG_FAVORITE], + [], + [ + 200 => [] + ], + ], + // request both with none set, receive both + [ + [], + [self::TAGS_PROPERTYNAME, self::FAVORITE_PROPERTYNAME], + [ + 200 => [ + self::TAGS_PROPERTYNAME => new TagList([]), + self::FAVORITE_PROPERTYNAME => false, + ] + ] + ], + ]; + } + + public function testGetPropertiesSkipChunks(): void { + $sabreNode = $this->createMock(UploadFile::class); + + $propFind = new \Sabre\DAV\PropFind( + '/dummyPath', + [self::TAGS_PROPERTYNAME, self::TAG_FAVORITE], + 0 + ); + + $this->plugin->handleGetProperties( + $propFind, + $sabreNode + ); + + $result = $propFind->getResultForMultiStatus(); + $this->assertCount(2, $result[404]); + } + + public function testUpdateTags(): void { + // this test will replace the existing tags "tagremove" with "tag1" and "tag2" + // and keep "tagkeep" + $node = $this->createMock(Node::class); + $node->expects($this->any()) + ->method('getId') + ->willReturn(123); + + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->with('/dummypath') + ->willReturn($node); + + $this->tagger->expects($this->once()) + ->method('getTagsForObjects') + ->with($this->equalTo([123])) + ->willReturn([123 => ['tagkeep', 'tagremove', self::TAG_FAVORITE]]); + + // then tag as tag1 and tag2 + $calls = [ + [123, 'tag1'], + [123, 'tag2'], + ]; + $this->tagger->expects($this->exactly(count($calls))) + ->method('tagAs') + ->willReturnCallback(function () use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + }); + + // it will untag tag3 + $this->tagger->expects($this->once()) + ->method('unTag') + ->with(123, 'tagremove'); + + // properties to set + $propPatch = new \Sabre\DAV\PropPatch([ + self::TAGS_PROPERTYNAME => new TagList(['tag1', 'tag2', 'tagkeep']) + ]); + + $this->plugin->handleUpdateProperties( + '/dummypath', + $propPatch + ); + + $propPatch->commit(); + + // all requested properties removed, as they were processed already + $this->assertEmpty($propPatch->getRemainingMutations()); + + $result = $propPatch->getResult(); + $this->assertEquals(200, $result[self::TAGS_PROPERTYNAME]); + $this->assertArrayNotHasKey(self::FAVORITE_PROPERTYNAME, $result); + } + + public function testUpdateTagsFromScratch(): void { + $node = $this->createMock(Node::class); + $node->expects($this->any()) + ->method('getId') + ->willReturn(123); + + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->with('/dummypath') + ->willReturn($node); + + $this->tagger->expects($this->once()) + ->method('getTagsForObjects') + ->with($this->equalTo([123])) + ->willReturn([]); + + // then tag as tag1 and tag2 + $calls = [ + [123, 'tag1'], + [123, 'tag2'], + ]; + $this->tagger->expects($this->exactly(count($calls))) + ->method('tagAs') + ->willReturnCallback(function () use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + }); + + // properties to set + $propPatch = new \Sabre\DAV\PropPatch([ + self::TAGS_PROPERTYNAME => new TagList(['tag1', 'tag2']) + ]); + + $this->plugin->handleUpdateProperties( + '/dummypath', + $propPatch + ); + + $propPatch->commit(); + + // all requested properties removed, as they were processed already + $this->assertEmpty($propPatch->getRemainingMutations()); + + $result = $propPatch->getResult(); + $this->assertEquals(200, $result[self::TAGS_PROPERTYNAME]); + $this->assertArrayNotHasKey(self::FAVORITE_PROPERTYNAME, $result); + } + + public function testUpdateFav(): void { + // this test will replace the existing tags "tagremove" with "tag1" and "tag2" + // and keep "tagkeep" + $node = $this->createMock(Node::class); + $node->expects($this->any()) + ->method('getId') + ->willReturn(123); + + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->with('/dummypath') + ->willReturn($node); + + // set favorite tag + $this->tagger->expects($this->once()) + ->method('tagAs') + ->with(123, self::TAG_FAVORITE); + + // properties to set + $propPatch = new \Sabre\DAV\PropPatch([ + self::FAVORITE_PROPERTYNAME => true + ]); + + $this->plugin->handleUpdateProperties( + '/dummypath', + $propPatch + ); + + $propPatch->commit(); + + // all requested properties removed, as they were processed already + $this->assertEmpty($propPatch->getRemainingMutations()); + + $result = $propPatch->getResult(); + $this->assertArrayNotHasKey(self::TAGS_PROPERTYNAME, $result); + $this->assertEquals(200, $result[self::FAVORITE_PROPERTYNAME]); + + // unfavorite now + // set favorite tag + $this->tagger->expects($this->once()) + ->method('unTag') + ->with(123, self::TAG_FAVORITE); + + // properties to set + $propPatch = new \Sabre\DAV\PropPatch([ + self::FAVORITE_PROPERTYNAME => false + ]); + + $this->plugin->handleUpdateProperties( + '/dummypath', + $propPatch + ); + + $propPatch->commit(); + + // all requested properties removed, as they were processed already + $this->assertEmpty($propPatch->getRemainingMutations()); + + $result = $propPatch->getResult(); + $this->assertArrayNotHasKey(self::TAGS_PROPERTYNAME, $result); + $this->assertEquals(200, $result[self::FAVORITE_PROPERTYNAME]); + } +} diff --git a/apps/dav/tests/unit/Controller/BirthdayCalendarControllerTest.php b/apps/dav/tests/unit/Controller/BirthdayCalendarControllerTest.php new file mode 100644 index 00000000000..9aa0ef3a2a7 --- /dev/null +++ b/apps/dav/tests/unit/Controller/BirthdayCalendarControllerTest.php @@ -0,0 +1,96 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\DAV\Controller; + +use OCA\DAV\BackgroundJob\GenerateBirthdayCalendarBackgroundJob; +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\Controller\BirthdayCalendarController; +use OCP\AppFramework\Http\JSONResponse; +use OCP\BackgroundJob\IJobList; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IRequest; +use OCP\IUser; +use OCP\IUserManager; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class BirthdayCalendarControllerTest extends TestCase { + private IConfig&MockObject $config; + private IRequest&MockObject $request; + private IDBConnection&MockObject $db; + private IJobList&MockObject $jobList; + private IUserManager&MockObject $userManager; + private CalDavBackend&MockObject $caldav; + private BirthdayCalendarController $controller; + + protected function setUp(): void { + parent::setUp(); + + $this->config = $this->createMock(IConfig::class); + $this->request = $this->createMock(IRequest::class); + $this->db = $this->createMock(IDBConnection::class); + $this->jobList = $this->createMock(IJobList::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->caldav = $this->createMock(CalDavBackend::class); + + $this->controller = new BirthdayCalendarController('dav', + $this->request, $this->db, $this->config, $this->jobList, + $this->userManager, $this->caldav); + } + + public function testEnable(): void { + $this->config->expects($this->once()) + ->method('setAppValue') + ->with('dav', 'generateBirthdayCalendar', 'yes'); + + $this->userManager->expects($this->once()) + ->method('callForSeenUsers') + ->willReturnCallback(function ($closure): void { + $user1 = $this->createMock(IUser::class); + $user1->method('getUID')->willReturn('uid1'); + $user2 = $this->createMock(IUser::class); + $user2->method('getUID')->willReturn('uid2'); + $user3 = $this->createMock(IUser::class); + $user3->method('getUID')->willReturn('uid3'); + + $closure($user1); + $closure($user2); + $closure($user3); + }); + + $calls = [ + [GenerateBirthdayCalendarBackgroundJob::class, ['userId' => 'uid1']], + [GenerateBirthdayCalendarBackgroundJob::class, ['userId' => 'uid2']], + [GenerateBirthdayCalendarBackgroundJob::class, ['userId' => 'uid3']], + ]; + $this->jobList->expects($this->exactly(3)) + ->method('add') + ->willReturnCallback(function () use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + }); + + $response = $this->controller->enable(); + $this->assertInstanceOf(JSONResponse::class, $response); + } + + public function testDisable(): void { + $this->config->expects($this->once()) + ->method('setAppValue') + ->with('dav', 'generateBirthdayCalendar', 'no'); + $this->jobList->expects($this->once()) + ->method('remove') + ->with(GenerateBirthdayCalendarBackgroundJob::class); + $this->caldav->expects($this->once()) + ->method('deleteAllBirthdayCalendars'); + + $response = $this->controller->disable(); + $this->assertInstanceOf(JSONResponse::class, $response); + } +} diff --git a/apps/dav/tests/unit/Controller/DirectControllerTest.php b/apps/dav/tests/unit/Controller/DirectControllerTest.php new file mode 100644 index 00000000000..837adde1da7 --- /dev/null +++ b/apps/dav/tests/unit/Controller/DirectControllerTest.php @@ -0,0 +1,138 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\DAV\Controller; + +use OCA\DAV\Controller\DirectController; +use OCA\DAV\Db\Direct; +use OCA\DAV\Db\DirectMapper; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCS\OCSBadRequestException; +use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\File; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\IRequest; +use OCP\IURLGenerator; +use OCP\Security\ISecureRandom; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class DirectControllerTest extends TestCase { + private IRootFolder&MockObject $rootFolder; + private DirectMapper&MockObject $directMapper; + private ISecureRandom&MockObject $random; + private ITimeFactory&MockObject $timeFactory; + private IURLGenerator&MockObject $urlGenerator; + private IEventDispatcher&MockObject $eventDispatcher; + + private DirectController $controller; + + protected function setUp(): void { + parent::setUp(); + + $this->rootFolder = $this->createMock(IRootFolder::class); + $this->directMapper = $this->createMock(DirectMapper::class); + $this->random = $this->createMock(ISecureRandom::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + $this->eventDispatcher = $this->createMock(IEventDispatcher::class); + + $this->controller = new DirectController( + 'dav', + $this->createMock(IRequest::class), + $this->rootFolder, + 'awesomeUser', + $this->directMapper, + $this->random, + $this->timeFactory, + $this->urlGenerator, + $this->eventDispatcher + ); + } + + public function testGetUrlNonExistingFileId(): void { + $userFolder = $this->createMock(Folder::class); + $this->rootFolder->method('getUserFolder') + ->with('awesomeUser') + ->willReturn($userFolder); + + $userFolder->method('getById') + ->with(101) + ->willReturn([]); + + $this->expectException(OCSNotFoundException::class); + $this->controller->getUrl(101); + } + + public function testGetUrlForFolder(): void { + $userFolder = $this->createMock(Folder::class); + $this->rootFolder->method('getUserFolder') + ->with('awesomeUser') + ->willReturn($userFolder); + + $folder = $this->createMock(Folder::class); + + $userFolder->method('getFirstNodeById') + ->with(101) + ->willReturn($folder); + + $this->expectException(OCSBadRequestException::class); + $this->controller->getUrl(101); + } + + public function testGetUrlValid(): void { + $userFolder = $this->createMock(Folder::class); + $this->rootFolder->method('getUserFolder') + ->with('awesomeUser') + ->willReturn($userFolder); + + $file = $this->createMock(File::class); + + $this->timeFactory->method('getTime') + ->willReturn(42); + + $userFolder->method('getFirstNodeById') + ->with(101) + ->willReturn($file); + + $userFolder->method('getRelativePath') + ->willReturn('/path'); + + $this->random->method('generate') + ->with( + 60, + ISecureRandom::CHAR_ALPHANUMERIC + )->willReturn('superduperlongtoken'); + + $this->directMapper->expects($this->once()) + ->method('insert') + ->willReturnCallback(function (Direct $direct) { + $this->assertSame('awesomeUser', $direct->getUserId()); + $this->assertSame(101, $direct->getFileId()); + $this->assertSame('superduperlongtoken', $direct->getToken()); + $this->assertSame(42 + 60 * 60 * 8, $direct->getExpiration()); + + return $direct; + }); + + $this->urlGenerator->method('getAbsoluteURL') + ->willReturnCallback(function (string $url) { + return 'https://my.nextcloud/' . $url; + }); + + $result = $this->controller->getUrl(101); + + $this->assertInstanceOf(DataResponse::class, $result); + $this->assertSame([ + 'url' => 'https://my.nextcloud/remote.php/direct/superduperlongtoken', + ], $result->getData()); + } +} diff --git a/apps/dav/tests/unit/Controller/InvitationResponseControllerTest.php b/apps/dav/tests/unit/Controller/InvitationResponseControllerTest.php new file mode 100644 index 00000000000..15b18d6c1b1 --- /dev/null +++ b/apps/dav/tests/unit/Controller/InvitationResponseControllerTest.php @@ -0,0 +1,461 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Tests\unit\DAV\Controller; + +use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer; +use OCA\DAV\Controller\InvitationResponseController; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\DB\IResult; +use OCP\DB\QueryBuilder\IExpressionBuilder; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use OCP\IRequest; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\VObject\ITip\Message; +use Test\TestCase; + +class InvitationResponseControllerTest extends TestCase { + private IDBConnection&MockObject $dbConnection; + private IRequest&MockObject $request; + private ITimeFactory&MockObject $timeFactory; + private InvitationResponseServer&MockObject $responseServer; + private InvitationResponseController $controller; + + protected function setUp(): void { + parent::setUp(); + + $this->dbConnection = $this->createMock(IDBConnection::class); + $this->request = $this->createMock(IRequest::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->responseServer = $this->createMock(InvitationResponseServer::class); + + $this->controller = new InvitationResponseController( + 'appName', + $this->request, + $this->dbConnection, + $this->timeFactory, + $this->responseServer + ); + } + + public static function attendeeProvider(): array { + return [ + 'local attendee' => [false], + 'external attendee' => [true] + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('attendeeProvider')] + public function testAccept(bool $isExternalAttendee): void { + $this->buildQueryExpects('TOKEN123', [ + 'id' => 0, + 'uid' => 'this-is-the-events-uid', + 'recurrenceid' => null, + 'attendee' => 'mailto:attendee@foo.bar', + 'organizer' => 'mailto:organizer@foo.bar', + 'sequence' => null, + 'token' => 'TOKEN123', + 'expiration' => 420000, + ], 1337); + + $expected = <<<EOF +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN +METHOD:REPLY +BEGIN:VEVENT +ATTENDEE;PARTSTAT=ACCEPTED:mailto:attendee@foo.bar +ORGANIZER:mailto:organizer@foo.bar +UID:this-is-the-events-uid +SEQUENCE:0 +REQUEST-STATUS:2.0;Success +DTSTAMP:19700101T002217Z +END:VEVENT +END:VCALENDAR + +EOF; + $expected = preg_replace('~\R~u', "\r\n", $expected); + + $called = false; + $this->responseServer->expects($this->once()) + ->method('handleITipMessage') + ->willReturnCallback(function (Message $iTipMessage) use (&$called, $isExternalAttendee, $expected): void { + $called = true; + $this->assertEquals('this-is-the-events-uid', $iTipMessage->uid); + $this->assertEquals('VEVENT', $iTipMessage->component); + $this->assertEquals('REPLY', $iTipMessage->method); + $this->assertEquals(null, $iTipMessage->sequence); + $this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->sender); + if ($isExternalAttendee) { + $this->assertEquals('mailto:organizer@foo.bar', $iTipMessage->recipient); + } else { + $this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->recipient); + } + + $iTipMessage->scheduleStatus = '1.2;Message delivered locally'; + + $this->assertEquals($expected, $iTipMessage->message->serialize()); + }); + $this->responseServer->expects($this->once()) + ->method('isExternalAttendee') + ->willReturn($isExternalAttendee); + + $response = $this->controller->accept('TOKEN123'); + $this->assertInstanceOf(TemplateResponse::class, $response); + $this->assertEquals('schedule-response-success', $response->getTemplateName()); + $this->assertEquals([], $response->getParams()); + $this->assertTrue($called); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('attendeeProvider')] + public function testAcceptSequence(bool $isExternalAttendee): void { + $this->buildQueryExpects('TOKEN123', [ + 'id' => 0, + 'uid' => 'this-is-the-events-uid', + 'recurrenceid' => null, + 'attendee' => 'mailto:attendee@foo.bar', + 'organizer' => 'mailto:organizer@foo.bar', + 'sequence' => 1337, + 'token' => 'TOKEN123', + 'expiration' => 420000, + ], 1337); + + $expected = <<<EOF +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN +METHOD:REPLY +BEGIN:VEVENT +ATTENDEE;PARTSTAT=ACCEPTED:mailto:attendee@foo.bar +ORGANIZER:mailto:organizer@foo.bar +UID:this-is-the-events-uid +SEQUENCE:1337 +REQUEST-STATUS:2.0;Success +DTSTAMP:19700101T002217Z +END:VEVENT +END:VCALENDAR + +EOF; + $expected = preg_replace('~\R~u', "\r\n", $expected); + + $called = false; + $this->responseServer->expects($this->once()) + ->method('handleITipMessage') + ->willReturnCallback(function (Message $iTipMessage) use (&$called, $isExternalAttendee, $expected): void { + $called = true; + $this->assertEquals('this-is-the-events-uid', $iTipMessage->uid); + $this->assertEquals('VEVENT', $iTipMessage->component); + $this->assertEquals('REPLY', $iTipMessage->method); + $this->assertEquals(1337, $iTipMessage->sequence); + $this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->sender); + if ($isExternalAttendee) { + $this->assertEquals('mailto:organizer@foo.bar', $iTipMessage->recipient); + } else { + $this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->recipient); + } + + $iTipMessage->scheduleStatus = '1.2;Message delivered locally'; + + $this->assertEquals($expected, $iTipMessage->message->serialize()); + }); + $this->responseServer->expects($this->once()) + ->method('isExternalAttendee') + ->willReturn($isExternalAttendee); + + $response = $this->controller->accept('TOKEN123'); + $this->assertInstanceOf(TemplateResponse::class, $response); + $this->assertEquals('schedule-response-success', $response->getTemplateName()); + $this->assertEquals([], $response->getParams()); + $this->assertTrue($called); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('attendeeProvider')] + public function testAcceptRecurrenceId(bool $isExternalAttendee): void { + $this->buildQueryExpects('TOKEN123', [ + 'id' => 0, + 'uid' => 'this-is-the-events-uid', + 'recurrenceid' => "RECURRENCE-ID;TZID=Europe/Berlin:20180726T150000\n", + 'attendee' => 'mailto:attendee@foo.bar', + 'organizer' => 'mailto:organizer@foo.bar', + 'sequence' => null, + 'token' => 'TOKEN123', + 'expiration' => 420000, + ], 1337); + + $expected = <<<EOF +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN +METHOD:REPLY +BEGIN:VEVENT +ATTENDEE;PARTSTAT=ACCEPTED:mailto:attendee@foo.bar +ORGANIZER:mailto:organizer@foo.bar +UID:this-is-the-events-uid +SEQUENCE:0 +REQUEST-STATUS:2.0;Success +RECURRENCE-ID;TZID=Europe/Berlin:20180726T150000 +DTSTAMP:19700101T002217Z +END:VEVENT +END:VCALENDAR + +EOF; + $expected = preg_replace('~\R~u', "\r\n", $expected); + + $called = false; + $this->responseServer->expects($this->once()) + ->method('handleITipMessage') + ->willReturnCallback(function (Message $iTipMessage) use (&$called, $isExternalAttendee, $expected): void { + $called = true; + $this->assertEquals('this-is-the-events-uid', $iTipMessage->uid); + $this->assertEquals('VEVENT', $iTipMessage->component); + $this->assertEquals('REPLY', $iTipMessage->method); + $this->assertEquals(0, $iTipMessage->sequence); + $this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->sender); + if ($isExternalAttendee) { + $this->assertEquals('mailto:organizer@foo.bar', $iTipMessage->recipient); + } else { + $this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->recipient); + } + + $iTipMessage->scheduleStatus = '1.2;Message delivered locally'; + + $this->assertEquals($expected, $iTipMessage->message->serialize()); + }); + $this->responseServer->expects($this->once()) + ->method('isExternalAttendee') + ->willReturn($isExternalAttendee); + + $response = $this->controller->accept('TOKEN123'); + $this->assertInstanceOf(TemplateResponse::class, $response); + $this->assertEquals('schedule-response-success', $response->getTemplateName()); + $this->assertEquals([], $response->getParams()); + $this->assertTrue($called); + } + + public function testAcceptTokenNotFound(): void { + $this->buildQueryExpects('TOKEN123', null, 1337); + + $response = $this->controller->accept('TOKEN123'); + $this->assertInstanceOf(TemplateResponse::class, $response); + $this->assertEquals('schedule-response-error', $response->getTemplateName()); + $this->assertEquals([], $response->getParams()); + } + + public function testAcceptExpiredToken(): void { + $this->buildQueryExpects('TOKEN123', [ + 'id' => 0, + 'uid' => 'this-is-the-events-uid', + 'recurrenceid' => null, + 'attendee' => 'mailto:attendee@foo.bar', + 'organizer' => 'mailto:organizer@foo.bar', + 'sequence' => null, + 'token' => 'TOKEN123', + 'expiration' => 42, + ], 1337); + + $response = $this->controller->accept('TOKEN123'); + $this->assertInstanceOf(TemplateResponse::class, $response); + $this->assertEquals('schedule-response-error', $response->getTemplateName()); + $this->assertEquals([], $response->getParams()); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('attendeeProvider')] + public function testDecline(bool $isExternalAttendee): void { + $this->buildQueryExpects('TOKEN123', [ + 'id' => 0, + 'uid' => 'this-is-the-events-uid', + 'recurrenceid' => null, + 'attendee' => 'mailto:attendee@foo.bar', + 'organizer' => 'mailto:organizer@foo.bar', + 'sequence' => null, + 'token' => 'TOKEN123', + 'expiration' => 420000, + ], 1337); + + $expected = <<<EOF +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN +METHOD:REPLY +BEGIN:VEVENT +ATTENDEE;PARTSTAT=DECLINED:mailto:attendee@foo.bar +ORGANIZER:mailto:organizer@foo.bar +UID:this-is-the-events-uid +SEQUENCE:0 +REQUEST-STATUS:2.0;Success +DTSTAMP:19700101T002217Z +END:VEVENT +END:VCALENDAR + +EOF; + $expected = preg_replace('~\R~u', "\r\n", $expected); + + $called = false; + $this->responseServer->expects($this->once()) + ->method('handleITipMessage') + ->willReturnCallback(function (Message $iTipMessage) use (&$called, $isExternalAttendee, $expected): void { + $called = true; + $this->assertEquals('this-is-the-events-uid', $iTipMessage->uid); + $this->assertEquals('VEVENT', $iTipMessage->component); + $this->assertEquals('REPLY', $iTipMessage->method); + $this->assertEquals(null, $iTipMessage->sequence); + $this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->sender); + if ($isExternalAttendee) { + $this->assertEquals('mailto:organizer@foo.bar', $iTipMessage->recipient); + } else { + $this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->recipient); + } + + $iTipMessage->scheduleStatus = '1.2;Message delivered locally'; + + $this->assertEquals($expected, $iTipMessage->message->serialize()); + }); + $this->responseServer->expects($this->once()) + ->method('isExternalAttendee') + ->willReturn($isExternalAttendee); + + $response = $this->controller->decline('TOKEN123'); + $this->assertInstanceOf(TemplateResponse::class, $response); + $this->assertEquals('schedule-response-success', $response->getTemplateName()); + $this->assertEquals([], $response->getParams()); + $this->assertTrue($called); + } + + public function testOptions(): void { + $response = $this->controller->options('TOKEN123'); + $this->assertInstanceOf(TemplateResponse::class, $response); + $this->assertEquals('schedule-response-options', $response->getTemplateName()); + $this->assertEquals(['token' => 'TOKEN123'], $response->getParams()); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('attendeeProvider')] + public function testProcessMoreOptionsResult(bool $isExternalAttendee): void { + $this->request->expects($this->once()) + ->method('getParam') + ->with('partStat') + ->willReturn('TENTATIVE'); + + $this->buildQueryExpects('TOKEN123', [ + 'id' => 0, + 'uid' => 'this-is-the-events-uid', + 'recurrenceid' => null, + 'attendee' => 'mailto:attendee@foo.bar', + 'organizer' => 'mailto:organizer@foo.bar', + 'sequence' => null, + 'token' => 'TOKEN123', + 'expiration' => 420000, + ], 1337); + + $expected = <<<EOF +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN +METHOD:REPLY +BEGIN:VEVENT +ATTENDEE;PARTSTAT=TENTATIVE:mailto:attendee@foo.bar +ORGANIZER:mailto:organizer@foo.bar +UID:this-is-the-events-uid +SEQUENCE:0 +REQUEST-STATUS:2.0;Success +DTSTAMP:19700101T002217Z +END:VEVENT +END:VCALENDAR + +EOF; + $expected = preg_replace('~\R~u', "\r\n", $expected); + + $called = false; + $this->responseServer->expects($this->once()) + ->method('handleITipMessage') + ->willReturnCallback(function (Message $iTipMessage) use (&$called, $isExternalAttendee, $expected): void { + $called = true; + $this->assertEquals('this-is-the-events-uid', $iTipMessage->uid); + $this->assertEquals('VEVENT', $iTipMessage->component); + $this->assertEquals('REPLY', $iTipMessage->method); + $this->assertEquals(null, $iTipMessage->sequence); + $this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->sender); + if ($isExternalAttendee) { + $this->assertEquals('mailto:organizer@foo.bar', $iTipMessage->recipient); + } else { + $this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->recipient); + } + + $iTipMessage->scheduleStatus = '1.2;Message delivered locally'; + + $this->assertEquals($expected, $iTipMessage->message->serialize()); + }); + $this->responseServer->expects($this->once()) + ->method('isExternalAttendee') + ->willReturn($isExternalAttendee); + + + $response = $this->controller->processMoreOptionsResult('TOKEN123'); + $this->assertInstanceOf(TemplateResponse::class, $response); + $this->assertEquals('schedule-response-success', $response->getTemplateName()); + $this->assertEquals([], $response->getParams()); + $this->assertTrue($called); + } + + private function buildQueryExpects(string $token, ?array $return, int $time): void { + $queryBuilder = $this->createMock(IQueryBuilder::class); + $stmt = $this->createMock(IResult::class); + $expr = $this->createMock(IExpressionBuilder::class); + + $this->dbConnection->expects($this->once()) + ->method('getQueryBuilder') + ->with() + ->willReturn($queryBuilder); + $queryBuilder->method('expr') + ->willReturn($expr); + $queryBuilder->method('createNamedParameter') + ->willReturnMap([ + [$token, \PDO::PARAM_STR, null, 'namedParameterToken'] + ]); + + $stmt->expects($this->once()) + ->method('fetch') + ->with(\PDO::FETCH_ASSOC) + ->willReturn($return); + $stmt->expects($this->once()) + ->method('closeCursor'); + + $function = 'functionToken'; + $expr->expects($this->once()) + ->method('eq') + ->with('token', 'namedParameterToken') + ->willReturn((string)$function); + + $this->dbConnection->expects($this->once()) + ->method('getQueryBuilder') + ->with() + ->willReturn($queryBuilder); + + $queryBuilder->expects($this->once()) + ->method('select') + ->with('*') + ->willReturn($queryBuilder); + $queryBuilder->expects($this->once()) + ->method('from') + ->with('calendar_invitations') + ->willReturn($queryBuilder); + $queryBuilder->expects($this->once()) + ->method('where') + ->with($function) + ->willReturn($queryBuilder); + $queryBuilder->expects($this->once()) + ->method('executeQuery') + ->with() + ->willReturn($stmt); + + $this->timeFactory->method('getTime') + ->willReturn($time); + } +} diff --git a/apps/dav/tests/unit/Controller/UpcomingEventsControllerTest.php b/apps/dav/tests/unit/Controller/UpcomingEventsControllerTest.php new file mode 100644 index 00000000000..527943e5221 --- /dev/null +++ b/apps/dav/tests/unit/Controller/UpcomingEventsControllerTest.php @@ -0,0 +1,73 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Tests\unit\DAV\Service; + +use OCA\DAV\CalDAV\UpcomingEvent; +use OCA\DAV\CalDAV\UpcomingEventsService; +use OCA\DAV\Controller\UpcomingEventsController; +use OCP\IRequest; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class UpcomingEventsControllerTest extends TestCase { + private IRequest&MockObject $request; + private UpcomingEventsService&MockObject $service; + + protected function setUp(): void { + parent::setUp(); + + $this->request = $this->createMock(IRequest::class); + $this->service = $this->createMock(UpcomingEventsService::class); + } + + public function testGetEventsAnonymously(): void { + $controller = new UpcomingEventsController( + $this->request, + null, + $this->service, + ); + + $response = $controller->getEvents('https://cloud.example.com/call/123'); + + self::assertNull($response->getData()); + self::assertSame(401, $response->getStatus()); + } + + public function testGetEventsByLocation(): void { + $controller = new UpcomingEventsController( + $this->request, + 'u1', + $this->service, + ); + $this->service->expects(self::once()) + ->method('getEvents') + ->with('u1', 'https://cloud.example.com/call/123') + ->willReturn([ + new UpcomingEvent( + 'abc-123', + null, + 'personal', + 123, + 'Test', + 'https://cloud.example.com/call/123', + null, + ), + ]); + + $response = $controller->getEvents('https://cloud.example.com/call/123'); + + self::assertNotNull($response->getData()); + self::assertIsArray($response->getData()); + self::assertCount(1, $response->getData()['events']); + self::assertSame(200, $response->getStatus()); + $event1 = $response->getData()['events'][0]; + self::assertEquals('abc-123', $event1['uri']); + } +} diff --git a/apps/dav/tests/unit/DAV/AnonymousOptionsTest.php b/apps/dav/tests/unit/DAV/AnonymousOptionsTest.php new file mode 100644 index 00000000000..c99ebf327c8 --- /dev/null +++ b/apps/dav/tests/unit/DAV/AnonymousOptionsTest.php @@ -0,0 +1,92 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\DAV; + +use OCA\DAV\Connector\Sabre\AnonymousOptionsPlugin; +use Sabre\DAV\Auth\Backend\BasicCallBack; +use Sabre\DAV\Auth\Plugin; +use Sabre\DAV\Server; +use Sabre\HTTP\ResponseInterface; +use Sabre\HTTP\Sapi; +use Test\TestCase; + +class AnonymousOptionsTest extends TestCase { + private function sendRequest(string $method, string $path, string $userAgent = '') { + $server = new Server(); + $server->addPlugin(new AnonymousOptionsPlugin()); + $server->addPlugin(new Plugin(new BasicCallBack(function () { + return false; + }))); + + $server->httpRequest->setMethod($method); + $server->httpRequest->setUrl($path); + $server->httpRequest->setHeader('User-Agent', $userAgent); + + $server->sapi = new SapiMock(); + $server->exec(); + return $server->httpResponse; + } + + public function testAnonymousOptionsRoot(): void { + $response = $this->sendRequest('OPTIONS', ''); + + $this->assertEquals(401, $response->getStatus()); + } + + public function testAnonymousOptionsNonRoot(): void { + $response = $this->sendRequest('OPTIONS', 'foo'); + + $this->assertEquals(401, $response->getStatus()); + } + + public function testAnonymousOptionsNonRootSubDir(): void { + $response = $this->sendRequest('OPTIONS', 'foo/bar'); + + $this->assertEquals(401, $response->getStatus()); + } + + public function testAnonymousOptionsRootOffice(): void { + $response = $this->sendRequest('OPTIONS', '', 'Microsoft Office does strange things'); + + $this->assertEquals(200, $response->getStatus()); + } + + public function testAnonymousOptionsNonRootOffice(): void { + $response = $this->sendRequest('OPTIONS', 'foo', 'Microsoft Office does strange things'); + + $this->assertEquals(200, $response->getStatus()); + } + + public function testAnonymousOptionsNonRootSubDirOffice(): void { + $response = $this->sendRequest('OPTIONS', 'foo/bar', 'Microsoft Office does strange things'); + + $this->assertEquals(200, $response->getStatus()); + } + + public function testAnonymousHead(): void { + $response = $this->sendRequest('HEAD', '', 'Microsoft Office does strange things'); + + $this->assertEquals(200, $response->getStatus()); + } + + public function testAnonymousHeadNoOffice(): void { + $response = $this->sendRequest('HEAD', ''); + + $this->assertEquals(401, $response->getStatus(), 'curl'); + } +} + +class SapiMock extends Sapi { + /** + * Overriding this so nothing is ever echo'd. + * + * @return void + */ + public static function sendResponse(ResponseInterface $response): void { + } +} diff --git a/apps/dav/tests/unit/DAV/BrowserErrorPagePluginTest.php b/apps/dav/tests/unit/DAV/BrowserErrorPagePluginTest.php new file mode 100644 index 00000000000..0e82ef0a3ae --- /dev/null +++ b/apps/dav/tests/unit/DAV/BrowserErrorPagePluginTest.php @@ -0,0 +1,42 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\DAV; + +use OCA\DAV\Files\BrowserErrorPagePlugin; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\Exception\NotFound; +use Sabre\HTTP\Response; + +class BrowserErrorPagePluginTest extends \Test\TestCase { + + #[\PHPUnit\Framework\Attributes\DataProvider('providesExceptions')] + public function test(int $expectedCode, \Throwable $exception): void { + /** @var BrowserErrorPagePlugin&MockObject $plugin */ + $plugin = $this->getMockBuilder(BrowserErrorPagePlugin::class)->onlyMethods(['sendResponse', 'generateBody'])->getMock(); + $plugin->expects($this->once())->method('generateBody')->willReturn(':boom:'); + $plugin->expects($this->once())->method('sendResponse'); + /** @var \Sabre\DAV\Server&MockObject $server */ + $server = $this->createMock('Sabre\DAV\Server'); + $server->expects($this->once())->method('on'); + $httpResponse = $this->createMock(Response::class); + $httpResponse->expects($this->once())->method('addHeaders'); + $httpResponse->expects($this->once())->method('setStatus')->with($expectedCode); + $httpResponse->expects($this->once())->method('setBody')->with(':boom:'); + $server->httpResponse = $httpResponse; + $plugin->initialize($server); + $plugin->logException($exception); + } + + public static function providesExceptions(): array { + return [ + [ 404, new NotFound()], + [ 500, new \RuntimeException()], + ]; + } +} diff --git a/apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php b/apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php new file mode 100644 index 00000000000..517969fc9a3 --- /dev/null +++ b/apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php @@ -0,0 +1,466 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\DAV; + +use OCA\DAV\CalDAV\Calendar; +use OCA\DAV\CalDAV\DefaultCalendarValidator; +use OCA\DAV\DAV\CustomPropertiesBackend; +use OCA\DAV\Db\PropertyMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use OCP\IUser; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\PropFind; +use Sabre\DAV\PropPatch; +use Sabre\DAV\Server; +use Sabre\DAV\Tree; +use Sabre\DAV\Xml\Property\Href; +use Sabre\DAVACL\IACL; +use Sabre\DAVACL\IPrincipal; +use Test\TestCase; + +/** + * @group DB + */ +class CustomPropertiesBackendTest extends TestCase { + private const BASE_URI = '/remote.php/dav/'; + + private Server&MockObject $server; + private Tree&MockObject $tree; + private IDBConnection $dbConnection; + private IUser&MockObject $user; + private DefaultCalendarValidator&MockObject $defaultCalendarValidator; + private CustomPropertiesBackend $backend; + private PropertyMapper $propertyMapper; + + protected function setUp(): void { + parent::setUp(); + + $this->server = $this->createMock(Server::class); + $this->server->method('getBaseUri') + ->willReturn(self::BASE_URI); + $this->tree = $this->createMock(Tree::class); + $this->user = $this->createMock(IUser::class); + $this->user->method('getUID') + ->with() + ->willReturn('dummy_user_42'); + $this->dbConnection = \OCP\Server::get(IDBConnection::class); + $this->propertyMapper = \OCP\Server::get(PropertyMapper::class); + $this->defaultCalendarValidator = $this->createMock(DefaultCalendarValidator::class); + + $this->backend = new CustomPropertiesBackend( + $this->server, + $this->tree, + $this->dbConnection, + $this->user, + $this->propertyMapper, + $this->defaultCalendarValidator, + ); + } + + protected function tearDown(): void { + $query = $this->dbConnection->getQueryBuilder(); + $query->delete('properties'); + $query->execute(); + + parent::tearDown(); + } + + private function formatPath(string $path): string { + if (strlen($path) > 250) { + return sha1($path); + } else { + return $path; + } + } + + protected function insertProps(string $user, string $path, array $props): void { + foreach ($props as $name => $value) { + $this->insertProp($user, $path, $name, $value); + } + } + + protected function insertProp(string $user, string $path, string $name, mixed $value): void { + $type = CustomPropertiesBackend::PROPERTY_TYPE_STRING; + if ($value instanceof Href) { + $value = $value->getHref(); + $type = CustomPropertiesBackend::PROPERTY_TYPE_HREF; + } + + $query = $this->dbConnection->getQueryBuilder(); + $query->insert('properties') + ->values([ + 'userid' => $query->createNamedParameter($user), + 'propertypath' => $query->createNamedParameter($this->formatPath($path)), + 'propertyname' => $query->createNamedParameter($name), + 'propertyvalue' => $query->createNamedParameter($value), + 'valuetype' => $query->createNamedParameter($type, IQueryBuilder::PARAM_INT) + ]); + $query->execute(); + } + + protected function getProps(string $user, string $path): array { + $query = $this->dbConnection->getQueryBuilder(); + $query->select('propertyname', 'propertyvalue', 'valuetype') + ->from('properties') + ->where($query->expr()->eq('userid', $query->createNamedParameter($user))) + ->andWhere($query->expr()->eq('propertypath', $query->createNamedParameter($this->formatPath($path)))); + + $result = $query->execute(); + $data = []; + while ($row = $result->fetch()) { + $value = $row['propertyvalue']; + if ((int)$row['valuetype'] === CustomPropertiesBackend::PROPERTY_TYPE_HREF) { + $value = new Href($value); + } + $data[$row['propertyname']] = $value; + } + $result->closeCursor(); + + return $data; + } + + public function testPropFindNoDbCalls(): void { + $db = $this->createMock(IDBConnection::class); + $backend = new CustomPropertiesBackend( + $this->server, + $this->tree, + $db, + $this->user, + $this->propertyMapper, + $this->defaultCalendarValidator, + ); + + $propFind = $this->createMock(PropFind::class); + $propFind->expects($this->once()) + ->method('get404Properties') + ->with() + ->willReturn([ + '{http://owncloud.org/ns}permissions', + '{http://owncloud.org/ns}downloadURL', + '{http://owncloud.org/ns}dDC', + '{http://owncloud.org/ns}size', + ]); + + $db->expects($this->never()) + ->method($this->anything()); + + $backend->propFind('foo_bar_path_1337_0', $propFind); + } + + public function testPropFindCalendarCall(): void { + $propFind = $this->createMock(PropFind::class); + $propFind->method('get404Properties') + ->with() + ->willReturn([ + '{DAV:}getcontentlength', + '{DAV:}getcontenttype', + '{DAV:}getetag', + '{abc}def', + ]); + + $propFind->method('getRequestedProperties') + ->with() + ->willReturn([ + '{DAV:}getcontentlength', + '{DAV:}getcontenttype', + '{DAV:}getetag', + '{DAV:}displayname', + '{urn:ietf:params:xml:ns:caldav}calendar-description', + '{urn:ietf:params:xml:ns:caldav}calendar-timezone', + '{abc}def', + ]); + + $props = [ + '{abc}def' => 'a', + '{DAV:}displayname' => 'b', + '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'c', + '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => 'd', + ]; + + $this->insertProps('dummy_user_42', 'calendars/foo/bar_path_1337_0', $props); + + $setProps = []; + $propFind->method('set') + ->willReturnCallback(function ($name, $value, $status) use (&$setProps): void { + $setProps[$name] = $value; + }); + + $this->backend->propFind('calendars/foo/bar_path_1337_0', $propFind); + $this->assertEquals($props, $setProps); + } + + public function testPropFindPrincipalCall(): void { + $this->tree->method('getNodeForPath') + ->willReturnCallback(function ($uri) { + $node = $this->createMock(Calendar::class); + $node->method('getOwner') + ->willReturn('principals/users/dummy_user_42'); + return $node; + }); + + $propFind = $this->createMock(PropFind::class); + $propFind->method('get404Properties') + ->with() + ->willReturn([ + '{DAV:}getcontentlength', + '{DAV:}getcontenttype', + '{DAV:}getetag', + '{abc}def', + ]); + + $propFind->method('getRequestedProperties') + ->with() + ->willReturn([ + '{DAV:}getcontentlength', + '{DAV:}getcontenttype', + '{DAV:}getetag', + '{abc}def', + '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL', + ]); + + $props = [ + '{abc}def' => 'a', + '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/admin/personal'), + ]; + $this->insertProps('dummy_user_42', 'principals/users/dummy_user_42', $props); + + $setProps = []; + $propFind->method('set') + ->willReturnCallback(function ($name, $value, $status) use (&$setProps): void { + $setProps[$name] = $value; + }); + + $this->backend->propFind('principals/users/dummy_user_42', $propFind); + $this->assertEquals($props, $setProps); + } + + public static function propFindPrincipalScheduleDefaultCalendarProviderUrlProvider(): array { + // [ user, nodes, existingProps, requestedProps, returnedProps ] + return [ + [ // Exists + 'dummy_user_42', + ['calendars/dummy_user_42/foo/' => Calendar::class], + ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/dummy_user_42/foo/')], + ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'], + ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/dummy_user_42/foo/')], + ], + [ // Doesn't exist + 'dummy_user_42', + ['calendars/dummy_user_42/foo/' => Calendar::class], + ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/dummy_user_42/bar/')], + ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'], + [], + ], + [ // No privilege + 'dummy_user_42', + ['calendars/user2/baz/' => Calendar::class], + ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/user2/baz/')], + ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'], + [], + ], + [ // Not a calendar + 'dummy_user_42', + ['foo/dummy_user_42/bar/' => IACL::class], + ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('foo/dummy_user_42/bar/')], + ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'], + [], + ], + ]; + + } + + #[\PHPUnit\Framework\Attributes\DataProvider('propFindPrincipalScheduleDefaultCalendarProviderUrlProvider')] + public function testPropFindPrincipalScheduleDefaultCalendarUrl( + string $user, + array $nodes, + array $existingProps, + array $requestedProps, + array $returnedProps, + ): void { + $propFind = $this->createMock(PropFind::class); + $propFind->method('get404Properties') + ->with() + ->willReturn([ + '{DAV:}getcontentlength', + '{DAV:}getcontenttype', + '{DAV:}getetag', + ]); + + $propFind->method('getRequestedProperties') + ->with() + ->willReturn(array_merge([ + '{DAV:}getcontentlength', + '{DAV:}getcontenttype', + '{DAV:}getetag', + '{abc}def', + ], + $requestedProps, + )); + + $this->server->method('calculateUri') + ->willReturnCallback(function ($uri) { + if (!str_starts_with($uri, self::BASE_URI)) { + return trim(substr($uri, strlen(self::BASE_URI)), '/'); + } + return null; + }); + $this->tree->method('getNodeForPath') + ->willReturnCallback(function ($uri) use ($nodes) { + if (str_starts_with($uri, 'principals/')) { + return $this->createMock(IPrincipal::class); + } + if (array_key_exists($uri, $nodes)) { + $owner = explode('/', $uri)[1]; + $node = $this->createMock($nodes[$uri]); + $node->method('getOwner') + ->willReturn("principals/users/$owner"); + return $node; + } + throw new NotFound('Node not found'); + }); + + $this->insertProps($user, "principals/users/$user", $existingProps); + + $setProps = []; + $propFind->method('set') + ->willReturnCallback(function ($name, $value, $status) use (&$setProps): void { + $setProps[$name] = $value; + }); + + $this->backend->propFind("principals/users/$user", $propFind); + $this->assertEquals($returnedProps, $setProps); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('propPatchProvider')] + public function testPropPatch(string $path, array $existing, array $props, array $result): void { + $this->server->method('calculateUri') + ->willReturnCallback(function ($uri) { + if (str_starts_with($uri, self::BASE_URI)) { + return trim(substr($uri, strlen(self::BASE_URI)), '/'); + } + return null; + }); + $this->tree->method('getNodeForPath') + ->willReturnCallback(function ($uri) { + $node = $this->createMock(Calendar::class); + $node->method('getOwner') + ->willReturn('principals/users/' . $this->user->getUID()); + return $node; + }); + + $this->insertProps($this->user->getUID(), $path, $existing); + $propPatch = new PropPatch($props); + + $this->backend->propPatch($path, $propPatch); + $propPatch->commit(); + + $storedProps = $this->getProps($this->user->getUID(), $path); + $this->assertEquals($result, $storedProps); + } + + public static function propPatchProvider(): array { + $longPath = str_repeat('long_path', 100); + return [ + ['foo_bar_path_1337', [], ['{DAV:}displayname' => 'anything'], ['{DAV:}displayname' => 'anything']], + ['foo_bar_path_1337', ['{DAV:}displayname' => 'foo'], ['{DAV:}displayname' => 'anything'], ['{DAV:}displayname' => 'anything']], + ['foo_bar_path_1337', ['{DAV:}displayname' => 'foo'], ['{DAV:}displayname' => null], []], + [$longPath, [], ['{DAV:}displayname' => 'anything'], ['{DAV:}displayname' => 'anything']], + ['principals/users/dummy_user_42', [], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('foo/bar/')], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('foo/bar/')]], + ['principals/users/dummy_user_42', [], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href(self::BASE_URI . 'foo/bar/')], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('foo/bar/')]], + ]; + } + + public function testPropPatchWithUnsuitableCalendar(): void { + $path = 'principals/users/' . $this->user->getUID(); + + $node = $this->createMock(Calendar::class); + $node->expects(self::once()) + ->method('getOwner') + ->willReturn($path); + + $this->defaultCalendarValidator->expects(self::once()) + ->method('validateScheduleDefaultCalendar') + ->with($node) + ->willThrowException(new \Sabre\DAV\Exception('Invalid calendar')); + + $this->server->method('calculateUri') + ->willReturnCallback(function ($uri) { + if (str_starts_with($uri, self::BASE_URI)) { + return trim(substr($uri, strlen(self::BASE_URI)), '/'); + } + return null; + }); + $this->tree->expects(self::once()) + ->method('getNodeForPath') + ->with('foo/bar/') + ->willReturn($node); + + $storedProps = $this->getProps($this->user->getUID(), $path); + $this->assertEquals([], $storedProps); + + $propPatch = new PropPatch([ + '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('foo/bar/'), + ]); + $this->backend->propPatch($path, $propPatch); + try { + $propPatch->commit(); + } catch (\Throwable $e) { + $this->assertInstanceOf(\Sabre\DAV\Exception::class, $e); + } + + $storedProps = $this->getProps($this->user->getUID(), $path); + $this->assertEquals([], $storedProps); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('deleteProvider')] + public function testDelete(string $path): void { + $this->insertProps('dummy_user_42', $path, ['foo' => 'bar']); + $this->backend->delete($path); + $this->assertEquals([], $this->getProps('dummy_user_42', $path)); + } + + public static function deleteProvider(): array { + return [ + ['foo_bar_path_1337'], + [str_repeat('long_path', 100)] + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('moveProvider')] + public function testMove(string $source, string $target): void { + $this->insertProps('dummy_user_42', $source, ['foo' => 'bar']); + $this->backend->move($source, $target); + $this->assertEquals([], $this->getProps('dummy_user_42', $source)); + $this->assertEquals(['foo' => 'bar'], $this->getProps('dummy_user_42', $target)); + } + + public static function moveProvider(): array { + return [ + ['foo_bar_path_1337', 'foo_bar_path_7333'], + [str_repeat('long_path1', 100), str_repeat('long_path2', 100)] + ]; + } + + public function testDecodeValueFromDatabaseObjectCurrent(): void { + $propertyValue = 'O:48:"Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp":1:{s:8:"\x00*\x00value";s:6:"opaque";}'; + $propertyType = 3; + $decodeValue = $this->invokePrivate($this->backend, 'decodeValueFromDatabase', [$propertyValue, $propertyType]); + $this->assertInstanceOf(\Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp::class, $decodeValue); + $this->assertEquals('opaque', $decodeValue->getValue()); + } + + public function testDecodeValueFromDatabaseObjectLegacy(): void { + $propertyValue = 'O:48:"Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp":1:{s:8:"' . chr(0) . '*' . chr(0) . 'value";s:6:"opaque";}'; + $propertyType = 3; + $decodeValue = $this->invokePrivate($this->backend, 'decodeValueFromDatabase', [$propertyValue, $propertyType]); + $this->assertInstanceOf(\Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp::class, $decodeValue); + $this->assertEquals('opaque', $decodeValue->getValue()); + } +} diff --git a/apps/dav/tests/unit/DAV/GroupPrincipalTest.php b/apps/dav/tests/unit/DAV/GroupPrincipalTest.php new file mode 100644 index 00000000000..2756152a6e2 --- /dev/null +++ b/apps/dav/tests/unit/DAV/GroupPrincipalTest.php @@ -0,0 +1,331 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\DAV; + +use OC\Group\Group; +use OCA\DAV\DAV\GroupPrincipalBackend; +use OCP\IConfig; +use OCP\IGroup; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\IUserSession; +use OCP\Share\IManager; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\PropPatch; + +class GroupPrincipalTest extends \Test\TestCase { + private IConfig&MockObject $config; + private IGroupManager&MockObject $groupManager; + private IUserSession&MockObject $userSession; + private IManager&MockObject $shareManager; + private GroupPrincipalBackend $connector; + + protected function setUp(): void { + $this->groupManager = $this->createMock(IGroupManager::class); + $this->userSession = $this->createMock(IUserSession::class); + $this->shareManager = $this->createMock(IManager::class); + $this->config = $this->createMock(IConfig::class); + + $this->connector = new GroupPrincipalBackend( + $this->groupManager, + $this->userSession, + $this->shareManager, + $this->config + ); + parent::setUp(); + } + + public function testGetPrincipalsByPrefixWithoutPrefix(): void { + $response = $this->connector->getPrincipalsByPrefix(''); + $this->assertSame([], $response); + } + + public function testGetPrincipalsByPrefixWithUsers(): void { + $group1 = $this->mockGroup('foo'); + $group2 = $this->mockGroup('bar'); + $this->groupManager + ->expects($this->once()) + ->method('search') + ->with('') + ->willReturn([$group1, $group2]); + + $expectedResponse = [ + 0 => [ + 'uri' => 'principals/groups/foo', + '{DAV:}displayname' => 'Group foo', + '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'GROUP', + ], + 1 => [ + 'uri' => 'principals/groups/bar', + '{DAV:}displayname' => 'Group bar', + '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'GROUP', + ] + ]; + $response = $this->connector->getPrincipalsByPrefix('principals/groups'); + $this->assertSame($expectedResponse, $response); + } + + public function testGetPrincipalsByPrefixEmpty(): void { + $this->groupManager + ->expects($this->once()) + ->method('search') + ->with('') + ->willReturn([]); + + $response = $this->connector->getPrincipalsByPrefix('principals/groups'); + $this->assertSame([], $response); + } + + public function testGetPrincipalsByPathWithoutMail(): void { + $group1 = $this->mockGroup('foo'); + $this->groupManager + ->expects($this->once()) + ->method('get') + ->with('foo') + ->willReturn($group1); + + $expectedResponse = [ + 'uri' => 'principals/groups/foo', + '{DAV:}displayname' => 'Group foo', + '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'GROUP', + ]; + $response = $this->connector->getPrincipalByPath('principals/groups/foo'); + $this->assertSame($expectedResponse, $response); + } + + public function testGetPrincipalsByPathWithMail(): void { + $fooUser = $this->mockGroup('foo'); + $this->groupManager + ->expects($this->once()) + ->method('get') + ->with('foo') + ->willReturn($fooUser); + + $expectedResponse = [ + 'uri' => 'principals/groups/foo', + '{DAV:}displayname' => 'Group foo', + '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'GROUP', + ]; + $response = $this->connector->getPrincipalByPath('principals/groups/foo'); + $this->assertSame($expectedResponse, $response); + } + + public function testGetPrincipalsByPathEmpty(): void { + $this->groupManager + ->expects($this->once()) + ->method('get') + ->with('foo') + ->willReturn(null); + + $response = $this->connector->getPrincipalByPath('principals/groups/foo'); + $this->assertSame(null, $response); + } + + public function testGetPrincipalsByPathGroupWithSlash(): void { + $group1 = $this->mockGroup('foo/bar'); + $this->groupManager + ->expects($this->once()) + ->method('get') + ->with('foo/bar') + ->willReturn($group1); + + $expectedResponse = [ + 'uri' => 'principals/groups/foo%2Fbar', + '{DAV:}displayname' => 'Group foo/bar', + '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'GROUP', + ]; + $response = $this->connector->getPrincipalByPath('principals/groups/foo/bar'); + $this->assertSame($expectedResponse, $response); + } + + public function testGetPrincipalsByPathGroupWithHash(): void { + $group1 = $this->mockGroup('foo#bar'); + $this->groupManager + ->expects($this->once()) + ->method('get') + ->with('foo#bar') + ->willReturn($group1); + + $expectedResponse = [ + 'uri' => 'principals/groups/foo%23bar', + '{DAV:}displayname' => 'Group foo#bar', + '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'GROUP', + ]; + $response = $this->connector->getPrincipalByPath('principals/groups/foo#bar'); + $this->assertSame($expectedResponse, $response); + } + + public function testGetGroupMemberSet(): void { + $response = $this->connector->getGroupMemberSet('principals/groups/foo'); + $this->assertSame([], $response); + } + + public function testGetGroupMembership(): void { + $response = $this->connector->getGroupMembership('principals/groups/foo'); + $this->assertSame([], $response); + } + + + public function testSetGroupMembership(): void { + $this->expectException(\Sabre\DAV\Exception::class); + $this->expectExceptionMessage('Setting members of the group is not supported yet'); + + $this->connector->setGroupMemberSet('principals/groups/foo', ['foo']); + } + + public function testUpdatePrincipal(): void { + $this->assertSame(0, $this->connector->updatePrincipal('foo', new PropPatch([]))); + } + + public function testSearchPrincipalsWithEmptySearchProperties(): void { + $this->assertSame([], $this->connector->searchPrincipals('principals/groups', [])); + } + + public function testSearchPrincipalsWithWrongPrefixPath(): void { + $this->assertSame([], $this->connector->searchPrincipals('principals/users', + ['{DAV:}displayname' => 'Foo'])); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('searchPrincipalsDataProvider')] + public function testSearchPrincipals(bool $sharingEnabled, bool $groupSharingEnabled, bool $groupsOnly, string $test, array $result): void { + $this->shareManager->expects($this->once()) + ->method('shareAPIEnabled') + ->willReturn($sharingEnabled); + + $this->shareManager->expects($sharingEnabled ? $this->once() : $this->never()) + ->method('allowGroupSharing') + ->willReturn($groupSharingEnabled); + + if ($sharingEnabled && $groupSharingEnabled) { + $this->shareManager->expects($this->once()) + ->method('shareWithGroupMembersOnly') + ->willReturn($groupsOnly); + + if ($groupsOnly) { + $user = $this->createMock(IUser::class); + $this->userSession->expects($this->once()) + ->method('getUser') + ->willReturn($user); + + $this->groupManager->expects($this->once()) + ->method('getUserGroupIds') + ->with($user) + ->willReturn(['group1', 'group2', 'group5']); + } + } else { + $this->shareManager->expects($this->never()) + ->method('shareWithGroupMembersOnly'); + $this->groupManager->expects($this->never()) + ->method($this->anything()); + } + + $group1 = $this->createMock(IGroup::class); + $group1->method('getGID')->willReturn('group1'); + $group2 = $this->createMock(IGroup::class); + $group2->method('getGID')->willReturn('group2'); + $group3 = $this->createMock(IGroup::class); + $group3->method('getGID')->willReturn('group3'); + $group4 = $this->createMock(IGroup::class); + $group4->method('getGID')->willReturn('group4'); + $group5 = $this->createMock(IGroup::class); + $group5->method('getGID')->willReturn('group5'); + + if ($sharingEnabled && $groupSharingEnabled) { + $this->groupManager->expects($this->once()) + ->method('search') + ->with('Foo') + ->willReturn([$group1, $group2, $group3, $group4, $group5]); + } else { + $this->groupManager->expects($this->never()) + ->method('search'); + } + + $this->assertSame($result, $this->connector->searchPrincipals('principals/groups', + ['{DAV:}displayname' => 'Foo'], $test)); + } + + public static function searchPrincipalsDataProvider(): array { + return [ + [true, true, false, 'allof', ['principals/groups/group1', 'principals/groups/group2', 'principals/groups/group3', 'principals/groups/group4', 'principals/groups/group5']], + [true, true, false, 'anyof', ['principals/groups/group1', 'principals/groups/group2', 'principals/groups/group3', 'principals/groups/group4', 'principals/groups/group5']], + [true, true, true, 'allof', ['principals/groups/group1', 'principals/groups/group2', 'principals/groups/group5']], + [true, true, true, 'anyof', ['principals/groups/group1', 'principals/groups/group2', 'principals/groups/group5']], + [true, false, false, 'allof', []], + [false, true, false, 'anyof', []], + [false, false, false, 'allof', []], + [false, false, false, 'anyof', []], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('findByUriDataProvider')] + public function testFindByUri(bool $sharingEnabled, bool $groupSharingEnabled, bool $groupsOnly, string $findUri, ?string $result): void { + $this->shareManager->expects($this->once()) + ->method('shareAPIEnabled') + ->willReturn($sharingEnabled); + + $this->shareManager->expects($sharingEnabled ? $this->once() : $this->never()) + ->method('allowGroupSharing') + ->willReturn($groupSharingEnabled); + + if ($sharingEnabled && $groupSharingEnabled) { + $this->shareManager->expects($this->once()) + ->method('shareWithGroupMembersOnly') + ->willReturn($groupsOnly); + + if ($groupsOnly) { + $user = $this->createMock(IUser::class); + $this->userSession->expects($this->once()) + ->method('getUser') + ->willReturn($user); + + $this->groupManager->expects($this->once()) + ->method('getUserGroupIds') + ->with($user) + ->willReturn(['group1', 'group2', 'group5']); + } + } else { + $this->shareManager->expects($this->never()) + ->method('shareWithGroupMembersOnly'); + $this->groupManager->expects($this->never()) + ->method($this->anything()); + } + + $this->assertEquals($result, $this->connector->findByUri($findUri, 'principals/groups')); + } + + public static function findByUriDataProvider(): array { + return [ + [false, false, false, 'principal:principals/groups/group1', null], + [false, false, false, 'principal:principals/groups/group3', null], + [false, true, false, 'principal:principals/groups/group1', null], + [false, true, false, 'principal:principals/groups/group3', null], + [false, false, true, 'principal:principals/groups/group1', null], + [false, false, true, 'principal:principals/groups/group3', null], + [true, false, true, 'principal:principals/groups/group1', null], + [true, false, true, 'principal:principals/groups/group3', null], + [true, true, true, 'principal:principals/groups/group1', 'principals/groups/group1'], + [true, true, true, 'principal:principals/groups/group3', null], + [true, true, false, 'principal:principals/groups/group1', 'principals/groups/group1'], + [true, true, false, 'principal:principals/groups/group3', 'principals/groups/group3'], + ]; + } + + private function mockGroup(string $gid): Group&MockObject { + $fooGroup = $this->createMock(Group::class); + $fooGroup + ->expects($this->exactly(1)) + ->method('getGID') + ->willReturn($gid); + $fooGroup + ->expects($this->exactly(1)) + ->method('getDisplayName') + ->willReturn('Group ' . $gid); + return $fooGroup; + } +} diff --git a/apps/dav/tests/unit/DAV/Listener/UserEventsListenerTest.php b/apps/dav/tests/unit/DAV/Listener/UserEventsListenerTest.php new file mode 100644 index 00000000000..8e410eb0a78 --- /dev/null +++ b/apps/dav/tests/unit/DAV/Listener/UserEventsListenerTest.php @@ -0,0 +1,183 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace OCA\DAV\Tests\unit\DAV\Listener; + +use OCA\DAV\BackgroundJob\UserStatusAutomation; +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CardDAV\CardDavBackend; +use OCA\DAV\CardDAV\SyncService; +use OCA\DAV\Listener\UserEventsListener; +use OCA\DAV\Service\ExampleContactService; +use OCA\DAV\Service\ExampleEventService; +use OCP\BackgroundJob\IJobList; +use OCP\Defaults; +use OCP\IUser; +use OCP\IUserManager; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class UserEventsListenerTest extends TestCase { + private IUserManager&MockObject $userManager; + private SyncService&MockObject $syncService; + private CalDavBackend&MockObject $calDavBackend; + private CardDavBackend&MockObject $cardDavBackend; + private Defaults&MockObject $defaults; + private ExampleContactService&MockObject $exampleContactService; + private ExampleEventService&MockObject $exampleEventService; + private LoggerInterface&MockObject $logger; + + private UserEventsListener $userEventsListener; + + protected function setUp(): void { + parent::setUp(); + + $this->userManager = $this->createMock(IUserManager::class); + $this->syncService = $this->createMock(SyncService::class); + $this->calDavBackend = $this->createMock(CalDavBackend::class); + $this->cardDavBackend = $this->createMock(CardDavBackend::class); + $this->defaults = $this->createMock(Defaults::class); + $this->exampleContactService = $this->createMock(ExampleContactService::class); + $this->exampleEventService = $this->createMock(ExampleEventService::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->jobList = $this->createMock(IJobList::class); + + $this->userEventsListener = new UserEventsListener( + $this->userManager, + $this->syncService, + $this->calDavBackend, + $this->cardDavBackend, + $this->defaults, + $this->exampleContactService, + $this->exampleEventService, + $this->logger, + $this->jobList, + ); + } + + public function test(): void { + $user = $this->createMock(IUser::class); + $user->expects($this->once())->method('getUID')->willReturn('newUser'); + + $this->defaults->expects($this->once())->method('getColorPrimary')->willReturn('#745bca'); + + $this->calDavBackend->expects($this->once())->method('getCalendarsForUserCount')->willReturn(0); + $this->calDavBackend->expects($this->once())->method('createCalendar')->with( + 'principals/users/newUser', + 'personal', [ + '{DAV:}displayname' => 'Personal', + '{http://apple.com/ns/ical/}calendar-color' => '#745bca', + 'components' => 'VEVENT' + ]) + ->willReturn(1000); + $this->calDavBackend->expects(self::never()) + ->method('getCalendarsForUser'); + $this->exampleEventService->expects(self::once()) + ->method('createExampleEvent') + ->with(1000); + + $this->cardDavBackend->expects($this->once())->method('getAddressBooksForUserCount')->willReturn(0); + $this->cardDavBackend->expects($this->once())->method('createAddressBook')->with( + 'principals/users/newUser', + 'contacts', ['{DAV:}displayname' => 'Contacts']); + + $this->userEventsListener->firstLogin($user); + } + + public function testWithExisting(): void { + $user = $this->createMock(IUser::class); + $user->expects($this->once())->method('getUID')->willReturn('newUser'); + + $this->calDavBackend->expects($this->once())->method('getCalendarsForUserCount')->willReturn(1); + $this->calDavBackend->expects($this->never())->method('createCalendar'); + $this->calDavBackend->expects(self::never()) + ->method('createCalendar'); + $this->exampleEventService->expects(self::never()) + ->method('createExampleEvent'); + + $this->cardDavBackend->expects($this->once())->method('getAddressBooksForUserCount')->willReturn(1); + $this->cardDavBackend->expects($this->never())->method('createAddressBook'); + + $this->userEventsListener->firstLogin($user); + } + + public function testWithBirthdayCalendar(): void { + $user = $this->createMock(IUser::class); + $user->expects($this->once())->method('getUID')->willReturn('newUser'); + + $this->defaults->expects($this->once())->method('getColorPrimary')->willReturn('#745bca'); + + $this->calDavBackend->expects($this->once())->method('getCalendarsForUserCount')->willReturn(0); + $this->calDavBackend->expects($this->once())->method('createCalendar')->with( + 'principals/users/newUser', + 'personal', [ + '{DAV:}displayname' => 'Personal', + '{http://apple.com/ns/ical/}calendar-color' => '#745bca', + 'components' => 'VEVENT' + ]); + + $this->cardDavBackend->expects($this->once())->method('getAddressBooksForUserCount')->willReturn(0); + $this->cardDavBackend->expects($this->once())->method('createAddressBook')->with( + 'principals/users/newUser', + 'contacts', ['{DAV:}displayname' => 'Contacts']); + + $this->userEventsListener->firstLogin($user); + } + + public function testDeleteCalendar(): void { + $user = $this->createMock(IUser::class); + $user->expects($this->once())->method('getUID')->willReturn('newUser'); + + $this->syncService->expects($this->once()) + ->method('deleteUser'); + + $this->calDavBackend->expects($this->once())->method('getUsersOwnCalendars')->willReturn([ + ['id' => 'personal'] + ]); + $this->calDavBackend->expects($this->once())->method('getSubscriptionsForUser')->willReturn([ + ['id' => 'some-subscription'] + ]); + $this->calDavBackend->expects($this->once())->method('deleteCalendar')->with('personal'); + $this->calDavBackend->expects($this->once())->method('deleteSubscription')->with('some-subscription'); + $this->calDavBackend->expects($this->once())->method('deleteAllSharesByUser'); + + $this->cardDavBackend->expects($this->once())->method('getUsersOwnAddressBooks')->willReturn([ + ['id' => 'personal'] + ]); + $this->cardDavBackend->expects($this->once())->method('deleteAddressBook'); + + $this->userEventsListener->preDeleteUser($user); + $this->userEventsListener->postDeleteUser('newUser'); + } + + public function testDeleteUserAutomationEvent(): void { + $user = $this->createMock(IUser::class); + $user->expects($this->once())->method('getUID')->willReturn('newUser'); + + $this->syncService->expects($this->once()) + ->method('deleteUser'); + + $this->calDavBackend->expects($this->once())->method('getUsersOwnCalendars')->willReturn([ + ['id' => []] + ]); + $this->calDavBackend->expects($this->once())->method('getSubscriptionsForUser')->willReturn([ + ['id' => []] + ]); + $this->cardDavBackend->expects($this->once())->method('getUsersOwnAddressBooks')->willReturn([ + ['id' => []] + ]); + + $this->jobList->expects(self::once())->method('remove')->with(UserStatusAutomation::class, ['userId' => 'newUser']); + + $this->userEventsListener->preDeleteUser($user); + $this->userEventsListener->postDeleteUser('newUser'); + } +} diff --git a/apps/dav/tests/unit/DAV/Sharing/BackendTest.php b/apps/dav/tests/unit/DAV/Sharing/BackendTest.php new file mode 100644 index 00000000000..556a623a73f --- /dev/null +++ b/apps/dav/tests/unit/DAV/Sharing/BackendTest.php @@ -0,0 +1,399 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\DAV\Sharing; + +use OCA\DAV\CalDAV\Sharing\Backend as CalendarSharingBackend; +use OCA\DAV\CalDAV\Sharing\Service; +use OCA\DAV\CardDAV\Sharing\Backend as ContactsSharingBackend; +use OCA\DAV\Connector\Sabre\Principal; +use OCA\DAV\DAV\Sharing\Backend; +use OCA\DAV\DAV\Sharing\IShareable; +use OCP\ICache; +use OCP\ICacheFactory; +use OCP\IDBConnection; +use OCP\IGroupManager; +use OCP\IUserManager; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class BackendTest extends TestCase { + + private IDBConnection&MockObject $db; + private IUserManager&MockObject $userManager; + private IGroupManager&MockObject $groupManager; + private Principal&MockObject $principalBackend; + private ICache&MockObject $shareCache; + private LoggerInterface&MockObject $logger; + private ICacheFactory&MockObject $cacheFactory; + private Service&MockObject $calendarService; + private CalendarSharingBackend $backend; + + protected function setUp(): void { + parent::setUp(); + $this->db = $this->createMock(IDBConnection::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->groupManager = $this->createMock(IGroupManager::class); + $this->principalBackend = $this->createMock(Principal::class); + $this->cacheFactory = $this->createMock(ICacheFactory::class); + $this->shareCache = $this->createMock(ICache::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->calendarService = $this->createMock(Service::class); + $this->cacheFactory->expects(self::any()) + ->method('createInMemory') + ->willReturn($this->shareCache); + + $this->backend = new CalendarSharingBackend( + $this->userManager, + $this->groupManager, + $this->principalBackend, + $this->cacheFactory, + $this->calendarService, + $this->logger, + ); + } + + public function testUpdateShareCalendarBob(): void { + $shareable = $this->createConfiguredMock(IShareable::class, [ + 'getOwner' => 'principals/users/alice', + 'getResourceId' => 42, + ]); + $add = [ + [ + 'href' => 'principal:principals/users/bob', + 'readOnly' => true, + ] + ]; + $principal = 'principals/users/bob'; + + $this->shareCache->expects(self::once()) + ->method('clear'); + $this->principalBackend->expects(self::once()) + ->method('findByUri') + ->willReturn($principal); + $this->userManager->expects(self::once()) + ->method('userExists') + ->willReturn(true); + $this->groupManager->expects(self::never()) + ->method('groupExists'); + $this->calendarService->expects(self::once()) + ->method('shareWith') + ->with($shareable->getResourceId(), $principal, Backend::ACCESS_READ); + + $this->backend->updateShares($shareable, $add, []); + } + + public function testUpdateShareCalendarGroup(): void { + $shareable = $this->createConfiguredMock(IShareable::class, [ + 'getOwner' => 'principals/users/alice', + 'getResourceId' => 42, + ]); + $add = [ + [ + 'href' => 'principal:principals/groups/bob', + 'readOnly' => true, + ] + ]; + $principal = 'principals/groups/bob'; + + $this->shareCache->expects(self::once()) + ->method('clear'); + $this->principalBackend->expects(self::once()) + ->method('findByUri') + ->willReturn($principal); + $this->userManager->expects(self::never()) + ->method('userExists'); + $this->groupManager->expects(self::once()) + ->method('groupExists') + ->willReturn(true); + $this->calendarService->expects(self::once()) + ->method('shareWith') + ->with($shareable->getResourceId(), $principal, Backend::ACCESS_READ); + + $this->backend->updateShares($shareable, $add, []); + } + + public function testUpdateShareContactsBob(): void { + $shareable = $this->createConfiguredMock(IShareable::class, [ + 'getOwner' => 'principals/users/alice', + 'getResourceId' => 42, + ]); + $add = [ + [ + 'href' => 'principal:principals/users/bob', + 'readOnly' => true, + ] + ]; + $principal = 'principals/users/bob'; + + $this->shareCache->expects(self::once()) + ->method('clear'); + $this->principalBackend->expects(self::once()) + ->method('findByUri') + ->willReturn($principal); + $this->userManager->expects(self::once()) + ->method('userExists') + ->willReturn(true); + $this->groupManager->expects(self::never()) + ->method('groupExists'); + $this->calendarService->expects(self::once()) + ->method('shareWith') + ->with($shareable->getResourceId(), $principal, Backend::ACCESS_READ); + + $this->backend->updateShares($shareable, $add, []); + } + + public function testUpdateShareContactsGroup(): void { + $shareable = $this->createConfiguredMock(IShareable::class, [ + 'getOwner' => 'principals/users/alice', + 'getResourceId' => 42, + ]); + $add = [ + [ + 'href' => 'principal:principals/groups/bob', + 'readOnly' => true, + ] + ]; + $principal = 'principals/groups/bob'; + + $this->shareCache->expects(self::once()) + ->method('clear'); + $this->principalBackend->expects(self::once()) + ->method('findByUri') + ->willReturn($principal); + $this->userManager->expects(self::never()) + ->method('userExists'); + $this->groupManager->expects(self::once()) + ->method('groupExists') + ->willReturn(true); + $this->calendarService->expects(self::once()) + ->method('shareWith') + ->with($shareable->getResourceId(), $principal, Backend::ACCESS_READ); + + $this->backend->updateShares($shareable, $add, []); + } + + public function testUpdateShareCircle(): void { + $shareable = $this->createConfiguredMock(IShareable::class, [ + 'getOwner' => 'principals/users/alice', + 'getResourceId' => 42, + ]); + $add = [ + [ + 'href' => 'principal:principals/circles/bob', + 'readOnly' => true, + ] + ]; + $principal = 'principals/groups/bob'; + + $this->shareCache->expects(self::once()) + ->method('clear'); + $this->principalBackend->expects(self::once()) + ->method('findByUri') + ->willReturn($principal); + $this->userManager->expects(self::never()) + ->method('userExists'); + $this->groupManager->expects(self::once()) + ->method('groupExists') + ->willReturn(true); + $this->calendarService->expects(self::once()) + ->method('shareWith') + ->with($shareable->getResourceId(), $principal, Backend::ACCESS_READ); + + $this->backend->updateShares($shareable, $add, []); + } + + public function testUnshareBob(): void { + $shareable = $this->createConfiguredMock(IShareable::class, [ + 'getOwner' => 'principals/users/alice', + 'getResourceId' => 42, + ]); + $remove = [ + 'principal:principals/users/bob', + ]; + $principal = 'principals/users/bob'; + + $this->shareCache->expects(self::once()) + ->method('clear'); + $this->principalBackend->expects(self::once()) + ->method('findByUri') + ->willReturn($principal); + $this->calendarService->expects(self::once()) + ->method('deleteShare') + ->with($shareable->getResourceId(), $principal); + $this->calendarService->expects(self::never()) + ->method('unshare'); + + $this->backend->updateShares($shareable, [], $remove); + } + + public function testUnshareWithBobGroup(): void { + $shareable = $this->createConfiguredMock(IShareable::class, [ + 'getOwner' => 'principals/users/alice', + 'getResourceId' => 42, + ]); + $remove = [ + 'principal:principals/users/bob', + ]; + $oldShares = [ + [ + 'href' => 'principal:principals/groups/bob', + 'commonName' => 'bob', + 'status' => 1, + 'readOnly' => true, + '{http://owncloud.org/ns}principal' => 'principals/groups/bob', + '{http://owncloud.org/ns}group-share' => true, + ] + ]; + + + $this->shareCache->expects(self::once()) + ->method('clear'); + $this->principalBackend->expects(self::once()) + ->method('findByUri') + ->willReturn('principals/users/bob'); + $this->calendarService->expects(self::once()) + ->method('deleteShare') + ->with($shareable->getResourceId(), 'principals/users/bob'); + $this->calendarService->expects(self::never()) + ->method('unshare'); + + $this->backend->updateShares($shareable, [], $remove, $oldShares); + } + + public function testGetShares(): void { + $resourceId = 42; + $principal = 'principals/groups/bob'; + $rows = [ + [ + 'principaluri' => $principal, + 'access' => Backend::ACCESS_READ, + ] + ]; + $expected = [ + [ + 'href' => 'principal:principals/groups/bob', + 'commonName' => 'bob', + 'status' => 1, + 'readOnly' => true, + '{http://owncloud.org/ns}principal' => $principal, + '{http://owncloud.org/ns}group-share' => true, + ] + ]; + + + $this->shareCache->expects(self::once()) + ->method('get') + ->with((string)$resourceId) + ->willReturn(null); + $this->calendarService->expects(self::once()) + ->method('getShares') + ->with($resourceId) + ->willReturn($rows); + $this->principalBackend->expects(self::once()) + ->method('getPrincipalByPath') + ->with($principal) + ->willReturn(['uri' => $principal, '{DAV:}displayname' => 'bob']); + $this->shareCache->expects(self::once()) + ->method('set') + ->with((string)$resourceId, $expected); + + $result = $this->backend->getShares($resourceId); + $this->assertEquals($expected, $result); + } + + public function testGetSharesAddressbooks(): void { + $service = $this->createMock(\OCA\DAV\CardDAV\Sharing\Service::class); + $backend = new ContactsSharingBackend( + $this->userManager, + $this->groupManager, + $this->principalBackend, + $this->cacheFactory, + $service, + $this->logger); + $resourceId = 42; + $principal = 'principals/groups/bob'; + $rows = [ + [ + 'principaluri' => $principal, + 'access' => Backend::ACCESS_READ, + ] + ]; + $expected = [ + [ + 'href' => 'principal:principals/groups/bob', + 'commonName' => 'bob', + 'status' => 1, + 'readOnly' => true, + '{http://owncloud.org/ns}principal' => $principal, + '{http://owncloud.org/ns}group-share' => true, + ] + ]; + + $this->shareCache->expects(self::once()) + ->method('get') + ->with((string)$resourceId) + ->willReturn(null); + $service->expects(self::once()) + ->method('getShares') + ->with($resourceId) + ->willReturn($rows); + $this->principalBackend->expects(self::once()) + ->method('getPrincipalByPath') + ->with($principal) + ->willReturn(['uri' => $principal, '{DAV:}displayname' => 'bob']); + $this->shareCache->expects(self::once()) + ->method('set') + ->with((string)$resourceId, $expected); + + $result = $backend->getShares($resourceId); + $this->assertEquals($expected, $result); + } + + public function testPreloadShares(): void { + $resourceIds = [42, 99]; + $rows = [ + [ + 'resourceid' => 42, + 'principaluri' => 'principals/groups/bob', + 'access' => Backend::ACCESS_READ, + ], + [ + 'resourceid' => 99, + 'principaluri' => 'principals/users/carlos', + 'access' => Backend::ACCESS_READ_WRITE, + ] + ]; + $principalResults = [ + ['uri' => 'principals/groups/bob', '{DAV:}displayname' => 'bob'], + ['uri' => 'principals/users/carlos', '{DAV:}displayname' => 'carlos'], + ]; + + $this->shareCache->expects(self::exactly(2)) + ->method('get') + ->willReturn(null); + $this->calendarService->expects(self::once()) + ->method('getSharesForIds') + ->with($resourceIds) + ->willReturn($rows); + $this->principalBackend->expects(self::exactly(2)) + ->method('getPrincipalByPath') + ->willReturnCallback(function (string $principal) use ($principalResults) { + switch ($principal) { + case 'principals/groups/bob': + return $principalResults[0]; + default: + return $principalResults[1]; + } + }); + $this->shareCache->expects(self::exactly(2)) + ->method('set'); + + $this->backend->preloadShares($resourceIds); + } +} diff --git a/apps/dav/tests/unit/DAV/Sharing/PluginTest.php b/apps/dav/tests/unit/DAV/Sharing/PluginTest.php new file mode 100644 index 00000000000..7a88f7cc5dd --- /dev/null +++ b/apps/dav/tests/unit/DAV/Sharing/PluginTest.php @@ -0,0 +1,62 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\DAV\Sharing; + +use OCA\DAV\Connector\Sabre\Auth; +use OCA\DAV\DAV\Sharing\IShareable; +use OCA\DAV\DAV\Sharing\Plugin; +use OCP\IConfig; +use OCP\IRequest; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\Server; +use Sabre\DAV\SimpleCollection; +use Sabre\HTTP\Request; +use Sabre\HTTP\Response; +use Test\TestCase; + +class PluginTest extends TestCase { + private Plugin $plugin; + private Server $server; + private IShareable&MockObject $book; + + protected function setUp(): void { + parent::setUp(); + + $authBackend = $this->createMock(Auth::class); + $authBackend->method('isDavAuthenticated')->willReturn(true); + + $request = $this->createMock(IRequest::class); + $config = $this->createMock(IConfig::class); + $this->plugin = new Plugin($authBackend, $request, $config); + + $root = new SimpleCollection('root'); + $this->server = new \Sabre\DAV\Server($root); + /** @var SimpleCollection $node */ + $this->book = $this->createMock(IShareable::class); + $this->book->method('getName')->willReturn('addressbook1.vcf'); + $root->addChild($this->book); + $this->plugin->initialize($this->server); + } + + public function testSharing(): void { + $this->book->expects($this->once())->method('updateShares')->with([[ + 'href' => 'principal:principals/admin', + 'commonName' => null, + 'summary' => null, + 'readOnly' => false + ]], ['mailto:wilfredo@example.com']); + + // setup request + $request = new Request('POST', 'addressbook1.vcf'); + $request->addHeader('Content-Type', 'application/xml'); + $request->setBody('<?xml version="1.0" encoding="utf-8" ?><CS:share xmlns:D="DAV:" xmlns:CS="http://owncloud.org/ns"><CS:set><D:href>principal:principals/admin</D:href><CS:read-write/></CS:set> <CS:remove><D:href>mailto:wilfredo@example.com</D:href></CS:remove></CS:share>'); + $response = new Response(); + $this->plugin->httpPost($request, $response); + } +} diff --git a/apps/dav/tests/unit/DAV/SystemPrincipalBackendTest.php b/apps/dav/tests/unit/DAV/SystemPrincipalBackendTest.php new file mode 100644 index 00000000000..3df861accf2 --- /dev/null +++ b/apps/dav/tests/unit/DAV/SystemPrincipalBackendTest.php @@ -0,0 +1,100 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\DAV; + +use OCA\DAV\DAV\SystemPrincipalBackend; +use Sabre\DAV\Exception; +use Test\TestCase; + +class SystemPrincipalBackendTest extends TestCase { + + #[\PHPUnit\Framework\Attributes\DataProvider('providesPrefix')] + public function testGetPrincipalsByPrefix(array $expected, string $prefix): void { + $backend = new SystemPrincipalBackend(); + $result = $backend->getPrincipalsByPrefix($prefix); + $this->assertEquals($expected, $result); + } + + public static function providesPrefix(): array { + return [ + [[], ''], + [[[ + 'uri' => 'principals/system/system', + '{DAV:}displayname' => 'system', + ], + [ + 'uri' => 'principals/system/public', + '{DAV:}displayname' => 'public', + ] + ], 'principals/system'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providesPath')] + public function testGetPrincipalByPath(?array $expected, string $path): void { + $backend = new SystemPrincipalBackend(); + $result = $backend->getPrincipalByPath($path); + $this->assertEquals($expected, $result); + } + + public static function providesPath(): array { + return [ + [null, ''], + [null, 'principals'], + [null, 'principals/system'], + [[ + 'uri' => 'principals/system/system', + '{DAV:}displayname' => 'system', + ], 'principals/system/system'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providesPrincipalForGetGroupMemberSet')] + public function testGetGroupMemberSetExceptional(?string $principal): void { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Principal not found'); + + $backend = new SystemPrincipalBackend(); + $backend->getGroupMemberSet($principal); + } + + public static function providesPrincipalForGetGroupMemberSet(): array { + return [ + [null], + ['principals/system'], + ]; + } + + public function testGetGroupMemberSet(): void { + $backend = new SystemPrincipalBackend(); + $result = $backend->getGroupMemberSet('principals/system/system'); + $this->assertEquals(['principals/system/system'], $result); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providesPrincipalForGetGroupMembership')] + public function testGetGroupMembershipExceptional(string $principal): void { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Principal not found'); + + $backend = new SystemPrincipalBackend(); + $backend->getGroupMembership($principal); + } + + public static function providesPrincipalForGetGroupMembership(): array { + return [ + ['principals/system/a'], + ]; + } + + public function testGetGroupMembership(): void { + $backend = new SystemPrincipalBackend(); + $result = $backend->getGroupMembership('principals/system/system'); + $this->assertEquals([], $result); + } +} diff --git a/apps/dav/tests/unit/DAV/ViewOnlyPluginTest.php b/apps/dav/tests/unit/DAV/ViewOnlyPluginTest.php new file mode 100644 index 00000000000..eefbc53fd22 --- /dev/null +++ b/apps/dav/tests/unit/DAV/ViewOnlyPluginTest.php @@ -0,0 +1,167 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2019 ownCloud GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\DAV; + +use OCA\DAV\Connector\Sabre\Exception\Forbidden; +use OCA\DAV\Connector\Sabre\File as DavFile; +use OCA\DAV\DAV\ViewOnlyPlugin; +use OCA\Files_Sharing\SharedStorage; +use OCA\Files_Versions\Sabre\VersionFile; +use OCA\Files_Versions\Versions\IVersion; +use OCP\Files\File; +use OCP\Files\Folder; +use OCP\Files\Storage\ISharedStorage; +use OCP\Files\Storage\IStorage; +use OCP\IUser; +use OCP\Share\IAttributes; +use OCP\Share\IShare; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\Server; +use Sabre\DAV\Tree; +use Sabre\HTTP\RequestInterface; +use Test\TestCase; + +class ViewOnlyPluginTest extends TestCase { + private Tree&MockObject $tree; + private RequestInterface&MockObject $request; + private Folder&MockObject $userFolder; + private ViewOnlyPlugin $plugin; + + public function setUp(): void { + parent::setUp(); + + $this->userFolder = $this->createMock(Folder::class); + $this->request = $this->createMock(RequestInterface::class); + $this->tree = $this->createMock(Tree::class); + $server = $this->createMock(Server::class); + + $this->plugin = new ViewOnlyPlugin( + $this->userFolder, + ); + $server->tree = $this->tree; + + $this->plugin->initialize($server); + } + + public function testCanGetNonDav(): void { + $this->request->expects($this->once())->method('getPath')->willReturn('files/test/target'); + $this->tree->method('getNodeForPath')->willReturn(null); + + $this->assertTrue($this->plugin->checkViewOnly($this->request)); + } + + public function testCanGetNonShared(): void { + $this->request->expects($this->once())->method('getPath')->willReturn('files/test/target'); + $davNode = $this->createMock(DavFile::class); + $this->tree->method('getNodeForPath')->willReturn($davNode); + + $file = $this->createMock(File::class); + $davNode->method('getNode')->willReturn($file); + + $storage = $this->createMock(IStorage::class); + $file->method('getStorage')->willReturn($storage); + $storage->method('instanceOfStorage')->with(ISharedStorage::class)->willReturn(false); + + $this->assertTrue($this->plugin->checkViewOnly($this->request)); + } + + public static function providesDataForCanGet(): array { + return [ + // has attribute permissions-download enabled - can get file + [false, true, true, true], + // has no attribute permissions-download - can get file + [false, null, true, true], + // has attribute permissions-download enabled - can get file version + [true, true, true, true], + // has no attribute permissions-download - can get file version + [true, null, true, true], + // has attribute permissions-download disabled - cannot get the file + [false, false, false, false], + // has attribute permissions-download disabled - cannot get the file version + [true, false, false, false], + + // Has global allowViewWithoutDownload option enabled + // has attribute permissions-download disabled - can get file + [false, false, false, true], + // has attribute permissions-download disabled - can get file version + [true, false, false, true], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providesDataForCanGet')] + public function testCanGet(bool $isVersion, ?bool $attrEnabled, bool $expectCanDownloadFile, bool $allowViewWithoutDownload): void { + $nodeInfo = $this->createMock(File::class); + if ($isVersion) { + $davPath = 'versions/alice/versions/117/123456'; + $version = $this->createMock(IVersion::class); + $version->expects($this->once()) + ->method('getSourceFile') + ->willReturn($nodeInfo); + $davNode = $this->createMock(VersionFile::class); + $davNode->expects($this->once()) + ->method('getVersion') + ->willReturn($version); + + $currentUser = $this->createMock(IUser::class); + $currentUser->expects($this->once()) + ->method('getUID') + ->willReturn('alice'); + $nodeInfo->expects($this->once()) + ->method('getOwner') + ->willReturn($currentUser); + + $nodeInfo = $this->createMock(File::class); + $owner = $this->createMock(IUser::class); + $owner->expects($this->once()) + ->method('getUID') + ->willReturn('bob'); + $this->userFolder->expects($this->once()) + ->method('getById') + ->willReturn([$nodeInfo]); + $this->userFolder->expects($this->once()) + ->method('getOwner') + ->willReturn($owner); + } else { + $davPath = 'files/path/to/file.odt'; + $davNode = $this->createMock(DavFile::class); + $davNode->method('getNode')->willReturn($nodeInfo); + } + + $this->request->expects($this->once())->method('getPath')->willReturn($davPath); + + $this->tree->expects($this->once()) + ->method('getNodeForPath') + ->with($davPath) + ->willReturn($davNode); + + $storage = $this->createMock(SharedStorage::class); + $share = $this->createMock(IShare::class); + $nodeInfo->expects($this->once()) + ->method('getStorage') + ->willReturn($storage); + $storage->method('instanceOfStorage')->with(ISharedStorage::class)->willReturn(true); + $storage->method('getShare')->willReturn($share); + + $extAttr = $this->createMock(IAttributes::class); + $share->method('getAttributes')->willReturn($extAttr); + $extAttr->expects($this->once()) + ->method('getAttribute') + ->with('permissions', 'download') + ->willReturn($attrEnabled); + + $share->expects($this->once()) + ->method('canSeeContent') + ->willReturn($allowViewWithoutDownload); + + if (!$expectCanDownloadFile) { + $this->expectException(Forbidden::class); + } + $this->plugin->checkViewOnly($this->request); + } +} diff --git a/apps/dav/tests/unit/Direct/DirectFileTest.php b/apps/dav/tests/unit/Direct/DirectFileTest.php new file mode 100644 index 00000000000..f6f0f49fa8c --- /dev/null +++ b/apps/dav/tests/unit/Direct/DirectFileTest.php @@ -0,0 +1,111 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\Direct; + +use OCA\DAV\Db\Direct; +use OCA\DAV\Direct\DirectFile; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\File; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\Exception\Forbidden; +use Test\TestCase; + +class DirectFileTest extends TestCase { + private Direct $direct; + private IRootFolder&MockObject $rootFolder; + private Folder&MockObject $userFolder; + private File&MockObject $file; + private IEventDispatcher&MockObject $eventDispatcher; + private DirectFile $directFile; + + protected function setUp(): void { + parent::setUp(); + + $this->direct = Direct::fromParams([ + 'userId' => 'directUser', + 'token' => 'directToken', + 'fileId' => 42, + ]); + + $this->rootFolder = $this->createMock(IRootFolder::class); + + $this->userFolder = $this->createMock(Folder::class); + $this->rootFolder->method('getUserFolder') + ->with('directUser') + ->willReturn($this->userFolder); + + $this->file = $this->createMock(File::class); + $this->userFolder->method('getFirstNodeById') + ->with(42) + ->willReturn($this->file); + + $this->eventDispatcher = $this->createMock(IEventDispatcher::class); + + $this->directFile = new DirectFile($this->direct, $this->rootFolder, $this->eventDispatcher); + } + + public function testPut(): void { + $this->expectException(Forbidden::class); + + $this->directFile->put('foo'); + } + + public function testGet(): void { + $this->file->expects($this->once()) + ->method('fopen') + ->with('rb'); + $this->directFile->get(); + } + + public function testGetContentType(): void { + $this->file->method('getMimeType') + ->willReturn('direct/type'); + + $this->assertSame('direct/type', $this->directFile->getContentType()); + } + + public function testGetETag(): void { + $this->file->method('getEtag') + ->willReturn('directEtag'); + + $this->assertSame('directEtag', $this->directFile->getETag()); + } + + public function testGetSize(): void { + $this->file->method('getSize') + ->willReturn(42); + + $this->assertSame(42, $this->directFile->getSize()); + } + + public function testDelete(): void { + $this->expectException(Forbidden::class); + + $this->directFile->delete(); + } + + public function testGetName(): void { + $this->assertSame('directToken', $this->directFile->getName()); + } + + public function testSetName(): void { + $this->expectException(Forbidden::class); + + $this->directFile->setName('foobar'); + } + + public function testGetLastModified(): void { + $this->file->method('getMTime') + ->willReturn(42); + + $this->assertSame(42, $this->directFile->getLastModified()); + } +} diff --git a/apps/dav/tests/unit/Direct/DirectHomeTest.php b/apps/dav/tests/unit/Direct/DirectHomeTest.php new file mode 100644 index 00000000000..94c82c2b7c5 --- /dev/null +++ b/apps/dav/tests/unit/Direct/DirectHomeTest.php @@ -0,0 +1,160 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\Direct; + +use OCA\DAV\Db\Direct; +use OCA\DAV\Db\DirectMapper; +use OCA\DAV\Direct\DirectFile; +use OCA\DAV\Direct\DirectHome; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\IRootFolder; +use OCP\IRequest; +use OCP\Security\Bruteforce\IThrottler; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Exception\MethodNotAllowed; +use Sabre\DAV\Exception\NotFound; +use Test\TestCase; + +class DirectHomeTest extends TestCase { + private DirectMapper&MockObject $directMapper; + private IRootFolder&MockObject $rootFolder; + private ITimeFactory&MockObject $timeFactory; + private IThrottler&MockObject $throttler; + private IRequest&MockObject $request; + private IEventDispatcher&MockObject $eventDispatcher; + private DirectHome $directHome; + + protected function setUp(): void { + parent::setUp(); + + $this->directMapper = $this->createMock(DirectMapper::class); + $this->rootFolder = $this->createMock(IRootFolder::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->throttler = $this->createMock(IThrottler::class); + $this->request = $this->createMock(IRequest::class); + $this->eventDispatcher = $this->createMock(IEventDispatcher::class); + + $this->timeFactory->method('getTime') + ->willReturn(42); + + $this->request->method('getRemoteAddress') + ->willReturn('1.2.3.4'); + + + $this->directHome = new DirectHome( + $this->rootFolder, + $this->directMapper, + $this->timeFactory, + $this->throttler, + $this->request, + $this->eventDispatcher + ); + } + + public function testCreateFile(): void { + $this->expectException(Forbidden::class); + + $this->directHome->createFile('foo', 'bar'); + } + + public function testCreateDirectory(): void { + $this->expectException(Forbidden::class); + + $this->directHome->createDirectory('foo'); + } + + public function testGetChildren(): void { + $this->expectException(MethodNotAllowed::class); + + $this->directHome->getChildren(); + } + + public function testChildExists(): void { + $this->assertFalse($this->directHome->childExists('foo')); + } + + public function testDelete(): void { + $this->expectException(Forbidden::class); + + $this->directHome->delete(); + } + + public function testGetName(): void { + $this->assertSame('direct', $this->directHome->getName()); + } + + public function testSetName(): void { + $this->expectException(Forbidden::class); + + $this->directHome->setName('foo'); + } + + public function testGetLastModified(): void { + $this->assertSame(0, $this->directHome->getLastModified()); + } + + public function testGetChildValid(): void { + $direct = Direct::fromParams([ + 'expiration' => 100, + ]); + + $this->directMapper->method('getByToken') + ->with('longtoken') + ->willReturn($direct); + + $this->throttler->expects($this->never()) + ->method($this->anything()); + + $result = $this->directHome->getChild('longtoken'); + $this->assertInstanceOf(DirectFile::class, $result); + } + + public function testGetChildExpired(): void { + $direct = Direct::fromParams([ + 'expiration' => 41, + ]); + + $this->directMapper->method('getByToken') + ->with('longtoken') + ->willReturn($direct); + + $this->throttler->expects($this->never()) + ->method($this->anything()); + + $this->expectException(NotFound::class); + + $this->directHome->getChild('longtoken'); + } + + public function testGetChildInvalid(): void { + $this->directMapper->method('getByToken') + ->with('longtoken') + ->willThrowException(new DoesNotExistException('not found')); + + $this->throttler->expects($this->once()) + ->method('registerAttempt') + ->with( + 'directlink', + '1.2.3.4' + ); + $this->throttler->expects($this->once()) + ->method('sleepDelayOrThrowOnMax') + ->with( + '1.2.3.4', + 'directlink' + ); + + $this->expectException(NotFound::class); + + $this->directHome->getChild('longtoken'); + } +} diff --git a/apps/dav/tests/unit/Files/FileSearchBackendTest.php b/apps/dav/tests/unit/Files/FileSearchBackendTest.php new file mode 100644 index 00000000000..c6d6f85347b --- /dev/null +++ b/apps/dav/tests/unit/Files/FileSearchBackendTest.php @@ -0,0 +1,421 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Tests\unit\Files; + +use OC\Files\Search\SearchComparison; +use OC\Files\Search\SearchQuery; +use OC\Files\View; +use OCA\DAV\Connector\Sabre\Directory; +use OCA\DAV\Connector\Sabre\File; +use OCA\DAV\Connector\Sabre\FilesPlugin; +use OCA\DAV\Connector\Sabre\ObjectTree; +use OCA\DAV\Connector\Sabre\Server; +use OCA\DAV\Files\FileSearchBackend; +use OCP\Files\FileInfo; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\Files\Search\ISearchBinaryOperator; +use OCP\Files\Search\ISearchComparison; +use OCP\Files\Search\ISearchQuery; +use OCP\FilesMetadata\IFilesMetadataManager; +use OCP\IUser; +use OCP\Share\IManager; +use PHPUnit\Framework\MockObject\MockObject; +use SearchDAV\Backend\SearchPropertyDefinition; +use SearchDAV\Query\Limit; +use SearchDAV\Query\Literal; +use SearchDAV\Query\Operator; +use SearchDAV\Query\Query; +use SearchDAV\Query\Scope; +use Test\TestCase; + +class FileSearchBackendTest extends TestCase { + private ObjectTree&MockObject $tree; + private Server&MockObject $server; + private IUser&MockObject $user; + private IRootFolder&MockObject $rootFolder; + private IManager&MockObject $shareManager; + private View&MockObject $view; + private Folder&MockObject $searchFolder; + private Directory&MockObject $davFolder; + private FileSearchBackend $search; + + protected function setUp(): void { + parent::setUp(); + + $this->user = $this->createMock(IUser::class); + $this->user->expects($this->any()) + ->method('getUID') + ->willReturn('test'); + + $this->tree = $this->createMock(ObjectTree::class); + $this->server = $this->createMock(Server::class); + $this->view = $this->createMock(View::class); + $this->rootFolder = $this->createMock(IRootFolder::class); + $this->shareManager = $this->createMock(IManager::class); + $this->searchFolder = $this->createMock(Folder::class); + $fileInfo = $this->createMock(FileInfo::class); + $this->davFolder = $this->createMock(Directory::class); + + $this->view->expects($this->any()) + ->method('getRoot') + ->willReturn(''); + + $this->view->expects($this->any()) + ->method('getRelativePath') + ->willReturnArgument(0); + + $this->davFolder->expects($this->any()) + ->method('getFileInfo') + ->willReturn($fileInfo); + + $this->rootFolder->expects($this->any()) + ->method('get') + ->willReturn($this->searchFolder); + + $filesMetadataManager = $this->createMock(IFilesMetadataManager::class); + + $this->search = new FileSearchBackend($this->server, $this->tree, $this->user, $this->rootFolder, $this->shareManager, $this->view, $filesMetadataManager); + } + + public function testSearchFilename(): void { + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->willReturn($this->davFolder); + + $this->searchFolder->expects($this->once()) + ->method('search') + ->with(new SearchQuery( + new SearchComparison( + ISearchComparison::COMPARE_EQUAL, + 'name', + 'foo' + ), + 0, + 0, + [], + $this->user + )) + ->willReturn([ + new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'), + ]); + + $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, '{DAV:}displayname', 'foo'); + $result = $this->search->search($query); + + $this->assertCount(1, $result); + $this->assertEquals('/files/test/test/path', $result[0]->href); + } + + public function testSearchMimetype(): void { + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->willReturn($this->davFolder); + + $this->searchFolder->expects($this->once()) + ->method('search') + ->with(new SearchQuery( + new SearchComparison( + ISearchComparison::COMPARE_EQUAL, + 'mimetype', + 'foo' + ), + 0, + 0, + [], + $this->user + )) + ->willReturn([ + new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'), + ]); + + $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, '{DAV:}getcontenttype', 'foo'); + $result = $this->search->search($query); + + $this->assertCount(1, $result); + $this->assertEquals('/files/test/test/path', $result[0]->href); + } + + public function testSearchSize(): void { + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->willReturn($this->davFolder); + + $this->searchFolder->expects($this->once()) + ->method('search') + ->with(new SearchQuery( + new SearchComparison( + ISearchComparison::COMPARE_GREATER_THAN, + 'size', + 10 + ), + 0, + 0, + [], + $this->user + )) + ->willReturn([ + new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'), + ]); + + $query = $this->getBasicQuery(Operator::OPERATION_GREATER_THAN, FilesPlugin::SIZE_PROPERTYNAME, 10); + $result = $this->search->search($query); + + $this->assertCount(1, $result); + $this->assertEquals('/files/test/test/path', $result[0]->href); + } + + public function testSearchMtime(): void { + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->willReturn($this->davFolder); + + $this->searchFolder->expects($this->once()) + ->method('search') + ->with(new SearchQuery( + new SearchComparison( + ISearchComparison::COMPARE_GREATER_THAN, + 'mtime', + 10 + ), + 0, + 0, + [], + $this->user + )) + ->willReturn([ + new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'), + ]); + + $query = $this->getBasicQuery(Operator::OPERATION_GREATER_THAN, '{DAV:}getlastmodified', 10); + $result = $this->search->search($query); + + $this->assertCount(1, $result); + $this->assertEquals('/files/test/test/path', $result[0]->href); + } + + public function testSearchIsCollection(): void { + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->willReturn($this->davFolder); + + $this->searchFolder->expects($this->once()) + ->method('search') + ->with(new SearchQuery( + new SearchComparison( + ISearchComparison::COMPARE_EQUAL, + 'mimetype', + FileInfo::MIMETYPE_FOLDER + ), + 0, + 0, + [], + $this->user + )) + ->willReturn([ + new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'), + ]); + + $query = $this->getBasicQuery(Operator::OPERATION_IS_COLLECTION, 'yes'); + $result = $this->search->search($query); + + $this->assertCount(1, $result); + $this->assertEquals('/files/test/test/path', $result[0]->href); + } + + + public function testSearchInvalidProp(): void { + $this->expectException(\InvalidArgumentException::class); + + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->willReturn($this->davFolder); + + $this->searchFolder->expects($this->never()) + ->method('search'); + + $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, '{DAV:}getetag', 'foo'); + $this->search->search($query); + } + + private function getBasicQuery(string $type, string $property, int|string|null $value = null) { + $scope = new Scope('/', 'infinite'); + $scope->path = '/'; + $from = [$scope]; + $orderBy = []; + $select = []; + if (is_null($value)) { + $where = new Operator( + $type, + [new Literal($property)] + ); + } else { + $where = new Operator( + $type, + [new SearchPropertyDefinition($property, true, true, true), new Literal($value)] + ); + } + $limit = new Limit(); + + return new Query($select, $from, $where, $orderBy, $limit); + } + + + public function testSearchNonFolder(): void { + $this->expectException(\InvalidArgumentException::class); + + $davNode = $this->createMock(File::class); + + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->willReturn($davNode); + + $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, '{DAV:}displayname', 'foo'); + $this->search->search($query); + } + + public function testSearchLimitOwnerBasic(): void { + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->willReturn($this->davFolder); + + /** @var ISearchQuery|null $receivedQuery */ + $receivedQuery = null; + $this->searchFolder + ->method('search') + ->willReturnCallback(function ($query) use (&$receivedQuery) { + $receivedQuery = $query; + return [ + new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'), + ]; + }); + + $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, FilesPlugin::OWNER_ID_PROPERTYNAME, $this->user->getUID()); + $this->search->search($query); + + $this->assertNotNull($receivedQuery); + $this->assertTrue($receivedQuery->limitToHome()); + + /** @var ISearchBinaryOperator $operator */ + $operator = $receivedQuery->getSearchOperation(); + $this->assertInstanceOf(ISearchBinaryOperator::class, $operator); + $this->assertEquals(ISearchBinaryOperator::OPERATOR_AND, $operator->getType()); + $this->assertEmpty($operator->getArguments()); + } + + public function testSearchLimitOwnerNested(): void { + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->willReturn($this->davFolder); + + /** @var ISearchQuery|null $receivedQuery */ + $receivedQuery = null; + $this->searchFolder + ->method('search') + ->willReturnCallback(function ($query) use (&$receivedQuery) { + $receivedQuery = $query; + return [ + new \OC\Files\Node\Folder($this->rootFolder, $this->view, '/test/path'), + ]; + }); + + $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, FilesPlugin::OWNER_ID_PROPERTYNAME, $this->user->getUID()); + $query->where = new Operator( + Operator::OPERATION_AND, + [ + new Operator( + Operator::OPERATION_EQUAL, + [new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true), new Literal('image/png')] + ), + new Operator( + Operator::OPERATION_EQUAL, + [new SearchPropertyDefinition(FilesPlugin::OWNER_ID_PROPERTYNAME, true, true, true), new Literal($this->user->getUID())] + ), + ] + ); + $this->search->search($query); + + $this->assertNotNull($receivedQuery); + $this->assertTrue($receivedQuery->limitToHome()); + + /** @var ISearchBinaryOperator $operator */ + $operator = $receivedQuery->getSearchOperation(); + $this->assertInstanceOf(ISearchBinaryOperator::class, $operator); + $this->assertEquals(ISearchBinaryOperator::OPERATOR_AND, $operator->getType()); + $this->assertCount(2, $operator->getArguments()); + + /** @var ISearchBinaryOperator $operator */ + $operator = $operator->getArguments()[1]; + $this->assertInstanceOf(ISearchBinaryOperator::class, $operator); + $this->assertEquals(ISearchBinaryOperator::OPERATOR_AND, $operator->getType()); + $this->assertEmpty($operator->getArguments()); + } + + public function testSearchOperatorLimit(): void { + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->willReturn($this->davFolder); + + $innerOperator = new Operator( + Operator::OPERATION_EQUAL, + [new SearchPropertyDefinition('{DAV:}getcontenttype', true, true, true), new Literal('image/png')] + ); + // 5 child operators + $level1Operator = new Operator( + Operator::OPERATION_AND, + [ + $innerOperator, + $innerOperator, + $innerOperator, + $innerOperator, + $innerOperator, + ] + ); + // 5^2 = 25 child operators + $level2Operator = new Operator( + Operator::OPERATION_AND, + [ + $level1Operator, + $level1Operator, + $level1Operator, + $level1Operator, + $level1Operator, + ] + ); + // 5^3 = 125 child operators + $level3Operator = new Operator( + Operator::OPERATION_AND, + [ + $level2Operator, + $level2Operator, + $level2Operator, + $level2Operator, + $level2Operator, + ] + ); + + $query = $this->getBasicQuery(Operator::OPERATION_EQUAL, FilesPlugin::OWNER_ID_PROPERTYNAME, $this->user->getUID()); + $query->where = $level3Operator; + $this->expectException(\InvalidArgumentException::class); + $this->search->search($query); + } + + public function testPreloadPropertyFor(): void { + $node1 = $this->createMock(File::class); + $node2 = $this->createMock(Directory::class); + $nodes = [$node1, $node2]; + $requestProperties = ['{DAV:}getcontenttype', '{DAV:}getlastmodified']; + + $this->server->expects($this->once()) + ->method('emit') + ->with('preloadProperties', [$nodes, $requestProperties]); + + $this->search->preloadPropertyFor($nodes, $requestProperties); + } +} diff --git a/apps/dav/tests/unit/Files/MultipartRequestParserTest.php b/apps/dav/tests/unit/Files/MultipartRequestParserTest.php new file mode 100644 index 00000000000..dc0e884f07c --- /dev/null +++ b/apps/dav/tests/unit/Files/MultipartRequestParserTest.php @@ -0,0 +1,322 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace OCA\DAV\Tests\unit\Files; + +use OCA\DAV\BulkUpload\MultipartRequestParser; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Sabre\HTTP\RequestInterface; +use Test\TestCase; + +class MultipartRequestParserTest extends TestCase { + + protected LoggerInterface&MockObject $logger; + + protected function setUp(): void { + parent::setUp(); + $this->logger = $this->createMock(LoggerInterface::class); + } + + private static function getValidBodyObject(): array { + return [ + [ + 'headers' => [ + 'Content-Length' => 7, + 'X-File-MD5' => '4f2377b4d911f7ec46325fe603c3af03', + 'OC-Checksum' => 'md5:4f2377b4d911f7ec46325fe603c3af03', + 'X-File-Path' => '/coucou.txt' + ], + 'content' => "Coucou\n" + ] + ]; + } + + private function getMultipartParser(array $parts, array $headers = [], string $boundary = 'boundary_azertyuiop'): MultipartRequestParser { + /** @var RequestInterface&MockObject $request */ + $request = $this->getMockBuilder(RequestInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $headers = array_merge(['Content-Type' => 'multipart/related; boundary=' . $boundary], $headers); + $request->expects($this->any()) + ->method('getHeader') + ->willReturnCallback(function (string $key) use (&$headers) { + return $headers[$key]; + }); + + $body = ''; + foreach ($parts as $part) { + $body .= '--' . $boundary . "\r\n"; + + foreach ($part['headers'] as $headerKey => $headerPart) { + $body .= $headerKey . ': ' . $headerPart . "\r\n"; + } + + $body .= "\r\n"; + $body .= $part['content'] . "\r\n"; + } + + $body .= '--' . $boundary . '--'; + + $stream = fopen('php://temp', 'r+'); + fwrite($stream, $body); + rewind($stream); + + $request->expects($this->any()) + ->method('getBody') + ->willReturn($stream); + + return new MultipartRequestParser($request, $this->logger); + } + + + /** + * Test validation of the request's body type + */ + public function testBodyTypeValidation(): void { + $bodyStream = 'I am not a stream, but pretend to be'; + /** @var RequestInterface&MockObject $request */ + $request = $this->getMockBuilder(RequestInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $request->expects($this->any()) + ->method('getBody') + ->willReturn($bodyStream); + + $this->expectExceptionMessage('Body should be of type resource'); + new MultipartRequestParser($request, $this->logger); + } + + /** + * Test with valid request. + * - valid boundary + * - valid hash + * - valid content-length + * - valid file content + * - valid file path + */ + public function testValidRequest(): void { + $bodyObject = self::getValidBodyObject(); + unset($bodyObject['0']['headers']['X-File-MD5']); + + $multipartParser = $this->getMultipartParser($bodyObject); + + [$headers, $content] = $multipartParser->parseNextPart(); + + $this->assertSame((int)$headers['content-length'], 7, 'Content-Length header should be the same as provided.'); + $this->assertSame($headers['oc-checksum'], 'md5:4f2377b4d911f7ec46325fe603c3af03', 'OC-Checksum header should be the same as provided.'); + $this->assertSame($headers['x-file-path'], '/coucou.txt', 'X-File-Path header should be the same as provided.'); + + $this->assertSame($content, "Coucou\n", 'Content should be the same'); + } + + /** + * Test with valid request. + * - valid boundary + * - valid md5 hash + * - valid content-length + * - valid file content + * - valid file path + */ + public function testValidRequestWithMd5(): void { + $bodyObject = self::getValidBodyObject(); + unset($bodyObject['0']['headers']['OC-Checksum']); + + $multipartParser = $this->getMultipartParser($bodyObject); + + [$headers, $content] = $multipartParser->parseNextPart(); + + $this->assertSame((int)$headers['content-length'], 7, 'Content-Length header should be the same as provided.'); + $this->assertSame($headers['x-file-md5'], '4f2377b4d911f7ec46325fe603c3af03', 'X-File-MD5 header should be the same as provided.'); + $this->assertSame($headers['x-file-path'], '/coucou.txt', 'X-File-Path header should be the same as provided.'); + + $this->assertSame($content, "Coucou\n", 'Content should be the same'); + } + + /** + * Test with invalid hash. + */ + public function testInvalidHash(): void { + $bodyObject = self::getValidBodyObject(); + $bodyObject['0']['headers']['OC-Checksum'] = 'md5:f2377b4d911f7ec46325fe603c3af03'; + unset($bodyObject['0']['headers']['X-File-MD5']); + $multipartParser = $this->getMultipartParser( + $bodyObject + ); + + $this->expectExceptionMessage('Computed md5 hash is incorrect (4f2377b4d911f7ec46325fe603c3af03).'); + $multipartParser->parseNextPart(); + } + + /** + * Test with invalid md5 hash. + */ + public function testInvalidMd5Hash(): void { + $bodyObject = self::getValidBodyObject(); + unset($bodyObject['0']['headers']['OC-Checksum']); + $bodyObject['0']['headers']['X-File-MD5'] = 'f2377b4d911f7ec46325fe603c3af03'; + $multipartParser = $this->getMultipartParser( + $bodyObject + ); + + $this->expectExceptionMessage('Computed md5 hash is incorrect (4f2377b4d911f7ec46325fe603c3af03).'); + $multipartParser->parseNextPart(); + } + + /** + * Test with a null hash headers. + */ + public function testNullHash(): void { + $bodyObject = self::getValidBodyObject(); + unset($bodyObject['0']['headers']['OC-Checksum']); + unset($bodyObject['0']['headers']['X-File-MD5']); + $multipartParser = $this->getMultipartParser( + $bodyObject + ); + + $this->expectExceptionMessage('The hash headers must not be null.'); + $multipartParser->parseNextPart(); + } + + /** + * Test with a null Content-Length. + */ + public function testNullContentLength(): void { + $bodyObject = self::getValidBodyObject(); + unset($bodyObject['0']['headers']['Content-Length']); + $multipartParser = $this->getMultipartParser( + $bodyObject + ); + + $this->expectExceptionMessage('The Content-Length header must not be null.'); + $multipartParser->parseNextPart(); + } + + /** + * Test with a lower Content-Length. + */ + public function testLowerContentLength(): void { + $bodyObject = self::getValidBodyObject(); + $bodyObject['0']['headers']['Content-Length'] = 6; + $multipartParser = $this->getMultipartParser( + $bodyObject + ); + + $this->expectExceptionMessage('Computed md5 hash is incorrect (41060d3ddfdf63e68fc2bf196f652ee9).'); + $multipartParser->parseNextPart(); + } + + /** + * Test with a higher Content-Length. + */ + public function testHigherContentLength(): void { + $bodyObject = self::getValidBodyObject(); + $bodyObject['0']['headers']['Content-Length'] = 8; + $multipartParser = $this->getMultipartParser( + $bodyObject + ); + + $this->expectExceptionMessage('Computed md5 hash is incorrect (0161002bbee6a744f18741b8a914e413).'); + $multipartParser->parseNextPart(); + } + + /** + * Test with wrong boundary in body. + */ + public function testWrongBoundary(): void { + $bodyObject = self::getValidBodyObject(); + $multipartParser = $this->getMultipartParser( + $bodyObject, + ['Content-Type' => 'multipart/related; boundary=boundary_poiuytreza'] + ); + + $this->expectExceptionMessage('Boundary not found where it should be.'); + $multipartParser->parseNextPart(); + } + + /** + * Test with no boundary in request headers. + */ + public function testNoBoundaryInHeader(): void { + $bodyObject = self::getValidBodyObject(); + $this->expectExceptionMessage('Error while parsing boundary in Content-Type header.'); + $this->getMultipartParser( + $bodyObject, + ['Content-Type' => 'multipart/related'] + ); + } + + /** + * Test with no boundary in the request's headers. + */ + public function testNoBoundaryInBody(): void { + $bodyObject = self::getValidBodyObject(); + $multipartParser = $this->getMultipartParser( + $bodyObject, + ['Content-Type' => 'multipart/related; boundary=boundary_azertyuiop'], + '' + ); + + $this->expectExceptionMessage('Boundary not found where it should be.'); + $multipartParser->parseNextPart(); + } + + /** + * Test with a boundary with quotes in the request's headers. + */ + public function testBoundaryWithQuotes(): void { + $bodyObject = self::getValidBodyObject(); + $multipartParser = $this->getMultipartParser( + $bodyObject, + ['Content-Type' => 'multipart/related; boundary="boundary_azertyuiop"'], + ); + + $multipartParser->parseNextPart(); + + // Dummy assertion, we just want to test that the parsing works. + $this->assertTrue(true); + } + + /** + * Test with a wrong Content-Type in the request's headers. + */ + public function testWrongContentType(): void { + $bodyObject = self::getValidBodyObject(); + $this->expectExceptionMessage('Content-Type must be multipart/related'); + $this->getMultipartParser( + $bodyObject, + ['Content-Type' => 'multipart/form-data; boundary="boundary_azertyuiop"'], + ); + } + + /** + * Test with a wrong key after the content type in the request's headers. + */ + public function testWrongKeyInContentType(): void { + $bodyObject = self::getValidBodyObject(); + $this->expectExceptionMessage('Boundary is invalid'); + $this->getMultipartParser( + $bodyObject, + ['Content-Type' => 'multipart/related; wrongkey="boundary_azertyuiop"'], + ); + } + + /** + * Test with a null Content-Type in the request's headers. + */ + public function testNullContentType(): void { + $bodyObject = self::getValidBodyObject(); + $this->expectExceptionMessage('Content-Type can not be null'); + $this->getMultipartParser( + $bodyObject, + ['Content-Type' => null], + + ); + } +} diff --git a/apps/dav/tests/unit/Files/Sharing/FilesDropPluginTest.php b/apps/dav/tests/unit/Files/Sharing/FilesDropPluginTest.php new file mode 100644 index 00000000000..1a7ab7179e1 --- /dev/null +++ b/apps/dav/tests/unit/Files/Sharing/FilesDropPluginTest.php @@ -0,0 +1,258 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\Files\Sharing; + +use OCA\DAV\Files\Sharing\FilesDropPlugin; +use OCP\Files\Folder; +use OCP\Files\NotFoundException; +use OCP\Share\IAttributes; +use OCP\Share\IShare; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\Exception\BadRequest; +use Sabre\DAV\Server; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; +use Test\TestCase; + +class FilesDropPluginTest extends TestCase { + + private FilesDropPlugin $plugin; + + private Folder&MockObject $node; + private IShare&MockObject $share; + private Server&MockObject $server; + private RequestInterface&MockObject $request; + private ResponseInterface&MockObject $response; + + protected function setUp(): void { + parent::setUp(); + + $this->node = $this->createMock(Folder::class); + $this->node->method('getPath') + ->willReturn('/files/token'); + + $this->share = $this->createMock(IShare::class); + $this->share->expects(self::any()) + ->method('getNode') + ->willReturn($this->node); + $this->server = $this->createMock(Server::class); + $this->plugin = new FilesDropPlugin(); + + $this->request = $this->createMock(RequestInterface::class); + $this->response = $this->createMock(ResponseInterface::class); + + $attributes = $this->createMock(IAttributes::class); + $this->share->expects($this->any()) + ->method('getAttributes') + ->willReturn($attributes); + + $this->share + ->method('getToken') + ->willReturn('token'); + } + + public function testNotEnabled(): void { + $this->request->expects($this->never()) + ->method($this->anything()); + + $this->plugin->beforeMethod($this->request, $this->response); + } + + public function testValid(): void { + $this->plugin->enable(); + $this->plugin->setShare($this->share); + + $this->request->method('getMethod') + ->willReturn('PUT'); + + $this->request->method('getPath') + ->willReturn('/files/token/file.txt'); + + $this->request->method('getBaseUrl') + ->willReturn('https://example.com'); + + $this->node->expects(self::once()) + ->method('getNonExistingName') + ->with('file.txt') + ->willReturn('file.txt'); + + $this->request->expects($this->once()) + ->method('setUrl') + ->with('https://example.com/files/token/file.txt'); + + $this->plugin->beforeMethod($this->request, $this->response); + } + + public function testFileAlreadyExistsValid(): void { + $this->plugin->enable(); + $this->plugin->setShare($this->share); + + $this->request->method('getMethod') + ->willReturn('PUT'); + + $this->request->method('getPath') + ->willReturn('/files/token/file.txt'); + + $this->request->method('getBaseUrl') + ->willReturn('https://example.com'); + + $this->node->method('getNonExistingName') + ->with('file.txt') + ->willReturn('file (2).txt'); + + $this->request->expects($this->once()) + ->method('setUrl') + ->with($this->equalTo('https://example.com/files/token/file (2).txt')); + + $this->plugin->beforeMethod($this->request, $this->response); + } + + public function testNoMKCOLWithoutNickname(): void { + $this->plugin->enable(); + $this->plugin->setShare($this->share); + + $this->request->method('getMethod') + ->willReturn('MKCOL'); + + $this->expectException(BadRequest::class); + + $this->plugin->beforeMethod($this->request, $this->response); + } + + public function testMKCOLWithNickname(): void { + $this->plugin->enable(); + $this->plugin->setShare($this->share); + + $this->request->method('getMethod') + ->willReturn('MKCOL'); + + $this->request->method('hasHeader') + ->with('X-NC-Nickname') + ->willReturn(true); + $this->request->method('getHeader') + ->with('X-NC-Nickname') + ->willReturn('nickname'); + + $this->expectNotToPerformAssertions(); + + $this->plugin->beforeMethod($this->request, $this->response); + } + + public function testSubdirPut(): void { + $this->plugin->enable(); + $this->plugin->setShare($this->share); + + $this->request->method('getMethod') + ->willReturn('PUT'); + + $this->request->method('hasHeader') + ->with('X-NC-Nickname') + ->willReturn(true); + $this->request->method('getHeader') + ->with('X-NC-Nickname') + ->willReturn('nickname'); + + $this->request->method('getPath') + ->willReturn('/files/token/folder/file.txt'); + + $this->request->method('getBaseUrl') + ->willReturn('https://example.com'); + + $nodeName = $this->createMock(Folder::class); + $nodeFolder = $this->createMock(Folder::class); + $nodeFolder->expects(self::once()) + ->method('getPath') + ->willReturn('/files/token/nickname/folder'); + $nodeFolder->method('getNonExistingName') + ->with('file.txt') + ->willReturn('file.txt'); + $nodeName->expects(self::once()) + ->method('get') + ->with('folder') + ->willThrowException(new NotFoundException()); + $nodeName->expects(self::once()) + ->method('newFolder') + ->with('folder') + ->willReturn($nodeFolder); + + $this->node->expects(self::once()) + ->method('get') + ->willThrowException(new NotFoundException()); + $this->node->expects(self::once()) + ->method('newFolder') + ->with('nickname') + ->willReturn($nodeName); + + $this->request->expects($this->once()) + ->method('setUrl') + ->with($this->equalTo('https://example.com/files/token/nickname/folder/file.txt')); + + $this->plugin->beforeMethod($this->request, $this->response); + } + + public function testRecursiveFolderCreation(): void { + $this->plugin->enable(); + $this->plugin->setShare($this->share); + + $this->request->method('getMethod') + ->willReturn('PUT'); + $this->request->method('hasHeader') + ->with('X-NC-Nickname') + ->willReturn(true); + $this->request->method('getHeader') + ->with('X-NC-Nickname') + ->willReturn('nickname'); + + $this->request->method('getPath') + ->willReturn('/files/token/folder/subfolder/file.txt'); + $this->request->method('getBaseUrl') + ->willReturn('https://example.com'); + + $this->request->expects($this->once()) + ->method('setUrl') + ->with($this->equalTo('https://example.com/files/token/nickname/folder/subfolder/file.txt')); + + $subfolder = $this->createMock(Folder::class); + $subfolder->expects(self::once()) + ->method('getNonExistingName') + ->with('file.txt') + ->willReturn('file.txt'); + $subfolder->expects(self::once()) + ->method('getPath') + ->willReturn('/files/token/nickname/folder/subfolder'); + + $folder = $this->createMock(Folder::class); + $folder->expects(self::once()) + ->method('get') + ->with('subfolder') + ->willReturn($subfolder); + + $nickname = $this->createMock(Folder::class); + $nickname->expects(self::once()) + ->method('get') + ->with('folder') + ->willReturn($folder); + + $this->node->method('get') + ->with('nickname') + ->willReturn($nickname); + $this->plugin->beforeMethod($this->request, $this->response); + } + + public function testOnMkcol(): void { + $this->plugin->enable(); + $this->plugin->setShare($this->share); + + $this->response->expects($this->once()) + ->method('setStatus') + ->with(201); + + $response = $this->plugin->onMkcol($this->request, $this->response); + $this->assertFalse($response); + } +} diff --git a/apps/dav/tests/unit/Listener/ActivityUpdaterListenerTest.php b/apps/dav/tests/unit/Listener/ActivityUpdaterListenerTest.php new file mode 100644 index 00000000000..8519dca7126 --- /dev/null +++ b/apps/dav/tests/unit/Listener/ActivityUpdaterListenerTest.php @@ -0,0 +1,80 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\Listener; + +use OCA\DAV\CalDAV\Activity\Backend as ActivityBackend; +use OCA\DAV\CalDAV\Activity\Provider\Event; +use OCA\DAV\DAV\Sharing\Plugin as SharingPlugin; +use OCA\DAV\Events\CalendarDeletedEvent; +use OCA\DAV\Listener\ActivityUpdaterListener; +use OCP\Calendar\Events\CalendarObjectDeletedEvent; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class ActivityUpdaterListenerTest extends TestCase { + + private ActivityBackend&MockObject $activityBackend; + private LoggerInterface&MockObject $logger; + private ActivityUpdaterListener $listener; + + protected function setUp(): void { + parent::setUp(); + + $this->activityBackend = $this->createMock(ActivityBackend::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->listener = new ActivityUpdaterListener( + $this->activityBackend, + $this->logger + ); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataForTestHandleCalendarObjectDeletedEvent')] + public function testHandleCalendarObjectDeletedEvent(int $calendarId, array $calendarData, array $shares, array $objectData, bool $createsActivity): void { + $event = new CalendarObjectDeletedEvent($calendarId, $calendarData, $shares, $objectData); + $this->logger->expects($this->once())->method('debug')->with( + $createsActivity ? "Activity generated for deleted calendar object in calendar $calendarId" : "Calendar object in calendar $calendarId was already in trashbin, skipping deletion activity" + ); + $this->activityBackend->expects($createsActivity ? $this->once() : $this->never())->method('onTouchCalendarObject')->with( + Event::SUBJECT_OBJECT_DELETE, + $calendarData, + $shares, + $objectData + ); + $this->listener->handle($event); + } + + public static function dataForTestHandleCalendarObjectDeletedEvent(): array { + return [ + [1, [], [], [], true], + [1, [], [], ['{' . SharingPlugin::NS_NEXTCLOUD . '}deleted-at' => 120], false], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataForTestHandleCalendarDeletedEvent')] + public function testHandleCalendarDeletedEvent(int $calendarId, array $calendarData, array $shares, bool $createsActivity): void { + $event = new CalendarDeletedEvent($calendarId, $calendarData, $shares); + $this->logger->expects($this->once())->method('debug')->with( + $createsActivity ? "Activity generated for deleted calendar $calendarId" : "Calendar $calendarId was already in trashbin, skipping deletion activity" + ); + $this->activityBackend->expects($createsActivity ? $this->once() : $this->never())->method('onCalendarDelete')->with( + $calendarData, + $shares + ); + $this->listener->handle($event); + } + + public static function dataForTestHandleCalendarDeletedEvent(): array { + return [ + [1, [], [], true], + [1, ['{' . SharingPlugin::NS_NEXTCLOUD . '}deleted-at' => 120], [], false], + ]; + } +} diff --git a/apps/dav/tests/unit/Listener/CalendarContactInteractionListenerTest.php b/apps/dav/tests/unit/Listener/CalendarContactInteractionListenerTest.php new file mode 100644 index 00000000000..dc3dce8a62f --- /dev/null +++ b/apps/dav/tests/unit/Listener/CalendarContactInteractionListenerTest.php @@ -0,0 +1,173 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\Listener; + +use OCA\DAV\Connector\Sabre\Principal; +use OCA\DAV\Events\CalendarShareUpdatedEvent; +use OCA\DAV\Listener\CalendarContactInteractionListener; +use OCP\Calendar\Events\CalendarObjectCreatedEvent; +use OCP\Contacts\Events\ContactInteractedWithEvent; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IUser; +use OCP\IUserSession; +use OCP\Mail\IMailer; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class CalendarContactInteractionListenerTest extends TestCase { + private IEventDispatcher&MockObject $eventDispatcher; + private IUserSession&MockObject $userSession; + private Principal&MockObject $principalConnector; + private LoggerInterface&MockObject $logger; + private IMailer&MockObject $mailer; + private CalendarContactInteractionListener $listener; + + protected function setUp(): void { + parent::setUp(); + + $this->eventDispatcher = $this->createMock(IEventDispatcher::class); + $this->userSession = $this->createMock(IUserSession::class); + $this->principalConnector = $this->createMock(Principal::class); + $this->mailer = $this->createMock(IMailer::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->listener = new CalendarContactInteractionListener( + $this->eventDispatcher, + $this->userSession, + $this->principalConnector, + $this->mailer, + $this->logger + ); + } + + public function testParseUnrelated(): void { + $event = new Event(); + $this->eventDispatcher->expects(self::never())->method('dispatchTyped'); + + $this->listener->handle($event); + } + + public function testHandleWithoutAnythingInteresting(): void { + $event = new CalendarShareUpdatedEvent(123, [], [], [], []); + $user = $this->createMock(IUser::class); + $this->userSession->expects(self::once())->method('getUser')->willReturn($user); + $this->eventDispatcher->expects(self::never())->method('dispatchTyped'); + + $this->listener->handle($event); + } + + public function testParseInvalidData(): void { + $event = new CalendarObjectCreatedEvent(123, [], [], ['calendardata' => 'BEGIN:FOO']); + $user = $this->createMock(IUser::class); + $this->userSession->expects(self::once())->method('getUser')->willReturn($user); + $this->eventDispatcher->expects(self::never())->method('dispatchTyped'); + $this->logger->expects(self::once())->method('warning'); + + $this->listener->handle($event); + } + + public function testParseCalendarEventWithInvalidEmail(): void { + $event = new CalendarObjectCreatedEvent(123, [], [], ['calendardata' => <<<EVENT +BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +PRODID:-//IDN nextcloud.com//Calendar app 2.1.3//EN +BEGIN:VTIMEZONE +TZID:Europe/Vienna +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20210202T091151Z +DTSTAMP:20210203T130231Z +LAST-MODIFIED:20210203T130231Z +SEQUENCE:9 +UID:b74a0c8e-93b0-447f-aed5-b679b19e874a +DTSTART;TZID=Europe/Vienna:20210202T103000 +DTEND;TZID=Europe/Vienna:20210202T133000 +SUMMARY:tes +ORGANIZER;CN=admin:mailto:christoph.wurst@nextcloud.com +ATTENDEE;CN=somethingbutnotanemail;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION; + ROLE=REQ-PARTICIPANT;RSVP=FALSE:mailto:somethingbutnotanemail +DESCRIPTION:test +END:VEVENT +END:VCALENDAR +EVENT]); + $user = $this->createMock(IUser::class); + $this->userSession->expects(self::once())->method('getUser')->willReturn($user); + $this->eventDispatcher->expects(self::never())->method('dispatchTyped'); + $this->logger->expects(self::never())->method('warning'); + + $this->listener->handle($event); + } + + public function testParseCalendarEvent(): void { + $event = new CalendarObjectCreatedEvent(123, [], [], ['calendardata' => <<<EVENT +BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +PRODID:-//IDN nextcloud.com//Calendar app 2.1.3//EN +BEGIN:VTIMEZONE +TZID:Europe/Vienna +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20210202T091151Z +DTSTAMP:20210203T130231Z +LAST-MODIFIED:20210203T130231Z +SEQUENCE:9 +UID:b74a0c8e-93b0-447f-aed5-b679b19e874a +DTSTART;TZID=Europe/Vienna:20210202T103000 +DTEND;TZID=Europe/Vienna:20210202T133000 +SUMMARY:tes +ORGANIZER;CN=admin:mailto:christoph.wurst@nextcloud.com +ATTENDEE;CN=user@domain.tld;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION; + ROLE=REQ-PARTICIPANT;RSVP=FALSE:mailto:user@domain.tld +DESCRIPTION:test +END:VEVENT +END:VCALENDAR +EVENT]); + $user = $this->createMock(IUser::class); + $this->userSession->expects(self::once())->method('getUser')->willReturn($user); + $this->mailer->expects(self::once())->method('validateMailAddress')->willReturn(true); + $this->eventDispatcher->expects(self::once()) + ->method('dispatchTyped') + ->with(self::equalTo((new ContactInteractedWithEvent($user))->setEmail('user@domain.tld'))); + $this->logger->expects(self::never())->method('warning'); + + $this->listener->handle($event); + } +} diff --git a/apps/dav/tests/unit/Listener/OutOfOfficeListenerTest.php b/apps/dav/tests/unit/Listener/OutOfOfficeListenerTest.php new file mode 100644 index 00000000000..971d113b742 --- /dev/null +++ b/apps/dav/tests/unit/Listener/OutOfOfficeListenerTest.php @@ -0,0 +1,606 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Tests\unit\Listener; + +use DateTimeImmutable; +use InvalidArgumentException; +use OCA\DAV\CalDAV\Calendar; +use OCA\DAV\CalDAV\CalendarHome; +use OCA\DAV\CalDAV\CalendarObject; +use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer; +use OCA\DAV\CalDAV\Plugin; +use OCA\DAV\CalDAV\TimezoneService; +use OCA\DAV\Connector\Sabre\Server; +use OCA\DAV\Listener\OutOfOfficeListener; +use OCA\DAV\ServerFactory; +use OCP\EventDispatcher\Event; +use OCP\IConfig; +use OCP\IUser; +use OCP\User\Events\OutOfOfficeChangedEvent; +use OCP\User\Events\OutOfOfficeClearedEvent; +use OCP\User\Events\OutOfOfficeScheduledEvent; +use OCP\User\IOutOfOfficeData; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Tree; +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Component\VEvent; +use Sabre\VObject\Reader; +use Test\TestCase; + +/** + * @covers \OCA\DAV\Listener\OutOfOfficeListener + */ +class OutOfOfficeListenerTest extends TestCase { + + private ServerFactory&MockObject $serverFactory; + private IConfig&MockObject $appConfig; + private LoggerInterface&MockObject $loggerInterface; + private TimezoneService&MockObject $timezoneService; + private OutOfOfficeListener $listener; + + protected function setUp(): void { + parent::setUp(); + + $this->serverFactory = $this->createMock(ServerFactory::class); + $this->appConfig = $this->createMock(IConfig::class); + $this->timezoneService = $this->createMock(TimezoneService::class); + $this->loggerInterface = $this->createMock(LoggerInterface::class); + + $this->listener = new OutOfOfficeListener( + $this->serverFactory, + $this->appConfig, + $this->timezoneService, + $this->loggerInterface, + ); + } + + public function testHandleUnrelated(): void { + $event = new Event(); + + $this->listener->handle($event); + + $this->addToAssertionCount(1); + } + + public function testHandleSchedulingNoCalendarHome(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $data = $this->createMock(IOutOfOfficeData::class); + $data->method('getUser')->willReturn($user); + $davServer = $this->createMock(Server::class); + $invitationServer = $this->createMock(InvitationResponseServer::class); + $invitationServer->method('getServer')->willReturn($davServer); + $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer); + $caldavPlugin = $this->createMock(Plugin::class); + $davServer->expects(self::once()) + ->method('getPlugin') + ->with('caldav') + ->willReturn($caldavPlugin); + $event = new OutOfOfficeScheduledEvent($data); + + $this->listener->handle($event); + } + + public function testHandleSchedulingNoCalendarHomeNode(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $data = $this->createMock(IOutOfOfficeData::class); + $data->method('getUser')->willReturn($user); + $davServer = $this->createMock(Server::class); + $invitationServer = $this->createMock(InvitationResponseServer::class); + $invitationServer->method('getServer')->willReturn($davServer); + $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer); + $caldavPlugin = $this->createMock(Plugin::class); + $davServer->expects(self::once()) + ->method('getPlugin') + ->with('caldav') + ->willReturn($caldavPlugin); + $caldavPlugin->expects(self::once()) + ->method('getCalendarHomeForPrincipal') + ->willReturn('/home/calendar'); + $tree = $this->createMock(Tree::class); + $davServer->tree = $tree; + $tree->expects(self::once()) + ->method('getNodeForPath') + ->with('/home/calendar') + ->willThrowException(new NotFound('nope')); + $event = new OutOfOfficeScheduledEvent($data); + + $this->listener->handle($event); + } + + public function testHandleSchedulingPersonalCalendarNotFound(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $data = $this->createMock(IOutOfOfficeData::class); + $data->method('getUser')->willReturn($user); + $davServer = $this->createMock(Server::class); + $invitationServer = $this->createMock(InvitationResponseServer::class); + $invitationServer->method('getServer')->willReturn($davServer); + $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer); + $caldavPlugin = $this->createMock(Plugin::class); + $davServer->expects(self::once()) + ->method('getPlugin') + ->with('caldav') + ->willReturn($caldavPlugin); + $caldavPlugin->expects(self::once()) + ->method('getCalendarHomeForPrincipal') + ->willReturn('/home/calendar'); + $tree = $this->createMock(Tree::class); + $davServer->tree = $tree; + $calendarHome = $this->createMock(CalendarHome::class); + $tree->expects(self::once()) + ->method('getNodeForPath') + ->with('/home/calendar') + ->willReturn($calendarHome); + $this->appConfig->expects(self::once()) + ->method('getUserValue') + ->with('user123', 'dav', 'defaultCalendar', 'personal') + ->willReturn('personal-1'); + $calendarHome->expects(self::once()) + ->method('getChild') + ->with('personal-1') + ->willThrowException(new NotFound('nope')); + $event = new OutOfOfficeScheduledEvent($data); + + $this->listener->handle($event); + } + + public function testHandleSchedulingWithDefaultTimezone(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $data = $this->createMock(IOutOfOfficeData::class); + $data->method('getUser')->willReturn($user); + $data->method('getStartDate') + ->willReturn((new DateTimeImmutable('2023-12-12T00:00:00Z'))->getTimestamp()); + $data->method('getEndDate') + ->willReturn((new DateTimeImmutable('2023-12-13T00:00:00Z'))->getTimestamp()); + $davServer = $this->createMock(Server::class); + $invitationServer = $this->createMock(InvitationResponseServer::class); + $invitationServer->method('getServer')->willReturn($davServer); + $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer); + $caldavPlugin = $this->createMock(Plugin::class); + $davServer->expects(self::once()) + ->method('getPlugin') + ->with('caldav') + ->willReturn($caldavPlugin); + $caldavPlugin->expects(self::once()) + ->method('getCalendarHomeForPrincipal') + ->willReturn('/home/calendar'); + $tree = $this->createMock(Tree::class); + $davServer->tree = $tree; + $calendarHome = $this->createMock(CalendarHome::class); + $tree->expects(self::once()) + ->method('getNodeForPath') + ->with('/home/calendar') + ->willReturn($calendarHome); + $this->appConfig->expects(self::once()) + ->method('getUserValue') + ->with('user123', 'dav', 'defaultCalendar', 'personal') + ->willReturn('personal-1'); + $calendar = $this->createMock(Calendar::class); + $this->timezoneService->expects(self::once()) + ->method('getUserTimezone') + ->with('user123') + ->willReturn('Europe/Prague'); + $calendarHome->expects(self::once()) + ->method('getChild') + ->with('personal-1') + ->willReturn($calendar); + $calendar->expects(self::once()) + ->method('createFile') + ->willReturnCallback(function ($name, $data): void { + $vcalendar = Reader::read($data); + if (!($vcalendar instanceof VCalendar)) { + throw new InvalidArgumentException('Calendar data should be a VCALENDAR'); + } + $vevent = $vcalendar->VEVENT; + if ($vevent === null || !($vevent instanceof VEvent)) { + throw new InvalidArgumentException('Calendar data should contain a VEVENT'); + } + self::assertSame('Europe/Prague', $vevent->DTSTART['TZID']?->getValue()); + self::assertSame('Europe/Prague', $vevent->DTEND['TZID']?->getValue()); + }); + $event = new OutOfOfficeScheduledEvent($data); + + $this->listener->handle($event); + } + + public function testHandleChangeNoCalendarHome(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $data = $this->createMock(IOutOfOfficeData::class); + $data->method('getUser')->willReturn($user); + $davServer = $this->createMock(Server::class); + $invitationServer = $this->createMock(InvitationResponseServer::class); + $invitationServer->method('getServer')->willReturn($davServer); + $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer); + $caldavPlugin = $this->createMock(Plugin::class); + $davServer->expects(self::once()) + ->method('getPlugin') + ->with('caldav') + ->willReturn($caldavPlugin); + $event = new OutOfOfficeChangedEvent($data); + + $this->listener->handle($event); + } + + public function testHandleChangeNoCalendarHomeNode(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $data = $this->createMock(IOutOfOfficeData::class); + $data->method('getUser')->willReturn($user); + $davServer = $this->createMock(Server::class); + $invitationServer = $this->createMock(InvitationResponseServer::class); + $invitationServer->method('getServer')->willReturn($davServer); + $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer); + $caldavPlugin = $this->createMock(Plugin::class); + $davServer->expects(self::once()) + ->method('getPlugin') + ->with('caldav') + ->willReturn($caldavPlugin); + $caldavPlugin->expects(self::once()) + ->method('getCalendarHomeForPrincipal') + ->willReturn('/home/calendar'); + $tree = $this->createMock(Tree::class); + $davServer->tree = $tree; + $tree->expects(self::once()) + ->method('getNodeForPath') + ->with('/home/calendar') + ->willThrowException(new NotFound('nope')); + $event = new OutOfOfficeChangedEvent($data); + + $this->listener->handle($event); + } + + public function testHandleChangePersonalCalendarNotFound(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $data = $this->createMock(IOutOfOfficeData::class); + $data->method('getUser')->willReturn($user); + $davServer = $this->createMock(Server::class); + $invitationServer = $this->createMock(InvitationResponseServer::class); + $invitationServer->method('getServer')->willReturn($davServer); + $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer); + $caldavPlugin = $this->createMock(Plugin::class); + $davServer->expects(self::once()) + ->method('getPlugin') + ->with('caldav') + ->willReturn($caldavPlugin); + $caldavPlugin->expects(self::once()) + ->method('getCalendarHomeForPrincipal') + ->willReturn('/home/calendar'); + $tree = $this->createMock(Tree::class); + $davServer->tree = $tree; + $calendarHome = $this->createMock(CalendarHome::class); + $tree->expects(self::once()) + ->method('getNodeForPath') + ->with('/home/calendar') + ->willReturn($calendarHome); + $this->appConfig->expects(self::once()) + ->method('getUserValue') + ->with('user123', 'dav', 'defaultCalendar', 'personal') + ->willReturn('personal-1'); + $calendarHome->expects(self::once()) + ->method('getChild') + ->with('personal-1') + ->willThrowException(new NotFound('nope')); + $event = new OutOfOfficeChangedEvent($data); + + $this->listener->handle($event); + } + + public function testHandleChangeRecreate(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $data = $this->createMock(IOutOfOfficeData::class); + $data->method('getUser')->willReturn($user); + $data->method('getStartDate') + ->willReturn((new DateTimeImmutable('2023-12-12T00:00:00Z'))->getTimestamp()); + $data->method('getEndDate') + ->willReturn((new DateTimeImmutable('2023-12-14T00:00:00Z'))->getTimestamp()); + $davServer = $this->createMock(Server::class); + $invitationServer = $this->createMock(InvitationResponseServer::class); + $invitationServer->method('getServer')->willReturn($davServer); + $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer); + $caldavPlugin = $this->createMock(Plugin::class); + $davServer->expects(self::once()) + ->method('getPlugin') + ->with('caldav') + ->willReturn($caldavPlugin); + $caldavPlugin->expects(self::once()) + ->method('getCalendarHomeForPrincipal') + ->willReturn('/home/calendar'); + $tree = $this->createMock(Tree::class); + $davServer->tree = $tree; + $calendarHome = $this->createMock(CalendarHome::class); + $tree->expects(self::once()) + ->method('getNodeForPath') + ->with('/home/calendar') + ->willReturn($calendarHome); + $this->appConfig->expects(self::once()) + ->method('getUserValue') + ->with('user123', 'dav', 'defaultCalendar', 'personal') + ->willReturn('personal-1'); + $calendar = $this->createMock(Calendar::class); + $this->timezoneService->expects(self::once()) + ->method('getUserTimezone') + ->with('user123') + ->willReturn(null); + $this->timezoneService->expects(self::once()) + ->method('getDefaultTimezone') + ->willReturn('Europe/Berlin'); + $calendarHome->expects(self::once()) + ->method('getChild') + ->with('personal-1') + ->willReturn($calendar); + $calendar->expects(self::once()) + ->method('getChild') + ->willThrowException(new NotFound()); + $calendar->expects(self::once()) + ->method('createFile') + ->willReturnCallback(function ($name, $data): void { + $vcalendar = Reader::read($data); + if (!($vcalendar instanceof VCalendar)) { + throw new InvalidArgumentException('Calendar data should be a VCALENDAR'); + } + $vevent = $vcalendar->VEVENT; + if ($vevent === null || !($vevent instanceof VEvent)) { + throw new InvalidArgumentException('Calendar data should contain a VEVENT'); + } + self::assertSame('Europe/Berlin', $vevent->DTSTART['TZID']?->getValue()); + self::assertSame('Europe/Berlin', $vevent->DTEND['TZID']?->getValue()); + }); + $event = new OutOfOfficeChangedEvent($data); + + $this->listener->handle($event); + } + + public function testHandleChangeWithoutTimezone(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $data = $this->createMock(IOutOfOfficeData::class); + $data->method('getUser')->willReturn($user); + $data->method('getStartDate') + ->willReturn((new DateTimeImmutable('2023-01-12T00:00:00Z'))->getTimestamp()); + $data->method('getEndDate') + ->willReturn((new DateTimeImmutable('2023-12-14T00:00:00Z'))->getTimestamp()); + $davServer = $this->createMock(Server::class); + $invitationServer = $this->createMock(InvitationResponseServer::class); + $invitationServer->method('getServer')->willReturn($davServer); + $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer); + $caldavPlugin = $this->createMock(Plugin::class); + $davServer->expects(self::once()) + ->method('getPlugin') + ->with('caldav') + ->willReturn($caldavPlugin); + $caldavPlugin->expects(self::once()) + ->method('getCalendarHomeForPrincipal') + ->willReturn('/home/calendar'); + $tree = $this->createMock(Tree::class); + $davServer->tree = $tree; + $calendarHome = $this->createMock(CalendarHome::class); + $tree->expects(self::once()) + ->method('getNodeForPath') + ->with('/home/calendar') + ->willReturn($calendarHome); + $this->appConfig->expects(self::once()) + ->method('getUserValue') + ->with('user123', 'dav', 'defaultCalendar', 'personal') + ->willReturn('personal-1'); + $calendar = $this->createMock(Calendar::class); + $calendarHome->expects(self::once()) + ->method('getChild') + ->with('personal-1') + ->willReturn($calendar); + $eventNode = $this->createMock(CalendarObject::class); + $this->timezoneService->expects(self::once()) + ->method('getUserTimezone') + ->with('user123') + ->willReturn(null); + $this->timezoneService->expects(self::once()) + ->method('getDefaultTimezone') + ->willReturn('UTC'); + $calendar->expects(self::once()) + ->method('getChild') + ->willReturn($eventNode); + $eventNode->expects(self::once()) + ->method('put') + ->willReturnCallback(function ($data): void { + $vcalendar = Reader::read($data); + if (!($vcalendar instanceof VCalendar)) { + throw new InvalidArgumentException('Calendar data should be a VCALENDAR'); + } + $vevent = $vcalendar->VEVENT; + if ($vevent === null || !($vevent instanceof VEvent)) { + throw new InvalidArgumentException('Calendar data should contain a VEVENT'); + } + // UTC datetimes are stored without a TZID + self::assertSame(null, $vevent->DTSTART['TZID']?->getValue()); + self::assertSame(null, $vevent->DTEND['TZID']?->getValue()); + }); + $calendar->expects(self::never()) + ->method('createFile'); + $event = new OutOfOfficeChangedEvent($data); + + $this->listener->handle($event); + } + + public function testHandleClearNoCalendarHome(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $data = $this->createMock(IOutOfOfficeData::class); + $data->method('getUser')->willReturn($user); + $davServer = $this->createMock(Server::class); + $invitationServer = $this->createMock(InvitationResponseServer::class); + $invitationServer->method('getServer')->willReturn($davServer); + $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer); + $caldavPlugin = $this->createMock(Plugin::class); + $davServer->expects(self::once()) + ->method('getPlugin') + ->with('caldav') + ->willReturn($caldavPlugin); + $event = new OutOfOfficeClearedEvent($data); + + $this->listener->handle($event); + } + + public function testHandleClearNoCalendarHomeNode(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $data = $this->createMock(IOutOfOfficeData::class); + $data->method('getUser')->willReturn($user); + $davServer = $this->createMock(Server::class); + $invitationServer = $this->createMock(InvitationResponseServer::class); + $invitationServer->method('getServer')->willReturn($davServer); + $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer); + $caldavPlugin = $this->createMock(Plugin::class); + $davServer->expects(self::once()) + ->method('getPlugin') + ->with('caldav') + ->willReturn($caldavPlugin); + $caldavPlugin->expects(self::once()) + ->method('getCalendarHomeForPrincipal') + ->willReturn('/home/calendar'); + $tree = $this->createMock(Tree::class); + $davServer->tree = $tree; + $tree->expects(self::once()) + ->method('getNodeForPath') + ->with('/home/calendar') + ->willThrowException(new NotFound('nope')); + $event = new OutOfOfficeClearedEvent($data); + + $this->listener->handle($event); + } + + public function testHandleClearPersonalCalendarNotFound(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $data = $this->createMock(IOutOfOfficeData::class); + $data->method('getUser')->willReturn($user); + $davServer = $this->createMock(Server::class); + $invitationServer = $this->createMock(InvitationResponseServer::class); + $invitationServer->method('getServer')->willReturn($davServer); + $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer); + $caldavPlugin = $this->createMock(Plugin::class); + $davServer->expects(self::once()) + ->method('getPlugin') + ->with('caldav') + ->willReturn($caldavPlugin); + $caldavPlugin->expects(self::once()) + ->method('getCalendarHomeForPrincipal') + ->willReturn('/home/calendar'); + $tree = $this->createMock(Tree::class); + $davServer->tree = $tree; + $calendarHome = $this->createMock(CalendarHome::class); + $tree->expects(self::once()) + ->method('getNodeForPath') + ->with('/home/calendar') + ->willReturn($calendarHome); + $this->appConfig->expects(self::once()) + ->method('getUserValue') + ->with('user123', 'dav', 'defaultCalendar', 'personal') + ->willReturn('personal-1'); + $calendarHome->expects(self::once()) + ->method('getChild') + ->with('personal-1') + ->willThrowException(new NotFound('nope')); + $event = new OutOfOfficeClearedEvent($data); + + $this->listener->handle($event); + } + + public function testHandleClearRecreate(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $data = $this->createMock(IOutOfOfficeData::class); + $data->method('getUser')->willReturn($user); + $davServer = $this->createMock(Server::class); + $invitationServer = $this->createMock(InvitationResponseServer::class); + $invitationServer->method('getServer')->willReturn($davServer); + $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer); + $caldavPlugin = $this->createMock(Plugin::class); + $davServer->expects(self::once()) + ->method('getPlugin') + ->with('caldav') + ->willReturn($caldavPlugin); + $caldavPlugin->expects(self::once()) + ->method('getCalendarHomeForPrincipal') + ->willReturn('/home/calendar'); + $tree = $this->createMock(Tree::class); + $davServer->tree = $tree; + $calendarHome = $this->createMock(CalendarHome::class); + $tree->expects(self::once()) + ->method('getNodeForPath') + ->with('/home/calendar') + ->willReturn($calendarHome); + $this->appConfig->expects(self::once()) + ->method('getUserValue') + ->with('user123', 'dav', 'defaultCalendar', 'personal') + ->willReturn('personal-1'); + $calendar = $this->createMock(Calendar::class); + $calendarHome->expects(self::once()) + ->method('getChild') + ->with('personal-1') + ->willReturn($calendar); + $calendar->expects(self::once()) + ->method('getChild') + ->willThrowException(new NotFound()); + $event = new OutOfOfficeClearedEvent($data); + + $this->listener->handle($event); + } + + public function testHandleClear(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $data = $this->createMock(IOutOfOfficeData::class); + $data->method('getUser')->willReturn($user); + $davServer = $this->createMock(Server::class); + $invitationServer = $this->createMock(InvitationResponseServer::class); + $invitationServer->method('getServer')->willReturn($davServer); + $this->serverFactory->method('createInviationResponseServer')->willReturn($invitationServer); + $caldavPlugin = $this->createMock(Plugin::class); + $davServer->expects(self::once()) + ->method('getPlugin') + ->with('caldav') + ->willReturn($caldavPlugin); + $caldavPlugin->expects(self::once()) + ->method('getCalendarHomeForPrincipal') + ->willReturn('/home/calendar'); + $tree = $this->createMock(Tree::class); + $davServer->tree = $tree; + $calendarHome = $this->createMock(CalendarHome::class); + $tree->expects(self::once()) + ->method('getNodeForPath') + ->with('/home/calendar') + ->willReturn($calendarHome); + $this->appConfig->expects(self::once()) + ->method('getUserValue') + ->with('user123', 'dav', 'defaultCalendar', 'personal') + ->willReturn('personal-1'); + $calendar = $this->createMock(Calendar::class); + $calendarHome->expects(self::once()) + ->method('getChild') + ->with('personal-1') + ->willReturn($calendar); + $eventNode = $this->createMock(CalendarObject::class); + $calendar->expects(self::once()) + ->method('getChild') + ->willReturn($eventNode); + $eventNode->expects(self::once()) + ->method('delete'); + $event = new OutOfOfficeClearedEvent($data); + + $this->listener->handle($event); + } +} diff --git a/apps/dav/tests/unit/Migration/CalDAVRemoveEmptyValueTest.php b/apps/dav/tests/unit/Migration/CalDAVRemoveEmptyValueTest.php new file mode 100644 index 00000000000..1852d2709c1 --- /dev/null +++ b/apps/dav/tests/unit/Migration/CalDAVRemoveEmptyValueTest.php @@ -0,0 +1,230 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\DAV\Migration; + +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\Migration\CalDAVRemoveEmptyValue; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use OCP\Server; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Sabre\VObject\InvalidDataException; +use Test\TestCase; + +/** + * Class CalDAVRemoveEmptyValueTest + * + * @package OCA\DAV\Tests\Unit\DAV\Migration + * @group DB + */ +class CalDAVRemoveEmptyValueTest extends TestCase { + private LoggerInterface&MockObject $logger; + private CalDavBackend&MockObject $backend; + private IOutput&MockObject $output; + private string $invalid = 'BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//Mac OS X 10.11.2//EN +CALSCALE:GREGORIAN +BEGIN:VEVENT +TRANSP:OPAQUE +DTEND;VALUE=:20151223T223000Z +LAST-MODIFIED:20151214T091032Z +ORGANIZER;CN="User 1":mailto:user1@example.com +UID:1234567890@example.com +DTSTAMP:20151214T091032Z +STATUS:CONFIRMED +SEQUENCE:0 +SUMMARY:Ein Geburtstag +DTSTART;VALUE=:20151223T173000Z +X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC +CREATED;VALUE=:20151214T091032Z +END:VEVENT +END:VCALENDAR'; + + private string $valid = 'BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//Mac OS X 10.11.2//EN +CALSCALE:GREGORIAN +BEGIN:VEVENT +TRANSP:OPAQUE +DTEND:20151223T223000Z +LAST-MODIFIED:20151214T091032Z +ORGANIZER;CN="User 1":mailto:user1@example.com +UID:1234567890@example.com +DTSTAMP:20151214T091032Z +STATUS:CONFIRMED +SEQUENCE:0 +SUMMARY:Ein Geburtstag +DTSTART:20151223T173000Z +X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC +CREATED:20151214T091032Z +END:VEVENT +END:VCALENDAR'; + + protected function setUp(): void { + parent::setUp(); + + $this->logger = $this->createMock(LoggerInterface::class); + $this->backend = $this->createMock(CalDavBackend::class); + $this->output = $this->createMock(IOutput::class); + } + + public function testRunAllValid(): void { + /** @var CalDAVRemoveEmptyValue&MockObject $step */ + $step = $this->getMockBuilder(CalDAVRemoveEmptyValue::class) + ->setConstructorArgs([ + Server::get(IDBConnection::class), + $this->backend, + $this->logger + ]) + ->onlyMethods(['getInvalidObjects']) + ->getMock(); + + $step->expects($this->once()) + ->method('getInvalidObjects') + ->willReturn([]); + + $this->output->expects($this->once()) + ->method('startProgress') + ->with(0); + $this->output->expects($this->once()) + ->method('finishProgress'); + + $step->run($this->output); + } + + public function testRunInvalid(): void { + /** @var CalDAVRemoveEmptyValue&MockObject $step */ + $step = $this->getMockBuilder(CalDAVRemoveEmptyValue::class) + ->setConstructorArgs([ + Server::get(IDBConnection::class), + $this->backend, + $this->logger + ]) + ->onlyMethods(['getInvalidObjects']) + ->getMock(); + + $step->expects($this->once()) + ->method('getInvalidObjects') + ->willReturn([ + ['calendarid' => '42', 'uri' => 'myuri'], + ]); + + $this->output->expects($this->once()) + ->method('startProgress') + ->with(1); + $this->output->expects($this->once()) + ->method('finishProgress'); + + $this->backend->expects($this->exactly(1)) + ->method('getCalendarObject') + ->with(42, 'myuri') + ->willReturn([ + 'calendardata' => $this->invalid + ]); + + $this->output->expects($this->exactly(1)) + ->method('advance'); + $this->backend->expects($this->exactly(1)) + ->method('getDenormalizedData') + ->with($this->valid); + + $this->backend->expects($this->exactly(1)) + ->method('updateCalendarObject') + ->with(42, 'myuri', $this->valid); + + $step->run($this->output); + } + + public function testRunValid(): void { + /** @var CalDAVRemoveEmptyValue&MockObject $step */ + $step = $this->getMockBuilder(CalDAVRemoveEmptyValue::class) + ->setConstructorArgs([ + Server::get(IDBConnection::class), + $this->backend, + $this->logger + ]) + ->onlyMethods(['getInvalidObjects']) + ->getMock(); + + $step->expects($this->once()) + ->method('getInvalidObjects') + ->willReturn([ + ['calendarid' => '42', 'uri' => 'myuri'], + ]); + + $this->output->expects($this->once()) + ->method('startProgress') + ->with(1); + $this->output->expects($this->once()) + ->method('finishProgress'); + + + $this->backend->expects($this->exactly(1)) + ->method('getCalendarObject') + ->with(42, 'myuri') + ->willReturn([ + 'calendardata' => $this->valid + ]); + + $this->output->expects($this->never()) + ->method('advance'); + $this->backend->expects($this->never()) + ->method('getDenormalizedData'); + + $this->backend->expects($this->never()) + ->method('updateCalendarObject'); + + $step->run($this->output); + } + + public function testRunStillInvalid(): void { + /** @var CalDAVRemoveEmptyValue&MockObject $step */ + $step = $this->getMockBuilder(CalDAVRemoveEmptyValue::class) + ->setConstructorArgs([ + Server::get(IDBConnection::class), + $this->backend, + $this->logger + ]) + ->onlyMethods(['getInvalidObjects']) + ->getMock(); + + $step->expects($this->once()) + ->method('getInvalidObjects') + ->willReturn([ + ['calendarid' => '42', 'uri' => 'myuri'], + ]); + + $this->output->expects($this->once()) + ->method('startProgress') + ->with(1); + $this->output->expects($this->once()) + ->method('finishProgress'); + + + $this->backend->expects($this->exactly(1)) + ->method('getCalendarObject') + ->with(42, 'myuri') + ->willReturn([ + 'calendardata' => $this->invalid + ]); + + $this->output->expects($this->exactly(1)) + ->method('advance'); + $this->backend->expects($this->exactly(1)) + ->method('getDenormalizedData') + ->with($this->valid) + ->willThrowException(new InvalidDataException()); + + $this->backend->expects($this->never()) + ->method('updateCalendarObject'); + + $step->run($this->output); + } +} diff --git a/apps/dav/tests/unit/Migration/CreateSystemAddressBookStepTest.php b/apps/dav/tests/unit/Migration/CreateSystemAddressBookStepTest.php new file mode 100644 index 00000000000..667d2e39d3a --- /dev/null +++ b/apps/dav/tests/unit/Migration/CreateSystemAddressBookStepTest.php @@ -0,0 +1,47 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Tests\unit\Migration; + +use OCA\DAV\CardDAV\SyncService; +use OCA\DAV\Migration\CreateSystemAddressBookStep; +use OCP\Migration\IOutput; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class CreateSystemAddressBookStepTest extends TestCase { + + private SyncService&MockObject $syncService; + private CreateSystemAddressBookStep $step; + + protected function setUp(): void { + parent::setUp(); + + $this->syncService = $this->createMock(SyncService::class); + + $this->step = new CreateSystemAddressBookStep( + $this->syncService, + ); + } + + public function testGetName(): void { + $name = $this->step->getName(); + + self::assertEquals('Create system address book', $name); + } + + public function testRun(): void { + $output = $this->createMock(IOutput::class); + + $this->step->run($output); + + $this->addToAssertionCount(1); + } + +} diff --git a/apps/dav/tests/unit/Migration/RefreshWebcalJobRegistrarTest.php b/apps/dav/tests/unit/Migration/RefreshWebcalJobRegistrarTest.php new file mode 100644 index 00000000000..8e7bf366cbf --- /dev/null +++ b/apps/dav/tests/unit/Migration/RefreshWebcalJobRegistrarTest.php @@ -0,0 +1,119 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\DAV\Migration; + +use OCA\DAV\BackgroundJob\RefreshWebcalJob; +use OCA\DAV\Migration\RefreshWebcalJobRegistrar; +use OCP\BackgroundJob\IJobList; +use OCP\DB\IResult; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class RefreshWebcalJobRegistrarTest extends TestCase { + private IDBConnection&MockObject $db; + private IJobList&MockObject $jobList; + private RefreshWebcalJobRegistrar $migration; + + protected function setUp(): void { + parent::setUp(); + + $this->db = $this->createMock(IDBConnection::class); + $this->jobList = $this->createMock(IJobList::class); + + $this->migration = new RefreshWebcalJobRegistrar($this->db, $this->jobList); + } + + public function testGetName(): void { + $this->assertEquals($this->migration->getName(), 'Registering background jobs to update cache for webcal calendars'); + } + + public function testRun(): void { + $output = $this->createMock(IOutput::class); + + $queryBuilder = $this->createMock(IQueryBuilder::class); + $statement = $this->createMock(IResult::class); + + $this->db->expects($this->once()) + ->method('getQueryBuilder') + ->willReturn($queryBuilder); + + $queryBuilder->expects($this->once()) + ->method('select') + ->with(['principaluri', 'uri']) + ->willReturn($queryBuilder); + $queryBuilder->expects($this->once()) + ->method('from') + ->with('calendarsubscriptions') + ->willReturn($queryBuilder); + $queryBuilder->expects($this->once()) + ->method('execute') + ->willReturn($statement); + + $statement->expects($this->exactly(4)) + ->method('fetch') + ->with(\PDO::FETCH_ASSOC) + ->willReturnOnConsecutiveCalls( + [ + 'principaluri' => 'foo1', + 'uri' => 'bar1', + ], + [ + 'principaluri' => 'foo2', + 'uri' => 'bar2', + ], + [ + 'principaluri' => 'foo3', + 'uri' => 'bar3', + ], + null + ); + + $this->jobList->expects($this->exactly(3)) + ->method('has') + ->willReturnMap([ + [RefreshWebcalJob::class, [ + 'principaluri' => 'foo1', + 'uri' => 'bar1', + ], false], + [RefreshWebcalJob::class, [ + 'principaluri' => 'foo2', + 'uri' => 'bar2', + ], true ], + [RefreshWebcalJob::class, [ + 'principaluri' => 'foo3', + 'uri' => 'bar3', + ], false], + ]); + + $calls = [ + [RefreshWebcalJob::class, [ + 'principaluri' => 'foo1', + 'uri' => 'bar1', + ]], + [RefreshWebcalJob::class, [ + 'principaluri' => 'foo3', + 'uri' => 'bar3', + ]] + ]; + $this->jobList->expects($this->exactly(2)) + ->method('add') + ->willReturnCallback(function () use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + }); + + $output->expects($this->once()) + ->method('info') + ->with('Added 2 background jobs to update webcal calendars'); + + $this->migration->run($output); + } +} diff --git a/apps/dav/tests/unit/Migration/RegenerateBirthdayCalendarsTest.php b/apps/dav/tests/unit/Migration/RegenerateBirthdayCalendarsTest.php new file mode 100644 index 00000000000..6f681badb8b --- /dev/null +++ b/apps/dav/tests/unit/Migration/RegenerateBirthdayCalendarsTest.php @@ -0,0 +1,78 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\DAV\Migration; + +use OCA\DAV\BackgroundJob\RegisterRegenerateBirthdayCalendars; +use OCA\DAV\Migration\RegenerateBirthdayCalendars; +use OCP\BackgroundJob\IJobList; +use OCP\IConfig; +use OCP\Migration\IOutput; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class RegenerateBirthdayCalendarsTest extends TestCase { + private IJobList&MockObject $jobList; + private IConfig&MockObject $config; + private RegenerateBirthdayCalendars $migration; + + protected function setUp(): void { + parent::setUp(); + + $this->jobList = $this->createMock(IJobList::class); + $this->config = $this->createMock(IConfig::class); + + $this->migration = new RegenerateBirthdayCalendars($this->jobList, + $this->config); + } + + public function testGetName(): void { + $this->assertEquals( + 'Regenerating birthday calendars to use new icons and fix old birthday events without year', + $this->migration->getName() + ); + } + + public function testRun(): void { + $this->config->expects($this->once()) + ->method('getAppValue') + ->with('dav', 'regeneratedBirthdayCalendarsForYearFix') + ->willReturn(null); + + $output = $this->createMock(IOutput::class); + $output->expects($this->once()) + ->method('info') + ->with('Adding background jobs to regenerate birthday calendar'); + + $this->jobList->expects($this->once()) + ->method('add') + ->with(RegisterRegenerateBirthdayCalendars::class); + + $this->config->expects($this->once()) + ->method('setAppValue') + ->with('dav', 'regeneratedBirthdayCalendarsForYearFix', 'yes'); + + $this->migration->run($output); + } + + public function testRunSecondTime(): void { + $this->config->expects($this->once()) + ->method('getAppValue') + ->with('dav', 'regeneratedBirthdayCalendarsForYearFix') + ->willReturn('yes'); + + $output = $this->createMock(IOutput::class); + $output->expects($this->once()) + ->method('info') + ->with('Repair step already executed'); + + $this->jobList->expects($this->never()) + ->method('add'); + + $this->migration->run($output); + } +} diff --git a/apps/dav/tests/unit/Migration/RemoveDeletedUsersCalendarSubscriptionsTest.php b/apps/dav/tests/unit/Migration/RemoveDeletedUsersCalendarSubscriptionsTest.php new file mode 100644 index 00000000000..a9758470573 --- /dev/null +++ b/apps/dav/tests/unit/Migration/RemoveDeletedUsersCalendarSubscriptionsTest.php @@ -0,0 +1,140 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\DAV\Migration; + +use OCA\DAV\Migration\RemoveDeletedUsersCalendarSubscriptions; +use OCP\DB\IResult; +use OCP\DB\QueryBuilder\IExpressionBuilder; +use OCP\DB\QueryBuilder\IFunctionBuilder; +use OCP\DB\QueryBuilder\IParameter; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\DB\QueryBuilder\IQueryFunction; +use OCP\IDBConnection; +use OCP\IUserManager; +use OCP\Migration\IOutput; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class RemoveDeletedUsersCalendarSubscriptionsTest extends TestCase { + private IDBConnection&MockObject $dbConnection; + private IUserManager&MockObject $userManager; + private IOutput&MockObject $output; + private RemoveDeletedUsersCalendarSubscriptions $migration; + + + protected function setUp(): void { + parent::setUp(); + + $this->dbConnection = $this->createMock(IDBConnection::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->output = $this->createMock(IOutput::class); + + $this->migration = new RemoveDeletedUsersCalendarSubscriptions($this->dbConnection, $this->userManager); + } + + public function testGetName(): void { + $this->assertEquals( + 'Clean up old calendar subscriptions from deleted users that were not cleaned-up', + $this->migration->getName() + ); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestRun')] + public function testRun(array $subscriptions, array $userExists, int $deletions): void { + $qb = $this->createMock(IQueryBuilder::class); + + $qb->method('select')->willReturn($qb); + + $functionBuilder = $this->createMock(IFunctionBuilder::class); + + $qb->method('func')->willReturn($functionBuilder); + $functionBuilder->method('count')->willReturn($this->createMock(IQueryFunction::class)); + + $qb->method('selectDistinct') + ->with(['id', 'principaluri']) + ->willReturn($qb); + + $qb->method('from') + ->with('calendarsubscriptions') + ->willReturn($qb); + + $qb->method('setMaxResults') + ->willReturn($qb); + + $qb->method('setFirstResult') + ->willReturn($qb); + + $result = $this->createMock(IResult::class); + + $qb->method('execute') + ->willReturn($result); + + $result->expects($this->once()) + ->method('fetchOne') + ->willReturn(count($subscriptions)); + + $result + ->method('fetch') + ->willReturnOnConsecutiveCalls(...$subscriptions); + + $qb->method('delete') + ->with('calendarsubscriptions') + ->willReturn($qb); + + $expr = $this->createMock(IExpressionBuilder::class); + + $qb->method('expr')->willReturn($expr); + $qb->method('createNamedParameter')->willReturn($this->createMock(IParameter::class)); + $qb->method('where')->willReturn($qb); + // Only when user exists + $qb->expects($this->exactly($deletions))->method('executeStatement'); + + $this->dbConnection->method('getQueryBuilder')->willReturn($qb); + + + $this->output->expects($this->once())->method('startProgress'); + + $this->output->expects($subscriptions === [] ? $this->never(): $this->once())->method('advance'); + if (count($subscriptions)) { + $this->userManager->method('userExists') + ->willReturnCallback(function (string $username) use ($userExists) { + return $userExists[$username]; + }); + } + $this->output->expects($this->once())->method('finishProgress'); + $this->output->expects($this->once())->method('info')->with(sprintf('%d calendar subscriptions without an user have been cleaned up', $deletions)); + + $this->migration->run($this->output); + } + + public static function dataTestRun(): array { + return [ + [[], [], 0], + [ + [ + [ + 'id' => 1, + 'principaluri' => 'users/principals/foo1', + ], + [ + 'id' => 2, + 'principaluri' => 'users/principals/bar1', + ], + [ + 'id' => 3, + 'principaluri' => 'users/principals/bar1', + ], + [], + ], + ['foo1' => true, 'bar1' => false], + 2 + ], + ]; + } +} diff --git a/apps/dav/tests/unit/Provisioning/Apple/AppleProvisioningNodeTest.php b/apps/dav/tests/unit/Provisioning/Apple/AppleProvisioningNodeTest.php new file mode 100644 index 00000000000..4f04aebb3e8 --- /dev/null +++ b/apps/dav/tests/unit/Provisioning/Apple/AppleProvisioningNodeTest.php @@ -0,0 +1,70 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\Provisioning\Apple; + +use OCA\DAV\Provisioning\Apple\AppleProvisioningNode; +use OCP\AppFramework\Utility\ITimeFactory; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\PropPatch; +use Test\TestCase; + +class AppleProvisioningNodeTest extends TestCase { + private ITimeFactory&MockObject $timeFactory; + private AppleProvisioningNode $node; + + protected function setUp(): void { + parent::setUp(); + + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->node = new AppleProvisioningNode($this->timeFactory); + } + + public function testGetName(): void { + $this->assertEquals('apple-provisioning.mobileconfig', $this->node->getName()); + } + + public function testSetName(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + $this->expectExceptionMessage('Renaming apple-provisioning.mobileconfig is forbidden'); + + $this->node->setName('foo'); + } + + public function testGetLastModified(): void { + $this->assertEquals(null, $this->node->getLastModified()); + } + + + public function testDelete(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + $this->expectExceptionMessage('apple-provisioning.mobileconfig may not be deleted'); + + $this->node->delete(); + } + + public function testGetProperties(): void { + $this->timeFactory->expects($this->once()) + ->method('getDateTime') + ->willReturn(new \DateTime('2000-01-01')); + + $this->assertEquals([ + '{DAV:}getcontentlength' => 42, + '{DAV:}getlastmodified' => 'Sat, 01 Jan 2000 00:00:00 GMT', + ], $this->node->getProperties([])); + } + + + public function testGetPropPatch(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + $this->expectExceptionMessage('apple-provisioning.mobileconfig\'s properties may not be altered.'); + + $propPatch = $this->createMock(PropPatch::class); + + $this->node->propPatch($propPatch); + } +} diff --git a/apps/dav/tests/unit/Provisioning/Apple/AppleProvisioningPluginTest.php b/apps/dav/tests/unit/Provisioning/Apple/AppleProvisioningPluginTest.php new file mode 100644 index 00000000000..58e588aa68d --- /dev/null +++ b/apps/dav/tests/unit/Provisioning/Apple/AppleProvisioningPluginTest.php @@ -0,0 +1,240 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\Provisioning\Apple; + +use OCA\DAV\Provisioning\Apple\AppleProvisioningPlugin; +use OCA\Theming\ThemingDefaults; +use OCP\IL10N; +use OCP\IRequest; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\IUserSession; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\Server; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; +use Test\TestCase; + +class AppleProvisioningPluginTest extends TestCase { + protected Server&MockObject $server; + protected IUserSession&MockObject $userSession; + protected IURLGenerator&MockObject $urlGenerator; + protected ThemingDefaults&MockObject $themingDefaults; + protected IRequest&MockObject $request; + protected IL10N&MockObject $l10n; + protected RequestInterface&MockObject $sabreRequest; + protected ResponseInterface&MockObject $sabreResponse; + protected AppleProvisioningPlugin $plugin; + + protected function setUp(): void { + parent::setUp(); + + $this->server = $this->createMock(Server::class); + $this->userSession = $this->createMock(IUserSession::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + $this->themingDefaults = $this->createMock(ThemingDefaults::class); + $this->request = $this->createMock(IRequest::class); + $this->l10n = $this->createMock(IL10N::class); + + $this->plugin = new AppleProvisioningPlugin($this->userSession, + $this->urlGenerator, + $this->themingDefaults, + $this->request, + $this->l10n, + function () { + return 'generated-uuid'; + } + ); + + $this->sabreRequest = $this->createMock(RequestInterface::class); + $this->sabreResponse = $this->createMock(ResponseInterface::class); + } + + public function testInitialize(): void { + $server = $this->createMock(Server::class); + + $plugin = new AppleProvisioningPlugin($this->userSession, + $this->urlGenerator, $this->themingDefaults, $this->request, $this->l10n, + function (): void { + }); + + $server->expects($this->once()) + ->method('on') + ->with('method:GET', [$plugin, 'httpGet'], 90); + + $plugin->initialize($server); + } + + public function testHttpGetOnHttp(): void { + $this->sabreRequest->expects($this->once()) + ->method('getPath') + ->with() + ->willReturn('provisioning/apple-provisioning.mobileconfig'); + + $user = $this->createMock(IUser::class); + $this->userSession->expects($this->once()) + ->method('getUser') + ->willReturn($user); + + $this->request->expects($this->once()) + ->method('getServerProtocol') + ->wilLReturn('http'); + + $this->themingDefaults->expects($this->once()) + ->method('getName') + ->willReturn('InstanceName'); + + $this->l10n->expects($this->once()) + ->method('t') + ->with('Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS.', ['InstanceName']) + ->willReturn('LocalizedErrorMessage'); + + $this->sabreResponse->expects($this->once()) + ->method('setStatus') + ->with(200); + $this->sabreResponse->expects($this->once()) + ->method('setHeader') + ->with('Content-Type', 'text/plain; charset=utf-8'); + $this->sabreResponse->expects($this->once()) + ->method('setBody') + ->with('LocalizedErrorMessage'); + + $returnValue = $this->plugin->httpGet($this->sabreRequest, $this->sabreResponse); + + $this->assertFalse($returnValue); + } + + public function testHttpGetOnHttps(): void { + $this->sabreRequest->expects($this->once()) + ->method('getPath') + ->with() + ->willReturn('provisioning/apple-provisioning.mobileconfig'); + + $user = $this->createMock(IUser::class); + $user->expects($this->once()) + ->method('getUID') + ->willReturn('userName'); + + $this->userSession->expects($this->once()) + ->method('getUser') + ->willReturn($user); + + $this->request->expects($this->once()) + ->method('getServerProtocol') + ->wilLReturn('https'); + + $this->urlGenerator->expects($this->once()) + ->method('getBaseUrl') + ->willReturn('https://nextcloud.tld/nextcloud'); + + $this->themingDefaults->expects($this->once()) + ->method('getName') + ->willReturn('InstanceName'); + + $this->l10n->expects($this->exactly(2)) + ->method('t') + ->willReturnMap([ + ['Configures a CalDAV account', [], 'LocalizedConfiguresCalDAV'], + ['Configures a CardDAV account', [], 'LocalizedConfiguresCardDAV'], + ]); + + $this->sabreResponse->expects($this->once()) + ->method('setStatus') + ->with(200); + + $calls = [ + ['Content-Disposition', 'attachment; filename="userName-apple-provisioning.mobileconfig"'], + ['Content-Type', 'application/xml; charset=utf-8'], + ]; + $this->sabreResponse->expects($this->exactly(2)) + ->method('setHeader') + ->willReturnCallback(function () use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + }); + $this->sabreResponse->expects($this->once()) + ->method('setBody') + ->with(<<<EOF +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>PayloadContent</key> + <array> + <dict> + <key>CalDAVAccountDescription</key> + <string>InstanceName</string> + <key>CalDAVHostName</key> + <string>nextcloud.tld</string> + <key>CalDAVUsername</key> + <string>userName</string> + <key>CalDAVUseSSL</key> + <true/> + <key>CalDAVPort</key> + <integer>443</integer> + <key>PayloadDescription</key> + <string>LocalizedConfiguresCalDAV</string> + <key>PayloadDisplayName</key> + <string>InstanceName CalDAV</string> + <key>PayloadIdentifier</key> + <string>tld.nextcloud.generated-uuid</string> + <key>PayloadType</key> + <string>com.apple.caldav.account</string> + <key>PayloadUUID</key> + <string>generated-uuid</string> + <key>PayloadVersion</key> + <integer>1</integer> + </dict> + <dict> + <key>CardDAVAccountDescription</key> + <string>InstanceName</string> + <key>CardDAVHostName</key> + <string>nextcloud.tld</string> + <key>CardDAVUsername</key> + <string>userName</string> + <key>CardDAVUseSSL</key> + <true/> + <key>CardDAVPort</key> + <integer>443</integer> + <key>PayloadDescription</key> + <string>LocalizedConfiguresCardDAV</string> + <key>PayloadDisplayName</key> + <string>InstanceName CardDAV</string> + <key>PayloadIdentifier</key> + <string>tld.nextcloud.generated-uuid</string> + <key>PayloadType</key> + <string>com.apple.carddav.account</string> + <key>PayloadUUID</key> + <string>generated-uuid</string> + <key>PayloadVersion</key> + <integer>1</integer> + </dict> + </array> + <key>PayloadDisplayName</key> + <string>InstanceName</string> + <key>PayloadIdentifier</key> + <string>tld.nextcloud.generated-uuid</string> + <key>PayloadRemovalDisallowed</key> + <false/> + <key>PayloadType</key> + <string>Configuration</string> + <key>PayloadUUID</key> + <string>generated-uuid</string> + <key>PayloadVersion</key> + <integer>1</integer> +</dict> +</plist> + +EOF + ); + + $returnValue = $this->plugin->httpGet($this->sabreRequest, $this->sabreResponse); + + $this->assertFalse($returnValue); + } +} diff --git a/apps/dav/tests/unit/Search/ContactsSearchProviderTest.php b/apps/dav/tests/unit/Search/ContactsSearchProviderTest.php new file mode 100644 index 00000000000..f4dc13a5c06 --- /dev/null +++ b/apps/dav/tests/unit/Search/ContactsSearchProviderTest.php @@ -0,0 +1,259 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit; + +use OCA\DAV\CardDAV\CardDavBackend; +use OCA\DAV\Search\ContactsSearchProvider; +use OCP\App\IAppManager; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\Search\ISearchQuery; +use OCP\Search\SearchResult; +use OCP\Search\SearchResultEntry; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\VObject\Reader; +use Test\TestCase; + +class ContactsSearchProviderTest extends TestCase { + private IAppManager&MockObject $appManager; + private IL10N&MockObject $l10n; + private IURLGenerator&MockObject $urlGenerator; + private CardDavBackend&MockObject $backend; + private ContactsSearchProvider $provider; + + private string $vcardTest0 = 'BEGIN:VCARD' . PHP_EOL + . 'VERSION:3.0' . PHP_EOL + . 'PRODID:-//Sabre//Sabre VObject 4.1.2//EN' . PHP_EOL + . 'UID:Test' . PHP_EOL + . 'FN:FN of Test' . PHP_EOL + . 'N:Test;;;;' . PHP_EOL + . 'EMAIL:forrestgump@example.com' . PHP_EOL + . 'END:VCARD'; + + private string $vcardTest1 = 'BEGIN:VCARD' . PHP_EOL + . 'VERSION:3.0' . PHP_EOL + . 'PRODID:-//Sabre//Sabre VObject 4.1.2//EN' . PHP_EOL + . 'PHOTO;ENCODING=b;TYPE=image/jpeg:' . PHP_EOL + . 'UID:Test2' . PHP_EOL + . 'FN:FN of Test2' . PHP_EOL + . 'N:Test2;;;;' . PHP_EOL + . 'END:VCARD'; + + protected function setUp(): void { + parent::setUp(); + + $this->appManager = $this->createMock(IAppManager::class); + $this->l10n = $this->createMock(IL10N::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + $this->backend = $this->createMock(CardDavBackend::class); + + $this->provider = new ContactsSearchProvider( + $this->appManager, + $this->l10n, + $this->urlGenerator, + $this->backend + ); + } + + public function testGetId(): void { + $this->assertEquals('contacts', $this->provider->getId()); + } + + public function testGetName(): void { + $this->l10n->expects($this->exactly(1)) + ->method('t') + ->with('Contacts') + ->willReturnArgument(0); + + $this->assertEquals('Contacts', $this->provider->getName()); + } + + public function testSearchAppDisabled(): void { + $user = $this->createMock(IUser::class); + $query = $this->createMock(ISearchQuery::class); + $this->appManager->expects($this->once()) + ->method('isEnabledForUser') + ->with('contacts', $user) + ->willReturn(false); + $this->l10n->expects($this->exactly(1)) + ->method('t') + ->with('Contacts') + ->willReturnArgument(0); + $this->backend->expects($this->never()) + ->method('getAddressBooksForUser'); + $this->backend->expects($this->never()) + ->method('searchPrincipalUri'); + + $actual = $this->provider->search($user, $query); + $data = $actual->jsonSerialize(); + $this->assertInstanceOf(SearchResult::class, $actual); + $this->assertEquals('Contacts', $data['name']); + $this->assertEmpty($data['entries']); + $this->assertFalse($data['isPaginated']); + $this->assertNull($data['cursor']); + } + + public function testSearch(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('john.doe'); + $query = $this->createMock(ISearchQuery::class); + $query->method('getTerm')->willReturn('search term'); + $query->method('getLimit')->willReturn(5); + $query->method('getCursor')->willReturn(20); + $this->appManager->expects($this->once()) + ->method('isEnabledForUser') + ->with('contacts', $user) + ->willReturn(true); + $this->l10n->expects($this->exactly(1)) + ->method('t') + ->with('Contacts') + ->willReturnArgument(0); + + $this->backend->expects($this->once()) + ->method('getAddressBooksForUser') + ->with('principals/users/john.doe') + ->willReturn([ + [ + 'id' => 99, + 'principaluri' => 'principals/users/john.doe', + 'uri' => 'addressbook-uri-99', + ], [ + 'id' => 123, + 'principaluri' => 'principals/users/john.doe', + 'uri' => 'addressbook-uri-123', + ] + ]); + $this->backend->expects($this->once()) + ->method('searchPrincipalUri') + ->with('principals/users/john.doe', '', + [ + 'N', + 'FN', + 'NICKNAME', + 'EMAIL', + 'TEL', + 'ADR', + 'TITLE', + 'ORG', + 'NOTE', + ], + ['limit' => 5, 'offset' => 20, 'since' => null, 'until' => null, 'person' => null, 'company' => null]) + ->willReturn([ + [ + 'addressbookid' => 99, + 'uri' => 'vcard0.vcf', + 'carddata' => $this->vcardTest0, + ], + [ + 'addressbookid' => 123, + 'uri' => 'vcard1.vcf', + 'carddata' => $this->vcardTest1, + ], + ]); + + $provider = $this->getMockBuilder(ContactsSearchProvider::class) + ->setConstructorArgs([ + $this->appManager, + $this->l10n, + $this->urlGenerator, + $this->backend, + ]) + ->onlyMethods([ + 'getDavUrlForContact', + 'getDeepLinkToContactsApp', + 'generateSubline', + ]) + ->getMock(); + + $provider->expects($this->once()) + ->method('getDavUrlForContact') + ->with('principals/users/john.doe', 'addressbook-uri-123', 'vcard1.vcf') + ->willReturn('absolute-thumbnail-url'); + + $provider->expects($this->exactly(2)) + ->method('generateSubline') + ->willReturn('subline'); + $provider->expects($this->exactly(2)) + ->method('getDeepLinkToContactsApp') + ->willReturnMap([ + ['addressbook-uri-99', 'Test', 'deep-link-to-contacts'], + ['addressbook-uri-123', 'Test2', 'deep-link-to-contacts'], + ]); + + $actual = $provider->search($user, $query); + $data = $actual->jsonSerialize(); + $this->assertInstanceOf(SearchResult::class, $actual); + $this->assertEquals('Contacts', $data['name']); + $this->assertCount(2, $data['entries']); + $this->assertTrue($data['isPaginated']); + $this->assertEquals(22, $data['cursor']); + + $result0 = $data['entries'][0]; + $result0Data = $result0->jsonSerialize(); + $result1 = $data['entries'][1]; + $result1Data = $result1->jsonSerialize(); + + $this->assertInstanceOf(SearchResultEntry::class, $result0); + $this->assertEquals('', $result0Data['thumbnailUrl']); + $this->assertEquals('FN of Test', $result0Data['title']); + $this->assertEquals('subline', $result0Data['subline']); + $this->assertEquals('deep-link-to-contacts', $result0Data['resourceUrl']); + $this->assertEquals('icon-contacts-dark', $result0Data['icon']); + $this->assertTrue($result0Data['rounded']); + + $this->assertInstanceOf(SearchResultEntry::class, $result1); + $this->assertEquals('absolute-thumbnail-url?photo', $result1Data['thumbnailUrl']); + $this->assertEquals('FN of Test2', $result1Data['title']); + $this->assertEquals('subline', $result1Data['subline']); + $this->assertEquals('deep-link-to-contacts', $result1Data['resourceUrl']); + $this->assertEquals('icon-contacts-dark', $result1Data['icon']); + $this->assertTrue($result1Data['rounded']); + } + + public function testGetDavUrlForContact(): void { + $this->urlGenerator->expects($this->once()) + ->method('linkTo') + ->with('', 'remote.php') + ->willReturn('link-to-remote.php'); + $this->urlGenerator->expects($this->once()) + ->method('getAbsoluteURL') + ->with('link-to-remote.php/dav/addressbooks/users/john.doe/foo/bar.vcf') + ->willReturn('absolute-url-link-to-remote.php/dav/addressbooks/users/john.doe/foo/bar.vcf'); + + $actual = self::invokePrivate($this->provider, 'getDavUrlForContact', ['principals/users/john.doe', 'foo', 'bar.vcf']); + + $this->assertEquals('absolute-url-link-to-remote.php/dav/addressbooks/users/john.doe/foo/bar.vcf', $actual); + } + + public function testGetDeepLinkToContactsApp(): void { + $this->urlGenerator->expects($this->once()) + ->method('linkToRoute') + ->with('contacts.contacts.direct', ['contact' => 'uid123~uri-john.doe']) + ->willReturn('link-to-route-contacts.contacts.direct/direct/uid123~uri-john.doe'); + $this->urlGenerator->expects($this->once()) + ->method('getAbsoluteURL') + ->with('link-to-route-contacts.contacts.direct/direct/uid123~uri-john.doe') + ->willReturn('absolute-url-link-to-route-contacts.contacts.direct/direct/uid123~uri-john.doe'); + + $actual = self::invokePrivate($this->provider, 'getDeepLinkToContactsApp', ['uri-john.doe', 'uid123']); + $this->assertEquals('absolute-url-link-to-route-contacts.contacts.direct/direct/uid123~uri-john.doe', $actual); + } + + public function testGenerateSubline(): void { + $vCard0 = Reader::read($this->vcardTest0); + $vCard1 = Reader::read($this->vcardTest1); + + $actual1 = self::invokePrivate($this->provider, 'generateSubline', [$vCard0]); + $actual2 = self::invokePrivate($this->provider, 'generateSubline', [$vCard1]); + + $this->assertEquals('forrestgump@example.com', $actual1); + $this->assertEquals('', $actual2); + } +} diff --git a/apps/dav/tests/unit/Search/EventsSearchProviderTest.php b/apps/dav/tests/unit/Search/EventsSearchProviderTest.php new file mode 100644 index 00000000000..d5d536fd201 --- /dev/null +++ b/apps/dav/tests/unit/Search/EventsSearchProviderTest.php @@ -0,0 +1,450 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\Search; + +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\Search\EventsSearchProvider; +use OCP\App\IAppManager; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\Search\IFilter; +use OCP\Search\ISearchQuery; +use OCP\Search\SearchResult; +use OCP\Search\SearchResultEntry; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\VObject\Reader; +use Test\TestCase; + +class EventsSearchProviderTest extends TestCase { + private IAppManager&MockObject $appManager; + private IL10N&MockObject $l10n; + private IURLGenerator&MockObject $urlGenerator; + private CalDavBackend&MockObject $backend; + private EventsSearchProvider $provider; + + // NO SUMMARY + private static string $vEvent0 = 'BEGIN:VCALENDAR' . PHP_EOL + . 'VERSION:2.0' . PHP_EOL + . 'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN' . PHP_EOL + . 'CALSCALE:GREGORIAN' . PHP_EOL + . 'BEGIN:VEVENT' . PHP_EOL + . 'CREATED:20161004T144433Z' . PHP_EOL + . 'UID:85560E76-1B0D-47E1-A735-21625767FCA4' . PHP_EOL + . 'DTEND;VALUE=DATE:20161008' . PHP_EOL + . 'TRANSP:TRANSPARENT' . PHP_EOL + . 'DTSTART;VALUE=DATE:20161005' . PHP_EOL + . 'DTSTAMP:20161004T144437Z' . PHP_EOL + . 'SEQUENCE:0' . PHP_EOL + . 'END:VEVENT' . PHP_EOL + . 'END:VCALENDAR'; + + // TIMED SAME DAY + private static string $vEvent1 = 'BEGIN:VCALENDAR' . PHP_EOL + . 'VERSION:2.0' . PHP_EOL + . 'PRODID:-//Tests//' . PHP_EOL + . 'CALSCALE:GREGORIAN' . PHP_EOL + . 'BEGIN:VTIMEZONE' . PHP_EOL + . 'TZID:Europe/Berlin' . PHP_EOL + . 'BEGIN:DAYLIGHT' . PHP_EOL + . 'TZOFFSETFROM:+0100' . PHP_EOL + . 'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU' . PHP_EOL + . 'DTSTART:19810329T020000' . PHP_EOL + . 'TZNAME:GMT+2' . PHP_EOL + . 'TZOFFSETTO:+0200' . PHP_EOL + . 'END:DAYLIGHT' . PHP_EOL + . 'BEGIN:STANDARD' . PHP_EOL + . 'TZOFFSETFROM:+0200' . PHP_EOL + . 'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU' . PHP_EOL + . 'DTSTART:19961027T030000' . PHP_EOL + . 'TZNAME:GMT+1' . PHP_EOL + . 'TZOFFSETTO:+0100' . PHP_EOL + . 'END:STANDARD' . PHP_EOL + . 'END:VTIMEZONE' . PHP_EOL + . 'BEGIN:VEVENT' . PHP_EOL + . 'CREATED:20160809T163629Z' . PHP_EOL + . 'UID:0AD16F58-01B3-463B-A215-FD09FC729A02' . PHP_EOL + . 'DTEND;TZID=Europe/Berlin:20160816T100000' . PHP_EOL + . 'TRANSP:OPAQUE' . PHP_EOL + . 'SUMMARY:Test Europe Berlin' . PHP_EOL + . 'DTSTART;TZID=Europe/Berlin:20160816T090000' . PHP_EOL + . 'DTSTAMP:20160809T163632Z' . PHP_EOL + . 'SEQUENCE:0' . PHP_EOL + . 'END:VEVENT' . PHP_EOL + . 'END:VCALENDAR'; + + // TIMED DIFFERENT DAY + private static string $vEvent2 = 'BEGIN:VCALENDAR' . PHP_EOL + . 'VERSION:2.0' . PHP_EOL + . 'PRODID:-//Tests//' . PHP_EOL + . 'CALSCALE:GREGORIAN' . PHP_EOL + . 'BEGIN:VTIMEZONE' . PHP_EOL + . 'TZID:Europe/Berlin' . PHP_EOL + . 'BEGIN:DAYLIGHT' . PHP_EOL + . 'TZOFFSETFROM:+0100' . PHP_EOL + . 'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU' . PHP_EOL + . 'DTSTART:19810329T020000' . PHP_EOL + . 'TZNAME:GMT+2' . PHP_EOL + . 'TZOFFSETTO:+0200' . PHP_EOL + . 'END:DAYLIGHT' . PHP_EOL + . 'BEGIN:STANDARD' . PHP_EOL + . 'TZOFFSETFROM:+0200' . PHP_EOL + . 'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU' . PHP_EOL + . 'DTSTART:19961027T030000' . PHP_EOL + . 'TZNAME:GMT+1' . PHP_EOL + . 'TZOFFSETTO:+0100' . PHP_EOL + . 'END:STANDARD' . PHP_EOL + . 'END:VTIMEZONE' . PHP_EOL + . 'BEGIN:VEVENT' . PHP_EOL + . 'CREATED:20160809T163629Z' . PHP_EOL + . 'UID:0AD16F58-01B3-463B-A215-FD09FC729A02' . PHP_EOL + . 'DTEND;TZID=Europe/Berlin:20160817T100000' . PHP_EOL + . 'TRANSP:OPAQUE' . PHP_EOL + . 'SUMMARY:Test Europe Berlin' . PHP_EOL + . 'DTSTART;TZID=Europe/Berlin:20160816T090000' . PHP_EOL + . 'DTSTAMP:20160809T163632Z' . PHP_EOL + . 'SEQUENCE:0' . PHP_EOL + . 'END:VEVENT' . PHP_EOL + . 'END:VCALENDAR'; + + // ALL-DAY ONE-DAY + private static string $vEvent3 = 'BEGIN:VCALENDAR' . PHP_EOL + . 'VERSION:2.0' . PHP_EOL + . 'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN' . PHP_EOL + . 'CALSCALE:GREGORIAN' . PHP_EOL + . 'BEGIN:VEVENT' . PHP_EOL + . 'CREATED:20161004T144433Z' . PHP_EOL + . 'UID:85560E76-1B0D-47E1-A735-21625767FCA4' . PHP_EOL + . 'DTEND;VALUE=DATE:20161006' . PHP_EOL + . 'TRANSP:TRANSPARENT' . PHP_EOL + . 'DTSTART;VALUE=DATE:20161005' . PHP_EOL + . 'DTSTAMP:20161004T144437Z' . PHP_EOL + . 'SEQUENCE:0' . PHP_EOL + . 'END:VEVENT' . PHP_EOL + . 'END:VCALENDAR'; + + // ALL-DAY MULTIPLE DAYS + private static string $vEvent4 = 'BEGIN:VCALENDAR' . PHP_EOL + . 'VERSION:2.0' . PHP_EOL + . 'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN' . PHP_EOL + . 'CALSCALE:GREGORIAN' . PHP_EOL + . 'BEGIN:VEVENT' . PHP_EOL + . 'CREATED:20161004T144433Z' . PHP_EOL + . 'UID:85560E76-1B0D-47E1-A735-21625767FCA4' . PHP_EOL + . 'DTEND;VALUE=DATE:20161008' . PHP_EOL + . 'TRANSP:TRANSPARENT' . PHP_EOL + . 'DTSTART;VALUE=DATE:20161005' . PHP_EOL + . 'DTSTAMP:20161004T144437Z' . PHP_EOL + . 'SEQUENCE:0' . PHP_EOL + . 'END:VEVENT' . PHP_EOL + . 'END:VCALENDAR'; + + // DURATION + private static string $vEvent5 = 'BEGIN:VCALENDAR' . PHP_EOL + . 'VERSION:2.0' . PHP_EOL + . 'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN' . PHP_EOL + . 'CALSCALE:GREGORIAN' . PHP_EOL + . 'BEGIN:VEVENT' . PHP_EOL + . 'CREATED:20161004T144433Z' . PHP_EOL + . 'UID:85560E76-1B0D-47E1-A735-21625767FCA4' . PHP_EOL + . 'DURATION:P5D' . PHP_EOL + . 'TRANSP:TRANSPARENT' . PHP_EOL + . 'DTSTART;VALUE=DATE:20161005' . PHP_EOL + . 'DTSTAMP:20161004T144437Z' . PHP_EOL + . 'SEQUENCE:0' . PHP_EOL + . 'END:VEVENT' . PHP_EOL + . 'END:VCALENDAR'; + + // NO DTEND - DATE + private static string $vEvent6 = 'BEGIN:VCALENDAR' . PHP_EOL + . 'VERSION:2.0' . PHP_EOL + . 'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN' . PHP_EOL + . 'CALSCALE:GREGORIAN' . PHP_EOL + . 'BEGIN:VEVENT' . PHP_EOL + . 'CREATED:20161004T144433Z' . PHP_EOL + . 'UID:85560E76-1B0D-47E1-A735-21625767FCA4' . PHP_EOL + . 'TRANSP:TRANSPARENT' . PHP_EOL + . 'DTSTART;VALUE=DATE:20161005' . PHP_EOL + . 'DTSTAMP:20161004T144437Z' . PHP_EOL + . 'SEQUENCE:0' . PHP_EOL + . 'END:VEVENT' . PHP_EOL + . 'END:VCALENDAR'; + + // NO DTEND - DATE-TIME + private static string $vEvent7 = 'BEGIN:VCALENDAR' . PHP_EOL + . 'VERSION:2.0' . PHP_EOL + . 'PRODID:-//Tests//' . PHP_EOL + . 'CALSCALE:GREGORIAN' . PHP_EOL + . 'BEGIN:VTIMEZONE' . PHP_EOL + . 'TZID:Europe/Berlin' . PHP_EOL + . 'BEGIN:DAYLIGHT' . PHP_EOL + . 'TZOFFSETFROM:+0100' . PHP_EOL + . 'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU' . PHP_EOL + . 'DTSTART:19810329T020000' . PHP_EOL + . 'TZNAME:GMT+2' . PHP_EOL + . 'TZOFFSETTO:+0200' . PHP_EOL + . 'END:DAYLIGHT' . PHP_EOL + . 'BEGIN:STANDARD' . PHP_EOL + . 'TZOFFSETFROM:+0200' . PHP_EOL + . 'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU' . PHP_EOL + . 'DTSTART:19961027T030000' . PHP_EOL + . 'TZNAME:GMT+1' . PHP_EOL + . 'TZOFFSETTO:+0100' . PHP_EOL + . 'END:STANDARD' . PHP_EOL + . 'END:VTIMEZONE' . PHP_EOL + . 'BEGIN:VEVENT' . PHP_EOL + . 'CREATED:20160809T163629Z' . PHP_EOL + . 'UID:0AD16F58-01B3-463B-A215-FD09FC729A02' . PHP_EOL + . 'TRANSP:OPAQUE' . PHP_EOL + . 'SUMMARY:Test Europe Berlin' . PHP_EOL + . 'DTSTART;TZID=Europe/Berlin:20160816T090000' . PHP_EOL + . 'DTSTAMP:20160809T163632Z' . PHP_EOL + . 'SEQUENCE:0' . PHP_EOL + . 'END:VEVENT' . PHP_EOL + . 'END:VCALENDAR'; + + protected function setUp(): void { + parent::setUp(); + + $this->appManager = $this->createMock(IAppManager::class); + $this->l10n = $this->createMock(IL10N::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + $this->backend = $this->createMock(CalDavBackend::class); + + $this->provider = new EventsSearchProvider( + $this->appManager, + $this->l10n, + $this->urlGenerator, + $this->backend + ); + } + + public function testGetId(): void { + $this->assertEquals('calendar', $this->provider->getId()); + } + + public function testGetName(): void { + $this->l10n->expects($this->exactly(1)) + ->method('t') + ->with('Events') + ->willReturnArgument(0); + + $this->assertEquals('Events', $this->provider->getName()); + } + + public function testSearchAppDisabled(): void { + $user = $this->createMock(IUser::class); + $query = $this->createMock(ISearchQuery::class); + $this->appManager->expects($this->once()) + ->method('isEnabledForUser') + ->with('calendar', $user) + ->willReturn(false); + $this->l10n->expects($this->exactly(1)) + ->method('t') + ->willReturnArgument(0); + $this->backend->expects($this->never()) + ->method('getCalendarsForUser'); + $this->backend->expects($this->never()) + ->method('getSubscriptionsForUser'); + $this->backend->expects($this->never()) + ->method('searchPrincipalUri'); + + $actual = $this->provider->search($user, $query); + $data = $actual->jsonSerialize(); + $this->assertInstanceOf(SearchResult::class, $actual); + $this->assertEquals('Events', $data['name']); + $this->assertEmpty($data['entries']); + $this->assertFalse($data['isPaginated']); + $this->assertNull($data['cursor']); + } + + public function testSearch(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('john.doe'); + $query = $this->createMock(ISearchQuery::class); + $seachTermFilter = $this->createMock(IFilter::class); + $query->method('getFilter')->willReturnCallback(function ($name) use ($seachTermFilter) { + return match ($name) { + 'term' => $seachTermFilter, + default => null, + }; + }); + $seachTermFilter->method('get')->willReturn('search term'); + $query->method('getLimit')->willReturn(5); + $query->method('getCursor')->willReturn(20); + $this->appManager->expects($this->once()) + ->method('isEnabledForUser') + ->with('calendar', $user) + ->willReturn(true); + $this->l10n->method('t')->willReturnArgument(0); + + $this->backend->expects($this->once()) + ->method('getCalendarsForUser') + ->with('principals/users/john.doe') + ->willReturn([ + [ + 'id' => 99, + 'principaluri' => 'principals/users/john.doe', + 'uri' => 'calendar-uri-99', + ], [ + 'id' => 123, + 'principaluri' => 'principals/users/john.doe', + 'uri' => 'calendar-uri-123', + ] + ]); + $this->backend->expects($this->once()) + ->method('getSubscriptionsForUser') + ->with('principals/users/john.doe') + ->willReturn([ + [ + 'id' => 1337, + 'principaluri' => 'principals/users/john.doe', + 'uri' => 'subscription-uri-1337', + ] + ]); + $this->backend->expects($this->once()) + ->method('searchPrincipalUri') + ->with('principals/users/john.doe', 'search term', ['VEVENT'], + ['SUMMARY', 'LOCATION', 'DESCRIPTION', 'ATTENDEE', 'ORGANIZER', 'CATEGORIES'], + ['ATTENDEE' => ['CN'], 'ORGANIZER' => ['CN']], + ['limit' => 5, 'offset' => 20, 'timerange' => ['start' => null, 'end' => null]]) + ->willReturn([ + [ + 'calendarid' => 99, + 'calendartype' => CalDavBackend::CALENDAR_TYPE_CALENDAR, + 'uri' => 'event0.ics', + 'calendardata' => self::$vEvent0, + ], + [ + 'calendarid' => 123, + 'calendartype' => CalDavBackend::CALENDAR_TYPE_CALENDAR, + 'uri' => 'event1.ics', + 'calendardata' => self::$vEvent1, + ], + [ + 'calendarid' => 1337, + 'calendartype' => CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION, + 'uri' => 'event2.ics', + 'calendardata' => self::$vEvent2, + ] + ]); + + $provider = $this->getMockBuilder(EventsSearchProvider::class) + ->setConstructorArgs([ + $this->appManager, + $this->l10n, + $this->urlGenerator, + $this->backend, + ]) + ->onlyMethods([ + 'getDeepLinkToCalendarApp', + 'generateSubline', + ]) + ->getMock(); + + $provider->expects($this->exactly(3)) + ->method('generateSubline') + ->willReturn('subline'); + $provider->expects($this->exactly(3)) + ->method('getDeepLinkToCalendarApp') + ->willReturnMap([ + ['principals/users/john.doe', 'calendar-uri-99', 'event0.ics', 'deep-link-to-calendar'], + ['principals/users/john.doe', 'calendar-uri-123', 'event1.ics', 'deep-link-to-calendar'], + ['principals/users/john.doe', 'subscription-uri-1337', 'event2.ics', 'deep-link-to-calendar'] + ]); + + $actual = $provider->search($user, $query); + $data = $actual->jsonSerialize(); + $this->assertInstanceOf(SearchResult::class, $actual); + $this->assertEquals('Events', $data['name']); + $this->assertCount(3, $data['entries']); + $this->assertTrue($data['isPaginated']); + $this->assertEquals(23, $data['cursor']); + + $result0 = $data['entries'][0]; + $result0Data = $result0->jsonSerialize(); + $result1 = $data['entries'][1]; + $result1Data = $result1->jsonSerialize(); + $result2 = $data['entries'][2]; + $result2Data = $result2->jsonSerialize(); + + $this->assertInstanceOf(SearchResultEntry::class, $result0); + $this->assertEmpty($result0Data['thumbnailUrl']); + $this->assertEquals('Untitled event', $result0Data['title']); + $this->assertEquals('subline', $result0Data['subline']); + $this->assertEquals('deep-link-to-calendar', $result0Data['resourceUrl']); + $this->assertEquals('icon-calendar-dark', $result0Data['icon']); + $this->assertFalse($result0Data['rounded']); + + $this->assertInstanceOf(SearchResultEntry::class, $result1); + $this->assertEmpty($result1Data['thumbnailUrl']); + $this->assertEquals('Test Europe Berlin', $result1Data['title']); + $this->assertEquals('subline', $result1Data['subline']); + $this->assertEquals('deep-link-to-calendar', $result1Data['resourceUrl']); + $this->assertEquals('icon-calendar-dark', $result1Data['icon']); + $this->assertFalse($result1Data['rounded']); + + $this->assertInstanceOf(SearchResultEntry::class, $result2); + $this->assertEmpty($result2Data['thumbnailUrl']); + $this->assertEquals('Test Europe Berlin', $result2Data['title']); + $this->assertEquals('subline', $result2Data['subline']); + $this->assertEquals('deep-link-to-calendar', $result2Data['resourceUrl']); + $this->assertEquals('icon-calendar-dark', $result2Data['icon']); + $this->assertFalse($result2Data['rounded']); + } + + public function testGetDeepLinkToCalendarApp(): void { + $this->urlGenerator->expects($this->once()) + ->method('linkTo') + ->with('', 'remote.php') + ->willReturn('link-to-remote.php'); + $this->urlGenerator->expects($this->once()) + ->method('linkToRoute') + ->with('calendar.view.index') + ->willReturn('link-to-route-calendar/'); + $this->urlGenerator->expects($this->once()) + ->method('getAbsoluteURL') + ->with('link-to-route-calendar/edit/bGluay10by1yZW1vdGUucGhwL2Rhdi9jYWxlbmRhcnMvam9obi5kb2UvZm9vL2Jhci5pY3M=') + ->willReturn('absolute-url-to-route'); + + $actual = self::invokePrivate($this->provider, 'getDeepLinkToCalendarApp', ['principals/users/john.doe', 'foo', 'bar.ics']); + + $this->assertEquals('absolute-url-to-route', $actual); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('generateSublineDataProvider')] + public function testGenerateSubline(string $ics, string $expectedSubline): void { + $vCalendar = Reader::read($ics, Reader::OPTION_FORGIVING); + $eventComponent = $vCalendar->VEVENT; + + $this->l10n->method('l') + ->willReturnCallback(static function (string $type, \DateTime $date, $_):string { + if ($type === 'time') { + return $date->format('H:i'); + } + + return $date->format('m-d'); + }); + + $actual = self::invokePrivate($this->provider, 'generateSubline', [$eventComponent]); + $this->assertEquals($expectedSubline, $actual); + } + + public static function generateSublineDataProvider(): array { + return [ + [self::$vEvent1, '08-16 09:00 - 10:00'], + [self::$vEvent2, '08-16 09:00 - 08-17 10:00'], + [self::$vEvent3, '10-05'], + [self::$vEvent4, '10-05 - 10-07'], + [self::$vEvent5, '10-05 - 10-09'], + [self::$vEvent6, '10-05'], + [self::$vEvent7, '08-16 09:00 - 09:00'], + ]; + } +} diff --git a/apps/dav/tests/unit/Search/TasksSearchProviderTest.php b/apps/dav/tests/unit/Search/TasksSearchProviderTest.php new file mode 100644 index 00000000000..7f9a2842de9 --- /dev/null +++ b/apps/dav/tests/unit/Search/TasksSearchProviderTest.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\DAV\Tests\unit\Search; + +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\Search\TasksSearchProvider; +use OCP\App\IAppManager; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\Search\ISearchQuery; +use OCP\Search\SearchResult; +use OCP\Search\SearchResultEntry; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\VObject\Reader; +use Test\TestCase; + +class TasksSearchProviderTest extends TestCase { + private IAppManager&MockObject $appManager; + private IL10N&MockObject $l10n; + private IURLGenerator&MockObject $urlGenerator; + private CalDavBackend&MockObject $backend; + private TasksSearchProvider $provider; + + // NO DUE NOR COMPLETED NOR SUMMARY + private static string $vTodo0 = 'BEGIN:VCALENDAR' . PHP_EOL + . 'PRODID:TEST' . PHP_EOL + . 'VERSION:2.0' . PHP_EOL + . 'BEGIN:VTODO' . PHP_EOL + . 'UID:20070313T123432Z-456553@example.com' . PHP_EOL + . 'DTSTAMP:20070313T123432Z' . PHP_EOL + . 'STATUS:NEEDS-ACTION' . PHP_EOL + . 'END:VTODO' . PHP_EOL + . 'END:VCALENDAR'; + + // DUE AND COMPLETED + private static string $vTodo1 = 'BEGIN:VCALENDAR' . PHP_EOL + . 'PRODID:TEST' . PHP_EOL + . 'VERSION:2.0' . PHP_EOL + . 'BEGIN:VTODO' . PHP_EOL + . 'UID:20070313T123432Z-456553@example.com' . PHP_EOL + . 'DTSTAMP:20070313T123432Z' . PHP_EOL + . 'COMPLETED:20070707T100000Z' . PHP_EOL + . 'DUE;VALUE=DATE:20070501' . PHP_EOL + . 'SUMMARY:Task title' . PHP_EOL + . 'STATUS:NEEDS-ACTION' . PHP_EOL + . 'END:VTODO' . PHP_EOL + . 'END:VCALENDAR'; + + // COMPLETED ONLY + private static string $vTodo2 = 'BEGIN:VCALENDAR' . PHP_EOL + . 'PRODID:TEST' . PHP_EOL + . 'VERSION:2.0' . PHP_EOL + . 'BEGIN:VTODO' . PHP_EOL + . 'UID:20070313T123432Z-456553@example.com' . PHP_EOL + . 'DTSTAMP:20070313T123432Z' . PHP_EOL + . 'COMPLETED:20070707T100000Z' . PHP_EOL + . 'SUMMARY:Task title' . PHP_EOL + . 'STATUS:NEEDS-ACTION' . PHP_EOL + . 'END:VTODO' . PHP_EOL + . 'END:VCALENDAR'; + + // DUE DATE + private static string $vTodo3 = 'BEGIN:VCALENDAR' . PHP_EOL + . 'PRODID:TEST' . PHP_EOL + . 'VERSION:2.0' . PHP_EOL + . 'BEGIN:VTODO' . PHP_EOL + . 'UID:20070313T123432Z-456553@example.com' . PHP_EOL + . 'DTSTAMP:20070313T123432Z' . PHP_EOL + . 'DUE;VALUE=DATE:20070501' . PHP_EOL + . 'SUMMARY:Task title' . PHP_EOL + . 'STATUS:NEEDS-ACTION' . PHP_EOL + . 'END:VTODO' . PHP_EOL + . 'END:VCALENDAR'; + + // DUE DATETIME + private static string $vTodo4 = 'BEGIN:VCALENDAR' . PHP_EOL + . 'PRODID:TEST' . PHP_EOL + . 'VERSION:2.0' . PHP_EOL + . 'BEGIN:VTODO' . PHP_EOL + . 'UID:20070313T123432Z-456553@example.com' . PHP_EOL + . 'DTSTAMP:20070313T123432Z' . PHP_EOL + . 'DUE:20070709T130000Z' . PHP_EOL + . 'SUMMARY:Task title' . PHP_EOL + . 'STATUS:NEEDS-ACTION' . PHP_EOL + . 'END:VTODO' . PHP_EOL + . 'END:VCALENDAR'; + + protected function setUp(): void { + parent::setUp(); + + $this->appManager = $this->createMock(IAppManager::class); + $this->l10n = $this->createMock(IL10N::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + $this->backend = $this->createMock(CalDavBackend::class); + + $this->provider = new TasksSearchProvider( + $this->appManager, + $this->l10n, + $this->urlGenerator, + $this->backend + ); + } + + public function testGetId(): void { + $this->assertEquals('tasks', $this->provider->getId()); + } + + public function testGetName(): void { + $this->l10n->expects($this->exactly(1)) + ->method('t') + ->with('Tasks') + ->willReturnArgument(0); + + $this->assertEquals('Tasks', $this->provider->getName()); + } + + public function testSearchAppDisabled(): void { + $user = $this->createMock(IUser::class); + $query = $this->createMock(ISearchQuery::class); + $this->appManager->expects($this->once()) + ->method('isEnabledForUser') + ->with('tasks', $user) + ->willReturn(false); + $this->l10n->expects($this->exactly(1)) + ->method('t') + ->willReturnArgument(0); + $this->backend->expects($this->never()) + ->method('getCalendarsForUser'); + $this->backend->expects($this->never()) + ->method('getSubscriptionsForUser'); + $this->backend->expects($this->never()) + ->method('searchPrincipalUri'); + + $actual = $this->provider->search($user, $query); + $data = $actual->jsonSerialize(); + $this->assertInstanceOf(SearchResult::class, $actual); + $this->assertEquals('Tasks', $data['name']); + $this->assertEmpty($data['entries']); + $this->assertFalse($data['isPaginated']); + $this->assertNull($data['cursor']); + } + + public function testSearch(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('john.doe'); + $query = $this->createMock(ISearchQuery::class); + $query->method('getTerm')->willReturn('search term'); + $query->method('getLimit')->willReturn(5); + $query->method('getCursor')->willReturn(20); + $this->appManager->expects($this->once()) + ->method('isEnabledForUser') + ->with('tasks', $user) + ->willReturn(true); + $this->l10n->method('t')->willReturnArgument(0); + + $this->backend->expects($this->once()) + ->method('getCalendarsForUser') + ->with('principals/users/john.doe') + ->willReturn([ + [ + 'id' => 99, + 'principaluri' => 'principals/users/john.doe', + 'uri' => 'calendar-uri-99', + ], [ + 'id' => 123, + 'principaluri' => 'principals/users/john.doe', + 'uri' => 'calendar-uri-123', + ] + ]); + $this->backend->expects($this->once()) + ->method('getSubscriptionsForUser') + ->with('principals/users/john.doe') + ->willReturn([ + [ + 'id' => 1337, + 'principaluri' => 'principals/users/john.doe', + 'uri' => 'subscription-uri-1337', + ] + ]); + $this->backend->expects($this->once()) + ->method('searchPrincipalUri') + ->with('principals/users/john.doe', '', ['VTODO'], + ['SUMMARY', 'DESCRIPTION', 'CATEGORIES'], + [], + ['limit' => 5, 'offset' => 20, 'since' => null, 'until' => null]) + ->willReturn([ + [ + 'calendarid' => 99, + 'calendartype' => CalDavBackend::CALENDAR_TYPE_CALENDAR, + 'uri' => 'todo0.ics', + 'calendardata' => self::$vTodo0, + ], + [ + 'calendarid' => 123, + 'calendartype' => CalDavBackend::CALENDAR_TYPE_CALENDAR, + 'uri' => 'todo1.ics', + 'calendardata' => self::$vTodo1, + ], + [ + 'calendarid' => 1337, + 'calendartype' => CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION, + 'uri' => 'todo2.ics', + 'calendardata' => self::$vTodo2, + ] + ]); + + $provider = $this->getMockBuilder(TasksSearchProvider::class) + ->setConstructorArgs([ + $this->appManager, + $this->l10n, + $this->urlGenerator, + $this->backend, + ]) + ->onlyMethods([ + 'getDeepLinkToTasksApp', + 'generateSubline', + ]) + ->getMock(); + + $provider->expects($this->exactly(3)) + ->method('generateSubline') + ->willReturn('subline'); + $provider->expects($this->exactly(3)) + ->method('getDeepLinkToTasksApp') + ->willReturnMap([ + ['calendar-uri-99', 'todo0.ics', 'deep-link-to-tasks'], + ['calendar-uri-123', 'todo1.ics', 'deep-link-to-tasks'], + ['subscription-uri-1337', 'todo2.ics', 'deep-link-to-tasks'] + ]); + + $actual = $provider->search($user, $query); + $data = $actual->jsonSerialize(); + $this->assertInstanceOf(SearchResult::class, $actual); + $this->assertEquals('Tasks', $data['name']); + $this->assertCount(3, $data['entries']); + $this->assertTrue($data['isPaginated']); + $this->assertEquals(23, $data['cursor']); + + $result0 = $data['entries'][0]; + $result0Data = $result0->jsonSerialize(); + $result1 = $data['entries'][1]; + $result1Data = $result1->jsonSerialize(); + $result2 = $data['entries'][2]; + $result2Data = $result2->jsonSerialize(); + + $this->assertInstanceOf(SearchResultEntry::class, $result0); + $this->assertEmpty($result0Data['thumbnailUrl']); + $this->assertEquals('Untitled task', $result0Data['title']); + $this->assertEquals('subline', $result0Data['subline']); + $this->assertEquals('deep-link-to-tasks', $result0Data['resourceUrl']); + $this->assertEquals('icon-checkmark', $result0Data['icon']); + $this->assertFalse($result0Data['rounded']); + + $this->assertInstanceOf(SearchResultEntry::class, $result1); + $this->assertEmpty($result1Data['thumbnailUrl']); + $this->assertEquals('Task title', $result1Data['title']); + $this->assertEquals('subline', $result1Data['subline']); + $this->assertEquals('deep-link-to-tasks', $result1Data['resourceUrl']); + $this->assertEquals('icon-checkmark', $result1Data['icon']); + $this->assertFalse($result1Data['rounded']); + + $this->assertInstanceOf(SearchResultEntry::class, $result2); + $this->assertEmpty($result2Data['thumbnailUrl']); + $this->assertEquals('Task title', $result2Data['title']); + $this->assertEquals('subline', $result2Data['subline']); + $this->assertEquals('deep-link-to-tasks', $result2Data['resourceUrl']); + $this->assertEquals('icon-checkmark', $result2Data['icon']); + $this->assertFalse($result2Data['rounded']); + } + + public function testGetDeepLinkToTasksApp(): void { + $this->urlGenerator->expects($this->once()) + ->method('linkToRoute') + ->with('tasks.page.index') + ->willReturn('link-to-route-tasks.index'); + $this->urlGenerator->expects($this->once()) + ->method('getAbsoluteURL') + ->with('link-to-route-tasks.indexcalendars/uri-john.doe/tasks/task-uri.ics') + ->willReturn('absolute-url-link-to-route-tasks.indexcalendars/uri-john.doe/tasks/task-uri.ics'); + + $actual = self::invokePrivate($this->provider, 'getDeepLinkToTasksApp', ['uri-john.doe', 'task-uri.ics']); + $this->assertEquals('absolute-url-link-to-route-tasks.indexcalendars/uri-john.doe/tasks/task-uri.ics', $actual); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('generateSublineDataProvider')] + public function testGenerateSubline(string $ics, string $expectedSubline): void { + $vCalendar = Reader::read($ics, Reader::OPTION_FORGIVING); + $taskComponent = $vCalendar->VTODO; + + $this->l10n->method('t')->willReturnArgument(0); + $this->l10n->method('l')->willReturnArgument(0); + + $actual = self::invokePrivate($this->provider, 'generateSubline', [$taskComponent]); + $this->assertEquals($expectedSubline, $actual); + } + + public static function generateSublineDataProvider(): array { + return [ + [self::$vTodo0, ''], + [self::$vTodo1, 'Completed on %s'], + [self::$vTodo2, 'Completed on %s'], + [self::$vTodo3, 'Due on %s'], + [self::$vTodo4, 'Due on %s by %s'], + ]; + } +} diff --git a/apps/dav/tests/unit/ServerTest.php b/apps/dav/tests/unit/ServerTest.php new file mode 100644 index 00000000000..9ffe86d3053 --- /dev/null +++ b/apps/dav/tests/unit/ServerTest.php @@ -0,0 +1,42 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit; + +use OCA\DAV\Server; +use OCP\IRequest; + +/** + * Class ServerTest + * + * @group DB + * + * @package OCA\DAV\Tests\Unit + */ +class ServerTest extends \Test\TestCase { + + #[\PHPUnit\Framework\Attributes\DataProvider('providesUris')] + public function test(string $uri, array $plugins): void { + /** @var IRequest | \PHPUnit\Framework\MockObject\MockObject $r */ + $r = $this->createMock(IRequest::class); + $r->expects($this->any())->method('getRequestUri')->willReturn($uri); + $this->loginAsUser('admin'); + $s = new Server($r, '/'); + $this->assertNotNull($s->server); + foreach ($plugins as $plugin) { + $this->assertNotNull($s->server->getPlugin($plugin)); + } + } + public static function providesUris(): array { + return [ + 'principals' => ['principals/users/admin', ['caldav', 'oc-resource-sharing', 'carddav']], + 'calendars' => ['calendars/admin', ['caldav', 'oc-resource-sharing']], + 'addressbooks' => ['addressbooks/admin', ['carddav', 'oc-resource-sharing']], + ]; + } +} diff --git a/apps/dav/tests/unit/Service/AbsenceServiceTest.php b/apps/dav/tests/unit/Service/AbsenceServiceTest.php new file mode 100644 index 00000000000..c16c715d5c2 --- /dev/null +++ b/apps/dav/tests/unit/Service/AbsenceServiceTest.php @@ -0,0 +1,445 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +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\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class AbsenceServiceTest extends TestCase { + private AbsenceService $absenceService; + private AbsenceMapper&MockObject $absenceMapper; + private IEventDispatcher&MockObject $eventDispatcher; + private IJobList&MockObject $jobList; + private TimezoneService&MockObject $timezoneService; + private ITimeFactory&MockObject $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(): void { + $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(): void { + $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(): void { + $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(): void { + $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(): void { + $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(): void { + $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(): void { + $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(): void { + $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', + '', + '', + ); + } +} diff --git a/apps/dav/tests/unit/Service/ExampleContactServiceTest.php b/apps/dav/tests/unit/Service/ExampleContactServiceTest.php new file mode 100644 index 00000000000..027b66a6fb2 --- /dev/null +++ b/apps/dav/tests/unit/Service/ExampleContactServiceTest.php @@ -0,0 +1,194 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Tests\unit\Service; + +use OCA\DAV\CardDAV\CardDavBackend; +use OCA\DAV\Service\ExampleContactService; +use OCP\App\IAppManager; +use OCP\AppFramework\Services\IAppConfig; +use OCP\Files\AppData\IAppDataFactory; +use OCP\Files\IAppData; +use OCP\Files\NotFoundException; +use OCP\Files\SimpleFS\ISimpleFile; +use OCP\Files\SimpleFS\ISimpleFolder; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Symfony\Component\Uid\Uuid; +use Test\TestCase; + +class ExampleContactServiceTest extends TestCase { + protected ExampleContactService $service; + protected CardDavBackend&MockObject $cardDav; + protected IAppManager&MockObject $appManager; + protected IAppDataFactory&MockObject $appDataFactory; + protected LoggerInterface&MockObject $logger; + protected IAppConfig&MockObject $appConfig; + protected IAppData&MockObject $appData; + + protected function setUp(): void { + parent::setUp(); + + $this->cardDav = $this->createMock(CardDavBackend::class); + $this->appDataFactory = $this->createMock(IAppDataFactory::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->appConfig = $this->createMock(IAppConfig::class); + + $this->appData = $this->createMock(IAppData::class); + $this->appDataFactory->method('get') + ->with('dav') + ->willReturn($this->appData); + + $this->service = new ExampleContactService( + $this->appDataFactory, + $this->appConfig, + $this->logger, + $this->cardDav, + ); + } + + public function testCreateDefaultContactWithInvalidCard(): void { + // Invalid vCard missing required FN property + $vcardContent = "BEGIN:VCARD\nVERSION:3.0\nEND:VCARD"; + $this->appConfig->method('getAppValueBool') + ->with('enableDefaultContact', true) + ->willReturn(true); + $folder = $this->createMock(ISimpleFolder::class); + $file = $this->createMock(ISimpleFile::class); + $file->method('getContent')->willReturn($vcardContent); + $folder->method('getFile')->willReturn($file); + $this->appData->method('getFolder')->willReturn($folder); + + $this->logger->expects($this->once()) + ->method('error') + ->with('Default contact is invalid', $this->anything()); + + $this->cardDav->expects($this->never()) + ->method('createCard'); + + $this->service->createDefaultContact(123); + } + + public function testUidAndRevAreUpdated(): void { + $originalUid = 'original-uid'; + $originalRev = '20200101T000000Z'; + $vcardContent = "BEGIN:VCARD\nVERSION:3.0\nFN:Test User\nUID:$originalUid\nREV:$originalRev\nEND:VCARD"; + + $this->appConfig->method('getAppValueBool') + ->with('enableDefaultContact', true) + ->willReturn(true); + $folder = $this->createMock(ISimpleFolder::class); + $file = $this->createMock(ISimpleFile::class); + $file->method('getContent')->willReturn($vcardContent); + $folder->method('getFile')->willReturn($file); + $this->appData->method('getFolder')->willReturn($folder); + + $capturedCardData = null; + $this->cardDav->expects($this->once()) + ->method('createCard') + ->with( + $this->anything(), + $this->anything(), + $this->callback(function ($cardData) use (&$capturedCardData) { + $capturedCardData = $cardData; + return true; + }), + $this->anything() + )->willReturn(null); + + $this->service->createDefaultContact(123); + + $vcard = \Sabre\VObject\Reader::read($capturedCardData); + $this->assertNotEquals($originalUid, $vcard->UID->getValue()); + $this->assertTrue(Uuid::isValid($vcard->UID->getValue())); + $this->assertNotEquals($originalRev, $vcard->REV->getValue()); + } + + public function testDefaultContactFileDoesNotExist(): void { + $this->appConfig->method('getAppValueBool') + ->with('enableDefaultContact', true) + ->willReturn(true); + $this->appData->method('getFolder')->willThrowException(new NotFoundException()); + + $this->cardDav->expects($this->never()) + ->method('createCard'); + + $this->service->createDefaultContact(123); + } + + public function testUidAndRevAreAddedIfMissing(): void { + $vcardContent = "BEGIN:VCARD\nVERSION:3.0\nFN:Test User\nEND:VCARD"; + + $this->appConfig->method('getAppValueBool') + ->with('enableDefaultContact', true) + ->willReturn(true); + $folder = $this->createMock(ISimpleFolder::class); + $file = $this->createMock(ISimpleFile::class); + $file->method('getContent')->willReturn($vcardContent); + $folder->method('getFile')->willReturn($file); + $this->appData->method('getFolder')->willReturn($folder); + + $capturedCardData = 'new-card-data'; + + $this->cardDav + ->expects($this->once()) + ->method('createCard') + ->with( + $this->anything(), + $this->anything(), + $this->callback(function ($cardData) use (&$capturedCardData) { + $capturedCardData = $cardData; + return true; + }), + $this->anything() + ); + + $this->service->createDefaultContact(123); + $vcard = \Sabre\VObject\Reader::read($capturedCardData); + + $this->assertNotNull($vcard->REV); + $this->assertNotNull($vcard->UID); + $this->assertTrue(Uuid::isValid($vcard->UID->getValue())); + } + + public function testDefaultContactIsNotCreatedIfEnabled(): void { + $this->appConfig->method('getAppValueBool') + ->with('enableDefaultContact', true) + ->willReturn(false); + $this->logger->expects($this->never()) + ->method('error'); + $this->cardDav->expects($this->never()) + ->method('createCard'); + + $this->service->createDefaultContact(123); + } + + public static function provideDefaultContactEnableData(): array { + return [[true], [false]]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('provideDefaultContactEnableData')] + public function testIsDefaultContactEnabled(bool $enabled): void { + $this->appConfig->expects(self::once()) + ->method('getAppValueBool') + ->with('enableDefaultContact', true) + ->willReturn($enabled); + + $this->assertEquals($enabled, $this->service->isDefaultContactEnabled()); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('provideDefaultContactEnableData')] + public function testSetDefaultContactEnabled(bool $enabled): void { + $this->appConfig->expects(self::once()) + ->method('setAppValueBool') + ->with('enableDefaultContact', $enabled); + + $this->service->setDefaultContactEnabled($enabled); + } +} diff --git a/apps/dav/tests/unit/Service/ExampleEventServiceTest.php b/apps/dav/tests/unit/Service/ExampleEventServiceTest.php new file mode 100644 index 00000000000..0f423624fb8 --- /dev/null +++ b/apps/dav/tests/unit/Service/ExampleEventServiceTest.php @@ -0,0 +1,196 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Tests\unit\Service; + +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\Service\ExampleEventService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Files\IAppData; +use OCP\Files\NotFoundException; +use OCP\Files\SimpleFS\ISimpleFile; +use OCP\Files\SimpleFS\ISimpleFolder; +use OCP\IAppConfig; +use OCP\IL10N; +use OCP\Security\ISecureRandom; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class ExampleEventServiceTest extends TestCase { + private ExampleEventService $service; + + private CalDavBackend&MockObject $calDavBackend; + private ISecureRandom&MockObject $random; + private ITimeFactory&MockObject $time; + private IAppData&MockObject $appData; + private IAppConfig&MockObject $appConfig; + private IL10N&MockObject $l10n; + + protected function setUp(): void { + parent::setUp(); + + $this->calDavBackend = $this->createMock(CalDavBackend::class); + $this->random = $this->createMock(ISecureRandom::class); + $this->time = $this->createMock(ITimeFactory::class); + $this->appData = $this->createMock(IAppData::class); + $this->appConfig = $this->createMock(IAppConfig::class); + $this->l10n = $this->createMock(IL10N::class); + + $this->l10n->method('t') + ->willReturnArgument(0); + + $this->service = new ExampleEventService( + $this->calDavBackend, + $this->random, + $this->time, + $this->appData, + $this->appConfig, + $this->l10n, + ); + } + + public static function provideCustomEventData(): array { + return [ + [file_get_contents(__DIR__ . '/../test_fixtures/example-event.ics')], + [file_get_contents(__DIR__ . '/../test_fixtures/example-event-with-attendees.ics')], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('provideCustomEventData')] + public function testCreateExampleEventWithCustomEvent($customEventIcs): void { + $this->appConfig->expects(self::once()) + ->method('getValueBool') + ->with('dav', 'create_example_event', true) + ->willReturn(true); + + $exampleEventFolder = $this->createMock(ISimpleFolder::class); + $this->appData->expects(self::once()) + ->method('getFolder') + ->with('example_event') + ->willReturn($exampleEventFolder); + $exampleEventFile = $this->createMock(ISimpleFile::class); + $exampleEventFolder->expects(self::once()) + ->method('getFile') + ->with('example_event.ics') + ->willReturn($exampleEventFile); + $exampleEventFile->expects(self::once()) + ->method('getContent') + ->willReturn($customEventIcs); + + $this->random->expects(self::once()) + ->method('generate') + ->with(32, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') + ->willReturn('RANDOM-UID'); + + $now = new \DateTimeImmutable('2025-01-21T00:00:00Z'); + $this->time->expects(self::exactly(2)) + ->method('now') + ->willReturn($now); + + $expectedIcs = file_get_contents(__DIR__ . '/../test_fixtures/example-event-expected.ics'); + $this->calDavBackend->expects(self::once()) + ->method('createCalendarObject') + ->with(1000, 'RANDOM-UID.ics', $expectedIcs); + + $this->service->createExampleEvent(1000); + } + + public function testCreateExampleEventWithDefaultEvent(): void { + $this->appConfig->expects(self::once()) + ->method('getValueBool') + ->with('dav', 'create_example_event', true) + ->willReturn(true); + + $this->appData->expects(self::once()) + ->method('getFolder') + ->with('example_event') + ->willThrowException(new NotFoundException()); + + $this->random->expects(self::once()) + ->method('generate') + ->with(32, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') + ->willReturn('RANDOM-UID'); + + $now = new \DateTimeImmutable('2025-01-21T00:00:00Z'); + $this->time->expects(self::exactly(3)) + ->method('now') + ->willReturn($now); + + $expectedIcs = file_get_contents(__DIR__ . '/../test_fixtures/example-event-default-expected.ics'); + $this->calDavBackend->expects(self::once()) + ->method('createCalendarObject') + ->with(1000, 'RANDOM-UID.ics', $expectedIcs); + + $this->service->createExampleEvent(1000); + } + + public function testCreateExampleWhenDisabled(): void { + $this->appConfig->expects(self::once()) + ->method('getValueBool') + ->with('dav', 'create_example_event', true) + ->willReturn(false); + + $this->calDavBackend->expects(self::never()) + ->method('createCalendarObject'); + + $this->service->createExampleEvent(1000); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('provideCustomEventData')] + public function testGetExampleEventWithCustomEvent($customEventIcs): void { + $exampleEventFolder = $this->createMock(ISimpleFolder::class); + $this->appData->expects(self::once()) + ->method('getFolder') + ->with('example_event') + ->willReturn($exampleEventFolder); + $exampleEventFile = $this->createMock(ISimpleFile::class); + $exampleEventFolder->expects(self::once()) + ->method('getFile') + ->with('example_event.ics') + ->willReturn($exampleEventFile); + $exampleEventFile->expects(self::once()) + ->method('getContent') + ->willReturn($customEventIcs); + + $this->random->expects(self::once()) + ->method('generate') + ->with(32, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') + ->willReturn('RANDOM-UID'); + + $now = new \DateTimeImmutable('2025-01-21T00:00:00Z'); + $this->time->expects(self::exactly(2)) + ->method('now') + ->willReturn($now); + + $expectedIcs = file_get_contents(__DIR__ . '/../test_fixtures/example-event-expected.ics'); + $actualIcs = $this->service->getExampleEvent()->getIcs(); + $this->assertEquals($expectedIcs, $actualIcs); + } + + public function testGetExampleEventWithDefault(): void { + $this->appData->expects(self::once()) + ->method('getFolder') + ->with('example_event') + ->willThrowException(new NotFoundException()); + + $this->random->expects(self::once()) + ->method('generate') + ->with(32, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') + ->willReturn('RANDOM-UID'); + + $now = new \DateTimeImmutable('2025-01-21T00:00:00Z'); + $this->time->expects(self::exactly(3)) + ->method('now') + ->willReturn($now); + + $expectedIcs = file_get_contents(__DIR__ . '/../test_fixtures/example-event-default-expected.ics'); + $actualIcs = $this->service->getExampleEvent()->getIcs(); + $this->assertEquals($expectedIcs, $actualIcs); + } +} diff --git a/apps/dav/tests/unit/Service/UpcomingEventsServiceTest.php b/apps/dav/tests/unit/Service/UpcomingEventsServiceTest.php new file mode 100644 index 00000000000..fdfe37d8918 --- /dev/null +++ b/apps/dav/tests/unit/Service/UpcomingEventsServiceTest.php @@ -0,0 +1,89 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Tests\unit\DAV\Service; + +use DateTimeImmutable; +use OCA\DAV\CalDAV\UpcomingEventsService; +use OCP\App\IAppManager; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Calendar\ICalendarQuery; +use OCP\Calendar\IManager; +use OCP\IURLGenerator; +use OCP\IUserManager; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class UpcomingEventsServiceTest extends TestCase { + + private IManager&MockObject $calendarManager; + private ITimeFactory&MockObject $timeFactory; + private IUserManager&MockObject $userManager; + private IAppManager&MockObject $appManager; + private IURLGenerator&MockObject $urlGenerator; + private UpcomingEventsService $service; + + protected function setUp(): void { + parent::setUp(); + + $this->calendarManager = $this->createMock(IManager::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->appManager = $this->createMock(IAppManager::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + + $this->service = new UpcomingEventsService( + $this->calendarManager, + $this->timeFactory, + $this->userManager, + $this->appManager, + $this->urlGenerator, + ); + } + + public function testGetEventsByLocation(): void { + $now = new DateTimeImmutable('2024-07-08T18:20:20Z'); + $this->timeFactory->method('now') + ->willReturn($now); + $query = $this->createMock(ICalendarQuery::class); + $this->appManager->method('isEnabledForUser')->willReturn(false); + $this->calendarManager->method('newQuery') + ->with('principals/users/user1') + ->willReturn($query); + $query->expects(self::once()) + ->method('addSearchProperty') + ->with('LOCATION'); + $query->expects(self::once()) + ->method('setSearchPattern') + ->with('https://cloud.example.com/call/123'); + $this->calendarManager->expects(self::once()) + ->method('searchForPrincipal') + ->with($query) + ->willReturn([ + [ + 'uri' => 'ev1', + 'calendar-key' => '1', + 'calendar-uri' => 'personal', + 'objects' => [ + 0 => [ + 'DTSTART' => [ + new DateTimeImmutable('now'), + ], + ], + ], + ], + ]); + + $events = $this->service->getEvents('user1', 'https://cloud.example.com/call/123'); + + self::assertCount(1, $events); + $event1 = $events[0]; + self::assertEquals('ev1', $event1->getUri()); + } +} diff --git a/apps/dav/tests/unit/Settings/CalDAVSettingsTest.php b/apps/dav/tests/unit/Settings/CalDAVSettingsTest.php new file mode 100644 index 00000000000..032759d64b7 --- /dev/null +++ b/apps/dav/tests/unit/Settings/CalDAVSettingsTest.php @@ -0,0 +1,88 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\DAV\Settings; + +use OCA\DAV\Settings\CalDAVSettings; +use OCP\App\IAppManager; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; +use OCP\IConfig; +use OCP\IURLGenerator; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class CalDAVSettingsTest extends TestCase { + private IConfig&MockObject $config; + private IInitialState&MockObject $initialState; + private IURLGenerator&MockObject $urlGenerator; + private IAppManager&MockObject $appManager; + private CalDAVSettings $settings; + + protected function setUp(): void { + parent::setUp(); + + $this->config = $this->createMock(IConfig::class); + $this->initialState = $this->createMock(IInitialState::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + $this->appManager = $this->createMock(IAppManager::class); + $this->settings = new CalDAVSettings($this->config, $this->initialState, $this->urlGenerator, $this->appManager); + } + + public function testGetForm(): void { + $this->config->method('getAppValue') + ->willReturnMap([ + ['dav', 'sendInvitations', 'yes', 'yes'], + ['dav', 'generateBirthdayCalendar', 'yes', 'no'], + ['dav', 'sendEventReminders', 'yes', 'yes'], + ['dav', 'sendEventRemindersToSharedUsers', 'yes', 'yes'], + ['dav', 'sendEventRemindersPush', 'yes', 'yes'], + ]); + $this->urlGenerator + ->expects($this->once()) + ->method('linkToDocs') + ->with('user-sync-calendars') + ->willReturn('Some docs URL'); + + $calls = [ + ['userSyncCalendarsDocUrl', 'Some docs URL'], + ['sendInvitations', true], + ['generateBirthdayCalendar', false], + ['sendEventReminders', true], + ['sendEventRemindersToSharedUsers', true], + ['sendEventRemindersPush', true], + ]; + $this->initialState->method('provideInitialState') + ->willReturnCallback(function () use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + }); + $result = $this->settings->getForm(); + + $this->assertInstanceOf(TemplateResponse::class, $result); + } + + public function testGetSection(): void { + $this->appManager->expects(self::once()) + ->method('isBackendRequired') + ->with(IAppManager::BACKEND_CALDAV) + ->willReturn(true); + $this->assertEquals('groupware', $this->settings->getSection()); + } + + public function testGetSectionWithoutCaldavBackend(): void { + $this->appManager->expects(self::once()) + ->method('isBackendRequired') + ->with(IAppManager::BACKEND_CALDAV) + ->willReturn(false); + $this->assertEquals(null, $this->settings->getSection()); + } + + public function testGetPriority(): void { + $this->assertEquals(10, $this->settings->getPriority()); + } +} diff --git a/apps/dav/tests/unit/SystemTag/SystemTagMappingNodeTest.php b/apps/dav/tests/unit/SystemTag/SystemTagMappingNodeTest.php new file mode 100644 index 00000000000..39342811377 --- /dev/null +++ b/apps/dav/tests/unit/SystemTag/SystemTagMappingNodeTest.php @@ -0,0 +1,157 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\SystemTag; + +use OC\SystemTag\SystemTag; +use OCA\DAV\SystemTag\SystemTagMappingNode; +use OCP\IUser; +use OCP\SystemTag\ISystemTag; +use OCP\SystemTag\ISystemTagManager; +use OCP\SystemTag\ISystemTagObjectMapper; +use OCP\SystemTag\TagNotFoundException; +use PHPUnit\Framework\MockObject\MockObject; + +class SystemTagMappingNodeTest extends \Test\TestCase { + private ISystemTagManager&MockObject $tagManager; + private ISystemTagObjectMapper&MockObject $tagMapper; + private IUser&MockObject $user; + + protected function setUp(): void { + parent::setUp(); + + $this->tagManager = $this->createMock(ISystemTagManager::class); + $this->tagMapper = $this->createMock(ISystemTagObjectMapper::class); + $this->user = $this->createMock(IUser::class); + } + + public function getMappingNode($tag = null, array $writableNodeIds = []) { + if ($tag === null) { + $tag = new SystemTag('1', 'Test', true, true); + } + return new SystemTagMappingNode( + $tag, + '123', + 'files', + $this->user, + $this->tagManager, + $this->tagMapper, + fn ($id): bool => in_array($id, $writableNodeIds), + ); + } + + public function testGetters(): void { + $tag = new SystemTag('1', 'Test', true, false); + $node = $this->getMappingNode($tag); + $this->assertEquals('1', $node->getName()); + $this->assertEquals($tag, $node->getSystemTag()); + $this->assertEquals(123, $node->getObjectId()); + $this->assertEquals('files', $node->getObjectType()); + } + + public function testDeleteTag(): void { + $node = $this->getMappingNode(null, [123]); + $this->tagManager->expects($this->once()) + ->method('canUserSeeTag') + ->with($node->getSystemTag()) + ->willReturn(true); + $this->tagManager->expects($this->once()) + ->method('canUserAssignTag') + ->with($node->getSystemTag()) + ->willReturn(true); + $this->tagManager->expects($this->never()) + ->method('deleteTags'); + $this->tagMapper->expects($this->once()) + ->method('unassignTags') + ->with(123, 'files', 1); + + $node->delete(); + } + + public function testDeleteTagForbidden(): void { + $node = $this->getMappingNode(); + $this->tagManager->expects($this->once()) + ->method('canUserSeeTag') + ->with($node->getSystemTag()) + ->willReturn(true); + $this->tagManager->expects($this->once()) + ->method('canUserAssignTag') + ->with($node->getSystemTag()) + ->willReturn(true); + $this->tagManager->expects($this->never()) + ->method('deleteTags'); + $this->tagMapper->expects($this->never()) + ->method('unassignTags'); + + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + $node->delete(); + } + + public static function tagNodeDeleteProviderPermissionException(): array { + return [ + [ + // cannot unassign invisible tag + new SystemTag('1', 'Original', false, true), + 'Sabre\DAV\Exception\NotFound', + ], + [ + // cannot unassign non-assignable tag + new SystemTag('1', 'Original', true, false), + 'Sabre\DAV\Exception\Forbidden', + ], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('tagNodeDeleteProviderPermissionException')] + public function testDeleteTagExpectedException(ISystemTag $tag, $expectedException): void { + $this->tagManager->expects($this->any()) + ->method('canUserSeeTag') + ->with($tag) + ->willReturn($tag->isUserVisible()); + $this->tagManager->expects($this->any()) + ->method('canUserAssignTag') + ->with($tag) + ->willReturn($tag->isUserAssignable()); + $this->tagManager->expects($this->never()) + ->method('deleteTags'); + $this->tagMapper->expects($this->never()) + ->method('unassignTags'); + + $thrown = null; + try { + $this->getMappingNode($tag)->delete(); + } catch (\Exception $e) { + $thrown = $e; + } + + $this->assertInstanceOf($expectedException, $thrown); + } + + + public function testDeleteTagNotFound(): void { + $this->expectException(\Sabre\DAV\Exception\NotFound::class); + + // assuming the tag existed at the time the node was created, + // but got deleted concurrently in the database + $tag = new SystemTag('1', 'Test', true, true); + $this->tagManager->expects($this->once()) + ->method('canUserSeeTag') + ->with($tag) + ->willReturn($tag->isUserVisible()); + $this->tagManager->expects($this->once()) + ->method('canUserAssignTag') + ->with($tag) + ->willReturn($tag->isUserAssignable()); + $this->tagMapper->expects($this->once()) + ->method('unassignTags') + ->with(123, 'files', 1) + ->willThrowException(new TagNotFoundException()); + + $this->getMappingNode($tag, [123])->delete(); + } +} diff --git a/apps/dav/tests/unit/SystemTag/SystemTagNodeTest.php b/apps/dav/tests/unit/SystemTag/SystemTagNodeTest.php new file mode 100644 index 00000000000..594b5e15db6 --- /dev/null +++ b/apps/dav/tests/unit/SystemTag/SystemTagNodeTest.php @@ -0,0 +1,272 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\SystemTag; + +use OC\SystemTag\SystemTag; +use OCA\DAV\SystemTag\SystemTagNode; +use OCP\IUser; +use OCP\SystemTag\ISystemTag; +use OCP\SystemTag\ISystemTagManager; +use OCP\SystemTag\ISystemTagObjectMapper; +use OCP\SystemTag\TagAlreadyExistsException; +use OCP\SystemTag\TagNotFoundException; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\Exception\Forbidden; + +class SystemTagNodeTest extends \Test\TestCase { + private ISystemTagManager&MockObject $tagManager; + private ISystemTagObjectMapper&MockObject $tagMapper; + private IUser&MockObject $user; + + protected function setUp(): void { + parent::setUp(); + + $this->tagManager = $this->createMock(ISystemTagManager::class); + $this->tagMapper = $this->createMock(ISystemTagObjectMapper::class); + $this->user = $this->createMock(IUser::class); + } + + protected function getTagNode($isAdmin = true, $tag = null) { + if ($tag === null) { + $tag = new SystemTag('1', 'Test', true, true); + } + return new SystemTagNode( + $tag, + $this->user, + $isAdmin, + $this->tagManager, + $this->tagMapper, + ); + } + + public static function adminFlagProvider(): array { + return [[true], [false]]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('adminFlagProvider')] + public function testGetters(bool $isAdmin): void { + $tag = new SystemTag('1', 'Test', true, true); + $node = $this->getTagNode($isAdmin, $tag); + $this->assertEquals('1', $node->getName()); + $this->assertEquals($tag, $node->getSystemTag()); + } + + + public function testSetName(): void { + $this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class); + + $this->getTagNode()->setName('2'); + } + + public static function tagNodeProvider(): array { + return [ + // admin + [ + true, + new SystemTag('1', 'Original', true, true), + ['Renamed', true, true, null] + ], + [ + true, + new SystemTag('1', 'Original', true, true), + ['Original', false, false, null] + ], + // non-admin + [ + // renaming allowed + false, + new SystemTag('1', 'Original', true, true), + ['Rename', true, true, '0082c9'] + ], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('tagNodeProvider')] + public function testUpdateTag(bool $isAdmin, ISystemTag $originalTag, array $changedArgs): void { + $this->tagManager->expects($this->once()) + ->method('canUserSeeTag') + ->with($originalTag) + ->willReturn($originalTag->isUserVisible() || $isAdmin); + $this->tagManager->expects($this->once()) + ->method('canUserAssignTag') + ->with($originalTag) + ->willReturn($originalTag->isUserAssignable() || $isAdmin); + $this->tagManager->expects($this->once()) + ->method('updateTag') + ->with(1, $changedArgs[0], $changedArgs[1], $changedArgs[2], $changedArgs[3]); + $this->getTagNode($isAdmin, $originalTag) + ->update($changedArgs[0], $changedArgs[1], $changedArgs[2], $changedArgs[3]); + } + + public static function tagNodeProviderPermissionException(): array { + return [ + [ + // changing permissions not allowed + new SystemTag('1', 'Original', true, true), + ['Original', false, true, ''], + 'Sabre\DAV\Exception\Forbidden', + ], + [ + // changing permissions not allowed + new SystemTag('1', 'Original', true, true), + ['Original', true, false, ''], + 'Sabre\DAV\Exception\Forbidden', + ], + [ + // changing permissions not allowed + new SystemTag('1', 'Original', true, true), + ['Original', false, false, ''], + 'Sabre\DAV\Exception\Forbidden', + ], + [ + // changing non-assignable not allowed + new SystemTag('1', 'Original', true, false), + ['Rename', true, false, ''], + 'Sabre\DAV\Exception\Forbidden', + ], + [ + // changing non-assignable not allowed + new SystemTag('1', 'Original', true, false), + ['Original', true, true, ''], + 'Sabre\DAV\Exception\Forbidden', + ], + [ + // invisible tag does not exist + new SystemTag('1', 'Original', false, false), + ['Rename', false, false, ''], + 'Sabre\DAV\Exception\NotFound', + ], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('tagNodeProviderPermissionException')] + public function testUpdateTagPermissionException(ISystemTag $originalTag, array $changedArgs, string $expectedException): void { + $this->tagManager->expects($this->any()) + ->method('canUserSeeTag') + ->with($originalTag) + ->willReturn($originalTag->isUserVisible()); + $this->tagManager->expects($this->any()) + ->method('canUserAssignTag') + ->with($originalTag) + ->willReturn($originalTag->isUserAssignable()); + $this->tagManager->expects($this->never()) + ->method('updateTag'); + + $thrown = null; + + try { + $this->getTagNode(false, $originalTag) + ->update($changedArgs[0], $changedArgs[1], $changedArgs[2], $changedArgs[3]); + } catch (\Exception $e) { + $thrown = $e; + } + + $this->assertInstanceOf($expectedException, $thrown); + } + + + public function testUpdateTagAlreadyExists(): void { + $this->expectException(\Sabre\DAV\Exception\Conflict::class); + + $tag = new SystemTag('1', 'tag1', true, true); + $this->tagManager->expects($this->any()) + ->method('canUserSeeTag') + ->with($tag) + ->willReturn(true); + $this->tagManager->expects($this->any()) + ->method('canUserAssignTag') + ->with($tag) + ->willReturn(true); + $this->tagManager->expects($this->once()) + ->method('updateTag') + ->with(1, 'Renamed', true, true) + ->willThrowException(new TagAlreadyExistsException()); + $this->getTagNode(false, $tag)->update('Renamed', true, true, null); + } + + + public function testUpdateTagNotFound(): void { + $this->expectException(\Sabre\DAV\Exception\NotFound::class); + + $tag = new SystemTag('1', 'tag1', true, true); + $this->tagManager->expects($this->any()) + ->method('canUserSeeTag') + ->with($tag) + ->willReturn(true); + $this->tagManager->expects($this->any()) + ->method('canUserAssignTag') + ->with($tag) + ->willReturn(true); + $this->tagManager->expects($this->once()) + ->method('updateTag') + ->with(1, 'Renamed', true, true) + ->willThrowException(new TagNotFoundException()); + $this->getTagNode(false, $tag)->update('Renamed', true, true, null); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('adminFlagProvider')] + public function testDeleteTag(bool $isAdmin): void { + $tag = new SystemTag('1', 'tag1', true, true); + $this->tagManager->expects($isAdmin ? $this->once() : $this->never()) + ->method('canUserSeeTag') + ->with($tag) + ->willReturn(true); + $this->tagManager->expects($isAdmin ? $this->once() : $this->never()) + ->method('deleteTags') + ->with('1'); + if (!$isAdmin) { + $this->expectException(Forbidden::class); + } + $this->getTagNode($isAdmin, $tag)->delete(); + } + + public static function tagNodeDeleteProviderPermissionException(): array { + return [ + [ + // cannot delete invisible tag + new SystemTag('1', 'Original', false, true), + 'Sabre\DAV\Exception\Forbidden', + ], + [ + // cannot delete non-assignable tag + new SystemTag('1', 'Original', true, false), + 'Sabre\DAV\Exception\Forbidden', + ], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('tagNodeDeleteProviderPermissionException')] + public function testDeleteTagPermissionException(ISystemTag $tag, string $expectedException): void { + $this->tagManager->expects($this->any()) + ->method('canUserSeeTag') + ->with($tag) + ->willReturn($tag->isUserVisible()); + $this->tagManager->expects($this->never()) + ->method('deleteTags'); + + $this->expectException($expectedException); + $this->getTagNode(false, $tag)->delete(); + } + + + public function testDeleteTagNotFound(): void { + $this->expectException(\Sabre\DAV\Exception\NotFound::class); + + $tag = new SystemTag('1', 'tag1', true, true); + $this->tagManager->expects($this->any()) + ->method('canUserSeeTag') + ->with($tag) + ->willReturn($tag->isUserVisible()); + $this->tagManager->expects($this->once()) + ->method('deleteTags') + ->with('1') + ->willThrowException(new TagNotFoundException()); + $this->getTagNode(true, $tag)->delete(); + } +} diff --git a/apps/dav/tests/unit/SystemTag/SystemTagPluginTest.php b/apps/dav/tests/unit/SystemTag/SystemTagPluginTest.php new file mode 100644 index 00000000000..e0c4685c1fb --- /dev/null +++ b/apps/dav/tests/unit/SystemTag/SystemTagPluginTest.php @@ -0,0 +1,664 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\SystemTag; + +use OC\SystemTag\SystemTag; +use OCA\DAV\SystemTag\SystemTagNode; +use OCA\DAV\SystemTag\SystemTagPlugin; +use OCA\DAV\SystemTag\SystemTagsByIdCollection; +use OCA\DAV\SystemTag\SystemTagsObjectMappingCollection; +use OCP\Files\IRootFolder; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\IUserSession; +use OCP\SystemTag\ISystemTag; +use OCP\SystemTag\ISystemTagManager; +use OCP\SystemTag\ISystemTagObjectMapper; +use OCP\SystemTag\TagAlreadyExistsException; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\Tree; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +class SystemTagPluginTest extends \Test\TestCase { + public const ID_PROPERTYNAME = SystemTagPlugin::ID_PROPERTYNAME; + public const DISPLAYNAME_PROPERTYNAME = SystemTagPlugin::DISPLAYNAME_PROPERTYNAME; + public const USERVISIBLE_PROPERTYNAME = SystemTagPlugin::USERVISIBLE_PROPERTYNAME; + public const USERASSIGNABLE_PROPERTYNAME = SystemTagPlugin::USERASSIGNABLE_PROPERTYNAME; + public const CANASSIGN_PROPERTYNAME = SystemTagPlugin::CANASSIGN_PROPERTYNAME; + public const GROUPS_PROPERTYNAME = SystemTagPlugin::GROUPS_PROPERTYNAME; + + private \Sabre\DAV\Server $server; + private \Sabre\DAV\Tree&MockObject $tree; + private ISystemTagManager&MockObject $tagManager; + private IGroupManager&MockObject $groupManager; + private IUserSession&MockObject $userSession; + private IRootFolder&MockObject $rootFolder; + private IUser&MockObject $user; + private ISystemTagObjectMapper&MockObject $tagMapper; + private SystemTagPlugin $plugin; + + protected function setUp(): void { + parent::setUp(); + $this->tree = $this->createMock(Tree::class); + + $this->server = new \Sabre\DAV\Server($this->tree); + + $this->tagManager = $this->createMock(ISystemTagManager::class); + $this->groupManager = $this->createMock(IGroupManager::class); + $this->user = $this->createMock(IUser::class); + $this->userSession = $this->createMock(IUserSession::class); + $this->userSession + ->expects($this->any()) + ->method('getUser') + ->willReturn($this->user); + $this->userSession + ->expects($this->any()) + ->method('isLoggedIn') + ->willReturn(true); + + $this->tagMapper = $this->createMock(ISystemTagObjectMapper::class); + $this->rootFolder = $this->createMock(IRootFolder::class); + + $this->plugin = new SystemTagPlugin( + $this->tagManager, + $this->groupManager, + $this->userSession, + $this->rootFolder, + $this->tagMapper + ); + $this->plugin->initialize($this->server); + } + + public static function getPropertiesDataProvider(): array { + return [ + [ + new SystemTag('1', 'Test', true, true), + [], + [ + self::ID_PROPERTYNAME, + self::DISPLAYNAME_PROPERTYNAME, + self::USERVISIBLE_PROPERTYNAME, + self::USERASSIGNABLE_PROPERTYNAME, + self::CANASSIGN_PROPERTYNAME, + ], + [ + self::ID_PROPERTYNAME => '1', + self::DISPLAYNAME_PROPERTYNAME => 'Test', + self::USERVISIBLE_PROPERTYNAME => 'true', + self::USERASSIGNABLE_PROPERTYNAME => 'true', + self::CANASSIGN_PROPERTYNAME => 'true', + ] + ], + [ + new SystemTag('1', 'Test', true, false), + [], + [ + self::ID_PROPERTYNAME, + self::DISPLAYNAME_PROPERTYNAME, + self::USERVISIBLE_PROPERTYNAME, + self::USERASSIGNABLE_PROPERTYNAME, + self::CANASSIGN_PROPERTYNAME, + ], + [ + self::ID_PROPERTYNAME => '1', + self::DISPLAYNAME_PROPERTYNAME => 'Test', + self::USERVISIBLE_PROPERTYNAME => 'true', + self::USERASSIGNABLE_PROPERTYNAME => 'false', + self::CANASSIGN_PROPERTYNAME => 'false', + ] + ], + [ + new SystemTag('1', 'Test', true, false), + ['group1', 'group2'], + [ + self::ID_PROPERTYNAME, + self::GROUPS_PROPERTYNAME, + ], + [ + self::ID_PROPERTYNAME => '1', + self::GROUPS_PROPERTYNAME => 'group1|group2', + ] + ], + [ + new SystemTag('1', 'Test', true, true), + ['group1', 'group2'], + [ + self::ID_PROPERTYNAME, + self::GROUPS_PROPERTYNAME, + ], + [ + self::ID_PROPERTYNAME => '1', + // groups only returned when userAssignable is false + self::GROUPS_PROPERTYNAME => '', + ] + ], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('getPropertiesDataProvider')] + public function testGetProperties(ISystemTag $systemTag, array $groups, array $requestedProperties, array $expectedProperties): void { + $this->user->expects($this->any()) + ->method('getUID') + ->willReturn('admin'); + $this->groupManager + ->expects($this->any()) + ->method('isAdmin') + ->with('admin') + ->willReturn(true); + + $node = $this->getMockBuilder(SystemTagNode::class) + ->disableOriginalConstructor() + ->getMock(); + $node->expects($this->any()) + ->method('getSystemTag') + ->willReturn($systemTag); + + $this->tagManager->expects($this->any()) + ->method('canUserAssignTag') + ->willReturn($systemTag->isUserAssignable()); + + $this->tagManager->expects($this->any()) + ->method('getTagGroups') + ->willReturn($groups); + + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->with('/systemtag/1') + ->willReturn($node); + + $propFind = new \Sabre\DAV\PropFind( + '/systemtag/1', + $requestedProperties, + 0 + ); + + $this->plugin->handleGetProperties( + $propFind, + $node + ); + + $result = $propFind->getResultForMultiStatus(); + + $this->assertEmpty($result[404]); + $this->assertEquals($expectedProperties, $result[200]); + } + + + public function testGetPropertiesForbidden(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + $systemTag = new SystemTag('1', 'Test', true, false); + $requestedProperties = [ + self::ID_PROPERTYNAME, + self::GROUPS_PROPERTYNAME, + ]; + $this->user->expects($this->once()) + ->method('getUID') + ->willReturn('admin'); + $this->groupManager + ->expects($this->once()) + ->method('isAdmin') + ->with('admin') + ->willReturn(false); + + $node = $this->getMockBuilder(SystemTagNode::class) + ->disableOriginalConstructor() + ->getMock(); + $node->expects($this->any()) + ->method('getSystemTag') + ->willReturn($systemTag); + + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->with('/systemtag/1') + ->willReturn($node); + + $propFind = new \Sabre\DAV\PropFind( + '/systemtag/1', + $requestedProperties, + 0 + ); + + $this->plugin->handleGetProperties( + $propFind, + $node + ); + } + + public function testUpdatePropertiesAdmin(): void { + $systemTag = new SystemTag('1', 'Test', true, false); + $this->user->expects($this->any()) + ->method('getUID') + ->willReturn('admin'); + $this->groupManager + ->expects($this->any()) + ->method('isAdmin') + ->with('admin') + ->willReturn(true); + + $node = $this->getMockBuilder(SystemTagNode::class) + ->disableOriginalConstructor() + ->getMock(); + $node->expects($this->any()) + ->method('getSystemTag') + ->willReturn($systemTag); + + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->with('/systemtag/1') + ->willReturn($node); + + $node->expects($this->once()) + ->method('update') + ->with('Test changed', false, true); + + $this->tagManager->expects($this->once()) + ->method('setTagGroups') + ->with($systemTag, ['group1', 'group2']); + + // properties to set + $propPatch = new \Sabre\DAV\PropPatch([ + self::DISPLAYNAME_PROPERTYNAME => 'Test changed', + self::USERVISIBLE_PROPERTYNAME => 'false', + self::USERASSIGNABLE_PROPERTYNAME => 'true', + self::GROUPS_PROPERTYNAME => 'group1|group2', + ]); + + $this->plugin->handleUpdateProperties( + '/systemtag/1', + $propPatch + ); + + $propPatch->commit(); + + // all requested properties removed, as they were processed already + $this->assertEmpty($propPatch->getRemainingMutations()); + + $result = $propPatch->getResult(); + $this->assertEquals(200, $result[self::DISPLAYNAME_PROPERTYNAME]); + $this->assertEquals(200, $result[self::USERASSIGNABLE_PROPERTYNAME]); + $this->assertEquals(200, $result[self::USERVISIBLE_PROPERTYNAME]); + } + + + public function testUpdatePropertiesForbidden(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + $systemTag = new SystemTag('1', 'Test', true, false); + $this->user->expects($this->any()) + ->method('getUID') + ->willReturn('admin'); + $this->groupManager + ->expects($this->any()) + ->method('isAdmin') + ->with('admin') + ->willReturn(false); + + $node = $this->getMockBuilder(SystemTagNode::class) + ->disableOriginalConstructor() + ->getMock(); + $node->expects($this->any()) + ->method('getSystemTag') + ->willReturn($systemTag); + + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->with('/systemtag/1') + ->willReturn($node); + + $node->expects($this->never()) + ->method('update'); + + $this->tagManager->expects($this->never()) + ->method('setTagGroups'); + + // properties to set + $propPatch = new \Sabre\DAV\PropPatch([ + self::GROUPS_PROPERTYNAME => 'group1|group2', + ]); + + $this->plugin->handleUpdateProperties( + '/systemtag/1', + $propPatch + ); + + $propPatch->commit(); + } + + public static function createTagInsufficientPermissionsProvider(): array { + return [ + [true, false, ''], + [false, true, ''], + [true, true, 'group1|group2'], + ]; + } + #[\PHPUnit\Framework\Attributes\DataProvider('createTagInsufficientPermissionsProvider')] + public function testCreateNotAssignableTagAsRegularUser(bool $userVisible, bool $userAssignable, string $groups): void { + $this->expectException(\Sabre\DAV\Exception\BadRequest::class); + $this->expectExceptionMessage('Not sufficient permissions'); + + $this->user->expects($this->once()) + ->method('getUID') + ->willReturn('admin'); + $this->groupManager + ->expects($this->once()) + ->method('isAdmin') + ->with('admin') + ->willReturn(false); + + $requestData = [ + 'name' => 'Test', + 'userVisible' => $userVisible, + 'userAssignable' => $userAssignable, + ]; + if (!empty($groups)) { + $requestData['groups'] = $groups; + } + $requestData = json_encode($requestData); + + $node = $this->createMock(SystemTagsByIdCollection::class); + $this->tagManager->expects($this->never()) + ->method('createTag'); + $this->tagManager->expects($this->never()) + ->method('setTagGroups'); + + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->with('/systemtags') + ->willReturn($node); + + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $request->expects($this->once()) + ->method('getPath') + ->willReturn('/systemtags'); + + $request->expects($this->once()) + ->method('getBodyAsString') + ->willReturn($requestData); + + $request->expects($this->once()) + ->method('getHeader') + ->with('Content-Type') + ->willReturn('application/json'); + + $this->plugin->httpPost($request, $response); + } + + public function testCreateTagInByIdCollectionAsRegularUser(): void { + $systemTag = new SystemTag('1', 'Test', true, false); + + $requestData = json_encode([ + 'name' => 'Test', + 'userVisible' => true, + 'userAssignable' => true, + ]); + + $node = $this->createMock(SystemTagsByIdCollection::class); + $this->tagManager->expects($this->once()) + ->method('createTag') + ->with('Test', true, true) + ->willReturn($systemTag); + + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->with('/systemtags') + ->willReturn($node); + + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $request->expects($this->once()) + ->method('getPath') + ->willReturn('/systemtags'); + + $request->expects($this->once()) + ->method('getBodyAsString') + ->willReturn($requestData); + + $request->expects($this->once()) + ->method('getHeader') + ->with('Content-Type') + ->willReturn('application/json'); + + $request->expects($this->once()) + ->method('getUrl') + ->willReturn('http://example.com/dav/systemtags'); + + $response->expects($this->once()) + ->method('setHeader') + ->with('Content-Location', 'http://example.com/dav/systemtags/1'); + + $this->plugin->httpPost($request, $response); + } + + public static function createTagProvider(): array { + return [ + [true, false, ''], + [false, false, ''], + [true, false, 'group1|group2'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('createTagProvider')] + public function testCreateTagInByIdCollection(bool $userVisible, bool $userAssignable, string $groups): void { + $this->user->expects($this->once()) + ->method('getUID') + ->willReturn('admin'); + $this->groupManager + ->expects($this->once()) + ->method('isAdmin') + ->with('admin') + ->willReturn(true); + + $systemTag = new SystemTag('1', 'Test', true, false); + + $requestData = [ + 'name' => 'Test', + 'userVisible' => $userVisible, + 'userAssignable' => $userAssignable, + ]; + if (!empty($groups)) { + $requestData['groups'] = $groups; + } + $requestData = json_encode($requestData); + + $node = $this->createMock(SystemTagsByIdCollection::class); + $this->tagManager->expects($this->once()) + ->method('createTag') + ->with('Test', $userVisible, $userAssignable) + ->willReturn($systemTag); + + if (!empty($groups)) { + $this->tagManager->expects($this->once()) + ->method('setTagGroups') + ->with($systemTag, explode('|', $groups)) + ->willReturn($systemTag); + } else { + $this->tagManager->expects($this->never()) + ->method('setTagGroups'); + } + + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->with('/systemtags') + ->willReturn($node); + + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $request->expects($this->once()) + ->method('getPath') + ->willReturn('/systemtags'); + + $request->expects($this->once()) + ->method('getBodyAsString') + ->willReturn($requestData); + + $request->expects($this->once()) + ->method('getHeader') + ->with('Content-Type') + ->willReturn('application/json'); + + $request->expects($this->once()) + ->method('getUrl') + ->willReturn('http://example.com/dav/systemtags'); + + $response->expects($this->once()) + ->method('setHeader') + ->with('Content-Location', 'http://example.com/dav/systemtags/1'); + + $this->plugin->httpPost($request, $response); + } + + public static function nodeClassProvider(): array { + return [ + ['\OCA\DAV\SystemTag\SystemTagsByIdCollection'], + ['\OCA\DAV\SystemTag\SystemTagsObjectMappingCollection'], + ]; + } + + public function testCreateTagInMappingCollection(): void { + $this->user->expects($this->once()) + ->method('getUID') + ->willReturn('admin'); + $this->groupManager + ->expects($this->once()) + ->method('isAdmin') + ->with('admin') + ->willReturn(true); + + $systemTag = new SystemTag('1', 'Test', true, false); + + $requestData = json_encode([ + 'name' => 'Test', + 'userVisible' => true, + 'userAssignable' => false, + ]); + + $node = $this->createMock(SystemTagsObjectMappingCollection::class); + + $this->tagManager->expects($this->once()) + ->method('createTag') + ->with('Test', true, false) + ->willReturn($systemTag); + + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->with('/systemtags-relations/files/12') + ->willReturn($node); + + $node->expects($this->once()) + ->method('createFile') + ->with(1); + + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $request->expects($this->once()) + ->method('getPath') + ->willReturn('/systemtags-relations/files/12'); + + $request->expects($this->once()) + ->method('getBodyAsString') + ->willReturn($requestData); + + $request->expects($this->once()) + ->method('getHeader') + ->with('Content-Type') + ->willReturn('application/json'); + + $request->expects($this->once()) + ->method('getBaseUrl') + ->willReturn('http://example.com/dav/'); + + $response->expects($this->once()) + ->method('setHeader') + ->with('Content-Location', 'http://example.com/dav/systemtags/1'); + + $this->plugin->httpPost($request, $response); + } + + + public function testCreateTagToUnknownNode(): void { + $this->expectException(\Sabre\DAV\Exception\NotFound::class); + + $node = $this->createMock(SystemTagsObjectMappingCollection::class); + + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->willThrowException(new \Sabre\DAV\Exception\NotFound()); + + $this->tagManager->expects($this->never()) + ->method('createTag'); + + $node->expects($this->never()) + ->method('createFile'); + + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $request->expects($this->once()) + ->method('getPath') + ->willReturn('/systemtags-relations/files/12'); + + $this->plugin->httpPost($request, $response); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('nodeClassProvider')] + public function testCreateTagConflict(string $nodeClass): void { + $this->expectException(\Sabre\DAV\Exception\Conflict::class); + + $this->user->expects($this->once()) + ->method('getUID') + ->willReturn('admin'); + $this->groupManager + ->expects($this->once()) + ->method('isAdmin') + ->with('admin') + ->willReturn(true); + + $requestData = json_encode([ + 'name' => 'Test', + 'userVisible' => true, + 'userAssignable' => false, + ]); + + $node = $this->createMock($nodeClass); + $this->tagManager->expects($this->once()) + ->method('createTag') + ->with('Test', true, false) + ->willThrowException(new TagAlreadyExistsException('Tag already exists')); + + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->with('/systemtags') + ->willReturn($node); + + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + + $request->expects($this->once()) + ->method('getPath') + ->willReturn('/systemtags'); + + $request->expects($this->once()) + ->method('getBodyAsString') + ->willReturn($requestData); + + $request->expects($this->once()) + ->method('getHeader') + ->with('Content-Type') + ->willReturn('application/json'); + + $this->plugin->httpPost($request, $response); + } +} diff --git a/apps/dav/tests/unit/SystemTag/SystemTagsByIdCollectionTest.php b/apps/dav/tests/unit/SystemTag/SystemTagsByIdCollectionTest.php new file mode 100644 index 00000000000..8f7848452fe --- /dev/null +++ b/apps/dav/tests/unit/SystemTag/SystemTagsByIdCollectionTest.php @@ -0,0 +1,224 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\SystemTag; + +use OC\SystemTag\SystemTag; +use OCA\DAV\SystemTag\SystemTagsByIdCollection; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\IUserSession; +use OCP\SystemTag\ISystemTagManager; +use OCP\SystemTag\ISystemTagObjectMapper; +use OCP\SystemTag\TagNotFoundException; +use PHPUnit\Framework\MockObject\MockObject; + +class SystemTagsByIdCollectionTest extends \Test\TestCase { + private ISystemTagManager&MockObject $tagManager; + private IUser&MockObject $user; + + protected function setUp(): void { + parent::setUp(); + + $this->tagManager = $this->createMock(ISystemTagManager::class); + } + + public function getNode(bool $isAdmin = true) { + $this->user = $this->createMock(IUser::class); + $this->user->expects($this->any()) + ->method('getUID') + ->willReturn('testuser'); + + /** @var IUserSession&MockObject */ + $userSession = $this->createMock(IUserSession::class); + $userSession->expects($this->any()) + ->method('getUser') + ->willReturn($this->user); + + /** @var IGroupManager&MockObject */ + $groupManager = $this->createMock(IGroupManager::class); + $groupManager->expects($this->any()) + ->method('isAdmin') + ->with('testuser') + ->willReturn($isAdmin); + + /** @var ISystemTagObjectMapper&MockObject */ + $tagMapper = $this->createMock(ISystemTagObjectMapper::class); + return new SystemTagsByIdCollection( + $this->tagManager, + $userSession, + $groupManager, + $tagMapper, + ); + } + + public static function adminFlagProvider(): array { + return [[true], [false]]; + } + + + public function testForbiddenCreateFile(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + $this->getNode()->createFile('555'); + } + + + public function testForbiddenCreateDirectory(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + $this->getNode()->createDirectory('789'); + } + + public function testGetChild(): void { + $tag = new SystemTag('123', 'Test', true, false); + $this->tagManager->expects($this->once()) + ->method('canUserSeeTag') + ->with($tag) + ->willReturn(true); + + $this->tagManager->expects($this->once()) + ->method('getTagsByIds') + ->with(['123']) + ->willReturn([$tag]); + + $childNode = $this->getNode()->getChild('123'); + + $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagNode', $childNode); + $this->assertEquals('123', $childNode->getName()); + $this->assertEquals($tag, $childNode->getSystemTag()); + } + + + public function testGetChildInvalidName(): void { + $this->expectException(\Sabre\DAV\Exception\BadRequest::class); + + $this->tagManager->expects($this->once()) + ->method('getTagsByIds') + ->with(['invalid']) + ->willThrowException(new \InvalidArgumentException()); + + $this->getNode()->getChild('invalid'); + } + + + public function testGetChildNotFound(): void { + $this->expectException(\Sabre\DAV\Exception\NotFound::class); + + $this->tagManager->expects($this->once()) + ->method('getTagsByIds') + ->with(['444']) + ->willThrowException(new TagNotFoundException()); + + $this->getNode()->getChild('444'); + } + + + public function testGetChildUserNotVisible(): void { + $this->expectException(\Sabre\DAV\Exception\NotFound::class); + + $tag = new SystemTag('123', 'Test', false, false); + + $this->tagManager->expects($this->once()) + ->method('getTagsByIds') + ->with(['123']) + ->willReturn([$tag]); + + $this->getNode(false)->getChild('123'); + } + + public function testGetChildrenAdmin(): void { + $tag1 = new SystemTag('123', 'One', true, false); + $tag2 = new SystemTag('456', 'Two', true, true); + + $this->tagManager->expects($this->once()) + ->method('getAllTags') + ->with(null) + ->willReturn([$tag1, $tag2]); + + $children = $this->getNode(true)->getChildren(); + + $this->assertCount(2, $children); + + $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagNode', $children[0]); + $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagNode', $children[1]); + $this->assertEquals($tag1, $children[0]->getSystemTag()); + $this->assertEquals($tag2, $children[1]->getSystemTag()); + } + + public function testGetChildrenNonAdmin(): void { + $tag1 = new SystemTag('123', 'One', true, false); + $tag2 = new SystemTag('456', 'Two', true, true); + + $this->tagManager->expects($this->once()) + ->method('getAllTags') + ->with(true) + ->willReturn([$tag1, $tag2]); + + $children = $this->getNode(false)->getChildren(); + + $this->assertCount(2, $children); + + $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagNode', $children[0]); + $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagNode', $children[1]); + $this->assertEquals($tag1, $children[0]->getSystemTag()); + $this->assertEquals($tag2, $children[1]->getSystemTag()); + } + + public function testGetChildrenEmpty(): void { + $this->tagManager->expects($this->once()) + ->method('getAllTags') + ->with(null) + ->willReturn([]); + $this->assertCount(0, $this->getNode()->getChildren()); + } + + public static function childExistsProvider(): array { + return [ + [true, true], + [false, false], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('childExistsProvider')] + public function testChildExists(bool $userVisible, bool $expectedResult): void { + $tag = new SystemTag('123', 'One', $userVisible, false); + $this->tagManager->expects($this->once()) + ->method('canUserSeeTag') + ->with($tag) + ->willReturn($userVisible); + + $this->tagManager->expects($this->once()) + ->method('getTagsByIds') + ->with(['123']) + ->willReturn([$tag]); + + $this->assertEquals($expectedResult, $this->getNode()->childExists('123')); + } + + public function testChildExistsNotFound(): void { + $this->tagManager->expects($this->once()) + ->method('getTagsByIds') + ->with(['123']) + ->willThrowException(new TagNotFoundException()); + + $this->assertFalse($this->getNode()->childExists('123')); + } + + + public function testChildExistsBadRequest(): void { + $this->expectException(\Sabre\DAV\Exception\BadRequest::class); + + $this->tagManager->expects($this->once()) + ->method('getTagsByIds') + ->with(['invalid']) + ->willThrowException(new \InvalidArgumentException()); + + $this->getNode()->childExists('invalid'); + } +} diff --git a/apps/dav/tests/unit/SystemTag/SystemTagsObjectMappingCollectionTest.php b/apps/dav/tests/unit/SystemTag/SystemTagsObjectMappingCollectionTest.php new file mode 100644 index 00000000000..5aea1242e2a --- /dev/null +++ b/apps/dav/tests/unit/SystemTag/SystemTagsObjectMappingCollectionTest.php @@ -0,0 +1,347 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\SystemTag; + +use OC\SystemTag\SystemTag; +use OCA\DAV\SystemTag\SystemTagsObjectMappingCollection; +use OCP\IUser; +use OCP\SystemTag\ISystemTagManager; +use OCP\SystemTag\ISystemTagObjectMapper; +use OCP\SystemTag\TagNotFoundException; +use PHPUnit\Framework\MockObject\MockObject; + +class SystemTagsObjectMappingCollectionTest extends \Test\TestCase { + private ISystemTagManager&MockObject $tagManager; + private ISystemTagObjectMapper&MockObject $tagMapper; + private IUser&MockObject $user; + + protected function setUp(): void { + parent::setUp(); + + $this->tagManager = $this->createMock(ISystemTagManager::class); + $this->tagMapper = $this->createMock(ISystemTagObjectMapper::class); + $this->user = $this->createMock(IUser::class); + } + + public function getNode(array $writableNodeIds = []): SystemTagsObjectMappingCollection { + return new SystemTagsObjectMappingCollection( + '111', + 'files', + $this->user, + $this->tagManager, + $this->tagMapper, + fn ($id): bool => in_array($id, $writableNodeIds), + ); + } + + public function testAssignTag(): void { + $tag = new SystemTag('1', 'Test', true, true); + $this->tagManager->expects($this->once()) + ->method('canUserSeeTag') + ->with($tag) + ->willReturn(true); + $this->tagManager->expects($this->once()) + ->method('canUserAssignTag') + ->with($tag) + ->willReturn(true); + + $this->tagManager->expects($this->once()) + ->method('getTagsByIds') + ->with(['555']) + ->willReturn([$tag]); + $this->tagMapper->expects($this->once()) + ->method('assignTags') + ->with(111, 'files', '555'); + + $this->getNode([111])->createFile('555'); + } + + public function testAssignTagForbidden(): void { + $tag = new SystemTag('1', 'Test', true, true); + $this->tagManager->expects($this->once()) + ->method('canUserSeeTag') + ->with($tag) + ->willReturn(true); + $this->tagManager->expects($this->once()) + ->method('canUserAssignTag') + ->with($tag) + ->willReturn(true); + + $this->tagManager->expects($this->once()) + ->method('getTagsByIds') + ->with(['555']) + ->willReturn([$tag]); + $this->tagMapper->expects($this->never()) + ->method('assignTags'); + + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + $this->getNode()->createFile('555'); + } + + public static function permissionsProvider(): array { + return [ + // invisible, tag does not exist for user + [false, true, '\Sabre\DAV\Exception\PreconditionFailed'], + // visible but static, cannot assign tag + [true, false, '\Sabre\DAV\Exception\Forbidden'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('permissionsProvider')] + public function testAssignTagNoPermission(bool $userVisible, bool $userAssignable, string $expectedException): void { + $tag = new SystemTag('1', 'Test', $userVisible, $userAssignable); + $this->tagManager->expects($this->once()) + ->method('canUserSeeTag') + ->with($tag) + ->willReturn($userVisible); + $this->tagManager->expects($this->any()) + ->method('canUserAssignTag') + ->with($tag) + ->willReturn($userAssignable); + + $this->tagManager->expects($this->once()) + ->method('getTagsByIds') + ->with(['555']) + ->willReturn([$tag]); + $this->tagMapper->expects($this->never()) + ->method('assignTags'); + + $thrown = null; + try { + $this->getNode()->createFile('555'); + } catch (\Exception $e) { + $thrown = $e; + } + + $this->assertInstanceOf($expectedException, $thrown); + } + + + public function testAssignTagNotFound(): void { + $this->expectException(\Sabre\DAV\Exception\PreconditionFailed::class); + + $this->tagManager->expects($this->once()) + ->method('getTagsByIds') + ->with(['555']) + ->willThrowException(new TagNotFoundException()); + + $this->getNode()->createFile('555'); + } + + + public function testForbiddenCreateDirectory(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + $this->getNode()->createDirectory('789'); + } + + public function testGetChild(): void { + $tag = new SystemTag('555', 'TheTag', true, false); + $this->tagManager->expects($this->once()) + ->method('canUserSeeTag') + ->with($tag) + ->willReturn(true); + + $this->tagMapper->expects($this->once()) + ->method('haveTag') + ->with([111], 'files', '555', true) + ->willReturn(true); + + $this->tagManager->expects($this->once()) + ->method('getTagsByIds') + ->with(['555']) + ->willReturn(['555' => $tag]); + + $childNode = $this->getNode()->getChild('555'); + + $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagMappingNode', $childNode); + $this->assertEquals('555', $childNode->getName()); + } + + + public function testGetChildNonVisible(): void { + $this->expectException(\Sabre\DAV\Exception\NotFound::class); + + $tag = new SystemTag('555', 'TheTag', false, false); + $this->tagManager->expects($this->once()) + ->method('canUserSeeTag') + ->with($tag) + ->willReturn(false); + + $this->tagMapper->expects($this->once()) + ->method('haveTag') + ->with([111], 'files', '555', true) + ->willReturn(true); + + $this->tagManager->expects($this->once()) + ->method('getTagsByIds') + ->with(['555']) + ->willReturn(['555' => $tag]); + + $this->getNode()->getChild('555'); + } + + + public function testGetChildRelationNotFound(): void { + $this->expectException(\Sabre\DAV\Exception\NotFound::class); + + $this->tagMapper->expects($this->once()) + ->method('haveTag') + ->with([111], 'files', '777') + ->willReturn(false); + + $this->getNode()->getChild('777'); + } + + + public function testGetChildInvalidId(): void { + $this->expectException(\Sabre\DAV\Exception\BadRequest::class); + + $this->tagMapper->expects($this->once()) + ->method('haveTag') + ->with([111], 'files', 'badid') + ->willThrowException(new \InvalidArgumentException()); + + $this->getNode()->getChild('badid'); + } + + + public function testGetChildTagDoesNotExist(): void { + $this->expectException(\Sabre\DAV\Exception\NotFound::class); + + $this->tagMapper->expects($this->once()) + ->method('haveTag') + ->with([111], 'files', '777') + ->willThrowException(new TagNotFoundException()); + + $this->getNode()->getChild('777'); + } + + public function testGetChildren(): void { + $tag1 = new SystemTag('555', 'TagOne', true, false); + $tag2 = new SystemTag('556', 'TagTwo', true, true); + $tag3 = new SystemTag('557', 'InvisibleTag', false, true); + + $this->tagMapper->expects($this->once()) + ->method('getTagIdsForObjects') + ->with([111], 'files') + ->willReturn(['111' => ['555', '556', '557']]); + + $this->tagManager->expects($this->once()) + ->method('getTagsByIds') + ->with(['555', '556', '557']) + ->willReturn(['555' => $tag1, '556' => $tag2, '557' => $tag3]); + + $this->tagManager->expects($this->exactly(3)) + ->method('canUserSeeTag') + ->willReturnCallback(function ($tag) { + return $tag->isUserVisible(); + }); + + $children = $this->getNode()->getChildren(); + + $this->assertCount(2, $children); + + $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagMappingNode', $children[0]); + $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagMappingNode', $children[1]); + + $this->assertEquals(111, $children[0]->getObjectId()); + $this->assertEquals('files', $children[0]->getObjectType()); + $this->assertEquals($tag1, $children[0]->getSystemTag()); + + $this->assertEquals(111, $children[1]->getObjectId()); + $this->assertEquals('files', $children[1]->getObjectType()); + $this->assertEquals($tag2, $children[1]->getSystemTag()); + } + + public function testChildExistsWithVisibleTag(): void { + $tag = new SystemTag('555', 'TagOne', true, false); + + $this->tagMapper->expects($this->once()) + ->method('haveTag') + ->with([111], 'files', '555') + ->willReturn(true); + + $this->tagManager->expects($this->once()) + ->method('canUserSeeTag') + ->with($tag) + ->willReturn(true); + + $this->tagManager->expects($this->once()) + ->method('getTagsByIds') + ->with(['555']) + ->willReturn([$tag]); + + $this->assertTrue($this->getNode()->childExists('555')); + } + + public function testChildExistsWithInvisibleTag(): void { + $tag = new SystemTag('555', 'TagOne', false, false); + + $this->tagMapper->expects($this->once()) + ->method('haveTag') + ->with([111], 'files', '555') + ->willReturn(true); + + $this->tagManager->expects($this->once()) + ->method('getTagsByIds') + ->with(['555']) + ->willReturn([$tag]); + + $this->assertFalse($this->getNode()->childExists('555')); + } + + public function testChildExistsNotFound(): void { + $this->tagMapper->expects($this->once()) + ->method('haveTag') + ->with([111], 'files', '555') + ->willReturn(false); + + $this->assertFalse($this->getNode()->childExists('555')); + } + + public function testChildExistsTagNotFound(): void { + $this->tagMapper->expects($this->once()) + ->method('haveTag') + ->with([111], 'files', '555') + ->willThrowException(new TagNotFoundException()); + + $this->assertFalse($this->getNode()->childExists('555')); + } + + + public function testChildExistsInvalidId(): void { + $this->expectException(\Sabre\DAV\Exception\BadRequest::class); + + $this->tagMapper->expects($this->once()) + ->method('haveTag') + ->with([111], 'files', '555') + ->willThrowException(new \InvalidArgumentException()); + + $this->getNode()->childExists('555'); + } + + + public function testDelete(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + $this->getNode()->delete(); + } + + + public function testSetName(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + $this->getNode()->setName('somethingelse'); + } + + public function testGetName(): void { + $this->assertEquals('111', $this->getNode()->getName()); + } +} diff --git a/apps/dav/tests/unit/SystemTag/SystemTagsObjectTypeCollectionTest.php b/apps/dav/tests/unit/SystemTag/SystemTagsObjectTypeCollectionTest.php new file mode 100644 index 00000000000..301eb528436 --- /dev/null +++ b/apps/dav/tests/unit/SystemTag/SystemTagsObjectTypeCollectionTest.php @@ -0,0 +1,151 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\SystemTag; + +use OCA\DAV\SystemTag\SystemTagsObjectTypeCollection; +use OCP\Files\Folder; +use OCP\Files\Node; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\IUserSession; +use OCP\SystemTag\ISystemTagManager; +use OCP\SystemTag\ISystemTagObjectMapper; +use PHPUnit\Framework\MockObject\MockObject; + +class SystemTagsObjectTypeCollectionTest extends \Test\TestCase { + private ISystemTagManager&MockObject $tagManager; + private ISystemTagObjectMapper&MockObject $tagMapper; + private Folder&MockObject $userFolder; + private SystemTagsObjectTypeCollection $node; + + protected function setUp(): void { + parent::setUp(); + + $this->tagManager = $this->createMock(ISystemTagManager::class); + $this->tagMapper = $this->createMock(ISystemTagObjectMapper::class); + + $user = $this->createMock(IUser::class); + $user->expects($this->any()) + ->method('getUID') + ->willReturn('testuser'); + $userSession = $this->createMock(IUserSession::class); + $userSession->expects($this->any()) + ->method('getUser') + ->willReturn($user); + $groupManager = $this->createMock(IGroupManager::class); + $groupManager->expects($this->any()) + ->method('isAdmin') + ->with('testuser') + ->willReturn(true); + + $this->userFolder = $this->createMock(Folder::class); + $userFolder = $this->userFolder; + + $closure = function ($name) use ($userFolder) { + $node = $userFolder->getFirstNodeById((int)$name); + return $node !== null; + }; + $writeAccessClosure = function ($name) use ($userFolder) { + $nodes = $userFolder->getById((int)$name); + foreach ($nodes as $node) { + if (($node->getPermissions() & Constants::PERMISSION_UPDATE) === Constants::PERMISSION_UPDATE) { + return true; + } + } + return false; + }; + + $this->node = new SystemTagsObjectTypeCollection( + 'files', + $this->tagManager, + $this->tagMapper, + $userSession, + $groupManager, + $closure, + $writeAccessClosure, + ); + } + + + public function testForbiddenCreateFile(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + $this->node->createFile('555'); + } + + + public function testForbiddenCreateDirectory(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + $this->node->createDirectory('789'); + } + + public function testGetChild(): void { + $this->userFolder->expects($this->once()) + ->method('getFirstNodeById') + ->with('555') + ->willReturn($this->createMock(Node::class)); + $childNode = $this->node->getChild('555'); + + $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagsObjectMappingCollection', $childNode); + $this->assertEquals('555', $childNode->getName()); + } + + + public function testGetChildWithoutAccess(): void { + $this->expectException(\Sabre\DAV\Exception\NotFound::class); + + $this->userFolder->expects($this->once()) + ->method('getFirstNodeById') + ->with('555') + ->willReturn(null); + $this->node->getChild('555'); + } + + + public function testGetChildren(): void { + $this->expectException(\Sabre\DAV\Exception\MethodNotAllowed::class); + + $this->node->getChildren(); + } + + public function testChildExists(): void { + $this->userFolder->expects($this->once()) + ->method('getFirstNodeById') + ->with('123') + ->willReturn($this->createMock(Node::class)); + $this->assertTrue($this->node->childExists('123')); + } + + public function testChildExistsWithoutAccess(): void { + $this->userFolder->expects($this->once()) + ->method('getFirstNodeById') + ->with('555') + ->willReturn(null); + $this->assertFalse($this->node->childExists('555')); + } + + + public function testDelete(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + $this->node->delete(); + } + + + public function testSetName(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + $this->node->setName('somethingelse'); + } + + public function testGetName(): void { + $this->assertEquals('files', $this->node->getName()); + } +} diff --git a/apps/dav/tests/unit/Upload/AssemblyStreamTest.php b/apps/dav/tests/unit/Upload/AssemblyStreamTest.php new file mode 100644 index 00000000000..ec5d0a9ab5b --- /dev/null +++ b/apps/dav/tests/unit/Upload/AssemblyStreamTest.php @@ -0,0 +1,166 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Upload; + +use OCA\DAV\Upload\AssemblyStream; +use Sabre\DAV\File; + +class AssemblyStreamTest extends \Test\TestCase { + + #[\PHPUnit\Framework\Attributes\DataProvider('providesNodes')] + public function testGetContents(string $expected, array $nodeData): void { + $nodes = []; + foreach ($nodeData as $data) { + $nodes[] = $this->buildNode(...$data); + } + $stream = AssemblyStream::wrap($nodes); + $content = stream_get_contents($stream); + + $this->assertEquals($expected, $content); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providesNodes')] + public function testGetContentsFread(string $expected, array $nodeData, int $chunkLength = 3): void { + $nodes = []; + foreach ($nodeData as $data) { + $nodes[] = $this->buildNode(...$data); + } + $stream = AssemblyStream::wrap($nodes); + + $content = ''; + while (!feof($stream)) { + $chunk = fread($stream, $chunkLength); + $content .= $chunk; + if ($chunkLength !== 3) { + $this->assertEquals($chunkLength, strlen($chunk)); + } + } + + $this->assertEquals($expected, $content); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('providesNodes')] + public function testSeek(string $expected, array $nodeData): void { + $nodes = []; + foreach ($nodeData as $data) { + $nodes[] = $this->buildNode(...$data); + } + + $stream = AssemblyStream::wrap($nodes); + + $offset = floor(strlen($expected) * 0.6); + if (fseek($stream, $offset) === -1) { + $this->fail('fseek failed'); + } + + $content = stream_get_contents($stream); + $this->assertEquals(substr($expected, $offset), $content); + } + + public static function providesNodes(): array { + $data8k = self::makeData(8192); + $dataLess8k = self::makeData(8191); + + $tonofnodes = []; + $tonofdata = ''; + for ($i = 0; $i < 101; $i++) { + $thisdata = random_int(0, 100); // variable length and content + $tonofdata .= $thisdata; + $tonofnodes[] = [(string)$i, (string)$thisdata]; + } + + return[ + 'one node zero bytes' => [ + '', [ + ['0', ''], + ]], + 'one node only' => [ + '1234567890', [ + ['0', '1234567890'], + ]], + 'one node buffer boundary' => [ + $data8k, [ + ['0', $data8k], + ]], + 'two nodes' => [ + '1234567890', [ + ['1', '67890'], + ['0', '12345'], + ]], + 'two nodes end on buffer boundary' => [ + $data8k . $data8k, [ + ['1', $data8k], + ['0', $data8k], + ]], + 'two nodes with one on buffer boundary' => [ + $data8k . $dataLess8k, [ + ['1', $dataLess8k], + ['0', $data8k], + ]], + 'two nodes on buffer boundary plus one byte' => [ + $data8k . 'X' . $data8k, [ + ['1', $data8k], + ['0', $data8k . 'X'], + ]], + 'two nodes on buffer boundary plus one byte at the end' => [ + $data8k . $data8k . 'X', [ + ['1', $data8k . 'X'], + ['0', $data8k], + ]], + 'a ton of nodes' => [ + $tonofdata, $tonofnodes + ], + 'one read over multiple nodes' => [ + '1234567890', [ + ['0', '1234'], + ['1', '5678'], + ['2', '90'], + ], 10], + 'two reads over multiple nodes' => [ + '1234567890', [ + ['0', '1234'], + ['1', '5678'], + ['2', '90'], + ], 5], + ]; + } + + private static function makeData(int $count): string { + $data = ''; + $base = '1234567890'; + $j = 0; + for ($i = 0; $i < $count; $i++) { + $data .= $base[$j]; + $j++; + if (!isset($base[$j])) { + $j = 0; + } + } + return $data; + } + + private function buildNode(string $name, string $data) { + $node = $this->getMockBuilder(File::class) + ->onlyMethods(['getName', 'get', 'getSize']) + ->getMock(); + + $node->expects($this->any()) + ->method('getName') + ->willReturn($name); + + $node->expects($this->any()) + ->method('get') + ->willReturn($data); + + $node->expects($this->any()) + ->method('getSize') + ->willReturn(strlen($data)); + + return $node; + } +} diff --git a/apps/dav/tests/unit/Upload/ChunkingPluginTest.php b/apps/dav/tests/unit/Upload/ChunkingPluginTest.php new file mode 100644 index 00000000000..00ed7657dd3 --- /dev/null +++ b/apps/dav/tests/unit/Upload/ChunkingPluginTest.php @@ -0,0 +1,189 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2017 ownCloud GmbH + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Upload; + +use OCA\DAV\Connector\Sabre\Directory; +use OCA\DAV\Upload\ChunkingPlugin; +use OCA\DAV\Upload\FutureFile; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\Exception\NotFound; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; +use Test\TestCase; + +class ChunkingPluginTest extends TestCase { + private \Sabre\DAV\Server&MockObject $server; + private \Sabre\DAV\Tree&MockObject $tree; + private ChunkingPlugin $plugin; + private RequestInterface&MockObject $request; + private ResponseInterface&MockObject $response; + + protected function setUp(): void { + parent::setUp(); + + $this->server = $this->createMock('\Sabre\DAV\Server'); + $this->tree = $this->createMock('\Sabre\DAV\Tree'); + + $this->server->tree = $this->tree; + $this->plugin = new ChunkingPlugin(); + + $this->request = $this->createMock(RequestInterface::class); + $this->response = $this->createMock(ResponseInterface::class); + $this->server->httpRequest = $this->request; + $this->server->httpResponse = $this->response; + + $this->plugin->initialize($this->server); + } + + public function testBeforeMoveFutureFileSkip(): void { + $node = $this->createMock(Directory::class); + + $this->tree->expects($this->any()) + ->method('getNodeForPath') + ->with('source') + ->willReturn($node); + $this->response->expects($this->never()) + ->method('setStatus'); + + $this->assertNull($this->plugin->beforeMove('source', 'target')); + } + + public function testBeforeMoveDestinationIsDirectory(): void { + $this->expectException(\Sabre\DAV\Exception\BadRequest::class); + $this->expectExceptionMessage('The given destination target is a directory.'); + + $sourceNode = $this->createMock(FutureFile::class); + $targetNode = $this->createMock(Directory::class); + + $this->tree->expects($this->exactly(2)) + ->method('getNodeForPath') + ->willReturnMap([ + ['source', $sourceNode], + ['target', $targetNode], + ]); + $this->response->expects($this->never()) + ->method('setStatus'); + + $this->assertNull($this->plugin->beforeMove('source', 'target')); + } + + public function testBeforeMoveFutureFileSkipNonExisting(): void { + $sourceNode = $this->createMock(FutureFile::class); + $sourceNode->expects($this->once()) + ->method('getSize') + ->willReturn(4); + + $calls = [ + ['source', $sourceNode], + ['target', new NotFound()], + ]; + $this->tree->expects($this->exactly(2)) + ->method('getNodeForPath') + ->willReturnCallback(function (string $path) use (&$calls) { + $expected = array_shift($calls); + $this->assertSame($expected[0], $path); + if ($expected[1] instanceof \Throwable) { + throw $expected[1]; + } + return $expected[1]; + }); + $this->tree->expects($this->any()) + ->method('nodeExists') + ->with('target') + ->willReturn(false); + $this->response->expects($this->once()) + ->method('setHeader') + ->with('Content-Length', '0'); + $this->response->expects($this->once()) + ->method('setStatus') + ->with(201); + $this->request->expects($this->once()) + ->method('getHeader') + ->with('OC-Total-Length') + ->willReturn(4); + + $this->assertFalse($this->plugin->beforeMove('source', 'target')); + } + + public function testBeforeMoveFutureFileMoveIt(): void { + $sourceNode = $this->createMock(FutureFile::class); + $sourceNode->expects($this->once()) + ->method('getSize') + ->willReturn(4); + + $calls = [ + ['source', $sourceNode], + ['target', new NotFound()], + ]; + $this->tree->expects($this->exactly(2)) + ->method('getNodeForPath') + ->willReturnCallback(function (string $path) use (&$calls) { + $expected = array_shift($calls); + $this->assertSame($expected[0], $path); + if ($expected[1] instanceof \Throwable) { + throw $expected[1]; + } + return $expected[1]; + }); + + $this->tree->expects($this->any()) + ->method('nodeExists') + ->with('target') + ->willReturn(true); + $this->tree->expects($this->once()) + ->method('move') + ->with('source', 'target'); + + $this->response->expects($this->once()) + ->method('setHeader') + ->with('Content-Length', '0'); + $this->response->expects($this->once()) + ->method('setStatus') + ->with(204); + $this->request->expects($this->once()) + ->method('getHeader') + ->with('OC-Total-Length') + ->willReturn('4'); + + $this->assertFalse($this->plugin->beforeMove('source', 'target')); + } + + + public function testBeforeMoveSizeIsWrong(): void { + $this->expectException(\Sabre\DAV\Exception\BadRequest::class); + $this->expectExceptionMessage('Chunks on server do not sum up to 4 but to 3 bytes'); + + $sourceNode = $this->createMock(FutureFile::class); + $sourceNode->expects($this->once()) + ->method('getSize') + ->willReturn(3); + + $calls = [ + ['source', $sourceNode], + ['target', new NotFound()], + ]; + $this->tree->expects($this->exactly(2)) + ->method('getNodeForPath') + ->willReturnCallback(function (string $path) use (&$calls) { + $expected = array_shift($calls); + $this->assertSame($expected[0], $path); + if ($expected[1] instanceof \Throwable) { + throw $expected[1]; + } + return $expected[1]; + }); + + $this->request->expects($this->once()) + ->method('getHeader') + ->with('OC-Total-Length') + ->willReturn('4'); + + $this->assertFalse($this->plugin->beforeMove('source', 'target')); + } +} diff --git a/apps/dav/tests/unit/Upload/FutureFileTest.php b/apps/dav/tests/unit/Upload/FutureFileTest.php new file mode 100644 index 00000000000..1409df937c0 --- /dev/null +++ b/apps/dav/tests/unit/Upload/FutureFileTest.php @@ -0,0 +1,95 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OCA\DAV\Tests\unit\Upload; + +use OCA\DAV\Connector\Sabre\Directory; +use OCA\DAV\Upload\FutureFile; + +class FutureFileTest extends \Test\TestCase { + public function testGetContentType(): void { + $f = $this->mockFutureFile(); + $this->assertEquals('application/octet-stream', $f->getContentType()); + } + + public function testGetETag(): void { + $f = $this->mockFutureFile(); + $this->assertEquals('1234567890', $f->getETag()); + } + + public function testGetName(): void { + $f = $this->mockFutureFile(); + $this->assertEquals('foo.txt', $f->getName()); + } + + public function testGetLastModified(): void { + $f = $this->mockFutureFile(); + $this->assertEquals(12121212, $f->getLastModified()); + } + + public function testGetSize(): void { + $f = $this->mockFutureFile(); + $this->assertEquals(0, $f->getSize()); + } + + public function testGet(): void { + $f = $this->mockFutureFile(); + $stream = $f->get(); + $this->assertTrue(is_resource($stream)); + } + + public function testDelete(): void { + $d = $this->getMockBuilder(Directory::class) + ->disableOriginalConstructor() + ->onlyMethods(['delete']) + ->getMock(); + + $d->expects($this->once()) + ->method('delete'); + + $f = new FutureFile($d, 'foo.txt'); + $f->delete(); + } + + + public function testPut(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + $f = $this->mockFutureFile(); + $f->put(''); + } + + + public function testSetName(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + $f = $this->mockFutureFile(); + $f->setName(''); + } + + private function mockFutureFile(): FutureFile { + $d = $this->getMockBuilder(Directory::class) + ->disableOriginalConstructor() + ->onlyMethods(['getETag', 'getLastModified', 'getChildren']) + ->getMock(); + + $d->expects($this->any()) + ->method('getETag') + ->willReturn('1234567890'); + + $d->expects($this->any()) + ->method('getLastModified') + ->willReturn(12121212); + + $d->expects($this->any()) + ->method('getChildren') + ->willReturn([]); + + return new FutureFile($d, 'foo.txt'); + } +} diff --git a/apps/dav/tests/unit/Upload/UploadAutoMkcolPluginTest.php b/apps/dav/tests/unit/Upload/UploadAutoMkcolPluginTest.php new file mode 100644 index 00000000000..baae839c8da --- /dev/null +++ b/apps/dav/tests/unit/Upload/UploadAutoMkcolPluginTest.php @@ -0,0 +1,133 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\Upload; + +use Generator; +use OCA\DAV\Upload\UploadAutoMkcolPlugin; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\ICollection; +use Sabre\DAV\INode; +use Sabre\DAV\Server; +use Sabre\DAV\Tree; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; +use Test\TestCase; + +class UploadAutoMkcolPluginTest extends TestCase { + + private Tree&MockObject $tree; + private RequestInterface&MockObject $request; + private ResponseInterface&MockObject $response; + + public static function dataMissingHeaderShouldReturnTrue(): Generator { + yield 'missing X-NC-WebDAV-Auto-Mkcol header' => [null]; + yield 'empty X-NC-WebDAV-Auto-Mkcol header' => ['']; + yield 'invalid X-NC-WebDAV-Auto-Mkcol header' => ['enable']; + } + + public function testBeforeMethodWithRootNodeNotAnICollectionShouldReturnTrue(): void { + $this->request->method('getHeader')->willReturn('1'); + $this->request->expects(self::once()) + ->method('getPath') + ->willReturn('/non-relevant/path.txt'); + $this->tree->expects(self::once()) + ->method('nodeExists') + ->with('/non-relevant') + ->willReturn(false); + + $mockNode = $this->getMockBuilder(INode::class); + $this->tree->expects(self::once()) + ->method('getNodeForPath') + ->willReturn($mockNode); + + $return = $this->plugin->beforeMethod($this->request, $this->response); + $this->assertTrue($return); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataMissingHeaderShouldReturnTrue')] + public function testBeforeMethodWithMissingHeaderShouldReturnTrue(?string $header): void { + $this->request->expects(self::once()) + ->method('getHeader') + ->with('X-NC-WebDAV-Auto-Mkcol') + ->willReturn($header); + + $this->request->expects(self::never()) + ->method('getPath'); + + $return = $this->plugin->beforeMethod($this->request, $this->response); + self::assertTrue($return); + } + + public function testBeforeMethodWithExistingPathShouldReturnTrue(): void { + $this->request->method('getHeader')->willReturn('1'); + $this->request->expects(self::once()) + ->method('getPath') + ->willReturn('/files/user/deep/image.jpg'); + $this->tree->expects(self::once()) + ->method('nodeExists') + ->with('/files/user/deep') + ->willReturn(true); + + $this->tree->expects(self::never()) + ->method('getNodeForPath'); + + $return = $this->plugin->beforeMethod($this->request, $this->response); + self::assertTrue($return); + } + + public function testBeforeMethodShouldSucceed(): void { + $this->request->method('getHeader')->willReturn('1'); + $this->request->expects(self::once()) + ->method('getPath') + ->willReturn('/files/user/my/deep/path/image.jpg'); + $this->tree->expects(self::once()) + ->method('nodeExists') + ->with('/files/user/my/deep/path') + ->willReturn(false); + + $mockNode = $this->createMock(ICollection::class); + $this->tree->expects(self::once()) + ->method('getNodeForPath') + ->with('/files') + ->willReturn($mockNode); + $mockNode->expects(self::exactly(4)) + ->method('childExists') + ->willReturnMap([ + ['user', true], + ['my', true], + ['deep', false], + ['path', false], + ]); + $mockNode->expects(self::exactly(2)) + ->method('createDirectory'); + $mockNode->expects(self::exactly(4)) + ->method('getChild') + ->willReturn($mockNode); + + $return = $this->plugin->beforeMethod($this->request, $this->response); + self::assertTrue($return); + } + + protected function setUp(): void { + parent::setUp(); + + $server = $this->createMock(Server::class); + $this->tree = $this->createMock(Tree::class); + + $server->tree = $this->tree; + $this->plugin = new UploadAutoMkcolPlugin(); + + $this->request = $this->createMock(RequestInterface::class); + $this->response = $this->createMock(ResponseInterface::class); + $server->httpRequest = $this->request; + $server->httpResponse = $this->response; + + $this->plugin->initialize($server); + } +} diff --git a/apps/dav/tests/unit/appinfo/applicationtest.php b/apps/dav/tests/unit/appinfo/applicationtest.php deleted file mode 100644 index 7f533a185df..00000000000 --- a/apps/dav/tests/unit/appinfo/applicationtest.php +++ /dev/null @@ -1,62 +0,0 @@ -<?php -/** - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\AppInfo; - -use OCA\Dav\AppInfo\Application; -use OCP\Contacts\IManager; -use Test\TestCase; - -/** - * Class ApplicationTest - * - * @group DB - * - * @package OCA\DAV\Tests\Unit\AppInfo - */ -class ApplicationTest extends TestCase { - public function test() { - $app = new Application(); - $c = $app->getContainer(); - - // assert service instances in the container are properly setup - $s = $c->query('ContactsManager'); - $this->assertInstanceOf('OCA\DAV\CardDAV\ContactsManager', $s); - $s = $c->query('CardDavBackend'); - $this->assertInstanceOf('OCA\DAV\CardDAV\CardDavBackend', $s); - } - - public function testContactsManagerSetup() { - $app = new Application(); - $c = $app->getContainer(); - $c->registerService('CardDavBackend', function($c) { - $service = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend')->disableOriginalConstructor()->getMock(); - $service->method('getAddressBooksForUser')->willReturn([]); - return $service; - }); - - // assert setupContactsProvider() is proper - /** @var IManager | \PHPUnit_Framework_MockObject_MockObject $cm */ - $cm = $this->getMockBuilder('OCP\Contacts\IManager')->disableOriginalConstructor()->getMock(); - $app->setupContactsProvider($cm, 'xxx'); - $this->assertTrue(true); - } -} diff --git a/apps/dav/tests/unit/bootstrap.php b/apps/dav/tests/unit/bootstrap.php index f6733bc7a3e..ee76bb6677b 100644 --- a/apps/dav/tests/unit/bootstrap.php +++ b/apps/dav/tests/unit/bootstrap.php @@ -1,33 +1,21 @@ <?php + +declare(strict_types=1); + /** - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ -if (!defined('PHPUNIT_RUN')) { - define('PHPUNIT_RUN', 1); -} -require_once __DIR__.'/../../../../lib/base.php'; +use OCP\App\IAppManager; +use OCP\Server; -if(!class_exists('PHPUnit_Framework_TestCase')) { - require_once('PHPUnit/Autoload.php'); +if (!defined('PHPUNIT_RUN')) { + define('PHPUNIT_RUN', 1); } -\OC_App::loadApp('dav'); +require_once __DIR__ . '/../../../../lib/base.php'; +require_once __DIR__ . '/../../../../tests/autoload.php'; -OC_Hook::clear(); +Server::get(IAppManager::class)->loadApp('dav'); diff --git a/apps/dav/tests/unit/caldav/caldavbackendtest.php b/apps/dav/tests/unit/caldav/caldavbackendtest.php deleted file mode 100644 index 87a700a473d..00000000000 --- a/apps/dav/tests/unit/caldav/caldavbackendtest.php +++ /dev/null @@ -1,491 +0,0 @@ -<?php -/** - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace Tests\Connector\Sabre; - -use DateTime; -use DateTimeZone; -use OCA\DAV\CalDAV\CalDavBackend; -use OCA\DAV\CalDAV\Calendar; -use OCA\DAV\Connector\Sabre\Principal; -use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet; -use Sabre\DAV\PropPatch; -use Sabre\DAV\Xml\Property\Href; -use Sabre\DAVACL\IACL; -use Test\TestCase; - -/** - * Class CalDavBackendTest - * - * @group DB - * - * @package Tests\Connector\Sabre - */ -class CalDavBackendTest extends TestCase { - - /** @var CalDavBackend */ - private $backend; - - /** @var Principal | \PHPUnit_Framework_MockObject_MockObject */ - private $principal; - - const UNIT_TEST_USER = 'principals/users/caldav-unit-test'; - const UNIT_TEST_USER1 = 'principals/users/caldav-unit-test1'; - const UNIT_TEST_GROUP = 'principals/groups/caldav-unit-test-group'; - - public function setUp() { - parent::setUp(); - - $this->principal = $this->getMockBuilder('OCA\DAV\Connector\Sabre\Principal') - ->disableOriginalConstructor() - ->setMethods(['getPrincipalByPath', 'getGroupMembership']) - ->getMock(); - $this->principal->expects($this->any())->method('getPrincipalByPath') - ->willReturn([ - 'uri' => 'principals/best-friend' - ]); - $this->principal->expects($this->any())->method('getGroupMembership') - ->withAnyParameters() - ->willReturn([self::UNIT_TEST_GROUP]); - - $db = \OC::$server->getDatabaseConnection(); - $this->backend = new CalDavBackend($db, $this->principal); - - $this->tearDown(); - } - - public function tearDown() { - parent::tearDown(); - - if (is_null($this->backend)) { - return; - } - $books = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER); - foreach ($books as $book) { - $this->backend->deleteCalendar($book['id']); - } - $subscriptions = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER); - foreach ($subscriptions as $subscription) { - $this->backend->deleteSubscription($subscription['id']); - } - } - - public function testCalendarOperations() { - - $calendarId = $this->createTestCalendar(); - - // update it's display name - $patch = new PropPatch([ - '{DAV:}displayname' => 'Unit test', - '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'Calendar used for unit testing' - ]); - $this->backend->updateCalendar($calendarId, $patch); - $patch->commit(); - $books = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER); - $this->assertEquals(1, count($books)); - $this->assertEquals('Unit test', $books[0]['{DAV:}displayname']); - $this->assertEquals('Calendar used for unit testing', $books[0]['{urn:ietf:params:xml:ns:caldav}calendar-description']); - - // delete the address book - $this->backend->deleteCalendar($books[0]['id']); - $books = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER); - $this->assertEquals(0, count($books)); - } - - public function providesSharingData() { - return [ - [true, true, true, false, [ - [ - 'href' => 'principal:' . self::UNIT_TEST_USER1, - 'readOnly' => false - ], - [ - 'href' => 'principal:' . self::UNIT_TEST_GROUP, - 'readOnly' => true - ] - ]], - [true, false, false, false, [ - [ - 'href' => 'principal:' . self::UNIT_TEST_USER1, - 'readOnly' => true - ], - ]], - - ]; - } - - /** - * @dataProvider providesSharingData - */ - public function testCalendarSharing($userCanRead, $userCanWrite, $groupCanRead, $groupCanWrite, $add) { - - $calendarId = $this->createTestCalendar(); - $books = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER); - $this->assertEquals(1, count($books)); - $calendar = new Calendar($this->backend, $books[0]); - $this->backend->updateShares($calendar, $add, []); - $books = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER1); - $this->assertEquals(1, count($books)); - $calendar = new Calendar($this->backend, $books[0]); - $acl = $calendar->getACL(); - $this->assertAcl(self::UNIT_TEST_USER, '{DAV:}read', $acl); - $this->assertAcl(self::UNIT_TEST_USER, '{DAV:}write', $acl); - $this->assertAccess($userCanRead, self::UNIT_TEST_USER1, '{DAV:}read', $acl); - $this->assertAccess($userCanWrite, self::UNIT_TEST_USER1, '{DAV:}write', $acl); - $this->assertAccess($groupCanRead, self::UNIT_TEST_GROUP, '{DAV:}read', $acl); - $this->assertAccess($groupCanWrite, self::UNIT_TEST_GROUP, '{DAV:}write', $acl); - $this->assertEquals(self::UNIT_TEST_USER, $calendar->getOwner()); - - // test acls on the child - $uri = $this->getUniqueID('calobj'); - $calData = <<<'EOD' -BEGIN:VCALENDAR -VERSION:2.0 -PRODID:ownCloud Calendar -BEGIN:VEVENT -CREATED;VALUE=DATE-TIME:20130910T125139Z -UID:47d15e3ec8 -LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z -DTSTAMP;VALUE=DATE-TIME:20130910T125139Z -SUMMARY:Test Event -DTSTART;VALUE=DATE-TIME:20130912T130000Z -DTEND;VALUE=DATE-TIME:20130912T140000Z -CLASS:PUBLIC -END:VEVENT -END:VCALENDAR -EOD; - - $this->backend->createCalendarObject($calendarId, $uri, $calData); - - /** @var IACL $child */ - $child = $calendar->getChild($uri); - $acl = $child->getACL(); - $this->assertAcl(self::UNIT_TEST_USER, '{DAV:}read', $acl); - $this->assertAcl(self::UNIT_TEST_USER, '{DAV:}write', $acl); - $this->assertAccess($userCanRead, self::UNIT_TEST_USER1, '{DAV:}read', $acl); - $this->assertAccess($userCanWrite, self::UNIT_TEST_USER1, '{DAV:}write', $acl); - $this->assertAccess($groupCanRead, self::UNIT_TEST_GROUP, '{DAV:}read', $acl); - $this->assertAccess($groupCanWrite, self::UNIT_TEST_GROUP, '{DAV:}write', $acl); - - // delete the address book - $this->backend->deleteCalendar($books[0]['id']); - $books = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER); - $this->assertEquals(0, count($books)); - } - - public function testCalendarObjectsOperations() { - - $calendarId = $this->createTestCalendar(); - - // create a card - $uri = $this->getUniqueID('calobj'); - $calData = <<<'EOD' -BEGIN:VCALENDAR -VERSION:2.0 -PRODID:ownCloud Calendar -BEGIN:VEVENT -CREATED;VALUE=DATE-TIME:20130910T125139Z -UID:47d15e3ec8 -LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z -DTSTAMP;VALUE=DATE-TIME:20130910T125139Z -SUMMARY:Test Event -DTSTART;VALUE=DATE-TIME:20130912T130000Z -DTEND;VALUE=DATE-TIME:20130912T140000Z -CLASS:PUBLIC -END:VEVENT -END:VCALENDAR -EOD; - - $this->backend->createCalendarObject($calendarId, $uri, $calData); - - // get all the cards - $calendarObjects = $this->backend->getCalendarObjects($calendarId); - $this->assertEquals(1, count($calendarObjects)); - $this->assertEquals($calendarId, $calendarObjects[0]['calendarid']); - - // get the cards - $calendarObject = $this->backend->getCalendarObject($calendarId, $uri); - $this->assertNotNull($calendarObject); - $this->assertArrayHasKey('id', $calendarObject); - $this->assertArrayHasKey('uri', $calendarObject); - $this->assertArrayHasKey('lastmodified', $calendarObject); - $this->assertArrayHasKey('etag', $calendarObject); - $this->assertArrayHasKey('size', $calendarObject); - $this->assertEquals($calData, $calendarObject['calendardata']); - - // update the card - $calData = <<<'EOD' -BEGIN:VCALENDAR -VERSION:2.0 -PRODID:ownCloud Calendar -BEGIN:VEVENT -CREATED;VALUE=DATE-TIME:20130910T125139Z -UID:47d15e3ec8 -LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z -DTSTAMP;VALUE=DATE-TIME:20130910T125139Z -SUMMARY:Test Event -DTSTART;VALUE=DATE-TIME:20130912T130000Z -DTEND;VALUE=DATE-TIME:20130912T140000Z -END:VEVENT -END:VCALENDAR -EOD; - $this->backend->updateCalendarObject($calendarId, $uri, $calData); - $calendarObject = $this->backend->getCalendarObject($calendarId, $uri); - $this->assertEquals($calData, $calendarObject['calendardata']); - - // delete the card - $this->backend->deleteCalendarObject($calendarId, $uri); - $calendarObjects = $this->backend->getCalendarObjects($calendarId); - $this->assertEquals(0, count($calendarObjects)); - } - - public function testMultiCalendarObjects() { - - $calendarId = $this->createTestCalendar(); - - // create an event - $calData = <<<'EOD' -BEGIN:VCALENDAR -VERSION:2.0 -PRODID:ownCloud Calendar -BEGIN:VEVENT -CREATED;VALUE=DATE-TIME:20130910T125139Z -UID:47d15e3ec8 -LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z -DTSTAMP;VALUE=DATE-TIME:20130910T125139Z -SUMMARY:Test Event -DTSTART;VALUE=DATE-TIME:20130912T130000Z -DTEND;VALUE=DATE-TIME:20130912T140000Z -CLASS:PUBLIC -END:VEVENT -END:VCALENDAR -EOD; - $uri0 = $this->getUniqueID('card'); - $this->backend->createCalendarObject($calendarId, $uri0, $calData); - $uri1 = $this->getUniqueID('card'); - $this->backend->createCalendarObject($calendarId, $uri1, $calData); - $uri2 = $this->getUniqueID('card'); - $this->backend->createCalendarObject($calendarId, $uri2, $calData); - - // get all the cards - $calendarObjects = $this->backend->getCalendarObjects($calendarId); - $this->assertEquals(3, count($calendarObjects)); - - // get the cards - $calendarObjects = $this->backend->getMultipleCalendarObjects($calendarId, [$uri1, $uri2]); - $this->assertEquals(2, count($calendarObjects)); - foreach($calendarObjects as $card) { - $this->assertArrayHasKey('id', $card); - $this->assertArrayHasKey('uri', $card); - $this->assertArrayHasKey('lastmodified', $card); - $this->assertArrayHasKey('etag', $card); - $this->assertArrayHasKey('size', $card); - $this->assertEquals($calData, $card['calendardata']); - } - - // delete the card - $this->backend->deleteCalendarObject($calendarId, $uri0); - $this->backend->deleteCalendarObject($calendarId, $uri1); - $this->backend->deleteCalendarObject($calendarId, $uri2); - $calendarObjects = $this->backend->getCalendarObjects($calendarId); - $this->assertEquals(0, count($calendarObjects)); - } - - /** - * @dataProvider providesCalendarQueryParameters - */ - public function testCalendarQuery($expectedEventsInResult, $propFilters, $compFilter) { - $calendarId = $this->createTestCalendar(); - $events = []; - $events[0] = $this->createEvent($calendarId, '20130912T130000Z', '20130912T140000Z'); - $events[1] = $this->createEvent($calendarId, '20130912T150000Z', '20130912T170000Z'); - $events[2] = $this->createEvent($calendarId, '20130912T173000Z', '20130912T220000Z'); - - $result = $this->backend->calendarQuery($calendarId, [ - 'name' => '', - 'prop-filters' => $propFilters, - 'comp-filters' => $compFilter - ]); - - $expectedEventsInResult = array_map(function($index) use($events) { - return $events[$index]; - }, $expectedEventsInResult); - $this->assertEquals($expectedEventsInResult, $result, '', 0.0, 10, true); - } - - public function testGetCalendarObjectByUID() { - $calendarId = $this->createTestCalendar(); - $this->createEvent($calendarId, '20130912T130000Z', '20130912T140000Z'); - - $co = $this->backend->getCalendarObjectByUID(self::UNIT_TEST_USER, '47d15e3ec8'); - $this->assertNotNull($co); - } - - public function providesCalendarQueryParameters() { - return [ - 'all' => [[0, 1, 2], [], []], - 'only-todos' => [[], ['name' => 'VTODO'], []], - 'only-events' => [[0, 1, 2], [], [['name' => 'VEVENT', 'is-not-defined' => false, 'comp-filters' => [], 'time-range' => ['start' => null, 'end' => null], 'prop-filters' => []]],], - 'start' => [[1, 2], [], [['name' => 'VEVENT', 'is-not-defined' => false, 'comp-filters' => [], 'time-range' => ['start' => new DateTime('2013-09-12 14:00:00', new DateTimeZone('UTC')), 'end' => null], 'prop-filters' => []]],], - 'end' => [[0], [], [['name' => 'VEVENT', 'is-not-defined' => false, 'comp-filters' => [], 'time-range' => ['start' => null, 'end' => new DateTime('2013-09-12 14:00:00', new DateTimeZone('UTC'))], 'prop-filters' => []]],], - ]; - } - - private function createTestCalendar() { - $this->backend->createCalendar(self::UNIT_TEST_USER, 'Example', [ - '{http://apple.com/ns/ical/}calendar-color' => '#1C4587FF' - ]); - $calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER); - $this->assertEquals(1, count($calendars)); - $this->assertEquals(self::UNIT_TEST_USER, $calendars[0]['principaluri']); - /** @var SupportedCalendarComponentSet $components */ - $components = $calendars[0]['{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set']; - $this->assertEquals(['VEVENT','VTODO'], $components->getValue()); - $color = $calendars[0]['{http://apple.com/ns/ical/}calendar-color']; - $this->assertEquals('#1C4587FF', $color); - $this->assertEquals('Example', $calendars[0]['uri']); - $this->assertEquals('Example', $calendars[0]['{DAV:}displayname']); - $calendarId = $calendars[0]['id']; - - return $calendarId; - } - - private function createEvent($calendarId, $start = '20130912T130000Z', $end = '20130912T140000Z') { - - $calData = <<<EOD -BEGIN:VCALENDAR -VERSION:2.0 -PRODID:ownCloud Calendar -BEGIN:VEVENT -CREATED;VALUE=DATE-TIME:20130910T125139Z -UID:47d15e3ec8 -LAST-MODIFIED;VALUE=DATE-TIME:20130910T125139Z -DTSTAMP;VALUE=DATE-TIME:20130910T125139Z -SUMMARY:Test Event -DTSTART;VALUE=DATE-TIME:$start -DTEND;VALUE=DATE-TIME:$end -CLASS:PUBLIC -END:VEVENT -END:VCALENDAR -EOD; - $uri0 = $this->getUniqueID('event'); - $this->backend->createCalendarObject($calendarId, $uri0, $calData); - - return $uri0; - } - - public function testSyncSupport() { - $calendarId = $this->createTestCalendar(); - - // fist call without synctoken - $changes = $this->backend->getChangesForCalendar($calendarId, '', 1); - $syncToken = $changes['syncToken']; - - // add a change - $event = $this->createEvent($calendarId, '20130912T130000Z', '20130912T140000Z'); - - // look for changes - $changes = $this->backend->getChangesForCalendar($calendarId, $syncToken, 1); - $this->assertEquals($event, $changes['added'][0]); - } - - public function testSubscriptions() { - $id = $this->backend->createSubscription(self::UNIT_TEST_USER, 'Subscription', [ - '{http://calendarserver.org/ns/}source' => new Href('test-source') - ]); - - $subscriptions = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER); - $this->assertEquals(1, count($subscriptions)); - $this->assertEquals($id, $subscriptions[0]['id']); - - $patch = new PropPatch([ - '{DAV:}displayname' => 'Unit test', - ]); - $this->backend->updateSubscription($id, $patch); - $patch->commit(); - - $subscriptions = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER); - $this->assertEquals(1, count($subscriptions)); - $this->assertEquals($id, $subscriptions[0]['id']); - $this->assertEquals('Unit test', $subscriptions[0]['{DAV:}displayname']); - - $this->backend->deleteSubscription($id); - $subscriptions = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER); - $this->assertEquals(0, count($subscriptions)); - } - - public function testScheduling() { - $this->backend->createSchedulingObject(self::UNIT_TEST_USER, 'Sample Schedule', ''); - - $sos = $this->backend->getSchedulingObjects(self::UNIT_TEST_USER); - $this->assertEquals(1, count($sos)); - - $so = $this->backend->getSchedulingObject(self::UNIT_TEST_USER, 'Sample Schedule'); - $this->assertNotNull($so); - - $this->backend->deleteSchedulingObject(self::UNIT_TEST_USER, 'Sample Schedule'); - - $sos = $this->backend->getSchedulingObjects(self::UNIT_TEST_USER); - $this->assertEquals(0, count($sos)); - } - - /** - * @dataProvider providesCalDataForGetDenormalizedData - */ - public function testGetDenormalizedData($expectedFirstOccurance, $calData) { - $actual = $this->invokePrivate($this->backend, 'getDenormalizedData', [$calData]); - $this->assertEquals($expectedFirstOccurance, $actual['firstOccurence']); - } - - public function providesCalDataForGetDenormalizedData() { - return [ - [0, "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:413F269B-B51B-46B1-AFB6-40055C53A4DC\r\nDTSTAMP:20160309T095056Z\r\nDTSTART;VALUE=DATE:16040222\r\nDTEND;VALUE=DATE:16040223\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:SUMMARY\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"], - [null, "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:413F269B-B51B-46B1-AFB6-40055C53A4DC\r\nDTSTAMP:20160309T095056Z\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:SUMMARY\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"] - ]; - } - - private function assertAcl($principal, $privilege, $acl) { - foreach($acl as $a) { - if ($a['principal'] === $principal && $a['privilege'] === $privilege) { - $this->assertTrue(true); - return; - } - } - $this->fail("ACL does not contain $principal / $privilege"); - } - - private function assertNotAcl($principal, $privilege, $acl) { - foreach($acl as $a) { - if ($a['principal'] === $principal && $a['privilege'] === $privilege) { - $this->fail("ACL contains $principal / $privilege"); - return; - } - } - $this->assertTrue(true); - } - - private function assertAccess($shouldHaveAcl, $principal, $privilege, $acl) { - if ($shouldHaveAcl) { - $this->assertAcl($principal, $privilege, $acl); - } else { - $this->assertNotAcl($principal, $privilege, $acl); - } - } -} diff --git a/apps/dav/tests/unit/caldav/calendartest.php b/apps/dav/tests/unit/caldav/calendartest.php deleted file mode 100644 index 9e0c3c6c7e4..00000000000 --- a/apps/dav/tests/unit/caldav/calendartest.php +++ /dev/null @@ -1,166 +0,0 @@ -<?php -/** - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\CalDAV; - -use OCA\DAV\CalDAV\CalDavBackend; -use OCA\DAV\CalDAV\Calendar; -use Sabre\DAV\PropPatch; -use Test\TestCase; - -class CalendarTest extends TestCase { - - public function testDelete() { - /** @var \PHPUnit_Framework_MockObject_MockObject | CalDavBackend $backend */ - $backend = $this->getMockBuilder('OCA\DAV\CalDAV\CalDavBackend')->disableOriginalConstructor()->getMock(); - $backend->expects($this->once())->method('updateShares'); - $backend->expects($this->any())->method('getShares')->willReturn([ - ['href' => 'principal:user2'] - ]); - $calendarInfo = [ - '{http://owncloud.org/ns}owner-principal' => 'user1', - 'principaluri' => 'user2', - 'id' => 666, - 'uri' => 'cal', - ]; - $c = new Calendar($backend, $calendarInfo); - $c->delete(); - } - - /** - * @expectedException \Sabre\DAV\Exception\Forbidden - */ - public function testDeleteFromGroup() { - /** @var \PHPUnit_Framework_MockObject_MockObject | CalDavBackend $backend */ - $backend = $this->getMockBuilder('OCA\DAV\CalDAV\CalDavBackend')->disableOriginalConstructor()->getMock(); - $backend->expects($this->never())->method('updateShares'); - $backend->expects($this->any())->method('getShares')->willReturn([ - ['href' => 'principal:group2'] - ]); - $calendarInfo = [ - '{http://owncloud.org/ns}owner-principal' => 'user1', - 'principaluri' => 'user2', - 'id' => 666, - 'uri' => 'cal', - ]; - $c = new Calendar($backend, $calendarInfo); - $c->delete(); - } - - public function dataPropPatch() { - return [ - [[], true], - [[ - '{http://owncloud.org/ns}calendar-enabled' => true, - ], false], - [[ - '{DAV:}displayname' => true, - ], true], - [[ - '{DAV:}displayname' => true, - '{http://owncloud.org/ns}calendar-enabled' => true, - ], true], - ]; - } - - /** - * @dataProvider dataPropPatch - */ - public function testPropPatch($mutations, $throws) { - /** @var \PHPUnit_Framework_MockObject_MockObject | CalDavBackend $backend */ - $backend = $this->getMockBuilder('OCA\DAV\CalDAV\CalDavBackend')->disableOriginalConstructor()->getMock(); - $calendarInfo = [ - '{http://owncloud.org/ns}owner-principal' => 'user1', - 'principaluri' => 'user2', - 'id' => 666, - 'uri' => 'default' - ]; - $c = new Calendar($backend, $calendarInfo); - - if ($throws) { - $this->setExpectedException('\Sabre\DAV\Exception\Forbidden'); - } - $c->propPatch(new PropPatch($mutations)); - if (!$throws) { - $this->assertTrue(true); - } - } - - /** - * @dataProvider providesReadOnlyInfo - */ - public function testAcl($expectsWrite, $readOnlyValue, $hasOwnerSet) { - /** @var \PHPUnit_Framework_MockObject_MockObject | CalDavBackend $backend */ - $backend = $this->getMockBuilder('OCA\DAV\CalDAV\CalDavBackend')->disableOriginalConstructor()->getMock(); - $backend->expects($this->any())->method('applyShareAcl')->willReturnArgument(1); - $calendarInfo = [ - 'principaluri' => 'user2', - 'id' => 666, - 'uri' => 'default' - ]; - if (!is_null($readOnlyValue)) { - $calendarInfo['{http://owncloud.org/ns}read-only'] = $readOnlyValue; - } - if ($hasOwnerSet) { - $calendarInfo['{http://owncloud.org/ns}owner-principal'] = 'user1'; - } - $c = new Calendar($backend, $calendarInfo); - $acl = $c->getACL(); - $childAcl = $c->getChildACL(); - - $expectedAcl = [[ - 'privilege' => '{DAV:}read', - 'principal' => $hasOwnerSet ? 'user1' : 'user2', - 'protected' => true - ], [ - 'privilege' => '{DAV:}write', - 'principal' => $hasOwnerSet ? 'user1' : 'user2', - 'protected' => true - ]]; - if ($hasOwnerSet) { - $expectedAcl[] = [ - 'privilege' => '{DAV:}read', - 'principal' => 'user2', - 'protected' => true - ]; - if ($expectsWrite) { - $expectedAcl[] = [ - 'privilege' => '{DAV:}write', - 'principal' => 'user2', - 'protected' => true - ]; - } - } - $this->assertEquals($expectedAcl, $acl); - $this->assertEquals($expectedAcl, $childAcl); - } - - public function providesReadOnlyInfo() { - return [ - 'read-only property not set' => [true, null, true], - 'read-only property is false' => [true, false, true], - 'read-only property is true' => [false, true, true], - 'read-only property not set and no owner' => [true, null, false], - 'read-only property is false and no owner' => [true, false, false], - 'read-only property is true and no owner' => [false, true, false], - ]; - } -} diff --git a/apps/dav/tests/unit/caldav/schedule/imipplugintest.php b/apps/dav/tests/unit/caldav/schedule/imipplugintest.php deleted file mode 100644 index fcbf4fde04c..00000000000 --- a/apps/dav/tests/unit/caldav/schedule/imipplugintest.php +++ /dev/null @@ -1,91 +0,0 @@ -<?php -/** - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\CalDAV\Schedule; - -use OC\Mail\Mailer; -use OCA\DAV\CalDAV\Schedule\IMipPlugin; -use OCP\ILogger; -use Sabre\VObject\Component\VCalendar; -use Sabre\VObject\ITip\Message; -use Test\TestCase; - -class IMipPluginTest extends TestCase { - - public function testDelivery() { - $mailMessage = new \OC\Mail\Message(new \Swift_Message()); - /** @var Mailer | \PHPUnit_Framework_MockObject_MockObject $mailer */ - $mailer = $this->getMockBuilder('OC\Mail\Mailer')->disableOriginalConstructor()->getMock(); - $mailer->method('createMessage')->willReturn($mailMessage); - $mailer->expects($this->once())->method('send'); - /** @var ILogger | \PHPUnit_Framework_MockObject_MockObject $logger */ - $logger = $this->getMockBuilder('OC\Log')->disableOriginalConstructor()->getMock(); - - $plugin = new IMipPlugin($mailer, $logger); - $message = new Message(); - $message->method = 'REQUEST'; - $message->message = new VCalendar(); - $message->message->add('VEVENT', [ - 'UID' => $message->uid, - 'SEQUENCE' => $message->sequence, - 'SUMMARY' => 'Fellowship meeting', - ]); - $message->sender = 'mailto:gandalf@wiz.ard'; - $message->recipient = 'mailto:frodo@hobb.it'; - - $plugin->schedule($message); - $this->assertEquals('1.1', $message->getScheduleStatus()); - $this->assertEquals('Fellowship meeting', $mailMessage->getSubject()); - $this->assertEquals(['frodo@hobb.it' => null], $mailMessage->getTo()); - $this->assertEquals(['gandalf@wiz.ard' => null], $mailMessage->getReplyTo()); - $this->assertEquals('text/calendar; charset=UTF-8; method=REQUEST', $mailMessage->getSwiftMessage()->getContentType()); - } - - public function testFailedDelivery() { - $mailMessage = new \OC\Mail\Message(new \Swift_Message()); - /** @var Mailer | \PHPUnit_Framework_MockObject_MockObject $mailer */ - $mailer = $this->getMockBuilder('OC\Mail\Mailer')->disableOriginalConstructor()->getMock(); - $mailer->method('createMessage')->willReturn($mailMessage); - $mailer->method('send')->willThrowException(new \Exception()); - /** @var ILogger | \PHPUnit_Framework_MockObject_MockObject $logger */ - $logger = $this->getMockBuilder('OC\Log')->disableOriginalConstructor()->getMock(); - - $plugin = new IMipPlugin($mailer, $logger); - $message = new Message(); - $message->method = 'REQUEST'; - $message->message = new VCalendar(); - $message->message->add('VEVENT', [ - 'UID' => $message->uid, - 'SEQUENCE' => $message->sequence, - 'SUMMARY' => 'Fellowship meeting', - ]); - $message->sender = 'mailto:gandalf@wiz.ard'; - $message->recipient = 'mailto:frodo@hobb.it'; - - $plugin->schedule($message); - $this->assertEquals('5.0', $message->getScheduleStatus()); - $this->assertEquals('Fellowship meeting', $mailMessage->getSubject()); - $this->assertEquals(['frodo@hobb.it' => null], $mailMessage->getTo()); - $this->assertEquals(['gandalf@wiz.ard' => null], $mailMessage->getReplyTo()); - $this->assertEquals('text/calendar; charset=UTF-8; method=REQUEST', $mailMessage->getSwiftMessage()->getContentType()); - } - -} diff --git a/apps/dav/tests/unit/carddav/addressbookimpltest.php b/apps/dav/tests/unit/carddav/addressbookimpltest.php deleted file mode 100644 index ba537a631be..00000000000 --- a/apps/dav/tests/unit/carddav/addressbookimpltest.php +++ /dev/null @@ -1,288 +0,0 @@ -<?php -/** - * @author Björn Schießle <schiessle@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - - -namespace OCA\DAV\Tests\Unit\CardDAV; - - -use OCA\DAV\CardDAV\AddressBook; -use OCA\DAV\CardDAV\AddressBookImpl; -use OCA\DAV\CardDAV\CardDavBackend; -use Sabre\VObject\Component\VCard; -use Sabre\VObject\Property\Text; -use Test\TestCase; - -class AddressBookImplTest extends TestCase { - - /** @var AddressBookImpl */ - private $addressBookImpl; - - /** @var array */ - private $addressBookInfo; - - /** @var AddressBook | \PHPUnit_Framework_MockObject_MockObject */ - private $addressBook; - - /** @var CardDavBackend | \PHPUnit_Framework_MockObject_MockObject */ - private $backend; - - /** @var VCard | \PHPUnit_Framework_MockObject_MockObject */ - private $vCard; - - public function setUp() { - parent::setUp(); - - $this->addressBookInfo = [ - 'id' => 42, - '{DAV:}displayname' => 'display name' - ]; - $this->addressBook = $this->getMockBuilder('OCA\DAV\CardDAV\AddressBook') - ->disableOriginalConstructor()->getMock(); - $this->backend = $this->getMockBuilder('\OCA\DAV\CardDAV\CardDavBackend') - ->disableOriginalConstructor()->getMock(); - $this->vCard = $this->getMock('Sabre\VObject\Component\VCard'); - - $this->addressBookImpl = new AddressBookImpl( - $this->addressBook, - $this->addressBookInfo, - $this->backend - ); - } - - public function testGetKey() { - $this->assertSame($this->addressBookInfo['id'], - $this->addressBookImpl->getKey()); - } - - public function testGetDisplayName() { - $this->assertSame($this->addressBookInfo['{DAV:}displayname'], - $this->addressBookImpl->getDisplayName()); - } - - public function testSearch() { - - /** @var \PHPUnit_Framework_MockObject_MockObject | AddressBookImpl $addressBookImpl */ - $addressBookImpl = $this->getMockBuilder('OCA\DAV\CardDAV\AddressBookImpl') - ->setConstructorArgs( - [ - $this->addressBook, - $this->addressBookInfo, - $this->backend - ] - ) - ->setMethods(['vCard2Array', 'readCard']) - ->getMock(); - - $pattern = 'pattern'; - $searchProperties = 'properties'; - - $this->backend->expects($this->once())->method('search') - ->with($this->addressBookInfo['id'], $pattern, $searchProperties) - ->willReturn( - [ - 'cardData1', - 'cardData2' - ] - ); - - $addressBookImpl->expects($this->exactly(2))->method('readCard') - ->willReturn($this->vCard); - $addressBookImpl->expects($this->exactly(2))->method('vCard2Array') - ->with($this->vCard)->willReturn('vCard'); - - $result = $addressBookImpl->search($pattern, $searchProperties, []); - $this->assertTrue((is_array($result))); - $this->assertSame(2, count($result)); - } - - /** - * @dataProvider dataTestCreate - * - * @param array $properties - */ - public function testCreate($properties) { - - $uid = 'uid'; - - /** @var \PHPUnit_Framework_MockObject_MockObject | AddressBookImpl $addressBookImpl */ - $addressBookImpl = $this->getMockBuilder('OCA\DAV\CardDAV\AddressBookImpl') - ->setConstructorArgs( - [ - $this->addressBook, - $this->addressBookInfo, - $this->backend - ] - ) - ->setMethods(['vCard2Array', 'createUid', 'createEmptyVCard']) - ->getMock(); - - $addressBookImpl->expects($this->once())->method('createUid') - ->willReturn($uid); - $addressBookImpl->expects($this->once())->method('createEmptyVCard') - ->with($uid)->willReturn($this->vCard); - $this->vCard->expects($this->exactly(count($properties))) - ->method('createProperty'); - $this->backend->expects($this->once())->method('createCard'); - $this->backend->expects($this->never())->method('updateCard'); - $this->backend->expects($this->never())->method('getCard'); - $addressBookImpl->expects($this->once())->method('vCard2Array') - ->with($this->vCard)->willReturn(true); - - $this->assertTrue($addressBookImpl->createOrUpdate($properties)); - } - - public function dataTestCreate() { - return [ - [[]], - [['FN' => 'John Doe']] - ]; - } - - public function testUpdate() { - - $uid = 'uid'; - $properties = ['UID' => $uid, 'FN' => 'John Doe']; - - /** @var \PHPUnit_Framework_MockObject_MockObject | AddressBookImpl $addressBookImpl */ - $addressBookImpl = $this->getMockBuilder('OCA\DAV\CardDAV\AddressBookImpl') - ->setConstructorArgs( - [ - $this->addressBook, - $this->addressBookInfo, - $this->backend - ] - ) - ->setMethods(['vCard2Array', 'createUid', 'createEmptyVCard', 'readCard']) - ->getMock(); - - $addressBookImpl->expects($this->never())->method('createUid'); - $addressBookImpl->expects($this->never())->method('createEmptyVCard'); - $this->backend->expects($this->once())->method('getCard') - ->with($this->addressBookInfo['id'], $uid . '.vcf') - ->willReturn(['carddata' => 'data']); - $addressBookImpl->expects($this->once())->method('readCard') - ->with('data')->willReturn($this->vCard); - $this->vCard->expects($this->exactly(count($properties))) - ->method('createProperty'); - $this->backend->expects($this->never())->method('createCard'); - $this->backend->expects($this->once())->method('updateCard'); - $addressBookImpl->expects($this->once())->method('vCard2Array') - ->with($this->vCard)->willReturn(true); - - $this->assertTrue($addressBookImpl->createOrUpdate($properties)); - } - - /** - * @dataProvider dataTestGetPermissions - * - * @param array $permissions - * @param int $expected - */ - public function testGetPermissions($permissions, $expected) { - $this->addressBook->expects($this->once())->method('getACL') - ->willReturn($permissions); - - $this->assertSame($expected, - $this->addressBookImpl->getPermissions() - ); - } - - public function dataTestGetPermissions() { - return [ - [[], 0], - [[['privilege' => '{DAV:}read']], 1], - [[['privilege' => '{DAV:}write']], 6], - [[['privilege' => '{DAV:}all']], 31], - [[['privilege' => '{DAV:}read'],['privilege' => '{DAV:}write']], 7], - [[['privilege' => '{DAV:}read'],['privilege' => '{DAV:}all']], 31], - [[['privilege' => '{DAV:}all'],['privilege' => '{DAV:}write']], 31], - [[['privilege' => '{DAV:}read'],['privilege' => '{DAV:}write'],['privilege' => '{DAV:}all']], 31], - [[['privilege' => '{DAV:}all'],['privilege' => '{DAV:}read'],['privilege' => '{DAV:}write']], 31], - ]; - } - - public function testDelete() { - $cardId = 1; - $cardUri = 'cardUri'; - $this->backend->expects($this->once())->method('getCardUri') - ->with($cardId)->willReturn($cardUri); - $this->backend->expects($this->once())->method('deleteCard') - ->with($this->addressBookInfo['id'], $cardUri) - ->willReturn(true); - - $this->assertTrue($this->addressBookImpl->delete($cardId)); - } - - public function testReadCard() { - $vCard = new VCard(); - $vCard->add(new Text($vCard, 'UID', 'uid')); - $vCardSerialized = $vCard->serialize(); - - $result = $this->invokePrivate($this->addressBookImpl, 'readCard', [$vCardSerialized]); - $resultSerialized = $result->serialize(); - - $this->assertSame($vCardSerialized, $resultSerialized); - } - - public function testCreateUid() { - /** @var \PHPUnit_Framework_MockObject_MockObject | AddressBookImpl $addressBookImpl */ - $addressBookImpl = $this->getMockBuilder('OCA\DAV\CardDAV\AddressBookImpl') - ->setConstructorArgs( - [ - $this->addressBook, - $this->addressBookInfo, - $this->backend - ] - ) - ->setMethods(['getUid']) - ->getMock(); - - $addressBookImpl->expects($this->at(0))->method('getUid')->willReturn('uid0'); - $addressBookImpl->expects($this->at(1))->method('getUid')->willReturn('uid1'); - - // simulate that 'uid0' already exists, so the second uid will be returned - $this->backend->expects($this->exactly(2))->method('getContact') - ->willReturnCallback( - function($id, $uid) { - return ($uid === 'uid0.vcf'); - } - ); - - $this->assertSame('uid1', - $this->invokePrivate($addressBookImpl, 'createUid', []) - ); - - } - - public function testCreateEmptyVCard() { - $uid = 'uid'; - $expectedVCard = new VCard(); - $expectedVCard->add(new Text($expectedVCard, 'UID', $uid)); - $expectedVCardSerialized = $expectedVCard->serialize(); - - $result = $this->invokePrivate($this->addressBookImpl, 'createEmptyVCard', [$uid]); - $resultSerialized = $result->serialize(); - - $this->assertSame($expectedVCardSerialized, $resultSerialized); - } - -} diff --git a/apps/dav/tests/unit/carddav/addressbooktest.php b/apps/dav/tests/unit/carddav/addressbooktest.php deleted file mode 100644 index c5cf7e5f7ba..00000000000 --- a/apps/dav/tests/unit/carddav/addressbooktest.php +++ /dev/null @@ -1,139 +0,0 @@ -<?php -/** - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\CardDAV; - -use OCA\DAV\CardDAV\AddressBook; -use OCA\DAV\CardDAV\CardDavBackend; -use Sabre\DAV\PropPatch; -use Test\TestCase; - -class AddressBookTest extends TestCase { - - public function testDelete() { - /** @var \PHPUnit_Framework_MockObject_MockObject | CardDavBackend $backend */ - $backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend')->disableOriginalConstructor()->getMock(); - $backend->expects($this->once())->method('updateShares'); - $backend->expects($this->any())->method('getShares')->willReturn([ - ['href' => 'principal:user2'] - ]); - $calendarInfo = [ - '{http://owncloud.org/ns}owner-principal' => 'user1', - 'principaluri' => 'user2', - 'id' => 666 - ]; - $c = new AddressBook($backend, $calendarInfo); - $c->delete(); - } - - /** - * @expectedException \Sabre\DAV\Exception\Forbidden - */ - public function testDeleteFromGroup() { - /** @var \PHPUnit_Framework_MockObject_MockObject | CardDavBackend $backend */ - $backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend')->disableOriginalConstructor()->getMock(); - $backend->expects($this->never())->method('updateShares'); - $backend->expects($this->any())->method('getShares')->willReturn([ - ['href' => 'principal:group2'] - ]); - $calendarInfo = [ - '{http://owncloud.org/ns}owner-principal' => 'user1', - 'principaluri' => 'user2', - 'id' => 666 - ]; - $c = new AddressBook($backend, $calendarInfo); - $c->delete(); - } - - /** - * @expectedException \Sabre\DAV\Exception\Forbidden - */ - public function testPropPatch() { - /** @var \PHPUnit_Framework_MockObject_MockObject | CardDavBackend $backend */ - $backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend')->disableOriginalConstructor()->getMock(); - $calendarInfo = [ - '{http://owncloud.org/ns}owner-principal' => 'user1', - 'principaluri' => 'user2', - 'id' => 666 - ]; - $c = new AddressBook($backend, $calendarInfo); - $c->propPatch(new PropPatch([])); - } - - /** - * @dataProvider providesReadOnlyInfo - */ - public function testAcl($expectsWrite, $readOnlyValue, $hasOwnerSet) { - /** @var \PHPUnit_Framework_MockObject_MockObject | CardDavBackend $backend */ - $backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend')->disableOriginalConstructor()->getMock(); - $backend->expects($this->any())->method('applyShareAcl')->willReturnArgument(1); - $calendarInfo = [ - 'principaluri' => 'user2', - 'id' => 666, - 'uri' => 'default' - ]; - if (!is_null($readOnlyValue)) { - $calendarInfo['{http://owncloud.org/ns}read-only'] = $readOnlyValue; - } - if ($hasOwnerSet) { - $calendarInfo['{http://owncloud.org/ns}owner-principal'] = 'user1'; - } - $c = new AddressBook($backend, $calendarInfo); - $acl = $c->getACL(); - $childAcl = $c->getChildACL(); - - $expectedAcl = [[ - 'privilege' => '{DAV:}read', - 'principal' => $hasOwnerSet ? 'user1' : 'user2', - 'protected' => true - ], [ - 'privilege' => '{DAV:}write', - 'principal' => $hasOwnerSet ? 'user1' : 'user2', - 'protected' => true - ]]; - if ($hasOwnerSet) { - $expectedAcl[] = [ - 'privilege' => '{DAV:}read', - 'principal' => 'user2', - 'protected' => true - ]; - if ($expectsWrite) { - $expectedAcl[] = [ - 'privilege' => '{DAV:}write', - 'principal' => 'user2', - 'protected' => true - ]; - } - } - $this->assertEquals($expectedAcl, $acl); - $this->assertEquals($expectedAcl, $childAcl); - } - - public function providesReadOnlyInfo() { - return [ - 'read-only property not set' => [true, null, true], - 'read-only property is false' => [true, false, true], - 'read-only property is true' => [false, true, true], - 'read-only property not set and no owner' => [true, null, false], - 'read-only property is false and no owner' => [true, false, false], - 'read-only property is true and no owner' => [false, true, false], - ]; - }} diff --git a/apps/dav/tests/unit/carddav/birthdayservicetest.php b/apps/dav/tests/unit/carddav/birthdayservicetest.php deleted file mode 100644 index 2efb3c09aea..00000000000 --- a/apps/dav/tests/unit/carddav/birthdayservicetest.php +++ /dev/null @@ -1,171 +0,0 @@ -<?php -/** - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\CardDAV; - -use OCA\DAV\CalDAV\BirthdayService; -use OCA\DAV\CalDAV\CalDavBackend; -use OCA\DAV\CardDAV\CardDavBackend; -use Sabre\VObject\Component\VCalendar; -use Sabre\VObject\Reader; -use Test\TestCase; - -class BirthdayServiceTest extends TestCase { - - /** @var BirthdayService */ - private $service; - /** @var CalDavBackend | \PHPUnit_Framework_MockObject_MockObject */ - private $calDav; - /** @var CardDavBackend | \PHPUnit_Framework_MockObject_MockObject */ - private $cardDav; - - public function setUp() { - parent::setUp(); - - $this->calDav = $this->getMockBuilder('OCA\DAV\CalDAV\CalDavBackend')->disableOriginalConstructor()->getMock(); - $this->cardDav = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend')->disableOriginalConstructor()->getMock(); - - $this->service = new BirthdayService($this->calDav, $this->cardDav); - } - - /** - * @dataProvider providesVCards - * @param boolean $nullExpected - * @param string | null $data - */ - public function testBuildBirthdayFromContact($nullExpected, $data) { - $cal = $this->service->buildBirthdayFromContact($data); - if ($nullExpected) { - $this->assertNull($cal); - } else { - $this->assertInstanceOf('Sabre\VObject\Component\VCalendar', $cal); - $this->assertTrue(isset($cal->VEVENT)); - $this->assertEquals('FREQ=YEARLY', $cal->VEVENT->RRULE->getValue()); - $this->assertEquals('12345 (*1900)', $cal->VEVENT->SUMMARY->getValue()); - $this->assertEquals('TRANSPARENT', $cal->VEVENT->TRANSP->getValue()); - } - } - - public function testOnCardDeleted() { - $this->cardDav->expects($this->once())->method('getAddressBookById') - ->with(666) - ->willReturn([ - 'principaluri' => 'principals/users/user01', - 'uri' => 'default' - ]); - $this->calDav->expects($this->once())->method('getCalendarByUri') - ->with('principals/users/user01', 'contact_birthdays') - ->willReturn([ - 'id' => 1234 - ]); - $this->calDav->expects($this->once())->method('deleteCalendarObject')->with(1234, 'default-gump.vcf.ics'); - - $this->service->onCardDeleted(666, 'gump.vcf'); - } - - /** - * @dataProvider providesCardChanges - */ - public function testOnCardChanged($expectedOp) { - $this->cardDav->expects($this->once())->method('getAddressBookById') - ->with(666) - ->willReturn([ - 'principaluri' => 'principals/users/user01', - 'uri' => 'default' - ]); - $this->calDav->expects($this->once())->method('getCalendarByUri') - ->with('principals/users/user01', 'contact_birthdays') - ->willReturn([ - 'id' => 1234 - ]); - - /** @var BirthdayService | \PHPUnit_Framework_MockObject_MockObject $service */ - $service = $this->getMock('\OCA\DAV\CalDAV\BirthdayService', - ['buildBirthdayFromContact', 'birthdayEvenChanged'], [$this->calDav, $this->cardDav]); - - if ($expectedOp === 'delete') { - $this->calDav->expects($this->once())->method('getCalendarObject')->willReturn(''); - $service->expects($this->once())->method('buildBirthdayFromContact')->willReturn(null); - $this->calDav->expects($this->once())->method('deleteCalendarObject')->with(1234, 'default-gump.vcf.ics'); - } - if ($expectedOp === 'create') { - $service->expects($this->once())->method('buildBirthdayFromContact')->willReturn(new VCalendar()); - $this->calDav->expects($this->once())->method('createCalendarObject')->with(1234, 'default-gump.vcf.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nEND:VCALENDAR\r\n"); - } - if ($expectedOp === 'update') { - $service->expects($this->once())->method('buildBirthdayFromContact')->willReturn(new VCalendar()); - $service->expects($this->once())->method('birthdayEvenChanged')->willReturn(true); - $this->calDav->expects($this->once())->method('getCalendarObject')->willReturn([ - 'calendardata' => '']); - $this->calDav->expects($this->once())->method('updateCalendarObject')->with(1234, 'default-gump.vcf.ics', "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nEND:VCALENDAR\r\n"); - } - - $service->onCardChanged(666, 'gump.vcf', ''); - } - - /** - * @dataProvider providesBirthday - * @param $expected - * @param $old - * @param $new - */ - public function testBirthdayEvenChanged($expected, $old, $new) { - $new = Reader::read($new); - $this->assertEquals($expected, $this->service->birthdayEvenChanged($old, $new)); - } - - public function providesBirthday() { - return [ - [true, - '', - "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"], - [false, - "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", - "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"], - [true, - "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:4567's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", - "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"], - [true, - "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", - "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000102\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"] - ]; - } - - public function providesCardChanges(){ - return[ - ['delete'], - ['create'], - ['update'] - ]; - } - - public function providesVCards() { - return [ - [true, null], - [true, ''], - [true, 'yasfewf'], - [true, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nEND:VCARD\r\n", "Dr. Foo Bar"], - [true, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:\r\nEND:VCARD\r\n", "Dr. Foo Bar"], - [true, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:someday\r\nEND:VCARD\r\n", "Dr. Foo Bar"], - [false, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:1900-01-01\r\nEND:VCARD\r\n", "Dr. Foo Bar"], - ]; - } -} diff --git a/apps/dav/tests/unit/carddav/carddavbackendtest.php b/apps/dav/tests/unit/carddav/carddavbackendtest.php deleted file mode 100644 index 1ee09260c88..00000000000 --- a/apps/dav/tests/unit/carddav/carddavbackendtest.php +++ /dev/null @@ -1,631 +0,0 @@ -<?php -/** - * @author Arthur Schiwon <blizzz@owncloud.com> - * @author Björn Schießle <schiessle@owncloud.com> - * @author Joas Schilling <nickvergessen@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace OCA\DAV\Tests\Unit\CardDAV; - -use InvalidArgumentException; -use OCA\DAV\CardDAV\AddressBook; -use OCA\DAV\CardDAV\CardDavBackend; -use OCA\DAV\Connector\Sabre\Principal; -use OCP\DB\QueryBuilder\IQueryBuilder; -use OCP\IDBConnection; -use Sabre\DAV\PropPatch; -use Sabre\VObject\Component\VCard; -use Sabre\VObject\Property\Text; -use Test\TestCase; - -/** - * Class CardDavBackendTest - * - * @group DB - * - * @package OCA\DAV\Tests\Unit\CardDAV - */ -class CardDavBackendTest extends TestCase { - - /** @var CardDavBackend */ - private $backend; - - /** @var Principal | \PHPUnit_Framework_MockObject_MockObject */ - private $principal; - - /** @var IDBConnection */ - private $db; - - /** @var string */ - private $dbCardsTable = 'cards'; - - /** @var string */ - private $dbCardsPropertiesTable = 'cards_properties'; - - const UNIT_TEST_USER = 'principals/users/carddav-unit-test'; - const UNIT_TEST_USER1 = 'principals/users/carddav-unit-test1'; - const UNIT_TEST_GROUP = 'principals/groups/carddav-unit-test-group'; - - public function setUp() { - parent::setUp(); - - $this->principal = $this->getMockBuilder('OCA\DAV\Connector\Sabre\Principal') - ->disableOriginalConstructor() - ->setMethods(['getPrincipalByPath', 'getGroupMembership']) - ->getMock(); - $this->principal->method('getPrincipalByPath') - ->willReturn([ - 'uri' => 'principals/best-friend' - ]); - $this->principal->method('getGroupMembership') - ->withAnyParameters() - ->willReturn([self::UNIT_TEST_GROUP]); - - $this->db = \OC::$server->getDatabaseConnection(); - - $this->backend = new CardDavBackend($this->db, $this->principal, null); - - // start every test with a empty cards_properties and cards table - $query = $this->db->getQueryBuilder(); - $query->delete('cards_properties')->execute(); - $query = $this->db->getQueryBuilder(); - $query->delete('cards')->execute(); - - - $this->tearDown(); - } - - public function tearDown() { - parent::tearDown(); - - if (is_null($this->backend)) { - return; - } - $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); - foreach ($books as $book) { - $this->backend->deleteAddressBook($book['id']); - } - } - - public function testAddressBookOperations() { - - // create a new address book - $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); - - $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); - $this->assertEquals(1, count($books)); - $this->assertEquals('Example', $books[0]['{DAV:}displayname']); - - // update it's display name - $patch = new PropPatch([ - '{DAV:}displayname' => 'Unit test', - '{urn:ietf:params:xml:ns:carddav}addressbook-description' => 'Addressbook used for unit testing' - ]); - $this->backend->updateAddressBook($books[0]['id'], $patch); - $patch->commit(); - $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); - $this->assertEquals(1, count($books)); - $this->assertEquals('Unit test', $books[0]['{DAV:}displayname']); - $this->assertEquals('Addressbook used for unit testing', $books[0]['{urn:ietf:params:xml:ns:carddav}addressbook-description']); - - // delete the address book - $this->backend->deleteAddressBook($books[0]['id']); - $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); - $this->assertEquals(0, count($books)); - } - - public function testAddressBookSharing() { - - $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); - $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); - $this->assertEquals(1, count($books)); - $addressBook = new AddressBook($this->backend, $books[0]); - $this->backend->updateShares($addressBook, [ - [ - 'href' => 'principal:' . self::UNIT_TEST_USER1, - ], - [ - 'href' => 'principal:' . self::UNIT_TEST_GROUP, - ] - ], []); - $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER1); - $this->assertEquals(1, count($books)); - - // delete the address book - $this->backend->deleteAddressBook($books[0]['id']); - $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); - $this->assertEquals(0, count($books)); - } - - public function testCardOperations() { - - /** @var CardDavBackend | \PHPUnit_Framework_MockObject_MockObject $backend */ - $backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend') - ->setConstructorArgs([$this->db, $this->principal, null]) - ->setMethods(['updateProperties', 'purgeProperties'])->getMock(); - - // create a new address book - $backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); - $books = $backend->getAddressBooksForUser(self::UNIT_TEST_USER); - $this->assertEquals(1, count($books)); - $bookId = $books[0]['id']; - - $uri = $this->getUniqueID('card'); - // updateProperties is expected twice, once for createCard and once for updateCard - $backend->expects($this->at(0))->method('updateProperties')->with($bookId, $uri, ''); - $backend->expects($this->at(1))->method('updateProperties')->with($bookId, $uri, '***'); - // create a card - $backend->createCard($bookId, $uri, ''); - - // get all the cards - $cards = $backend->getCards($bookId); - $this->assertEquals(1, count($cards)); - $this->assertEquals('', $cards[0]['carddata']); - - // get the cards - $card = $backend->getCard($bookId, $uri); - $this->assertNotNull($card); - $this->assertArrayHasKey('id', $card); - $this->assertArrayHasKey('uri', $card); - $this->assertArrayHasKey('lastmodified', $card); - $this->assertArrayHasKey('etag', $card); - $this->assertArrayHasKey('size', $card); - $this->assertEquals('', $card['carddata']); - - // update the card - $backend->updateCard($bookId, $uri, '***'); - $card = $backend->getCard($bookId, $uri); - $this->assertEquals('***', $card['carddata']); - - // delete the card - $backend->expects($this->once())->method('purgeProperties')->with($bookId, $card['id']); - $backend->deleteCard($bookId, $uri); - $cards = $backend->getCards($bookId); - $this->assertEquals(0, count($cards)); - } - - public function testMultiCard() { - - $this->backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend') - ->setConstructorArgs([$this->db, $this->principal, null]) - ->setMethods(['updateProperties'])->getMock(); - - // create a new address book - $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); - $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); - $this->assertEquals(1, count($books)); - $bookId = $books[0]['id']; - - // create a card - $uri0 = $this->getUniqueID('card'); - $this->backend->createCard($bookId, $uri0, ''); - $uri1 = $this->getUniqueID('card'); - $this->backend->createCard($bookId, $uri1, ''); - $uri2 = $this->getUniqueID('card'); - $this->backend->createCard($bookId, $uri2, ''); - - // get all the cards - $cards = $this->backend->getCards($bookId); - $this->assertEquals(3, count($cards)); - $this->assertEquals('', $cards[0]['carddata']); - $this->assertEquals('', $cards[1]['carddata']); - $this->assertEquals('', $cards[2]['carddata']); - - // get the cards - $cards = $this->backend->getMultipleCards($bookId, [$uri1, $uri2]); - $this->assertEquals(2, count($cards)); - foreach($cards as $card) { - $this->assertArrayHasKey('id', $card); - $this->assertArrayHasKey('uri', $card); - $this->assertArrayHasKey('lastmodified', $card); - $this->assertArrayHasKey('etag', $card); - $this->assertArrayHasKey('size', $card); - $this->assertEquals('', $card['carddata']); - } - - // delete the card - $this->backend->deleteCard($bookId, $uri0); - $this->backend->deleteCard($bookId, $uri1); - $this->backend->deleteCard($bookId, $uri2); - $cards = $this->backend->getCards($bookId); - $this->assertEquals(0, count($cards)); - } - - public function testDeleteWithoutCard() { - - $this->backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend') - ->setConstructorArgs([$this->db, $this->principal, null]) - ->setMethods([ - 'getCardId', - 'addChange', - 'purgeProperties', - 'updateProperties', - ]) - ->getMock(); - - // create a new address book - $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); - $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); - $this->assertEquals(1, count($books)); - - $bookId = $books[0]['id']; - $uri = $this->getUniqueID('card'); - - // create a new address book - $this->backend->expects($this->once()) - ->method('getCardId') - ->with($bookId, $uri) - ->willThrowException(new \InvalidArgumentException()); - $this->backend->expects($this->exactly(2)) - ->method('addChange') - ->withConsecutive( - [$bookId, $uri, 1], - [$bookId, $uri, 3] - ); - $this->backend->expects($this->never()) - ->method('purgeProperties'); - - // create a card - $this->backend->createCard($bookId, $uri, ''); - - // delete the card - $this->assertTrue($this->backend->deleteCard($bookId, $uri)); - } - - public function testSyncSupport() { - - $this->backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend') - ->setConstructorArgs([$this->db, $this->principal, null]) - ->setMethods(['updateProperties'])->getMock(); - - // create a new address book - $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); - $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); - $this->assertEquals(1, count($books)); - $bookId = $books[0]['id']; - - // fist call without synctoken - $changes = $this->backend->getChangesForAddressBook($bookId, '', 1); - $syncToken = $changes['syncToken']; - - // add a change - $uri0 = $this->getUniqueID('card'); - $this->backend->createCard($bookId, $uri0, ''); - - // look for changes - $changes = $this->backend->getChangesForAddressBook($bookId, $syncToken, 1); - $this->assertEquals($uri0, $changes['added'][0]); - } - - public function testSharing() { - $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); - $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); - $this->assertEquals(1, count($books)); - - $exampleBook = new AddressBook($this->backend, $books[0]); - $this->backend->updateShares($exampleBook, [['href' => 'principal:principals/best-friend']], []); - - $shares = $this->backend->getShares($exampleBook->getResourceId()); - $this->assertEquals(1, count($shares)); - - // adding the same sharee again has no effect - $this->backend->updateShares($exampleBook, [['href' => 'principal:principals/best-friend']], []); - - $shares = $this->backend->getShares($exampleBook->getResourceId()); - $this->assertEquals(1, count($shares)); - - $books = $this->backend->getAddressBooksForUser('principals/best-friend'); - $this->assertEquals(1, count($books)); - - $this->backend->updateShares($exampleBook, [], ['principal:principals/best-friend']); - - $shares = $this->backend->getShares($exampleBook->getResourceId()); - $this->assertEquals(0, count($shares)); - - $books = $this->backend->getAddressBooksForUser('principals/best-friend'); - $this->assertEquals(0, count($books)); - } - - public function testUpdateProperties() { - - $bookId = 42; - $cardUri = 'card-uri'; - $cardId = 2; - - $backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend') - ->setConstructorArgs([$this->db, $this->principal, null]) - ->setMethods(['getCardId'])->getMock(); - - $backend->expects($this->any())->method('getCardId')->willReturn($cardId); - - // add properties for new vCard - $vCard = new VCard(); - $vCard->add(new Text($vCard, 'UID', $cardUri)); - $vCard->add(new Text($vCard, 'FN', 'John Doe')); - $this->invokePrivate($backend, 'updateProperties', [$bookId, $cardUri, $vCard->serialize()]); - - $query = $this->db->getQueryBuilder(); - $result = $query->select('*')->from('cards_properties')->execute()->fetchAll(); - - $this->assertSame(2, count($result)); - - $this->assertSame('UID', $result[0]['name']); - $this->assertSame($cardUri, $result[0]['value']); - $this->assertSame($bookId, (int)$result[0]['addressbookid']); - $this->assertSame($cardId, (int)$result[0]['cardid']); - - $this->assertSame('FN', $result[1]['name']); - $this->assertSame('John Doe', $result[1]['value']); - $this->assertSame($bookId, (int)$result[1]['addressbookid']); - $this->assertSame($cardId, (int)$result[1]['cardid']); - - // update properties for existing vCard - $vCard = new VCard(); - $vCard->add(new Text($vCard, 'FN', 'John Doe')); - $this->invokePrivate($backend, 'updateProperties', [$bookId, $cardUri, $vCard->serialize()]); - - $query = $this->db->getQueryBuilder(); - $result = $query->select('*')->from('cards_properties')->execute()->fetchAll(); - - $this->assertSame(1, count($result)); - - $this->assertSame('FN', $result[0]['name']); - $this->assertSame('John Doe', $result[0]['value']); - $this->assertSame($bookId, (int)$result[0]['addressbookid']); - $this->assertSame($cardId, (int)$result[0]['cardid']); - } - - public function testPurgeProperties() { - - $query = $this->db->getQueryBuilder(); - $query->insert('cards_properties') - ->values( - [ - 'addressbookid' => $query->createNamedParameter(1), - 'cardid' => $query->createNamedParameter(1), - 'name' => $query->createNamedParameter('name1'), - 'value' => $query->createNamedParameter('value1'), - 'preferred' => $query->createNamedParameter(0) - ] - ); - $query->execute(); - - $query = $this->db->getQueryBuilder(); - $query->insert('cards_properties') - ->values( - [ - 'addressbookid' => $query->createNamedParameter(1), - 'cardid' => $query->createNamedParameter(2), - 'name' => $query->createNamedParameter('name2'), - 'value' => $query->createNamedParameter('value2'), - 'preferred' => $query->createNamedParameter(0) - ] - ); - $query->execute(); - - $this->invokePrivate($this->backend, 'purgeProperties', [1, 1]); - - $query = $this->db->getQueryBuilder(); - $result = $query->select('*')->from('cards_properties')->execute()->fetchAll(); - $this->assertSame(1, count($result)); - $this->assertSame(1 ,(int)$result[0]['addressbookid']); - $this->assertSame(2 ,(int)$result[0]['cardid']); - - } - - public function testGetCardId() { - $query = $this->db->getQueryBuilder(); - - $query->insert('cards') - ->values( - [ - 'addressbookid' => $query->createNamedParameter(1), - 'carddata' => $query->createNamedParameter(''), - 'uri' => $query->createNamedParameter('uri'), - 'lastmodified' => $query->createNamedParameter(4738743), - 'etag' => $query->createNamedParameter('etag'), - 'size' => $query->createNamedParameter(120) - ] - ); - $query->execute(); - $id = $query->getLastInsertId(); - - $this->assertSame($id, - $this->invokePrivate($this->backend, 'getCardId', [1, 'uri'])); - } - - /** - * @expectedException InvalidArgumentException - */ - public function testGetCardIdFailed() { - $this->invokePrivate($this->backend, 'getCardId', [1, 'uri']); - } - - /** - * @dataProvider dataTestSearch - * - * @param string $pattern - * @param array $properties - * @param array $expected - */ - public function testSearch($pattern, $properties, $expected) { - /** @var VCard $vCards */ - $vCards = []; - $vCards[0] = new VCard(); - $vCards[0]->add(new Text($vCards[0], 'UID', 'uid')); - $vCards[0]->add(new Text($vCards[0], 'FN', 'John Doe')); - $vCards[0]->add(new Text($vCards[0], 'CLOUD', 'john@owncloud.org')); - $vCards[1] = new VCard(); - $vCards[1]->add(new Text($vCards[1], 'UID', 'uid')); - $vCards[1]->add(new Text($vCards[1], 'FN', 'John M. Doe')); - - $vCardIds = []; - $query = $this->db->getQueryBuilder(); - for($i=0; $i<2; $i++) { - $query->insert($this->dbCardsTable) - ->values( - [ - 'addressbookid' => $query->createNamedParameter(0), - 'carddata' => $query->createNamedParameter($vCards[$i]->serialize(), IQueryBuilder::PARAM_LOB), - 'uri' => $query->createNamedParameter('uri' . $i), - 'lastmodified' => $query->createNamedParameter(time()), - 'etag' => $query->createNamedParameter('etag' . $i), - 'size' => $query->createNamedParameter(120), - ] - ); - $query->execute(); - $vCardIds[] = $query->getLastInsertId(); - } - - $query->insert($this->dbCardsPropertiesTable) - ->values( - [ - 'addressbookid' => $query->createNamedParameter(0), - 'cardid' => $query->createNamedParameter($vCardIds[0]), - 'name' => $query->createNamedParameter('FN'), - 'value' => $query->createNamedParameter('John Doe'), - 'preferred' => $query->createNamedParameter(0) - ] - ); - $query->execute(); - $query->insert($this->dbCardsPropertiesTable) - ->values( - [ - 'addressbookid' => $query->createNamedParameter(0), - 'cardid' => $query->createNamedParameter($vCardIds[0]), - 'name' => $query->createNamedParameter('CLOUD'), - 'value' => $query->createNamedParameter('John@owncloud.org'), - 'preferred' => $query->createNamedParameter(0) - ] - ); - $query->execute(); - $query->insert($this->dbCardsPropertiesTable) - ->values( - [ - 'addressbookid' => $query->createNamedParameter(0), - 'cardid' => $query->createNamedParameter($vCardIds[1]), - 'name' => $query->createNamedParameter('FN'), - 'value' => $query->createNamedParameter('John M. Doe'), - 'preferred' => $query->createNamedParameter(0) - ] - ); - $query->execute(); - - $result = $this->backend->search(0, $pattern, $properties); - - // check result - $this->assertSame(count($expected), count($result)); - $found = []; - foreach ($result as $r) { - foreach ($expected as $exp) { - if (strpos($r, $exp) > 0) { - $found[$exp] = true; - break; - } - } - } - - $this->assertSame(count($expected), count($found)); - } - - public function dataTestSearch() { - return [ - ['John', ['FN'], ['John Doe', 'John M. Doe']], - ['M. Doe', ['FN'], ['John M. Doe']], - ['Do', ['FN'], ['John Doe', 'John M. Doe']], - 'check if duplicates are handled correctly' => ['John', ['FN', 'CLOUD'], ['John Doe', 'John M. Doe']], - 'case insensitive' => ['john', ['FN'], ['John Doe', 'John M. Doe']] - ]; - } - - public function testGetCardUri() { - $query = $this->db->getQueryBuilder(); - $query->insert($this->dbCardsTable) - ->values( - [ - 'addressbookid' => $query->createNamedParameter(1), - 'carddata' => $query->createNamedParameter('carddata', IQueryBuilder::PARAM_LOB), - 'uri' => $query->createNamedParameter('uri'), - 'lastmodified' => $query->createNamedParameter(5489543), - 'etag' => $query->createNamedParameter('etag'), - 'size' => $query->createNamedParameter(120), - ] - ); - $query->execute(); - - $id = $query->getLastInsertId(); - - $this->assertSame('uri', $this->backend->getCardUri($id)); - } - - /** - * @expectedException InvalidArgumentException - */ - public function testGetCardUriFailed() { - $this->backend->getCardUri(1); - } - - public function testGetContact() { - $query = $this->db->getQueryBuilder(); - for($i=0; $i<2; $i++) { - $query->insert($this->dbCardsTable) - ->values( - [ - 'addressbookid' => $query->createNamedParameter($i), - 'carddata' => $query->createNamedParameter('carddata' . $i, IQueryBuilder::PARAM_LOB), - 'uri' => $query->createNamedParameter('uri' . $i), - 'lastmodified' => $query->createNamedParameter(5489543), - 'etag' => $query->createNamedParameter('etag' . $i), - 'size' => $query->createNamedParameter(120), - ] - ); - $query->execute(); - } - - $result = $this->backend->getContact(0, 'uri0'); - $this->assertSame(7, count($result)); - $this->assertSame(0, (int)$result['addressbookid']); - $this->assertSame('uri0', $result['uri']); - $this->assertSame(5489543, (int)$result['lastmodified']); - $this->assertSame('etag0', $result['etag']); - $this->assertSame(120, (int)$result['size']); - } - - public function testGetContactFail() { - $this->assertEmpty($this->backend->getContact(0, 'uri')); - } - - public function testCollectCardProperties() { - $query = $this->db->getQueryBuilder(); - $query->insert($this->dbCardsPropertiesTable) - ->values( - [ - 'addressbookid' => $query->createNamedParameter(666), - 'cardid' => $query->createNamedParameter(777), - 'name' => $query->createNamedParameter('FN'), - 'value' => $query->createNamedParameter('John Doe'), - 'preferred' => $query->createNamedParameter(0) - ] - ) - ->execute(); - - $result = $this->backend->collectCardProperties(666, 'FN'); - $this->assertEquals(['John Doe'], $result); - } -} diff --git a/apps/dav/tests/unit/carddav/contactsmanagertest.php b/apps/dav/tests/unit/carddav/contactsmanagertest.php deleted file mode 100644 index 5a384550df5..00000000000 --- a/apps/dav/tests/unit/carddav/contactsmanagertest.php +++ /dev/null @@ -1,43 +0,0 @@ -<?php -/** - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\CardDAV; - -use OCA\DAV\CardDAV\CardDavBackend; -use OCA\DAV\CardDAV\ContactsManager; -use OCP\Contacts\IManager; -use Test\TestCase; - -class ContactsManagerTest extends TestCase { - public function test() { - /** @var IManager | \PHPUnit_Framework_MockObject_MockObject $cm */ - $cm = $this->getMockBuilder('OCP\Contacts\IManager')->disableOriginalConstructor()->getMock(); - $cm->expects($this->exactly(2))->method('registerAddressBook'); - /** @var CardDavBackend | \PHPUnit_Framework_MockObject_MockObject $backEnd */ - $backEnd = $this->getMockBuilder('OCA\DAV\CardDAV\CardDavBackend')->disableOriginalConstructor()->getMock(); - $backEnd->method('getAddressBooksForUser')->willReturn([ - [] - ]); - - $app = new ContactsManager($backEnd); - $app->setupContactsProvider($cm, 'user01'); - } -} diff --git a/apps/dav/tests/unit/carddav/convertertest.php b/apps/dav/tests/unit/carddav/convertertest.php deleted file mode 100644 index ba71b75686a..00000000000 --- a/apps/dav/tests/unit/carddav/convertertest.php +++ /dev/null @@ -1,137 +0,0 @@ -<?php -/** - * @author Roeland Jago Douma <rullzer@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit; - -use OCA\DAV\CardDAV\Converter; -use Test\TestCase; - -class ConverterTests extends TestCase { - - /** - * @dataProvider providesNewUsers - */ - public function testCreation($expectedVCard, $displayName = null, $eMailAddress = null, $cloudId = null) { - $user = $this->getUserMock($displayName, $eMailAddress, $cloudId); - - $converter = new Converter(); - $vCard = $converter->createCardFromUser($user); - $cardData = $vCard->serialize(); - - $this->assertEquals($expectedVCard, $cardData); - } - - public function providesNewUsers() { - return [ - ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nPHOTO;ENCODING=b;TYPE=JPEG:MTIzNDU2Nzg5\r\nEND:VCARD\r\n"], - ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:Dr. Foo Bar\r\nN:Bar;Dr.;Foo;;\r\nPHOTO;ENCODING=b;TYPE=JPEG:MTIzNDU2Nzg5\r\nEND:VCARD\r\n", "Dr. Foo Bar"], - ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:Dr. Foo Bar\r\nN:Bar;Dr.;Foo;;\r\nEMAIL;TYPE=OTHER:foo@bar.net\r\nPHOTO;ENCODING=b;TYPE=JPEG:MTIzNDU2Nzg5\r\nEND:VCARD\r\n", "Dr. Foo Bar", "foo@bar.net"], - ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:Dr. Foo Bar\r\nN:Bar;Dr.;Foo;;\r\nCLOUD:foo@bar.net\r\nPHOTO;ENCODING=b;TYPE=JPEG:MTIzNDU2Nzg5\r\nEND:VCARD\r\n", "Dr. Foo Bar", null, "foo@bar.net"], - ]; - } - - /** - * @dataProvider providesNewUsers - */ - public function testUpdateOfUnchangedUser($expectedVCard, $displayName = null, $eMailAddress = null, $cloudId = null) { - $user = $this->getUserMock($displayName, $eMailAddress, $cloudId); - - $converter = new Converter(); - $vCard = $converter->createCardFromUser($user); - $updated = $converter->updateCard($vCard, $user); - $this->assertFalse($updated); - $cardData = $vCard->serialize(); - - $this->assertEquals($expectedVCard, $cardData); - } - - /** - * @dataProvider providesUsersForUpdateOfRemovedElement - */ - public function testUpdateOfRemovedElement($expectedVCard, $displayName = null, $eMailAddress = null, $cloudId = null) { - $user = $this->getUserMock($displayName, $eMailAddress, $cloudId); - - $converter = new Converter(); - $vCard = $converter->createCardFromUser($user); - - $user1 = $this->getMockBuilder('OCP\IUser')->disableOriginalConstructor()->getMock(); - $user1->method('getUID')->willReturn('12345'); - $user1->method('getDisplayName')->willReturn(null); - $user1->method('getEMailAddress')->willReturn(null); - $user1->method('getCloudId')->willReturn(null); - $user1->method('getAvatarImage')->willReturn(null); - - $updated = $converter->updateCard($vCard, $user1); - $this->assertTrue($updated); - $cardData = $vCard->serialize(); - - $this->assertEquals($expectedVCard, $cardData); - } - - public function providesUsersForUpdateOfRemovedElement() { - return [ - ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nEND:VCARD\r\n", "Dr. Foo Bar"], - ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nEND:VCARD\r\n", "Dr. Foo Bar", "foo@bar.net"], - ["BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.5.0//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nEND:VCARD\r\n", "Dr. Foo Bar", null, "foo@bar.net"], - ]; - } - - /** - * @dataProvider providesNames - * @param $expected - * @param $fullName - */ - public function testNameSplitter($expected, $fullName) { - - $converter = new Converter(); - $r = $converter->splitFullName($fullName); - $r = implode(';', $r); - $this->assertEquals($expected, $r); - } - - public function providesNames() { - return [ - ['Sauron;;;;', 'Sauron'], - ['Baggins;Bilbo;;;', 'Bilbo Baggins'], - ['Tolkien;John;Ronald Reuel;;', 'John Ronald Reuel Tolkien'], - ]; - } - - /** - * @param $displayName - * @param $eMailAddress - * @param $cloudId - * @return \PHPUnit_Framework_MockObject_MockObject - */ - protected function getUserMock($displayName, $eMailAddress, $cloudId) { - $image0 = $this->getMockBuilder('OCP\IImage')->disableOriginalConstructor()->getMock(); - $image0->method('mimeType')->willReturn('JPEG'); - $image0->method('data')->willReturn('123456789'); - $user = $this->getMockBuilder('OCP\IUser')->disableOriginalConstructor()->getMock(); - $user->method('getUID')->willReturn('12345'); - $user->method('getDisplayName')->willReturn($displayName); - $user->method('getEMailAddress')->willReturn($eMailAddress); - $user->method('getCloudId')->willReturn($cloudId); - $user->method('getAvatarImage')->willReturn($image0); - return $user; - } -} diff --git a/apps/dav/tests/unit/carddav/sharing/plugintest.php b/apps/dav/tests/unit/carddav/sharing/plugintest.php deleted file mode 100644 index f7159c2d22d..00000000000 --- a/apps/dav/tests/unit/carddav/sharing/plugintest.php +++ /dev/null @@ -1,81 +0,0 @@ -<?php -/** - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\CardDAV; - - -use OCA\DAV\DAV\Sharing\IShareable; -use OCA\DAV\DAV\Sharing\Plugin; -use OCA\DAV\Connector\Sabre\Auth; -use OCP\IRequest; -use Sabre\DAV\Server; -use Sabre\DAV\SimpleCollection; -use Sabre\HTTP\Request; -use Sabre\HTTP\Response; -use Test\TestCase; - -class PluginTest extends TestCase { - - /** @var Plugin */ - private $plugin; - /** @var Server */ - private $server; - /** @var IShareable | \PHPUnit_Framework_MockObject_MockObject */ - private $book; - - public function setUp() { - parent::setUp(); - - /** @var Auth | \PHPUnit_Framework_MockObject_MockObject $authBackend */ - $authBackend = $this->getMockBuilder('OCA\DAV\Connector\Sabre\Auth')->disableOriginalConstructor()->getMock(); - $authBackend->method('isDavAuthenticated')->willReturn(true); - - /** @var IRequest $request */ - $request = $this->getMockBuilder('OCP\IRequest')->disableOriginalConstructor()->getMock(); - $this->plugin = new Plugin($authBackend, $request); - - $root = new SimpleCollection('root'); - $this->server = new \Sabre\DAV\Server($root); - /** @var SimpleCollection $node */ - $this->book = $this->getMockBuilder('OCA\DAV\DAV\Sharing\IShareable')->disableOriginalConstructor()->getMock(); - $this->book->method('getName')->willReturn('addressbook1.vcf'); - $root->addChild($this->book); - $this->plugin->initialize($this->server); - } - - public function testSharing() { - - $this->book->expects($this->once())->method('updateShares')->with([[ - 'href' => 'principal:principals/admin', - 'commonName' => null, - 'summary' => null, - 'readOnly' => false - ]], ['mailto:wilfredo@example.com']); - - // setup request - $request = new Request(); - $request->addHeader('Content-Type', 'application/xml'); - $request->setUrl('addressbook1.vcf'); - $request->setBody('<?xml version="1.0" encoding="utf-8" ?><CS:share xmlns:D="DAV:" xmlns:CS="http://owncloud.org/ns"><CS:set><D:href>principal:principals/admin</D:href><CS:read-write/></CS:set> <CS:remove><D:href>mailto:wilfredo@example.com</D:href></CS:remove></CS:share>'); - $response = new Response(); - $this->plugin->httpPost($request, $response); - } -} diff --git a/apps/dav/tests/unit/carddav/syncservicetest.php b/apps/dav/tests/unit/carddav/syncservicetest.php deleted file mode 100644 index e3ffaf472ed..00000000000 --- a/apps/dav/tests/unit/carddav/syncservicetest.php +++ /dev/null @@ -1,142 +0,0 @@ -<?php -/** - * @author Björn Schießle <schiessle@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace OCA\DAV\CardDAV; - -use OCP\IUser; -use OCP\IUserManager; -use Test\TestCase; - -class SyncServiceTest extends TestCase { - public function testEmptySync() { - $backend = $this->getBackendMock(0, 0, 0); - - $ss = $this->getSyncServiceMock($backend, []); - $return = $ss->syncRemoteAddressBook('', 'system', '1234567890', null, '1', 'principals/system/system', []); - $this->assertEquals('sync-token-1', $return); - } - - public function testSyncWithNewElement() { - $backend = $this->getBackendMock(1, 0, 0); - $backend->method('getCard')->willReturn(false); - - $ss = $this->getSyncServiceMock($backend, ['0' => [200 => '']]); - $return = $ss->syncRemoteAddressBook('', 'system', '1234567890', null, '1', 'principals/system/system', []); - $this->assertEquals('sync-token-1', $return); - } - - public function testSyncWithUpdatedElement() { - $backend = $this->getBackendMock(0, 1, 0); - $backend->method('getCard')->willReturn(true); - - $ss = $this->getSyncServiceMock($backend, ['0' => [200 => '']]); - $return = $ss->syncRemoteAddressBook('', 'system', '1234567890', null, '1', 'principals/system/system', []); - $this->assertEquals('sync-token-1', $return); - } - - public function testSyncWithDeletedElement() { - $backend = $this->getBackendMock(0, 0, 1); - - $ss = $this->getSyncServiceMock($backend, ['0' => [404 => '']]); - $return = $ss->syncRemoteAddressBook('', 'system', '1234567890', null, '1', 'principals/system/system', []); - $this->assertEquals('sync-token-1', $return); - } - - public function testEnsureSystemAddressBookExists() { - /** @var CardDavBackend | \PHPUnit_Framework_MockObject_MockObject $backend */ - $backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDAVBackend')->disableOriginalConstructor()->getMock(); - $backend->expects($this->exactly(1))->method('createAddressBook'); - $backend->expects($this->at(0))->method('getAddressBooksByUri')->willReturn(null); - $backend->expects($this->at(1))->method('getAddressBooksByUri')->willReturn([]); - - /** @var IUserManager $userManager */ - $userManager = $this->getMockBuilder('OCP\IUserManager')->disableOriginalConstructor()->getMock(); - $logger = $this->getMockBuilder('OCP\ILogger')->disableOriginalConstructor()->getMock(); - $ss = new SyncService($backend, $userManager, $logger); - $book = $ss->ensureSystemAddressBookExists('principals/users/adam', 'contacts', []); - } - - public function testUpdateAndDeleteUser() { - /** @var CardDavBackend | \PHPUnit_Framework_MockObject_MockObject $backend */ - $backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDAVBackend')->disableOriginalConstructor()->getMock(); - $logger = $this->getMockBuilder('OCP\ILogger')->disableOriginalConstructor()->getMock(); - - $backend->expects($this->once())->method('createCard'); - $backend->expects($this->once())->method('updateCard'); - $backend->expects($this->once())->method('deleteCard'); - - $backend->method('getCard')->willReturnOnConsecutiveCalls(false, [ - 'carddata' => "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.4.8//EN\r\nUID:test-user\r\nFN:test-user\r\nN:test-user;;;;\r\nEND:VCARD\r\n\r\n" - ]); - - /** @var IUserManager | \PHPUnit_Framework_MockObject_MockObject $userManager */ - $userManager = $this->getMockBuilder('OCP\IUserManager')->disableOriginalConstructor()->getMock(); - - /** @var IUser | \PHPUnit_Framework_MockObject_MockObject $user */ - $user = $this->getMockBuilder('OCP\IUser')->disableOriginalConstructor()->getMock(); - $user->method('getBackendClassName')->willReturn('unittest'); - $user->method('getUID')->willReturn('test-user'); - - $ss = new SyncService($backend, $userManager, $logger); - $ss->updateUser($user); - - $user->method('getDisplayName')->willReturn('A test user for unit testing'); - - $ss->updateUser($user); - - $ss->deleteUser($user); - } - - /** - * @param int $createCount - * @param int $updateCount - * @param int $deleteCount - * @return \PHPUnit_Framework_MockObject_MockObject - */ - private function getBackendMock($createCount, $updateCount, $deleteCount) { - $backend = $this->getMockBuilder('OCA\DAV\CardDAV\CardDAVBackend')->disableOriginalConstructor()->getMock(); - $backend->expects($this->exactly($createCount))->method('createCard'); - $backend->expects($this->exactly($updateCount))->method('updateCard'); - $backend->expects($this->exactly($deleteCount))->method('deleteCard'); - return $backend; - } - - /** - * @param $backend - * @param $response - * @return SyncService|\PHPUnit_Framework_MockObject_MockObject - */ - private function getSyncServiceMock($backend, $response) { - $userManager = $this->getMockBuilder('OCP\IUserManager')->disableOriginalConstructor()->getMock(); - $logger = $this->getMockBuilder('OCP\ILogger')->disableOriginalConstructor()->getMock(); - /** @var SyncService | \PHPUnit_Framework_MockObject_MockObject $ss */ - $ss = $this->getMock('OCA\DAV\CardDAV\SyncService', ['ensureSystemAddressBookExists', 'requestSyncReport', 'download'], [$backend, $userManager, $logger]); - $ss->method('requestSyncReport')->withAnyParameters()->willReturn(['response' => $response, 'token' => 'sync-token-1']); - $ss->method('ensureSystemAddressBookExists')->willReturn(['id' => 1]); - $ss->method('download')->willReturn([ - 'body' => '', - 'statusCode' => 200, - 'headers' => [] - ]); - return $ss; - } - -} diff --git a/apps/dav/tests/unit/comments/entitycollection.php b/apps/dav/tests/unit/comments/entitycollection.php deleted file mode 100644 index 5bf155f12ba..00000000000 --- a/apps/dav/tests/unit/comments/entitycollection.php +++ /dev/null @@ -1,116 +0,0 @@ -<?php -/** - * @author Arthur Schiwon <blizzz@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\Comments; - -class EntityCollection extends \Test\TestCase { - - protected $commentsManager; - protected $folder; - protected $userManager; - protected $logger; - protected $collection; - protected $userSession; - - public function setUp() { - parent::setUp(); - - $this->commentsManager = $this->getMock('\OCP\Comments\ICommentsManager'); - $this->folder = $this->getMock('\OCP\Files\Folder'); - $this->userManager = $this->getMock('\OCP\IUserManager'); - $this->userSession = $this->getMock('\OCP\IUserSession'); - $this->logger = $this->getMock('\OCP\ILogger'); - - $this->collection = new \OCA\DAV\Comments\EntityCollection( - '19', - 'files', - $this->commentsManager, - $this->folder, - $this->userManager, - $this->userSession, - $this->logger - ); - } - - public function testGetId() { - $this->assertSame($this->collection->getId(), '19'); - } - - public function testGetChild() { - $this->commentsManager->expects($this->once()) - ->method('get') - ->with('55') - ->will($this->returnValue($this->getMock('\OCP\Comments\IComment'))); - - $node = $this->collection->getChild('55'); - $this->assertTrue($node instanceof \OCA\DAV\Comments\CommentNode); - } - - /** - * @expectedException \Sabre\DAV\Exception\NotFound - */ - public function testGetChildException() { - $this->commentsManager->expects($this->once()) - ->method('get') - ->with('55') - ->will($this->throwException(new \OCP\Comments\NotFoundException())); - - $this->collection->getChild('55'); - } - - public function testGetChildren() { - $this->commentsManager->expects($this->once()) - ->method('getForObject') - ->with('files', '19') - ->will($this->returnValue([$this->getMock('\OCP\Comments\IComment')])); - - $result = $this->collection->getChildren(); - - $this->assertSame(count($result), 1); - $this->assertTrue($result[0] instanceof \OCA\DAV\Comments\CommentNode); - } - - public function testFindChildren() { - $dt = new \DateTime('2016-01-10 18:48:00'); - $this->commentsManager->expects($this->once()) - ->method('getForObject') - ->with('files', '19', 5, 15, $dt) - ->will($this->returnValue([$this->getMock('\OCP\Comments\IComment')])); - - $result = $this->collection->findChildren(5, 15, $dt); - - $this->assertSame(count($result), 1); - $this->assertTrue($result[0] instanceof \OCA\DAV\Comments\CommentNode); - } - - public function testChildExistsTrue() { - $this->assertTrue($this->collection->childExists('44')); - } - - public function testChildExistsFalse() { - $this->commentsManager->expects($this->once()) - ->method('get') - ->with('44') - ->will($this->throwException(new \OCP\Comments\NotFoundException())); - - $this->assertFalse($this->collection->childExists('44')); - } -} diff --git a/apps/dav/tests/unit/comments/entitytypecollection.php b/apps/dav/tests/unit/comments/entitytypecollection.php deleted file mode 100644 index f3aa2dbd71f..00000000000 --- a/apps/dav/tests/unit/comments/entitytypecollection.php +++ /dev/null @@ -1,97 +0,0 @@ -<?php -/** - * @author Arthur Schiwon <blizzz@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\Comments; - -use OCA\DAV\Comments\EntityCollection as EntityCollectionImplemantation; - -class EntityTypeCollection extends \Test\TestCase { - - protected $commentsManager; - protected $folder; - protected $userManager; - protected $logger; - protected $collection; - protected $userSession; - - public function setUp() { - parent::setUp(); - - $this->commentsManager = $this->getMock('\OCP\Comments\ICommentsManager'); - $this->folder = $this->getMock('\OCP\Files\Folder'); - $this->userManager = $this->getMock('\OCP\IUserManager'); - $this->userSession = $this->getMock('\OCP\IUserSession'); - $this->logger = $this->getMock('\OCP\ILogger'); - - $this->collection = new \OCA\DAV\Comments\EntityTypeCollection( - 'files', - $this->commentsManager, - $this->folder, - $this->userManager, - $this->userSession, - $this->logger - ); - } - - public function testChildExistsYes() { - $this->folder->expects($this->once()) - ->method('getById') - ->with('17') - ->will($this->returnValue([$this->getMock('\OCP\Files\Node')])); - $this->assertTrue($this->collection->childExists('17')); - } - - public function testChildExistsNo() { - $this->folder->expects($this->once()) - ->method('getById') - ->will($this->returnValue([])); - $this->assertFalse($this->collection->childExists('17')); - } - - public function testGetChild() { - $this->folder->expects($this->once()) - ->method('getById') - ->with('17') - ->will($this->returnValue([$this->getMock('\OCP\Files\Node')])); - - $ec = $this->collection->getChild('17'); - $this->assertTrue($ec instanceof EntityCollectionImplemantation); - } - - /** - * @expectedException \Sabre\DAV\Exception\NotFound - */ - public function testGetChildException() { - $this->folder->expects($this->once()) - ->method('getById') - ->with('17') - ->will($this->returnValue([])); - - $this->collection->getChild('17'); - } - - /** - * @expectedException \Sabre\DAV\Exception\MethodNotAllowed - */ - public function testGetChildren() { - $this->collection->getChildren(); - } -} diff --git a/apps/dav/tests/unit/comments/rootcollection.php b/apps/dav/tests/unit/comments/rootcollection.php deleted file mode 100644 index 369006e7159..00000000000 --- a/apps/dav/tests/unit/comments/rootcollection.php +++ /dev/null @@ -1,160 +0,0 @@ -<?php -/** - * @author Arthur Schiwon <blizzz@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\Comments; - -use OCA\DAV\Comments\EntityTypeCollection as EntityTypeCollectionImplementation; - -class RootCollection extends \Test\TestCase { - - protected $commentsManager; - protected $userManager; - protected $logger; - protected $collection; - protected $userSession; - protected $rootFolder; - protected $user; - - public function setUp() { - parent::setUp(); - - $this->user = $this->getMock('\OCP\IUser'); - - $this->commentsManager = $this->getMock('\OCP\Comments\ICommentsManager'); - $this->userManager = $this->getMock('\OCP\IUserManager'); - $this->userSession = $this->getMock('\OCP\IUserSession'); - $this->rootFolder = $this->getMock('\OCP\Files\IRootFolder'); - $this->logger = $this->getMock('\OCP\ILogger'); - - $this->collection = new \OCA\DAV\Comments\RootCollection( - $this->commentsManager, - $this->userManager, - $this->userSession, - $this->rootFolder, - $this->logger - ); - } - - protected function prepareForInitCollections() { - $this->user->expects($this->any()) - ->method('getUID') - ->will($this->returnValue('alice')); - - $this->userSession->expects($this->once()) - ->method('getUser') - ->will($this->returnValue($this->user)); - - $this->rootFolder->expects($this->once()) - ->method('getUserFolder') - ->with('alice') - ->will($this->returnValue($this->getMock('\OCP\Files\Folder'))); - } - - /** - * @expectedException \Sabre\DAV\Exception\Forbidden - */ - public function testCreateFile() { - $this->collection->createFile('foo'); - } - - /** - * @expectedException \Sabre\DAV\Exception\Forbidden - */ - public function testCreateDirectory() { - $this->collection->createDirectory('foo'); - } - - public function testGetChild() { - $this->prepareForInitCollections(); - $etc = $this->collection->getChild('files'); - $this->assertTrue($etc instanceof EntityTypeCollectionImplementation); - } - - /** - * @expectedException \Sabre\DAV\Exception\NotFound - */ - public function testGetChildInvalid() { - $this->prepareForInitCollections(); - $this->collection->getChild('robots'); - } - - /** - * @expectedException \Sabre\DAV\Exception\NotAuthenticated - */ - public function testGetChildNoAuth() { - $this->collection->getChild('files'); - } - - public function testGetChildren() { - $this->prepareForInitCollections(); - $children = $this->collection->getChildren(); - $this->assertFalse(empty($children)); - foreach($children as $child) { - $this->assertTrue($child instanceof EntityTypeCollectionImplementation); - } - } - - /** - * @expectedException \Sabre\DAV\Exception\NotAuthenticated - */ - public function testGetChildrenNoAuth() { - $this->collection->getChildren(); - } - - public function testChildExistsYes() { - $this->prepareForInitCollections(); - $this->assertTrue($this->collection->childExists('files')); - } - - public function testChildExistsNo() { - $this->prepareForInitCollections(); - $this->assertFalse($this->collection->childExists('robots')); - } - - /** - * @expectedException \Sabre\DAV\Exception\NotAuthenticated - */ - public function testChildExistsNoAuth() { - $this->collection->childExists('files'); - } - - /** - * @expectedException \Sabre\DAV\Exception\Forbidden - */ - public function testDelete() { - $this->collection->delete(); - } - - public function testGetName() { - $this->assertSame('comments', $this->collection->getName()); - } - - /** - * @expectedException \Sabre\DAV\Exception\Forbidden - */ - public function testSetName() { - $this->collection->setName('foobar'); - } - - public function testGetLastModified() { - $this->assertSame(null, $this->collection->getLastModified()); - } -} diff --git a/apps/dav/tests/unit/connector/sabre/BlockLegacyClientPluginTest.php b/apps/dav/tests/unit/connector/sabre/BlockLegacyClientPluginTest.php deleted file mode 100644 index d02064531ab..00000000000 --- a/apps/dav/tests/unit/connector/sabre/BlockLegacyClientPluginTest.php +++ /dev/null @@ -1,130 +0,0 @@ -<?php -/** - * @author Lukas Reschke <lukas@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\Connector\Sabre; - -use OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin; -use Test\TestCase; -use OCP\IConfig; - -/** - * Class BlockLegacyClientPluginTest - * - * @package Test\Connector\Sabre - */ -class BlockLegacyClientPluginTest extends TestCase { - /** @var IConfig */ - private $config; - /** @var BlockLegacyClientPlugin */ - private $blockLegacyClientVersionPlugin; - - public function setUp() { - parent::setUp(); - - $this->config = $this->getMock('\OCP\IConfig'); - $this->blockLegacyClientVersionPlugin = new BlockLegacyClientPlugin($this->config); - } - - /** - * @return array - */ - public function oldDesktopClientProvider() { - return [ - ['Mozilla/5.0 (1.5.0) mirall/1.5.0'], - ['mirall/1.5.0'], - ['mirall/1.5.4'], - ['mirall/1.6.0'], - ['Mozilla/5.0 (Bogus Text) mirall/1.6.9'], - ]; - } - - /** - * @dataProvider oldDesktopClientProvider - * @param string $userAgent - * @expectedException \Sabre\DAV\Exception\Forbidden - * @expectedExceptionMessage Unsupported client version. - */ - public function testBeforeHandlerException($userAgent) { - /** @var \Sabre\HTTP\RequestInterface $request */ - $request = $this->getMock('\Sabre\HTTP\RequestInterface'); - $request - ->expects($this->once()) - ->method('getHeader') - ->with('User-Agent') - ->will($this->returnValue($userAgent)); - - $this->config - ->expects($this->once()) - ->method('getSystemValue') - ->with('minimum.supported.desktop.version', '1.7.0') - ->will($this->returnValue('1.7.0')); - - $this->blockLegacyClientVersionPlugin->beforeHandler($request); - } - - /** - * @return array - */ - public function newAndAlternateDesktopClientProvider() { - return [ - ['Mozilla/5.0 (1.7.0) mirall/1.7.0'], - ['mirall/1.8.3'], - ['mirall/1.7.2'], - ['mirall/1.7.0'], - ['Mozilla/5.0 (Bogus Text) mirall/1.9.3'], - ]; - } - - /** - * @dataProvider newAndAlternateDesktopClientProvider - * @param string $userAgent - */ - public function testBeforeHandlerSuccess($userAgent) { - /** @var \Sabre\HTTP\RequestInterface $request */ - $request = $this->getMock('\Sabre\HTTP\RequestInterface'); - $request - ->expects($this->once()) - ->method('getHeader') - ->with('User-Agent') - ->will($this->returnValue($userAgent)); - - $this->config - ->expects($this->once()) - ->method('getSystemValue') - ->with('minimum.supported.desktop.version', '1.7.0') - ->will($this->returnValue('1.7.0')); - - $this->blockLegacyClientVersionPlugin->beforeHandler($request); - } - - public function testBeforeHandlerNoUserAgent() { - /** @var \Sabre\HTTP\RequestInterface $request */ - $request = $this->getMock('\Sabre\HTTP\RequestInterface'); - $request - ->expects($this->once()) - ->method('getHeader') - ->with('User-Agent') - ->will($this->returnValue(null)); - $this->blockLegacyClientVersionPlugin->beforeHandler($request); - } - -} diff --git a/apps/dav/tests/unit/connector/sabre/DummyGetResponsePluginTest.php b/apps/dav/tests/unit/connector/sabre/DummyGetResponsePluginTest.php deleted file mode 100644 index 0ead617f461..00000000000 --- a/apps/dav/tests/unit/connector/sabre/DummyGetResponsePluginTest.php +++ /dev/null @@ -1,70 +0,0 @@ -<?php -/** - * @author Lukas Reschke <lukas@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\Connector\Sabre; - -use OCA\DAV\Connector\Sabre\DummyGetResponsePlugin; -use Test\TestCase; - -/** - * Class DummyGetResponsePluginTest - * - * @package Test\Connector\Sabre - */ -class DummyGetResponsePluginTest extends TestCase { - /** @var DummyGetResponsePlugin */ - private $dummyGetResponsePlugin; - - public function setUp() { - parent::setUp(); - - $this->dummyGetResponsePlugin = new DummyGetResponsePlugin(); - } - - public function testInitialize() { - /** @var \Sabre\DAV\Server $server */ - $server = $this->getMock('\Sabre\DAV\Server'); - $server - ->expects($this->once()) - ->method('on') - ->with('method:GET', [$this->dummyGetResponsePlugin, 'httpGet'], 200); - - $this->dummyGetResponsePlugin->initialize($server); - } - - - public function testHttpGet() { - /** @var \Sabre\HTTP\RequestInterface $request */ - $request = $this->getMock('\Sabre\HTTP\RequestInterface'); - /** @var \Sabre\HTTP\ResponseInterface $response */ - $response = $server = $this->getMock('\Sabre\HTTP\ResponseInterface'); - $response - ->expects($this->once()) - ->method('setBody'); - $response - ->expects($this->once()) - ->method('setStatus') - ->with(200); - - $this->assertSame(false, $this->dummyGetResponsePlugin->httpGet($request, $response)); - } -} diff --git a/apps/dav/tests/unit/connector/sabre/FakeLockerPluginTest.php b/apps/dav/tests/unit/connector/sabre/FakeLockerPluginTest.php deleted file mode 100644 index 30d2bf41810..00000000000 --- a/apps/dav/tests/unit/connector/sabre/FakeLockerPluginTest.php +++ /dev/null @@ -1,174 +0,0 @@ -<?php -/** - * @author Lukas Reschke <lukas@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace OCA\DAV\Tests\Unit\Connector\Sabre; - -use OCA\DAV\Connector\Sabre\FakeLockerPlugin; -use Sabre\HTTP\Response; -use Test\TestCase; - -/** - * Class FakeLockerPluginTest - * - * @package OCA\DAV\Tests\Unit\Connector\Sabre - */ -class FakeLockerPluginTest extends TestCase { - /** @var FakeLockerPlugin */ - private $fakeLockerPlugin; - - public function setUp() { - parent::setUp(); - $this->fakeLockerPlugin = new FakeLockerPlugin(); - } - - public function testInitialize() { - /** @var \Sabre\DAV\Server $server */ - $server = $this->getMock('\Sabre\DAV\Server'); - $server - ->expects($this->at(0)) - ->method('on') - ->with('method:LOCK', [$this->fakeLockerPlugin, 'fakeLockProvider'], 1); - $server - ->expects($this->at(1)) - ->method('on') - ->with('method:UNLOCK', [$this->fakeLockerPlugin, 'fakeUnlockProvider'], 1); - $server - ->expects($this->at(2)) - ->method('on') - ->with('propFind', [$this->fakeLockerPlugin, 'propFind']); - $server - ->expects($this->at(3)) - ->method('on') - ->with('validateTokens', [$this->fakeLockerPlugin, 'validateTokens']); - - $this->fakeLockerPlugin->initialize($server); - } - - public function testGetHTTPMethods() { - $expected = [ - 'LOCK', - 'UNLOCK', - ]; - $this->assertSame($expected, $this->fakeLockerPlugin->getHTTPMethods('Test')); - } - - public function testGetFeatures() { - $expected = [ - 2, - ]; - $this->assertSame($expected, $this->fakeLockerPlugin->getFeatures()); - } - - public function testPropFind() { - $propFind = $this->getMockBuilder('\Sabre\DAV\PropFind') - ->disableOriginalConstructor() - ->getMock(); - $node = $this->getMock('\Sabre\DAV\INode'); - - $propFind->expects($this->at(0)) - ->method('handle') - ->with('{DAV:}supportedlock'); - $propFind->expects($this->at(1)) - ->method('handle') - ->with('{DAV:}lockdiscovery'); - - $this->fakeLockerPlugin->propFind($propFind, $node); - } - - public function tokenDataProvider() { - return [ - [ - [ - [ - 'tokens' => [ - [ - 'token' => 'aToken', - 'validToken' => false, - ], - [], - [ - 'token' => 'opaquelocktoken:asdf', - 'validToken' => false, - ] - ], - ] - ], - [ - [ - 'tokens' => [ - [ - 'token' => 'aToken', - 'validToken' => false, - ], - [], - [ - 'token' => 'opaquelocktoken:asdf', - 'validToken' => true, - ] - ], - ] - ], - ] - ]; - } - - /** - * @dataProvider tokenDataProvider - * @param array $input - * @param array $expected - */ - public function testValidateTokens(array $input, array $expected) { - $request = $this->getMock('\Sabre\HTTP\RequestInterface'); - $this->fakeLockerPlugin->validateTokens($request, $input); - $this->assertSame($expected, $input); - } - - public function testFakeLockProvider() { - $request = $this->getMock('\Sabre\HTTP\RequestInterface'); - $response = new Response(); - $server = $this->getMock('\Sabre\DAV\Server'); - $this->fakeLockerPlugin->initialize($server); - - $request->expects($this->exactly(2)) - ->method('getPath') - ->will($this->returnValue('MyPath')); - - $this->assertSame(false, $this->fakeLockerPlugin->fakeLockProvider($request, $response)); - - $expectedXml = '<?xml version="1.0" encoding="utf-8"?><d:prop xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns"><d:lockdiscovery><d:activelock><d:lockscope><d:exclusive/></d:lockscope><d:locktype><d:write/></d:locktype><d:lockroot><d:href>MyPath</d:href></d:lockroot><d:depth>infinity</d:depth><d:timeout>Second-1800</d:timeout><d:locktoken><d:href>opaquelocktoken:fe4f7f2437b151fbcb4e9f5c8118c6b1</d:href></d:locktoken><d:owner/></d:activelock></d:lockdiscovery></d:prop>'; - - $this->assertXmlStringEqualsXmlString($expectedXml, $response->getBody()); - } - - public function testFakeUnlockProvider() { - $request = $this->getMock('\Sabre\HTTP\RequestInterface'); - $response = $this->getMock('\Sabre\HTTP\ResponseInterface'); - - $response->expects($this->once()) - ->method('setStatus') - ->with('204'); - $response->expects($this->once()) - ->method('setHeader') - ->with('Content-Length', '0'); - - $this->assertSame(false, $this->fakeLockerPlugin->fakeUnlockProvider($request, $response)); - } -} diff --git a/apps/dav/tests/unit/connector/sabre/MaintenancePluginTest.php b/apps/dav/tests/unit/connector/sabre/MaintenancePluginTest.php deleted file mode 100644 index dea1e64db1d..00000000000 --- a/apps/dav/tests/unit/connector/sabre/MaintenancePluginTest.php +++ /dev/null @@ -1,73 +0,0 @@ -<?php -/** - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\Connector\Sabre; - -use OCA\DAV\Connector\Sabre\MaintenancePlugin; -use Test\TestCase; -use OCP\IConfig; - -/** - * Class MaintenancePluginTest - * - * @package Test\Connector\Sabre - */ -class MaintenancePluginTest extends TestCase { - /** @var IConfig */ - private $config; - /** @var MaintenancePlugin */ - private $maintenancePlugin; - - public function setUp() { - parent::setUp(); - - $this->config = $this->getMock('\OCP\IConfig'); - $this->maintenancePlugin = new MaintenancePlugin($this->config); - } - - /** - * @expectedException \Sabre\DAV\Exception\ServiceUnavailable - * @expectedExceptionMessage System in single user mode. - */ - public function testSingleUserMode() { - $this->config - ->expects($this->once()) - ->method('getSystemValue') - ->with('singleuser', false) - ->will($this->returnValue(true)); - - $this->maintenancePlugin->checkMaintenanceMode(); - } - - /** - * @expectedException \Sabre\DAV\Exception\ServiceUnavailable - * @expectedExceptionMessage System in single user mode. - */ - public function testMaintenanceMode() { - $this->config - ->expects($this->exactly(1)) - ->method('getSystemValue') - ->will($this->onConsecutiveCalls([false, true])); - - $this->maintenancePlugin->checkMaintenanceMode(); - } - -} diff --git a/apps/dav/tests/unit/connector/sabre/auth.php b/apps/dav/tests/unit/connector/sabre/auth.php deleted file mode 100644 index b81a5e003b5..00000000000 --- a/apps/dav/tests/unit/connector/sabre/auth.php +++ /dev/null @@ -1,604 +0,0 @@ -<?php -/** - * @author Lukas Reschke <lukas@owncloud.com> - * @author Roeland Jago Douma <rullzer@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\Connector\Sabre; - -use OCP\IRequest; -use OCP\IUser; -use Test\TestCase; -use OCP\ISession; -use OCP\IUserSession; - -/** - * Class Auth - * - * @package OCA\DAV\Connector\Sabre - * @group DB - */ -class Auth extends TestCase { - /** @var ISession */ - private $session; - /** @var \OCA\DAV\Connector\Sabre\Auth */ - private $auth; - /** @var IUserSession */ - private $userSession; - /** @var IRequest */ - private $request; - - public function setUp() { - parent::setUp(); - $this->session = $this->getMockBuilder('\OCP\ISession') - ->disableOriginalConstructor()->getMock(); - $this->userSession = $this->getMockBuilder('\OCP\IUserSession') - ->disableOriginalConstructor()->getMock(); - $this->request = $this->getMockBuilder('\OCP\IRequest') - ->disableOriginalConstructor()->getMock(); - $this->auth = new \OCA\DAV\Connector\Sabre\Auth( - $this->session, - $this->userSession, - $this->request - ); - } - - public function testIsDavAuthenticatedWithoutDavSession() { - $this->session - ->expects($this->once()) - ->method('get') - ->with('AUTHENTICATED_TO_DAV_BACKEND') - ->will($this->returnValue(null)); - - $this->assertFalse($this->invokePrivate($this->auth, 'isDavAuthenticated', ['MyTestUser'])); - } - - public function testIsDavAuthenticatedWithWrongDavSession() { - $this->session - ->expects($this->exactly(2)) - ->method('get') - ->with('AUTHENTICATED_TO_DAV_BACKEND') - ->will($this->returnValue('AnotherUser')); - - $this->assertFalse($this->invokePrivate($this->auth, 'isDavAuthenticated', ['MyTestUser'])); - } - - public function testIsDavAuthenticatedWithCorrectDavSession() { - $this->session - ->expects($this->exactly(2)) - ->method('get') - ->with('AUTHENTICATED_TO_DAV_BACKEND') - ->will($this->returnValue('MyTestUser')); - - $this->assertTrue($this->invokePrivate($this->auth, 'isDavAuthenticated', ['MyTestUser'])); - } - - public function testValidateUserPassOfAlreadyDAVAuthenticatedUser() { - $user = $this->getMockBuilder('\OCP\IUser') - ->disableOriginalConstructor() - ->getMock(); - $user->expects($this->exactly(2)) - ->method('getUID') - ->will($this->returnValue('MyTestUser')); - $this->userSession - ->expects($this->once()) - ->method('isLoggedIn') - ->will($this->returnValue(true)); - $this->userSession - ->expects($this->exactly(2)) - ->method('getUser') - ->will($this->returnValue($user)); - $this->session - ->expects($this->exactly(2)) - ->method('get') - ->with('AUTHENTICATED_TO_DAV_BACKEND') - ->will($this->returnValue('MyTestUser')); - $this->session - ->expects($this->once()) - ->method('close'); - - $this->assertTrue($this->invokePrivate($this->auth, 'validateUserPass', ['MyTestUser', 'MyTestPassword'])); - } - - public function testValidateUserPassOfInvalidDAVAuthenticatedUser() { - $user = $this->getMockBuilder('\OCP\IUser') - ->disableOriginalConstructor() - ->getMock(); - $user->expects($this->once()) - ->method('getUID') - ->will($this->returnValue('MyTestUser')); - $this->userSession - ->expects($this->once()) - ->method('isLoggedIn') - ->will($this->returnValue(true)); - $this->userSession - ->expects($this->once()) - ->method('getUser') - ->will($this->returnValue($user)); - $this->session - ->expects($this->exactly(2)) - ->method('get') - ->with('AUTHENTICATED_TO_DAV_BACKEND') - ->will($this->returnValue('AnotherUser')); - $this->session - ->expects($this->once()) - ->method('close'); - - $this->assertFalse($this->invokePrivate($this->auth, 'validateUserPass', ['MyTestUser', 'MyTestPassword'])); - } - - public function testValidateUserPassOfInvalidDAVAuthenticatedUserWithValidPassword() { - $user = $this->getMockBuilder('\OCP\IUser') - ->disableOriginalConstructor() - ->getMock(); - $user->expects($this->exactly(3)) - ->method('getUID') - ->will($this->returnValue('MyTestUser')); - $this->userSession - ->expects($this->once()) - ->method('isLoggedIn') - ->will($this->returnValue(true)); - $this->userSession - ->expects($this->exactly(3)) - ->method('getUser') - ->will($this->returnValue($user)); - $this->session - ->expects($this->exactly(2)) - ->method('get') - ->with('AUTHENTICATED_TO_DAV_BACKEND') - ->will($this->returnValue('AnotherUser')); - $this->userSession - ->expects($this->once()) - ->method('login') - ->with('MyTestUser', 'MyTestPassword') - ->will($this->returnValue(true)); - $this->session - ->expects($this->once()) - ->method('set') - ->with('AUTHENTICATED_TO_DAV_BACKEND', 'MyTestUser'); - $this->session - ->expects($this->once()) - ->method('close'); - - $this->assertTrue($this->invokePrivate($this->auth, 'validateUserPass', ['MyTestUser', 'MyTestPassword'])); - } - - public function testValidateUserPassWithInvalidPassword() { - $this->userSession - ->expects($this->once()) - ->method('isLoggedIn') - ->will($this->returnValue(false)); - $this->userSession - ->expects($this->once()) - ->method('login') - ->with('MyTestUser', 'MyTestPassword') - ->will($this->returnValue(false)); - $this->session - ->expects($this->once()) - ->method('close'); - - $this->assertFalse($this->invokePrivate($this->auth, 'validateUserPass', ['MyTestUser', 'MyTestPassword'])); - } - - - public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenForNonGet() { - $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface') - ->disableOriginalConstructor() - ->getMock(); - $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface') - ->disableOriginalConstructor() - ->getMock(); - $this->userSession - ->expects($this->any()) - ->method('isLoggedIn') - ->will($this->returnValue(true)); - $this->request - ->expects($this->any()) - ->method('getMethod') - ->willReturn('POST'); - $this->session - ->expects($this->any()) - ->method('get') - ->with('AUTHENTICATED_TO_DAV_BACKEND') - ->will($this->returnValue(null)); - $user = $this->getMockBuilder('\OCP\IUser') - ->disableOriginalConstructor() - ->getMock(); - $user->expects($this->any()) - ->method('getUID') - ->will($this->returnValue('MyWrongDavUser')); - $this->userSession - ->expects($this->any()) - ->method('getUser') - ->will($this->returnValue($user)); - $this->request - ->expects($this->once()) - ->method('passesCSRFCheck') - ->willReturn(false); - - $expectedResponse = [ - false, - "No 'Authorization: Basic' header found. Either the client didn't send one, or the server is mis-configured", - ]; - $response = $this->auth->check($request, $response); - $this->assertSame($expectedResponse, $response); - } - - public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenAndCorrectlyDavAuthenticated() { - $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface') - ->disableOriginalConstructor() - ->getMock(); - $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface') - ->disableOriginalConstructor() - ->getMock(); - $this->userSession - ->expects($this->any()) - ->method('isLoggedIn') - ->willReturn(true); - $this->request - ->expects($this->any()) - ->method('getMethod') - ->willReturn('PROPFIND'); - $this->request - ->expects($this->any()) - ->method('isUserAgent') - ->with([ - '/^Mozilla\/5\.0 \([A-Za-z ]+\) (mirall|csyncoC)\/.*$/', - '/^Mozilla\/5\.0 \(Android\) ownCloud\-android.*$/', - '/^Mozilla\/5\.0 \(iOS\) ownCloud\-iOS.*$/', - ]) - ->willReturn(false); - $this->session - ->expects($this->any()) - ->method('get') - ->with('AUTHENTICATED_TO_DAV_BACKEND') - ->will($this->returnValue('LoggedInUser')); - $user = $this->getMockBuilder('\OCP\IUser') - ->disableOriginalConstructor() - ->getMock(); - $user->expects($this->any()) - ->method('getUID') - ->will($this->returnValue('LoggedInUser')); - $this->userSession - ->expects($this->any()) - ->method('getUser') - ->will($this->returnValue($user)); - $this->request - ->expects($this->once()) - ->method('passesCSRFCheck') - ->willReturn(false); - $this->auth->check($request, $response); - } - - /** - * @expectedException \Sabre\DAV\Exception\NotAuthenticated - * @expectedExceptionMessage CSRF check not passed. - */ - public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenAndIncorrectlyDavAuthenticated() { - $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface') - ->disableOriginalConstructor() - ->getMock(); - $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface') - ->disableOriginalConstructor() - ->getMock(); - $this->userSession - ->expects($this->any()) - ->method('isLoggedIn') - ->willReturn(true); - $this->request - ->expects($this->any()) - ->method('getMethod') - ->willReturn('PROPFIND'); - $this->request - ->expects($this->any()) - ->method('isUserAgent') - ->with([ - '/^Mozilla\/5\.0 \([A-Za-z ]+\) (mirall|csyncoC)\/.*$/', - '/^Mozilla\/5\.0 \(Android\) ownCloud\-android.*$/', - '/^Mozilla\/5\.0 \(iOS\) ownCloud\-iOS.*$/', - ]) - ->willReturn(false); - $this->session - ->expects($this->any()) - ->method('get') - ->with('AUTHENTICATED_TO_DAV_BACKEND') - ->will($this->returnValue('AnotherUser')); - $user = $this->getMockBuilder('\OCP\IUser') - ->disableOriginalConstructor() - ->getMock(); - $user->expects($this->any()) - ->method('getUID') - ->will($this->returnValue('LoggedInUser')); - $this->userSession - ->expects($this->any()) - ->method('getUser') - ->will($this->returnValue($user)); - $this->request - ->expects($this->once()) - ->method('passesCSRFCheck') - ->willReturn(false); - $this->auth->check($request, $response); - } - - public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenForNonGetAndDesktopClient() { - $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface') - ->disableOriginalConstructor() - ->getMock(); - $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface') - ->disableOriginalConstructor() - ->getMock(); - $this->userSession - ->expects($this->any()) - ->method('isLoggedIn') - ->will($this->returnValue(true)); - $this->request - ->expects($this->any()) - ->method('getMethod') - ->willReturn('POST'); - $this->request - ->expects($this->any()) - ->method('isUserAgent') - ->with([ - '/^Mozilla\/5\.0 \([A-Za-z ]+\) (mirall|csyncoC)\/.*$/', - '/^Mozilla\/5\.0 \(Android\) ownCloud\-android.*$/', - '/^Mozilla\/5\.0 \(iOS\) ownCloud\-iOS.*$/', - ]) - ->willReturn(true); - $this->session - ->expects($this->any()) - ->method('get') - ->with('AUTHENTICATED_TO_DAV_BACKEND') - ->will($this->returnValue(null)); - $user = $this->getMockBuilder('\OCP\IUser') - ->disableOriginalConstructor() - ->getMock(); - $user->expects($this->any()) - ->method('getUID') - ->will($this->returnValue('MyWrongDavUser')); - $this->userSession - ->expects($this->any()) - ->method('getUser') - ->will($this->returnValue($user)); - $this->request - ->expects($this->once()) - ->method('passesCSRFCheck') - ->willReturn(false); - - $this->auth->check($request, $response); - } - - public function testAuthenticateAlreadyLoggedInWithoutCsrfTokenForGet() { - $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface') - ->disableOriginalConstructor() - ->getMock(); - $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface') - ->disableOriginalConstructor() - ->getMock(); - $this->userSession - ->expects($this->any()) - ->method('isLoggedIn') - ->will($this->returnValue(true)); - $this->session - ->expects($this->any()) - ->method('get') - ->with('AUTHENTICATED_TO_DAV_BACKEND') - ->will($this->returnValue(null)); - $user = $this->getMockBuilder('\OCP\IUser') - ->disableOriginalConstructor() - ->getMock(); - $user->expects($this->any()) - ->method('getUID') - ->will($this->returnValue('MyWrongDavUser')); - $this->userSession - ->expects($this->any()) - ->method('getUser') - ->will($this->returnValue($user)); - $this->request - ->expects($this->any()) - ->method('getMethod') - ->willReturn('GET'); - - $response = $this->auth->check($request, $response); - $this->assertEquals([true, 'principals/users/MyWrongDavUser'], $response); - } - - public function testAuthenticateAlreadyLoggedInWithCsrfTokenForGet() { - $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface') - ->disableOriginalConstructor() - ->getMock(); - $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface') - ->disableOriginalConstructor() - ->getMock(); - $this->userSession - ->expects($this->any()) - ->method('isLoggedIn') - ->will($this->returnValue(true)); - $this->session - ->expects($this->any()) - ->method('get') - ->with('AUTHENTICATED_TO_DAV_BACKEND') - ->will($this->returnValue(null)); - $user = $this->getMockBuilder('\OCP\IUser') - ->disableOriginalConstructor() - ->getMock(); - $user->expects($this->any()) - ->method('getUID') - ->will($this->returnValue('MyWrongDavUser')); - $this->userSession - ->expects($this->any()) - ->method('getUser') - ->will($this->returnValue($user)); - $this->request - ->expects($this->once()) - ->method('passesCSRFCheck') - ->willReturn(true); - - $response = $this->auth->check($request, $response); - $this->assertEquals([true, 'principals/users/MyWrongDavUser'], $response); - } - - public function testAuthenticateNoBasicAuthenticateHeadersProvided() { - $server = $this->getMockBuilder('\Sabre\DAV\Server') - ->disableOriginalConstructor() - ->getMock(); - $server->httpRequest = $this->getMockBuilder('\Sabre\HTTP\RequestInterface') - ->disableOriginalConstructor() - ->getMock(); - $server->httpResponse = $this->getMockBuilder('\Sabre\HTTP\ResponseInterface') - ->disableOriginalConstructor() - ->getMock(); - $response = $this->auth->check($server->httpRequest, $server->httpResponse); - $this->assertEquals([false, 'No \'Authorization: Basic\' header found. Either the client didn\'t send one, or the server is mis-configured'], $response); - } - - /** - * @expectedException \Sabre\DAV\Exception\NotAuthenticated - * @expectedExceptionMessage Cannot authenticate over ajax calls - */ - public function testAuthenticateNoBasicAuthenticateHeadersProvidedWithAjax() { - /** @var \Sabre\HTTP\RequestInterface $httpRequest */ - $httpRequest = $this->getMockBuilder('\Sabre\HTTP\RequestInterface') - ->disableOriginalConstructor() - ->getMock(); - /** @var \Sabre\HTTP\ResponseInterface $httpResponse */ - $httpResponse = $this->getMockBuilder('\Sabre\HTTP\ResponseInterface') - ->disableOriginalConstructor() - ->getMock(); - $this->userSession - ->expects($this->any()) - ->method('isLoggedIn') - ->will($this->returnValue(false)); - $httpRequest - ->expects($this->once()) - ->method('getHeader') - ->with('X-Requested-With') - ->will($this->returnValue('XMLHttpRequest')); - $this->auth->check($httpRequest, $httpResponse); - } - - public function testAuthenticateNoBasicAuthenticateHeadersProvidedWithAjaxButUserIsStillLoggedIn() { - /** @var \Sabre\HTTP\RequestInterface $httpRequest */ - $httpRequest = $this->getMockBuilder('\Sabre\HTTP\RequestInterface') - ->disableOriginalConstructor() - ->getMock(); - /** @var \Sabre\HTTP\ResponseInterface $httpResponse */ - $httpResponse = $this->getMockBuilder('\Sabre\HTTP\ResponseInterface') - ->disableOriginalConstructor() - ->getMock(); - /** @var IUser */ - $user = $this->getMock('OCP\IUser'); - $user->method('getUID')->willReturn('MyTestUser'); - $this->userSession - ->expects($this->any()) - ->method('isLoggedIn') - ->will($this->returnValue(true)); - $this->userSession - ->expects($this->any()) - ->method('getUser') - ->willReturn($user); - $this->session - ->expects($this->atLeastOnce()) - ->method('get') - ->with('AUTHENTICATED_TO_DAV_BACKEND') - ->will($this->returnValue('MyTestUser')); - $this->request - ->expects($this->once()) - ->method('getMethod') - ->willReturn('GET'); - $httpRequest - ->expects($this->atLeastOnce()) - ->method('getHeader') - ->with('Authorization') - ->will($this->returnValue(null)); - $this->assertEquals( - [true, 'principals/users/MyTestUser'], - $this->auth->check($httpRequest, $httpResponse) - ); - } - - public function testAuthenticateValidCredentials() { - $server = $this->getMockBuilder('\Sabre\DAV\Server') - ->disableOriginalConstructor() - ->getMock(); - $server->httpRequest = $this->getMockBuilder('\Sabre\HTTP\RequestInterface') - ->disableOriginalConstructor() - ->getMock(); - $server->httpRequest - ->expects($this->at(0)) - ->method('getHeader') - ->with('X-Requested-With') - ->will($this->returnValue(null)); - $server->httpRequest - ->expects($this->at(1)) - ->method('getHeader') - ->with('Authorization') - ->will($this->returnValue('basic dXNlcm5hbWU6cGFzc3dvcmQ=')); - $server->httpResponse = $this->getMockBuilder('\Sabre\HTTP\ResponseInterface') - ->disableOriginalConstructor() - ->getMock(); - $this->userSession - ->expects($this->once()) - ->method('login') - ->with('username', 'password') - ->will($this->returnValue(true)); - $user = $this->getMockBuilder('\OCP\IUser') - ->disableOriginalConstructor() - ->getMock(); - $user->expects($this->exactly(3)) - ->method('getUID') - ->will($this->returnValue('MyTestUser')); - $this->userSession - ->expects($this->exactly(3)) - ->method('getUser') - ->will($this->returnValue($user)); - $response = $this->auth->check($server->httpRequest, $server->httpResponse); - $this->assertEquals([true, 'principals/users/MyTestUser'], $response); - } - - public function testAuthenticateInvalidCredentials() { - $server = $this->getMockBuilder('\Sabre\DAV\Server') - ->disableOriginalConstructor() - ->getMock(); - $server->httpRequest = $this->getMockBuilder('\Sabre\HTTP\RequestInterface') - ->disableOriginalConstructor() - ->getMock(); - $server->httpRequest - ->expects($this->at(0)) - ->method('getHeader') - ->with('X-Requested-With') - ->will($this->returnValue(null)); - $server->httpRequest - ->expects($this->at(1)) - ->method('getHeader') - ->with('Authorization') - ->will($this->returnValue('basic dXNlcm5hbWU6cGFzc3dvcmQ=')); - $server->httpResponse = $this->getMockBuilder('\Sabre\HTTP\ResponseInterface') - ->disableOriginalConstructor() - ->getMock(); - $this->userSession - ->expects($this->once()) - ->method('login') - ->with('username', 'password') - ->will($this->returnValue(false)); - $response = $this->auth->check($server->httpRequest, $server->httpResponse); - $this->assertEquals([false, 'Username or password was incorrect'], $response); - } -} diff --git a/apps/dav/tests/unit/connector/sabre/commentpropertiesplugin.php b/apps/dav/tests/unit/connector/sabre/commentpropertiesplugin.php deleted file mode 100644 index f915c83c4a7..00000000000 --- a/apps/dav/tests/unit/connector/sabre/commentpropertiesplugin.php +++ /dev/null @@ -1,148 +0,0 @@ -<?php -/** - * @author Arthur Schiwon <blizzz@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\Connector\Sabre; - -use \OCA\DAV\Connector\Sabre\CommentPropertiesPlugin as CommentPropertiesPluginImplementation; - -class CommentsPropertiesPlugin extends \Test\TestCase { - - /** @var CommentPropertiesPluginImplementation */ - protected $plugin; - protected $commentsManager; - protected $userSession; - protected $server; - - public function setUp() { - parent::setUp(); - - $this->commentsManager = $this->getMock('\OCP\Comments\ICommentsManager'); - $this->userSession = $this->getMock('\OCP\IUserSession'); - - $this->server = $this->getMockBuilder('\Sabre\DAV\Server') - ->disableOriginalConstructor() - ->getMock(); - - $this->plugin = new CommentPropertiesPluginImplementation($this->commentsManager, $this->userSession); - $this->plugin->initialize($this->server); - } - - public function nodeProvider() { - $mocks = []; - foreach(['\OCA\DAV\Connector\Sabre\File', '\OCA\DAV\Connector\Sabre\Directory', '\Sabre\DAV\INode'] as $class) { - $mocks[] = $this->getMockBuilder($class) - ->disableOriginalConstructor() - ->getMock(); - } - - return [ - [$mocks[0], true], - [$mocks[1], true], - [$mocks[2], false] - ]; - } - - /** - * @dataProvider nodeProvider - * @param $node - * @param $expectedSuccessful - */ - public function testHandleGetProperties($node, $expectedSuccessful) { - $propFind = $this->getMockBuilder('\Sabre\DAV\PropFind') - ->disableOriginalConstructor() - ->getMock(); - - if($expectedSuccessful) { - $propFind->expects($this->exactly(3)) - ->method('handle'); - } else { - $propFind->expects($this->never()) - ->method('handle'); - } - - $this->plugin->handleGetProperties($propFind, $node); - } - - public function baseUriProvider() { - return [ - ['owncloud/remote.php/webdav/', '4567', 'owncloud/remote.php/dav/comments/files/4567'], - ['owncloud/remote.php/wicked/', '4567', null] - ]; - } - - /** - * @dataProvider baseUriProvider - * @param $baseUri - * @param $fid - * @param $expectedHref - */ - public function testGetCommentsLink($baseUri, $fid, $expectedHref) { - $node = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\File') - ->disableOriginalConstructor() - ->getMock(); - $node->expects($this->any()) - ->method('getId') - ->will($this->returnValue($fid)); - - $this->server->expects($this->once()) - ->method('getBaseUri') - ->will($this->returnValue($baseUri)); - - $href = $this->plugin->getCommentsLink($node); - $this->assertSame($expectedHref, $href); - } - - public function userProvider() { - return [ - [$this->getMock('\OCP\IUser')], - [null] - ]; - } - - /** - * @dataProvider userProvider - * @param $user - */ - public function testGetUnreadCount($user) { - $node = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\File') - ->disableOriginalConstructor() - ->getMock(); - $node->expects($this->any()) - ->method('getId') - ->will($this->returnValue('4567')); - - $this->userSession->expects($this->once()) - ->method('getUser') - ->will($this->returnValue($user)); - - $this->commentsManager->expects($this->any()) - ->method('getNumberOfCommentsForObject') - ->will($this->returnValue(42)); - - $unread = $this->plugin->getUnreadCount($node); - if(is_null($user)) { - $this->assertNull($unread); - } else { - $this->assertSame($unread, 42); - } - } - -} diff --git a/apps/dav/tests/unit/connector/sabre/copyetagheaderplugintest.php b/apps/dav/tests/unit/connector/sabre/copyetagheaderplugintest.php deleted file mode 100644 index 7f6fb26e4d1..00000000000 --- a/apps/dav/tests/unit/connector/sabre/copyetagheaderplugintest.php +++ /dev/null @@ -1,62 +0,0 @@ -<?php -/** - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace OCA\DAV\Tests\Unit\Connector\Sabre; - -/** - * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com> - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. - */ -class CopyEtagPluginTest extends \Test\TestCase { - - /** - * @var \OCA\DAV\Connector\Sabre\CopyEtagHeaderPlugin - */ - private $plugin; - - public function setUp() { - parent::setUp(); - $this->server = new \Sabre\DAV\Server(); - $this->plugin = new \OCA\DAV\Connector\Sabre\CopyEtagHeaderPlugin(); - $this->plugin->initialize($this->server); - } - - public function testCopyEtag() { - $request = new \Sabre\Http\Request(); - $response = new \Sabre\Http\Response(); - $response->setHeader('Etag', 'abcd'); - - $this->plugin->afterMethod($request, $response); - - $this->assertEquals('abcd', $response->getHeader('OC-Etag')); - } - - public function testNoopWhenEmpty() { - $request = new \Sabre\Http\Request(); - $response = new \Sabre\Http\Response(); - - $this->plugin->afterMethod($request, $response); - - $this->assertNull($response->getHeader('OC-Etag')); - } -} diff --git a/apps/dav/tests/unit/connector/sabre/custompropertiesbackend.php b/apps/dav/tests/unit/connector/sabre/custompropertiesbackend.php deleted file mode 100644 index e0ba61e9134..00000000000 --- a/apps/dav/tests/unit/connector/sabre/custompropertiesbackend.php +++ /dev/null @@ -1,313 +0,0 @@ -<?php -/** - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace OCA\DAV\Tests\Unit\Connector\Sabre; - -/** - * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com> - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. - */ - -/** - * Class CustomPropertiesBackend - * - * @group DB - * - * @package Tests\Connector\Sabre - */ -class CustomPropertiesBackend extends \Test\TestCase { - - /** - * @var \Sabre\DAV\Server - */ - private $server; - - /** - * @var \Sabre\DAV\Tree - */ - private $tree; - - /** - * @var \OCA\DAV\Connector\Sabre\CustomPropertiesBackend - */ - private $plugin; - - /** - * @var \OCP\IUser - */ - private $user; - - public function setUp() { - parent::setUp(); - $this->server = new \Sabre\DAV\Server(); - $this->tree = $this->getMockBuilder('\Sabre\DAV\Tree') - ->disableOriginalConstructor() - ->getMock(); - - $userId = $this->getUniqueID('testcustompropertiesuser'); - - $this->user = $this->getMock('\OCP\IUser'); - $this->user->expects($this->any()) - ->method('getUID') - ->will($this->returnValue($userId)); - - $this->plugin = new \OCA\DAV\Connector\Sabre\CustomPropertiesBackend( - $this->tree, - \OC::$server->getDatabaseConnection(), - $this->user - ); - } - - public function tearDown() { - $connection = \OC::$server->getDatabaseConnection(); - $deleteStatement = $connection->prepare( - 'DELETE FROM `*PREFIX*properties`' . - ' WHERE `userid` = ?' - ); - $deleteStatement->execute( - array( - $this->user->getUID(), - ) - ); - $deleteStatement->closeCursor(); - } - - private function createTestNode($class) { - $node = $this->getMockBuilder($class) - ->disableOriginalConstructor() - ->getMock(); - $node->expects($this->any()) - ->method('getId') - ->will($this->returnValue(123)); - - $node->expects($this->any()) - ->method('getPath') - ->will($this->returnValue('/dummypath')); - - return $node; - } - - private function applyDefaultProps($path = '/dummypath') { - // properties to set - $propPatch = new \Sabre\DAV\PropPatch(array( - 'customprop' => 'value1', - 'customprop2' => 'value2', - )); - - $this->plugin->propPatch( - $path, - $propPatch - ); - - $propPatch->commit(); - - $this->assertEmpty($propPatch->getRemainingMutations()); - - $result = $propPatch->getResult(); - $this->assertEquals(200, $result['customprop']); - $this->assertEquals(200, $result['customprop2']); - } - - /** - * Test that propFind on a missing file soft fails - */ - public function testPropFindMissingFileSoftFail() { - $this->tree->expects($this->at(0)) - ->method('getNodeForPath') - ->with('/dummypath') - ->will($this->throwException(new \Sabre\DAV\Exception\NotFound())); - - $this->tree->expects($this->at(1)) - ->method('getNodeForPath') - ->with('/dummypath') - ->will($this->throwException(new \Sabre\DAV\Exception\ServiceUnavailable())); - - $propFind = new \Sabre\DAV\PropFind( - '/dummypath', - array( - 'customprop', - 'customprop2', - 'unsetprop', - ), - 0 - ); - - $this->plugin->propFind( - '/dummypath', - $propFind - ); - - $this->plugin->propFind( - '/dummypath', - $propFind - ); - - // no exception, soft fail - $this->assertTrue(true); - } - - /** - * Test setting/getting properties - */ - public function testSetGetPropertiesForFile() { - $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File'); - $this->tree->expects($this->any()) - ->method('getNodeForPath') - ->with('/dummypath') - ->will($this->returnValue($node)); - - $this->applyDefaultProps(); - - $propFind = new \Sabre\DAV\PropFind( - '/dummypath', - array( - 'customprop', - 'customprop2', - 'unsetprop', - ), - 0 - ); - - $this->plugin->propFind( - '/dummypath', - $propFind - ); - - $this->assertEquals('value1', $propFind->get('customprop')); - $this->assertEquals('value2', $propFind->get('customprop2')); - $this->assertEquals(array('unsetprop'), $propFind->get404Properties()); - } - - /** - * Test getting properties from directory - */ - public function testGetPropertiesForDirectory() { - $rootNode = $this->createTestNode('\OCA\DAV\Connector\Sabre\Directory'); - - $nodeSub = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\File') - ->disableOriginalConstructor() - ->getMock(); - $nodeSub->expects($this->any()) - ->method('getId') - ->will($this->returnValue(456)); - - $nodeSub->expects($this->any()) - ->method('getPath') - ->will($this->returnValue('/dummypath/test.txt')); - - $rootNode->expects($this->once()) - ->method('getChildren') - ->will($this->returnValue(array($nodeSub))); - - $this->tree->expects($this->at(0)) - ->method('getNodeForPath') - ->with('/dummypath') - ->will($this->returnValue($rootNode)); - - $this->tree->expects($this->at(1)) - ->method('getNodeForPath') - ->with('/dummypath/test.txt') - ->will($this->returnValue($nodeSub)); - - $this->tree->expects($this->at(2)) - ->method('getNodeForPath') - ->with('/dummypath') - ->will($this->returnValue($rootNode)); - - $this->tree->expects($this->at(3)) - ->method('getNodeForPath') - ->with('/dummypath/test.txt') - ->will($this->returnValue($nodeSub)); - - $this->applyDefaultProps('/dummypath'); - $this->applyDefaultProps('/dummypath/test.txt'); - - $propNames = array( - 'customprop', - 'customprop2', - 'unsetprop', - ); - - $propFindRoot = new \Sabre\DAV\PropFind( - '/dummypath', - $propNames, - 1 - ); - - $propFindSub = new \Sabre\DAV\PropFind( - '/dummypath/test.txt', - $propNames, - 0 - ); - - $this->plugin->propFind( - '/dummypath', - $propFindRoot - ); - - $this->plugin->propFind( - '/dummypath/test.txt', - $propFindSub - ); - - // TODO: find a way to assert that no additional SQL queries were - // run while doing the second propFind - - $this->assertEquals('value1', $propFindRoot->get('customprop')); - $this->assertEquals('value2', $propFindRoot->get('customprop2')); - $this->assertEquals(array('unsetprop'), $propFindRoot->get404Properties()); - - $this->assertEquals('value1', $propFindSub->get('customprop')); - $this->assertEquals('value2', $propFindSub->get('customprop2')); - $this->assertEquals(array('unsetprop'), $propFindSub->get404Properties()); - } - - /** - * Test delete property - */ - public function testDeleteProperty() { - $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File'); - $this->tree->expects($this->any()) - ->method('getNodeForPath') - ->with('/dummypath') - ->will($this->returnValue($node)); - - $this->applyDefaultProps(); - - $propPatch = new \Sabre\DAV\PropPatch(array( - 'customprop' => null, - )); - - $this->plugin->propPatch( - '/dummypath', - $propPatch - ); - - $propPatch->commit(); - - $this->assertEmpty($propPatch->getRemainingMutations()); - - $result = $propPatch->getResult(); - $this->assertEquals(204, $result['customprop']); - } -} diff --git a/apps/dav/tests/unit/connector/sabre/directory.php b/apps/dav/tests/unit/connector/sabre/directory.php deleted file mode 100644 index c4ddc38b3e1..00000000000 --- a/apps/dav/tests/unit/connector/sabre/directory.php +++ /dev/null @@ -1,264 +0,0 @@ -<?php -/** - * @author Joas Schilling <nickvergessen@owncloud.com> - * @author Robin Appelman <icewind@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\Connector\Sabre; - -use OCP\Files\ForbiddenException; - -class Directory extends \Test\TestCase { - - /** @var \OC\Files\View | \PHPUnit_Framework_MockObject_MockObject */ - private $view; - /** @var \OC\Files\FileInfo | \PHPUnit_Framework_MockObject_MockObject */ - private $info; - - protected function setUp() { - parent::setUp(); - - $this->view = $this->getMock('OC\Files\View', array(), array(), '', false); - $this->info = $this->getMock('OC\Files\FileInfo', array(), array(), '', false); - } - - private function getDir($path = '/') { - $this->view->expects($this->once()) - ->method('getRelativePath') - ->will($this->returnValue($path)); - - $this->info->expects($this->once()) - ->method('getPath') - ->will($this->returnValue($path)); - - return new \OCA\DAV\Connector\Sabre\Directory($this->view, $this->info); - } - - /** - * @expectedException \Sabre\DAV\Exception\Forbidden - */ - public function testDeleteRootFolderFails() { - $this->info->expects($this->any()) - ->method('isDeletable') - ->will($this->returnValue(true)); - $this->view->expects($this->never()) - ->method('rmdir'); - $dir = $this->getDir(); - $dir->delete(); - } - - /** - * @expectedException \OCA\DAV\Connector\Sabre\Exception\Forbidden - */ - public function testDeleteForbidden() { - // deletion allowed - $this->info->expects($this->once()) - ->method('isDeletable') - ->will($this->returnValue(true)); - - // but fails - $this->view->expects($this->once()) - ->method('rmdir') - ->with('sub') - ->willThrowException(new ForbiddenException('', true)); - - $dir = $this->getDir('sub'); - $dir->delete(); - } - - /** - * - */ - public function testDeleteFolderWhenAllowed() { - // deletion allowed - $this->info->expects($this->once()) - ->method('isDeletable') - ->will($this->returnValue(true)); - - // but fails - $this->view->expects($this->once()) - ->method('rmdir') - ->with('sub') - ->will($this->returnValue(true)); - - $dir = $this->getDir('sub'); - $dir->delete(); - } - - /** - * @expectedException \Sabre\DAV\Exception\Forbidden - */ - public function testDeleteFolderFailsWhenNotAllowed() { - $this->info->expects($this->once()) - ->method('isDeletable') - ->will($this->returnValue(false)); - - $dir = $this->getDir('sub'); - $dir->delete(); - } - - /** - * @expectedException \Sabre\DAV\Exception\Forbidden - */ - public function testDeleteFolderThrowsWhenDeletionFailed() { - // deletion allowed - $this->info->expects($this->once()) - ->method('isDeletable') - ->will($this->returnValue(true)); - - // but fails - $this->view->expects($this->once()) - ->method('rmdir') - ->with('sub') - ->will($this->returnValue(false)); - - $dir = $this->getDir('sub'); - $dir->delete(); - } - - public function testGetChildren() { - $info1 = $this->getMockBuilder('OC\Files\FileInfo') - ->disableOriginalConstructor() - ->getMock(); - $info2 = $this->getMockBuilder('OC\Files\FileInfo') - ->disableOriginalConstructor() - ->getMock(); - $info1->expects($this->any()) - ->method('getName') - ->will($this->returnValue('first')); - $info1->expects($this->any()) - ->method('getEtag') - ->will($this->returnValue('abc')); - $info2->expects($this->any()) - ->method('getName') - ->will($this->returnValue('second')); - $info2->expects($this->any()) - ->method('getEtag') - ->will($this->returnValue('def')); - - $this->view->expects($this->once()) - ->method('getDirectoryContent') - ->with('') - ->will($this->returnValue(array($info1, $info2))); - - $this->view->expects($this->any()) - ->method('getRelativePath') - ->will($this->returnValue('')); - - $dir = new \OCA\DAV\Connector\Sabre\Directory($this->view, $this->info); - $nodes = $dir->getChildren(); - - $this->assertEquals(2, count($nodes)); - - // calling a second time just returns the cached values, - // does not call getDirectoryContents again - $dir->getChildren(); - } - - /** - * @expectedException \Sabre\DAV\Exception\ServiceUnavailable - */ - public function testGetChildThrowStorageNotAvailableException() { - $this->view->expects($this->once()) - ->method('getFileInfo') - ->willThrowException(new \OCP\Files\StorageNotAvailableException()); - - $dir = new \OCA\DAV\Connector\Sabre\Directory($this->view, $this->info); - $dir->getChild('.'); - } - - /** - * @expectedException \OCA\DAV\Connector\Sabre\Exception\InvalidPath - */ - public function testGetChildThrowInvalidPath() { - $this->view->expects($this->once()) - ->method('verifyPath') - ->willThrowException(new \OCP\Files\InvalidPathException()); - $this->view->expects($this->never()) - ->method('getFileInfo'); - - $dir = new \OCA\DAV\Connector\Sabre\Directory($this->view, $this->info); - $dir->getChild('.'); - } - - public function testGetQuotaInfoUnlimited() { - $storage = $this->getMockBuilder('\OC\Files\Storage\Wrapper\Quota') - ->disableOriginalConstructor() - ->getMock(); - - $storage->expects($this->any()) - ->method('instanceOfStorage') - ->will($this->returnValueMap([ - '\OC\Files\Storage\Shared' => false, - '\OC\Files\Storage\Wrapper\Quota' => false, - ])); - - $storage->expects($this->never()) - ->method('getQuota'); - - $storage->expects($this->once()) - ->method('free_space') - ->will($this->returnValue(800)); - - $this->info->expects($this->once()) - ->method('getSize') - ->will($this->returnValue(200)); - - $this->info->expects($this->once()) - ->method('getStorage') - ->will($this->returnValue($storage)); - - $dir = new \OCA\DAV\Connector\Sabre\Directory($this->view, $this->info); - $this->assertEquals([200, -3], $dir->getQuotaInfo()); //200 used, unlimited - } - - public function testGetQuotaInfoSpecific() { - $storage = $this->getMockBuilder('\OC\Files\Storage\Wrapper\Quota') - ->disableOriginalConstructor() - ->getMock(); - - $storage->expects($this->any()) - ->method('instanceOfStorage') - ->will($this->returnValueMap([ - ['\OC\Files\Storage\Shared', false], - ['\OC\Files\Storage\Wrapper\Quota', true], - ])); - - $storage->expects($this->once()) - ->method('getQuota') - ->will($this->returnValue(1000)); - - $storage->expects($this->once()) - ->method('free_space') - ->will($this->returnValue(800)); - - $this->info->expects($this->once()) - ->method('getSize') - ->will($this->returnValue(200)); - - $this->info->expects($this->once()) - ->method('getStorage') - ->will($this->returnValue($storage)); - - $dir = new \OCA\DAV\Connector\Sabre\Directory($this->view, $this->info); - $this->assertEquals([200, 800], $dir->getQuotaInfo()); //200 used, 800 free - } -} diff --git a/apps/dav/tests/unit/connector/sabre/exception/forbiddentest.php b/apps/dav/tests/unit/connector/sabre/exception/forbiddentest.php deleted file mode 100644 index 36ea97df9f7..00000000000 --- a/apps/dav/tests/unit/connector/sabre/exception/forbiddentest.php +++ /dev/null @@ -1,57 +0,0 @@ -<?php -/** - * @author Joas Schilling <nickvergessen@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\Connector\Sabre\Exception; - -use OCA\DAV\Connector\Sabre\Exception\Forbidden; - -class ForbiddenTest extends \Test\TestCase { - - public function testSerialization() { - - // create xml doc - $DOM = new \DOMDocument('1.0','utf-8'); - $DOM->formatOutput = true; - $error = $DOM->createElementNS('DAV:','d:error'); - $error->setAttribute('xmlns:s', \Sabre\DAV\Server::NS_SABREDAV); - $DOM->appendChild($error); - - // serialize the exception - $message = "1234567890"; - $retry = false; - $expectedXml = <<<EOD -<?xml version="1.0" encoding="utf-8"?> -<d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:o="http://owncloud.org/ns"> - <o:retry xmlns:o="o:">false</o:retry> - <o:reason xmlns:o="o:">1234567890</o:reason> -</d:error> - -EOD; - - $ex = new Forbidden($message, $retry); - $server = $this->getMock('Sabre\DAV\Server'); - $ex->serialize($server, $error); - - // assert - $xml = $DOM->saveXML(); - $this->assertEquals($expectedXml, $xml); - } -} diff --git a/apps/dav/tests/unit/connector/sabre/exception/invalidpathtest.php b/apps/dav/tests/unit/connector/sabre/exception/invalidpathtest.php deleted file mode 100644 index 431a0484d65..00000000000 --- a/apps/dav/tests/unit/connector/sabre/exception/invalidpathtest.php +++ /dev/null @@ -1,58 +0,0 @@ -<?php -/** - * @author Joas Schilling <nickvergessen@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\Connector\Sabre\Exception; - -use OCA\DAV\Connector\Sabre\Exception\InvalidPath; - -class InvalidPathTest extends \Test\TestCase { - - public function testSerialization() { - - // create xml doc - $DOM = new \DOMDocument('1.0','utf-8'); - $DOM->formatOutput = true; - $error = $DOM->createElementNS('DAV:','d:error'); - $error->setAttribute('xmlns:s', \Sabre\DAV\Server::NS_SABREDAV); - $DOM->appendChild($error); - - // serialize the exception - $message = "1234567890"; - $retry = false; - $expectedXml = <<<EOD -<?xml version="1.0" encoding="utf-8"?> -<d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:o="http://owncloud.org/ns"> - <o:retry xmlns:o="o:">false</o:retry> - <o:reason xmlns:o="o:">1234567890</o:reason> -</d:error> - -EOD; - - $ex = new InvalidPath($message, $retry); - $server = $this->getMock('Sabre\DAV\Server'); - $ex->serialize($server, $error); - - // assert - $xml = $DOM->saveXML(); - $this->assertEquals($expectedXml, $xml); - } -} diff --git a/apps/dav/tests/unit/connector/sabre/exceptionloggerplugin.php b/apps/dav/tests/unit/connector/sabre/exceptionloggerplugin.php deleted file mode 100644 index b76285be336..00000000000 --- a/apps/dav/tests/unit/connector/sabre/exceptionloggerplugin.php +++ /dev/null @@ -1,83 +0,0 @@ -<?php -/** - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\Connector\Sabre; - -use OCA\DAV\Connector\Sabre\Exception\InvalidPath; -use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin as PluginToTest; -use OC\Log; -use OCP\ILogger; -use PHPUnit_Framework_MockObject_MockObject; -use Sabre\DAV\Exception\NotFound; -use Sabre\DAV\Server; -use Test\TestCase; - -class TestLogger extends Log { - public $message; - public $level; - - public function __construct($logger = null) { - //disable original constructor - } - - public function log($level, $message, array $context = array()) { - $this->level = $level; - $this->message = $message; - } -} - -class ExceptionLoggerPlugin extends TestCase { - - /** @var Server */ - private $server; - - /** @var PluginToTest */ - private $plugin; - - /** @var TestLogger | PHPUnit_Framework_MockObject_MockObject */ - private $logger; - - private function init() { - $this->server = new Server(); - $this->logger = new TestLogger(); - $this->plugin = new PluginToTest('unit-test', $this->logger); - $this->plugin->initialize($this->server); - } - - /** - * @dataProvider providesExceptions - */ - public function testLogging($expectedLogLevel, $expectedMessage, $exception) { - $this->init(); - $this->plugin->logException($exception); - - $this->assertEquals($expectedLogLevel, $this->logger->level); - $this->assertStringStartsWith('Exception: {"Message":"' . $expectedMessage, $this->logger->message); - } - - public function providesExceptions() { - return [ - [0, 'HTTP\/1.1 404 Not Found', new NotFound()], - [4, 'HTTP\/1.1 400 This path leads to nowhere', new InvalidPath('This path leads to nowhere')] - ]; - } - -} diff --git a/apps/dav/tests/unit/connector/sabre/file.php b/apps/dav/tests/unit/connector/sabre/file.php deleted file mode 100644 index eab7ece159c..00000000000 --- a/apps/dav/tests/unit/connector/sabre/file.php +++ /dev/null @@ -1,987 +0,0 @@ -<?php -/** - * @author Joas Schilling <nickvergessen@owncloud.com> - * @author Robin Appelman <icewind@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\Connector\Sabre; - -use OC\Files\Storage\Local; -use OCP\Files\ForbiddenException; -use Test\HookHelper; -use OC\Files\Filesystem; -use OCP\Lock\ILockingProvider; - -/** - * Class File - * - * @group DB - * - * @package Test\Connector\Sabre - */ -class File extends \Test\TestCase { - - /** - * @var string - */ - private $user; - - public function setUp() { - parent::setUp(); - - \OC_Hook::clear(); - - $this->user = $this->getUniqueID('user_'); - $userManager = \OC::$server->getUserManager(); - $userManager->createUser($this->user, 'pass'); - - $this->loginAsUser($this->user); - } - - public function tearDown() { - $userManager = \OC::$server->getUserManager(); - $userManager->get($this->user)->delete(); - unset($_SERVER['HTTP_OC_CHUNKED']); - - parent::tearDown(); - } - - private function getMockStorage() { - $storage = $this->getMock('\OCP\Files\Storage'); - $storage->expects($this->any()) - ->method('getId') - ->will($this->returnValue('home::someuser')); - return $storage; - } - - /** - * @param string $string - */ - private function getStream($string) { - $stream = fopen('php://temp', 'r+'); - fwrite($stream, $string); - fseek($stream, 0); - return $stream; - } - - - public function fopenFailuresProvider() { - return [ - [ - // return false - null, - '\Sabre\Dav\Exception', - false - ], - [ - new \OCP\Files\NotPermittedException(), - 'Sabre\DAV\Exception\Forbidden' - ], - [ - new \OCP\Files\EntityTooLargeException(), - 'OCA\DAV\Connector\Sabre\Exception\EntityTooLarge' - ], - [ - new \OCP\Files\InvalidContentException(), - 'OCA\DAV\Connector\Sabre\Exception\UnsupportedMediaType' - ], - [ - new \OCP\Files\InvalidPathException(), - 'Sabre\DAV\Exception\Forbidden' - ], - [ - new \OCP\Files\ForbiddenException('', true), - 'OCA\DAV\Connector\Sabre\Exception\Forbidden' - ], - [ - new \OCP\Files\LockNotAcquiredException('/test.txt', 1), - 'OCA\DAV\Connector\Sabre\Exception\FileLocked' - ], - [ - new \OCP\Lock\LockedException('/test.txt'), - 'OCA\DAV\Connector\Sabre\Exception\FileLocked' - ], - [ - new \OCP\Encryption\Exceptions\GenericEncryptionException(), - 'Sabre\DAV\Exception\ServiceUnavailable' - ], - [ - new \OCP\Files\StorageNotAvailableException(), - 'Sabre\DAV\Exception\ServiceUnavailable' - ], - [ - new \Sabre\DAV\Exception('Generic sabre exception'), - 'Sabre\DAV\Exception', - false - ], - [ - new \Exception('Generic exception'), - 'Sabre\DAV\Exception' - ], - ]; - } - - /** - * @dataProvider fopenFailuresProvider - */ - public function testSimplePutFails($thrownException, $expectedException, $checkPreviousClass = true) { - // setup - $storage = $this->getMock( - '\OC\Files\Storage\Local', - ['fopen'], - [['datadir' => \OC::$server->getTempManager()->getTemporaryFolder()]] - ); - \OC\Files\Filesystem::mount($storage, [], $this->user . '/'); - $view = $this->getMock('\OC\Files\View', array('getRelativePath', 'resolvePath'), array()); - $view->expects($this->atLeastOnce()) - ->method('resolvePath') - ->will($this->returnCallback( - function ($path) use ($storage) { - return [$storage, $path]; - } - )); - - if ($thrownException !== null) { - $storage->expects($this->once()) - ->method('fopen') - ->will($this->throwException($thrownException)); - } else { - $storage->expects($this->once()) - ->method('fopen') - ->will($this->returnValue(false)); - } - - $view->expects($this->any()) - ->method('getRelativePath') - ->will($this->returnArgument(0)); - - $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, array( - 'permissions' => \OCP\Constants::PERMISSION_ALL - ), null); - - $file = new \OCA\DAV\Connector\Sabre\File($view, $info); - - // action - $caughtException = null; - try { - $file->put('test data'); - } catch (\Exception $e) { - $caughtException = $e; - } - - $this->assertInstanceOf($expectedException, $caughtException); - if ($checkPreviousClass) { - $this->assertInstanceOf(get_class($thrownException), $caughtException->getPrevious()); - } - - $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files'); - } - - /** - * Test putting a file using chunking - * - * @dataProvider fopenFailuresProvider - */ - public function testChunkedPutFails($thrownException, $expectedException, $checkPreviousClass = false) { - // setup - $storage = $this->getMock( - '\OC\Files\Storage\Local', - ['fopen'], - [['datadir' => \OC::$server->getTempManager()->getTemporaryFolder()]] - ); - \OC\Files\Filesystem::mount($storage, [], $this->user . '/'); - $view = $this->getMock('\OC\Files\View', ['getRelativePath', 'resolvePath'], []); - $view->expects($this->atLeastOnce()) - ->method('resolvePath') - ->will($this->returnCallback( - function ($path) use ($storage) { - return [$storage, $path]; - } - )); - - if ($thrownException !== null) { - $storage->expects($this->once()) - ->method('fopen') - ->will($this->throwException($thrownException)); - } else { - $storage->expects($this->once()) - ->method('fopen') - ->will($this->returnValue(false)); - } - - $view->expects($this->any()) - ->method('getRelativePath') - ->will($this->returnArgument(0)); - - $_SERVER['HTTP_OC_CHUNKED'] = true; - - $info = new \OC\Files\FileInfo('/test.txt-chunking-12345-2-0', $this->getMockStorage(), null, [ - 'permissions' => \OCP\Constants::PERMISSION_ALL - ], null); - $file = new \OCA\DAV\Connector\Sabre\File($view, $info); - - // put first chunk - $file->acquireLock(ILockingProvider::LOCK_SHARED); - $this->assertNull($file->put('test data one')); - $file->releaseLock(ILockingProvider::LOCK_SHARED); - - $info = new \OC\Files\FileInfo('/test.txt-chunking-12345-2-1', $this->getMockStorage(), null, [ - 'permissions' => \OCP\Constants::PERMISSION_ALL - ], null); - $file = new \OCA\DAV\Connector\Sabre\File($view, $info); - - // action - $caughtException = null; - try { - // last chunk - $file->acquireLock(ILockingProvider::LOCK_SHARED); - $file->put('test data two'); - $file->releaseLock(ILockingProvider::LOCK_SHARED); - } catch (\Exception $e) { - $caughtException = $e; - } - - $this->assertInstanceOf($expectedException, $caughtException); - if ($checkPreviousClass) { - $this->assertInstanceOf(get_class($thrownException), $caughtException->getPrevious()); - } - - $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files'); - } - - /** - * Simulate putting a file to the given path. - * - * @param string $path path to put the file into - * @param string $viewRoot root to use for the view - * - * @return null|string of the PUT operaiton which is usually the etag - */ - private function doPut($path, $viewRoot = null) { - $view = \OC\Files\Filesystem::getView(); - if (!is_null($viewRoot)) { - $view = new \OC\Files\View($viewRoot); - } else { - $viewRoot = '/' . $this->user . '/files'; - } - - $info = new \OC\Files\FileInfo( - $viewRoot . '/' . ltrim($path, '/'), - $this->getMockStorage(), - null, - ['permissions' => \OCP\Constants::PERMISSION_ALL], - null - ); - - $file = new \OCA\DAV\Connector\Sabre\File($view, $info); - - // beforeMethod locks - $view->lockFile($path, ILockingProvider::LOCK_SHARED); - - $result = $file->put($this->getStream('test data')); - - // afterMethod unlocks - $view->unlockFile($path, ILockingProvider::LOCK_SHARED); - - return $result; - } - - /** - * Test putting a single file - */ - public function testPutSingleFile() { - $this->assertNotEmpty($this->doPut('/foo.txt')); - } - - /** - * Test putting a file using chunking - */ - public function testChunkedPut() { - $_SERVER['HTTP_OC_CHUNKED'] = true; - $this->assertNull($this->doPut('/test.txt-chunking-12345-2-0')); - $this->assertNotEmpty($this->doPut('/test.txt-chunking-12345-2-1')); - } - - /** - * Test that putting a file triggers create hooks - */ - public function testPutSingleFileTriggersHooks() { - HookHelper::setUpHooks(); - - $this->assertNotEmpty($this->doPut('/foo.txt')); - - $this->assertCount(4, HookHelper::$hookCalls); - $this->assertHookCall( - HookHelper::$hookCalls[0], - Filesystem::signal_create, - '/foo.txt' - ); - $this->assertHookCall( - HookHelper::$hookCalls[1], - Filesystem::signal_write, - '/foo.txt' - ); - $this->assertHookCall( - HookHelper::$hookCalls[2], - Filesystem::signal_post_create, - '/foo.txt' - ); - $this->assertHookCall( - HookHelper::$hookCalls[3], - Filesystem::signal_post_write, - '/foo.txt' - ); - } - - /** - * Test that putting a file triggers update hooks - */ - public function testPutOverwriteFileTriggersHooks() { - $view = \OC\Files\Filesystem::getView(); - $view->file_put_contents('/foo.txt', 'some content that will be replaced'); - - HookHelper::setUpHooks(); - - $this->assertNotEmpty($this->doPut('/foo.txt')); - - $this->assertCount(4, HookHelper::$hookCalls); - $this->assertHookCall( - HookHelper::$hookCalls[0], - Filesystem::signal_update, - '/foo.txt' - ); - $this->assertHookCall( - HookHelper::$hookCalls[1], - Filesystem::signal_write, - '/foo.txt' - ); - $this->assertHookCall( - HookHelper::$hookCalls[2], - Filesystem::signal_post_update, - '/foo.txt' - ); - $this->assertHookCall( - HookHelper::$hookCalls[3], - Filesystem::signal_post_write, - '/foo.txt' - ); - } - - /** - * Test that putting a file triggers hooks with the correct path - * if the passed view was chrooted (can happen with public webdav - * where the root is the share root) - */ - public function testPutSingleFileTriggersHooksDifferentRoot() { - $view = \OC\Files\Filesystem::getView(); - $view->mkdir('noderoot'); - - HookHelper::setUpHooks(); - - // happens with public webdav where the view root is the share root - $this->assertNotEmpty($this->doPut('/foo.txt', '/' . $this->user . '/files/noderoot')); - - $this->assertCount(4, HookHelper::$hookCalls); - $this->assertHookCall( - HookHelper::$hookCalls[0], - Filesystem::signal_create, - '/noderoot/foo.txt' - ); - $this->assertHookCall( - HookHelper::$hookCalls[1], - Filesystem::signal_write, - '/noderoot/foo.txt' - ); - $this->assertHookCall( - HookHelper::$hookCalls[2], - Filesystem::signal_post_create, - '/noderoot/foo.txt' - ); - $this->assertHookCall( - HookHelper::$hookCalls[3], - Filesystem::signal_post_write, - '/noderoot/foo.txt' - ); - } - - /** - * Test that putting a file with chunks triggers create hooks - */ - public function testPutChunkedFileTriggersHooks() { - HookHelper::setUpHooks(); - - $_SERVER['HTTP_OC_CHUNKED'] = true; - $this->assertNull($this->doPut('/foo.txt-chunking-12345-2-0')); - $this->assertNotEmpty($this->doPut('/foo.txt-chunking-12345-2-1')); - - $this->assertCount(4, HookHelper::$hookCalls); - $this->assertHookCall( - HookHelper::$hookCalls[0], - Filesystem::signal_create, - '/foo.txt' - ); - $this->assertHookCall( - HookHelper::$hookCalls[1], - Filesystem::signal_write, - '/foo.txt' - ); - $this->assertHookCall( - HookHelper::$hookCalls[2], - Filesystem::signal_post_create, - '/foo.txt' - ); - $this->assertHookCall( - HookHelper::$hookCalls[3], - Filesystem::signal_post_write, - '/foo.txt' - ); - } - - /** - * Test that putting a chunked file triggers update hooks - */ - public function testPutOverwriteChunkedFileTriggersHooks() { - $view = \OC\Files\Filesystem::getView(); - $view->file_put_contents('/foo.txt', 'some content that will be replaced'); - - HookHelper::setUpHooks(); - - $_SERVER['HTTP_OC_CHUNKED'] = true; - $this->assertNull($this->doPut('/foo.txt-chunking-12345-2-0')); - $this->assertNotEmpty($this->doPut('/foo.txt-chunking-12345-2-1')); - - $this->assertCount(4, HookHelper::$hookCalls); - $this->assertHookCall( - HookHelper::$hookCalls[0], - Filesystem::signal_update, - '/foo.txt' - ); - $this->assertHookCall( - HookHelper::$hookCalls[1], - Filesystem::signal_write, - '/foo.txt' - ); - $this->assertHookCall( - HookHelper::$hookCalls[2], - Filesystem::signal_post_update, - '/foo.txt' - ); - $this->assertHookCall( - HookHelper::$hookCalls[3], - Filesystem::signal_post_write, - '/foo.txt' - ); - } - - public static function cancellingHook($params) { - self::$hookCalls[] = array( - 'signal' => Filesystem::signal_post_create, - 'params' => $params - ); - } - - /** - * Test put file with cancelled hook - */ - public function testPutSingleFileCancelPreHook() { - \OCP\Util::connectHook( - Filesystem::CLASSNAME, - Filesystem::signal_create, - '\Test\HookHelper', - 'cancellingCallback' - ); - - // action - $thrown = false; - try { - $this->doPut('/foo.txt'); - } catch (\Sabre\DAV\Exception $e) { - $thrown = true; - } - - $this->assertTrue($thrown); - $this->assertEmpty($this->listPartFiles(), 'No stray part files'); - } - - /** - * Test exception when the uploaded size did not match - */ - public function testSimplePutFailsSizeCheck() { - // setup - $view = $this->getMock('\OC\Files\View', - array('rename', 'getRelativePath', 'filesize')); - $view->expects($this->any()) - ->method('rename') - ->withAnyParameters() - ->will($this->returnValue(false)); - $view->expects($this->any()) - ->method('getRelativePath') - ->will($this->returnArgument(0)); - - $view->expects($this->any()) - ->method('filesize') - ->will($this->returnValue(123456)); - - $_SERVER['CONTENT_LENGTH'] = 123456; - $_SERVER['REQUEST_METHOD'] = 'PUT'; - - $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, array( - 'permissions' => \OCP\Constants::PERMISSION_ALL - ), null); - - $file = new \OCA\DAV\Connector\Sabre\File($view, $info); - - // action - $thrown = false; - try { - // beforeMethod locks - $file->acquireLock(ILockingProvider::LOCK_SHARED); - - $file->put($this->getStream('test data')); - - // afterMethod unlocks - $file->releaseLock(ILockingProvider::LOCK_SHARED); - } catch (\Sabre\DAV\Exception\BadRequest $e) { - $thrown = true; - } - - $this->assertTrue($thrown); - $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files'); - } - - /** - * Test exception during final rename in simple upload mode - */ - public function testSimplePutFailsMoveFromStorage() { - $view = new \OC\Files\View('/' . $this->user . '/files'); - - // simulate situation where the target file is locked - $view->lockFile('/test.txt', ILockingProvider::LOCK_EXCLUSIVE); - - $info = new \OC\Files\FileInfo('/' . $this->user . '/files/test.txt', $this->getMockStorage(), null, array( - 'permissions' => \OCP\Constants::PERMISSION_ALL - ), null); - - $file = new \OCA\DAV\Connector\Sabre\File($view, $info); - - // action - $thrown = false; - try { - // beforeMethod locks - $view->lockFile($info->getPath(), ILockingProvider::LOCK_SHARED); - - $file->put($this->getStream('test data')); - - // afterMethod unlocks - $view->unlockFile($info->getPath(), ILockingProvider::LOCK_SHARED); - } catch (\OCA\DAV\Connector\Sabre\Exception\FileLocked $e) { - $thrown = true; - } - - $this->assertTrue($thrown); - $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files'); - } - - /** - * Test exception during final rename in chunk upload mode - */ - public function testChunkedPutFailsFinalRename() { - $view = new \OC\Files\View('/' . $this->user . '/files'); - - // simulate situation where the target file is locked - $view->lockFile('/test.txt', ILockingProvider::LOCK_EXCLUSIVE); - - $_SERVER['HTTP_OC_CHUNKED'] = true; - - $info = new \OC\Files\FileInfo('/' . $this->user . '/files/test.txt-chunking-12345-2-0', $this->getMockStorage(), null, [ - 'permissions' => \OCP\Constants::PERMISSION_ALL - ], null); - $file = new \OCA\DAV\Connector\Sabre\File($view, $info); - $file->acquireLock(ILockingProvider::LOCK_SHARED); - $this->assertNull($file->put('test data one')); - $file->releaseLock(ILockingProvider::LOCK_SHARED); - - $info = new \OC\Files\FileInfo('/' . $this->user . '/files/test.txt-chunking-12345-2-1', $this->getMockStorage(), null, [ - 'permissions' => \OCP\Constants::PERMISSION_ALL - ], null); - $file = new \OCA\DAV\Connector\Sabre\File($view, $info); - - // action - $thrown = false; - try { - $file->acquireLock(ILockingProvider::LOCK_SHARED); - $file->put($this->getStream('test data')); - $file->releaseLock(ILockingProvider::LOCK_SHARED); - } catch (\OCA\DAV\Connector\Sabre\Exception\FileLocked $e) { - $thrown = true; - } - - $this->assertTrue($thrown); - $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files'); - } - - /** - * Test put file with invalid chars - */ - public function testSimplePutInvalidChars() { - // setup - $view = $this->getMock('\OC\Files\View', array('getRelativePath')); - $view->expects($this->any()) - ->method('getRelativePath') - ->will($this->returnArgument(0)); - - $info = new \OC\Files\FileInfo('/*', $this->getMockStorage(), null, array( - 'permissions' => \OCP\Constants::PERMISSION_ALL - ), null); - $file = new \OCA\DAV\Connector\Sabre\File($view, $info); - - // action - $thrown = false; - try { - // beforeMethod locks - $view->lockFile($info->getPath(), ILockingProvider::LOCK_SHARED); - - $file->put($this->getStream('test data')); - - // afterMethod unlocks - $view->unlockFile($info->getPath(), ILockingProvider::LOCK_SHARED); - } catch (\OCA\DAV\Connector\Sabre\Exception\InvalidPath $e) { - $thrown = true; - } - - $this->assertTrue($thrown); - $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files'); - } - - /** - * Test setting name with setName() with invalid chars - * - * @expectedException \OCA\DAV\Connector\Sabre\Exception\InvalidPath - */ - public function testSetNameInvalidChars() { - // setup - $view = $this->getMock('\OC\Files\View', array('getRelativePath')); - - $view->expects($this->any()) - ->method('getRelativePath') - ->will($this->returnArgument(0)); - - $info = new \OC\Files\FileInfo('/*', $this->getMockStorage(), null, array( - 'permissions' => \OCP\Constants::PERMISSION_ALL - ), null); - $file = new \OCA\DAV\Connector\Sabre\File($view, $info); - $file->setName('/super*star.txt'); - } - - /** - */ - public function testUploadAbort() { - // setup - $view = $this->getMock('\OC\Files\View', - array('rename', 'getRelativePath', 'filesize')); - $view->expects($this->any()) - ->method('rename') - ->withAnyParameters() - ->will($this->returnValue(false)); - $view->expects($this->any()) - ->method('getRelativePath') - ->will($this->returnArgument(0)); - $view->expects($this->any()) - ->method('filesize') - ->will($this->returnValue(123456)); - - $_SERVER['CONTENT_LENGTH'] = 12345; - $_SERVER['REQUEST_METHOD'] = 'PUT'; - - $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, array( - 'permissions' => \OCP\Constants::PERMISSION_ALL - ), null); - - $file = new \OCA\DAV\Connector\Sabre\File($view, $info); - - // action - $thrown = false; - try { - // beforeMethod locks - $view->lockFile($info->getPath(), ILockingProvider::LOCK_SHARED); - - $file->put($this->getStream('test data')); - - // afterMethod unlocks - $view->unlockFile($info->getPath(), ILockingProvider::LOCK_SHARED); - } catch (\Sabre\DAV\Exception\BadRequest $e) { - $thrown = true; - } - - $this->assertTrue($thrown); - $this->assertEmpty($this->listPartFiles($view, ''), 'No stray part files'); - } - - /** - * - */ - public function testDeleteWhenAllowed() { - // setup - $view = $this->getMock('\OC\Files\View', - array()); - - $view->expects($this->once()) - ->method('unlink') - ->will($this->returnValue(true)); - - $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, array( - 'permissions' => \OCP\Constants::PERMISSION_ALL - ), null); - - $file = new \OCA\DAV\Connector\Sabre\File($view, $info); - - // action - $file->delete(); - } - - /** - * @expectedException \Sabre\DAV\Exception\Forbidden - */ - public function testDeleteThrowsWhenDeletionNotAllowed() { - // setup - $view = $this->getMock('\OC\Files\View', - array()); - - $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, array( - 'permissions' => 0 - ), null); - - $file = new \OCA\DAV\Connector\Sabre\File($view, $info); - - // action - $file->delete(); - } - - /** - * @expectedException \Sabre\DAV\Exception\Forbidden - */ - public function testDeleteThrowsWhenDeletionFailed() { - // setup - $view = $this->getMock('\OC\Files\View', - array()); - - // but fails - $view->expects($this->once()) - ->method('unlink') - ->will($this->returnValue(false)); - - $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, array( - 'permissions' => \OCP\Constants::PERMISSION_ALL - ), null); - - $file = new \OCA\DAV\Connector\Sabre\File($view, $info); - - // action - $file->delete(); - } - - /** - * @expectedException \OCA\DAV\Connector\Sabre\Exception\Forbidden - */ - public function testDeleteThrowsWhenDeletionThrows() { - // setup - $view = $this->getMock('\OC\Files\View', - array()); - - // but fails - $view->expects($this->once()) - ->method('unlink') - ->willThrowException(new ForbiddenException('', true)); - - $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, array( - 'permissions' => \OCP\Constants::PERMISSION_ALL - ), null); - - $file = new \OCA\DAV\Connector\Sabre\File($view, $info); - - // action - $file->delete(); - } - - /** - * Asserts hook call - * - * @param array $callData hook call data to check - * @param string $signal signal name - * @param string $hookPath hook path - */ - protected function assertHookCall($callData, $signal, $hookPath) { - $this->assertEquals($signal, $callData['signal']); - $params = $callData['params']; - $this->assertEquals( - $hookPath, - $params[Filesystem::signal_param_path] - ); - } - - /** - * Test whether locks are set before and after the operation - */ - public function testPutLocking() { - $view = new \OC\Files\View('/' . $this->user . '/files/'); - - $path = 'test-locking.txt'; - $info = new \OC\Files\FileInfo( - '/' . $this->user . '/files/' . $path, - $this->getMockStorage(), - null, - ['permissions' => \OCP\Constants::PERMISSION_ALL], - null - ); - - $file = new \OCA\DAV\Connector\Sabre\File($view, $info); - - $this->assertFalse( - $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_SHARED), - 'File unlocked before put' - ); - $this->assertFalse( - $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE), - 'File unlocked before put' - ); - - $wasLockedPre = false; - $wasLockedPost = false; - $eventHandler = $this->getMockBuilder('\stdclass') - ->setMethods(['writeCallback', 'postWriteCallback']) - ->getMock(); - - // both pre and post hooks might need access to the file, - // so only shared lock is acceptable - $eventHandler->expects($this->once()) - ->method('writeCallback') - ->will($this->returnCallback( - function () use ($view, $path, &$wasLockedPre) { - $wasLockedPre = $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_SHARED); - $wasLockedPre = $wasLockedPre && !$this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE); - } - )); - $eventHandler->expects($this->once()) - ->method('postWriteCallback') - ->will($this->returnCallback( - function () use ($view, $path, &$wasLockedPost) { - $wasLockedPost = $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_SHARED); - $wasLockedPost = $wasLockedPost && !$this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE); - } - )); - - \OCP\Util::connectHook( - Filesystem::CLASSNAME, - Filesystem::signal_write, - $eventHandler, - 'writeCallback' - ); - \OCP\Util::connectHook( - Filesystem::CLASSNAME, - Filesystem::signal_post_write, - $eventHandler, - 'postWriteCallback' - ); - - // beforeMethod locks - $view->lockFile($path, ILockingProvider::LOCK_SHARED); - - $this->assertNotEmpty($file->put($this->getStream('test data'))); - - // afterMethod unlocks - $view->unlockFile($path, ILockingProvider::LOCK_SHARED); - - $this->assertTrue($wasLockedPre, 'File was locked during pre-hooks'); - $this->assertTrue($wasLockedPost, 'File was locked during post-hooks'); - - $this->assertFalse( - $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_SHARED), - 'File unlocked after put' - ); - $this->assertFalse( - $this->isFileLocked($view, $path, \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE), - 'File unlocked after put' - ); - } - - /** - * Returns part files in the given path - * - * @param \OC\Files\View view which root is the current user's "files" folder - * @param string $path path for which to list part files - * - * @return array list of part files - */ - private function listPartFiles(\OC\Files\View $userView = null, $path = '') { - if ($userView === null) { - $userView = \OC\Files\Filesystem::getView(); - } - $files = []; - list($storage, $internalPath) = $userView->resolvePath($path); - if($storage instanceof Local) { - $realPath = $storage->getSourcePath($internalPath); - $dh = opendir($realPath); - while (($file = readdir($dh)) !== false) { - if (substr($file, strlen($file) - 5, 5) === '.part') { - $files[] = $file; - } - } - closedir($dh); - } - return $files; - } - - /** - * @expectedException \Sabre\DAV\Exception\ServiceUnavailable - */ - public function testGetFopenFails() { - $view = $this->getMock('\OC\Files\View', ['fopen'], array()); - $view->expects($this->atLeastOnce()) - ->method('fopen') - ->will($this->returnValue(false)); - - $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, array( - 'permissions' => \OCP\Constants::PERMISSION_ALL - ), null); - - $file = new \OCA\DAV\Connector\Sabre\File($view, $info); - - $file->get(); - } - - /** - * @expectedException \OCA\DAV\Connector\Sabre\Exception\Forbidden - */ - public function testGetFopenThrows() { - $view = $this->getMock('\OC\Files\View', ['fopen'], array()); - $view->expects($this->atLeastOnce()) - ->method('fopen') - ->willThrowException(new ForbiddenException('', true)); - - $info = new \OC\Files\FileInfo('/test.txt', $this->getMockStorage(), null, array( - 'permissions' => \OCP\Constants::PERMISSION_ALL - ), null); - - $file = new \OCA\DAV\Connector\Sabre\File($view, $info); - - $file->get(); - } -} diff --git a/apps/dav/tests/unit/connector/sabre/filesplugin.php b/apps/dav/tests/unit/connector/sabre/filesplugin.php deleted file mode 100644 index e88066a12da..00000000000 --- a/apps/dav/tests/unit/connector/sabre/filesplugin.php +++ /dev/null @@ -1,419 +0,0 @@ -<?php -/** - * @author Roeland Jago Douma <rullzer@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace OCA\DAV\Tests\Unit\Connector\Sabre; - -use OCP\Files\StorageNotAvailableException; - -/** - * Copyright (c) 2015 Vincent Petry <pvince81@owncloud.com> - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. - */ -class FilesPlugin extends \Test\TestCase { - const GETETAG_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::GETETAG_PROPERTYNAME; - const FILEID_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::FILEID_PROPERTYNAME; - const INTERNAL_FILEID_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::INTERNAL_FILEID_PROPERTYNAME; - const SIZE_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::SIZE_PROPERTYNAME; - const PERMISSIONS_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::PERMISSIONS_PROPERTYNAME; - const LASTMODIFIED_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::LASTMODIFIED_PROPERTYNAME; - const DOWNLOADURL_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::DOWNLOADURL_PROPERTYNAME; - const OWNER_ID_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::OWNER_ID_PROPERTYNAME; - const OWNER_DISPLAY_NAME_PROPERTYNAME = \OCA\DAV\Connector\Sabre\FilesPlugin::OWNER_DISPLAY_NAME_PROPERTYNAME; - - /** - * @var \Sabre\DAV\Server - */ - private $server; - - /** - * @var \Sabre\DAV\Tree - */ - private $tree; - - /** - * @var \OCA\DAV\Connector\Sabre\FilesPlugin - */ - private $plugin; - - /** - * @var \OC\Files\View - */ - private $view; - - public function setUp() { - parent::setUp(); - $this->server = $this->getMockBuilder('\Sabre\DAV\Server') - ->disableOriginalConstructor() - ->getMock(); - $this->tree = $this->getMockBuilder('\Sabre\DAV\Tree') - ->disableOriginalConstructor() - ->getMock(); - $this->view = $this->getMockBuilder('\OC\Files\View') - ->disableOriginalConstructor() - ->getMock(); - - $this->plugin = new \OCA\DAV\Connector\Sabre\FilesPlugin($this->tree, $this->view); - $this->plugin->initialize($this->server); - } - - /** - * @param string $class - */ - private function createTestNode($class) { - $node = $this->getMockBuilder($class) - ->disableOriginalConstructor() - ->getMock(); - $node->expects($this->any()) - ->method('getId') - ->will($this->returnValue(123)); - - $this->tree->expects($this->any()) - ->method('getNodeForPath') - ->with('/dummypath') - ->will($this->returnValue($node)); - - $node->expects($this->any()) - ->method('getFileId') - ->will($this->returnValue('00000123instanceid')); - $node->expects($this->any()) - ->method('getInternalFileId') - ->will($this->returnValue('123')); - $node->expects($this->any()) - ->method('getEtag') - ->will($this->returnValue('"abc"')); - $node->expects($this->any()) - ->method('getDavPermissions') - ->will($this->returnValue('DWCKMSR')); - - return $node; - } - - public function testGetPropertiesForFile() { - $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File'); - - $propFind = new \Sabre\DAV\PropFind( - '/dummyPath', - array( - self::GETETAG_PROPERTYNAME, - self::FILEID_PROPERTYNAME, - self::INTERNAL_FILEID_PROPERTYNAME, - self::SIZE_PROPERTYNAME, - self::PERMISSIONS_PROPERTYNAME, - self::DOWNLOADURL_PROPERTYNAME, - self::OWNER_ID_PROPERTYNAME, - self::OWNER_DISPLAY_NAME_PROPERTYNAME - ), - 0 - ); - - $user = $this->getMockBuilder('\OC\User\User') - ->disableOriginalConstructor()->getMock(); - $user - ->expects($this->once()) - ->method('getUID') - ->will($this->returnValue('foo')); - $user - ->expects($this->once()) - ->method('getDisplayName') - ->will($this->returnValue('M. Foo')); - - $node->expects($this->once()) - ->method('getDirectDownload') - ->will($this->returnValue(array('url' => 'http://example.com/'))); - $node->expects($this->exactly(2)) - ->method('getOwner') - ->will($this->returnValue($user)); - $node->expects($this->never()) - ->method('getSize'); - - $this->plugin->handleGetProperties( - $propFind, - $node - ); - - $this->assertEquals('"abc"', $propFind->get(self::GETETAG_PROPERTYNAME)); - $this->assertEquals('00000123instanceid', $propFind->get(self::FILEID_PROPERTYNAME)); - $this->assertEquals('123', $propFind->get(self::INTERNAL_FILEID_PROPERTYNAME)); - $this->assertEquals(null, $propFind->get(self::SIZE_PROPERTYNAME)); - $this->assertEquals('DWCKMSR', $propFind->get(self::PERMISSIONS_PROPERTYNAME)); - $this->assertEquals('http://example.com/', $propFind->get(self::DOWNLOADURL_PROPERTYNAME)); - $this->assertEquals('foo', $propFind->get(self::OWNER_ID_PROPERTYNAME)); - $this->assertEquals('M. Foo', $propFind->get(self::OWNER_DISPLAY_NAME_PROPERTYNAME)); - $this->assertEquals(array(self::SIZE_PROPERTYNAME), $propFind->get404Properties()); - } - - public function testGetPropertiesForFileHome() { - $node = $this->createTestNode('\OCA\DAV\Files\FilesHome'); - - $propFind = new \Sabre\DAV\PropFind( - '/dummyPath', - array( - self::GETETAG_PROPERTYNAME, - self::FILEID_PROPERTYNAME, - self::INTERNAL_FILEID_PROPERTYNAME, - self::SIZE_PROPERTYNAME, - self::PERMISSIONS_PROPERTYNAME, - self::DOWNLOADURL_PROPERTYNAME, - self::OWNER_ID_PROPERTYNAME, - self::OWNER_DISPLAY_NAME_PROPERTYNAME - ), - 0 - ); - - $user = $this->getMockBuilder('\OC\User\User') - ->disableOriginalConstructor()->getMock(); - $user->expects($this->never())->method('getUID'); - $user->expects($this->never())->method('getDisplayName'); - $node->expects($this->never())->method('getDirectDownload'); - $node->expects($this->never())->method('getOwner'); - $node->expects($this->never())->method('getSize'); - - $this->plugin->handleGetProperties( - $propFind, - $node - ); - - $this->assertEquals(null, $propFind->get(self::GETETAG_PROPERTYNAME)); - $this->assertEquals(null, $propFind->get(self::FILEID_PROPERTYNAME)); - $this->assertEquals(null, $propFind->get(self::INTERNAL_FILEID_PROPERTYNAME)); - $this->assertEquals(null, $propFind->get(self::SIZE_PROPERTYNAME)); - $this->assertEquals(null, $propFind->get(self::PERMISSIONS_PROPERTYNAME)); - $this->assertEquals(null, $propFind->get(self::DOWNLOADURL_PROPERTYNAME)); - $this->assertEquals(null, $propFind->get(self::OWNER_ID_PROPERTYNAME)); - $this->assertEquals(null, $propFind->get(self::OWNER_DISPLAY_NAME_PROPERTYNAME)); - $this->assertEquals(['{DAV:}getetag', - '{http://owncloud.org/ns}id', - '{http://owncloud.org/ns}fileid', - '{http://owncloud.org/ns}size', - '{http://owncloud.org/ns}permissions', - '{http://owncloud.org/ns}downloadURL', - '{http://owncloud.org/ns}owner-id', - '{http://owncloud.org/ns}owner-display-name' - ], $propFind->get404Properties()); - } - - public function testGetPropertiesStorageNotAvailable() { - $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File'); - - $propFind = new \Sabre\DAV\PropFind( - '/dummyPath', - array( - self::DOWNLOADURL_PROPERTYNAME, - ), - 0 - ); - - $node->expects($this->once()) - ->method('getDirectDownload') - ->will($this->throwException(new StorageNotAvailableException())); - - $this->plugin->handleGetProperties( - $propFind, - $node - ); - - $this->assertEquals(null, $propFind->get(self::DOWNLOADURL_PROPERTYNAME)); - } - - public function testGetPublicPermissions() { - $this->plugin = new \OCA\DAV\Connector\Sabre\FilesPlugin($this->tree, $this->view, true); - $this->plugin->initialize($this->server); - - $propFind = new \Sabre\DAV\PropFind( - '/dummyPath', - [ - self::PERMISSIONS_PROPERTYNAME, - ], - 0 - ); - - $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File'); - $node->expects($this->any()) - ->method('getDavPermissions') - ->will($this->returnValue('DWCKMSR')); - - $this->plugin->handleGetProperties( - $propFind, - $node - ); - - $this->assertEquals('DWCKR', $propFind->get(self::PERMISSIONS_PROPERTYNAME)); - } - - public function testGetPropertiesForDirectory() { - $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\Directory'); - - $propFind = new \Sabre\DAV\PropFind( - '/dummyPath', - array( - self::GETETAG_PROPERTYNAME, - self::FILEID_PROPERTYNAME, - self::SIZE_PROPERTYNAME, - self::PERMISSIONS_PROPERTYNAME, - self::DOWNLOADURL_PROPERTYNAME, - ), - 0 - ); - - $node->expects($this->never()) - ->method('getDirectDownload'); - $node->expects($this->once()) - ->method('getSize') - ->will($this->returnValue(1025)); - - $this->plugin->handleGetProperties( - $propFind, - $node - ); - - $this->assertEquals('"abc"', $propFind->get(self::GETETAG_PROPERTYNAME)); - $this->assertEquals('00000123instanceid', $propFind->get(self::FILEID_PROPERTYNAME)); - $this->assertEquals(1025, $propFind->get(self::SIZE_PROPERTYNAME)); - $this->assertEquals('DWCKMSR', $propFind->get(self::PERMISSIONS_PROPERTYNAME)); - $this->assertEquals(null, $propFind->get(self::DOWNLOADURL_PROPERTYNAME)); - $this->assertEquals(array(self::DOWNLOADURL_PROPERTYNAME), $propFind->get404Properties()); - } - - public function testUpdateProps() { - $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File'); - - $testDate = 'Fri, 13 Feb 2015 00:01:02 GMT'; - - $node->expects($this->once()) - ->method('touch') - ->with($testDate); - - $node->expects($this->once()) - ->method('setEtag') - ->with('newetag') - ->will($this->returnValue(true)); - - // properties to set - $propPatch = new \Sabre\DAV\PropPatch(array( - self::GETETAG_PROPERTYNAME => 'newetag', - self::LASTMODIFIED_PROPERTYNAME => $testDate - )); - - $this->plugin->handleUpdateProperties( - '/dummypath', - $propPatch - ); - - $propPatch->commit(); - - $this->assertEmpty($propPatch->getRemainingMutations()); - - $result = $propPatch->getResult(); - $this->assertEquals(200, $result[self::LASTMODIFIED_PROPERTYNAME]); - $this->assertEquals(200, $result[self::GETETAG_PROPERTYNAME]); - } - - public function testUpdatePropsForbidden() { - $node = $this->createTestNode('\OCA\DAV\Connector\Sabre\File'); - - $propPatch = new \Sabre\DAV\PropPatch(array( - self::OWNER_ID_PROPERTYNAME => 'user2', - self::OWNER_DISPLAY_NAME_PROPERTYNAME => 'User Two', - self::FILEID_PROPERTYNAME => 12345, - self::PERMISSIONS_PROPERTYNAME => 'C', - self::SIZE_PROPERTYNAME => 123, - self::DOWNLOADURL_PROPERTYNAME => 'http://example.com/', - )); - - $this->plugin->handleUpdateProperties( - '/dummypath', - $propPatch - ); - - $propPatch->commit(); - - $this->assertEmpty($propPatch->getRemainingMutations()); - - $result = $propPatch->getResult(); - $this->assertEquals(403, $result[self::OWNER_ID_PROPERTYNAME]); - $this->assertEquals(403, $result[self::OWNER_DISPLAY_NAME_PROPERTYNAME]); - $this->assertEquals(403, $result[self::FILEID_PROPERTYNAME]); - $this->assertEquals(403, $result[self::PERMISSIONS_PROPERTYNAME]); - $this->assertEquals(403, $result[self::SIZE_PROPERTYNAME]); - $this->assertEquals(403, $result[self::DOWNLOADURL_PROPERTYNAME]); - } - - /** - * Testcase from https://github.com/owncloud/core/issues/5251 - * - * |-FolderA - * |-text.txt - * |-test.txt - * - * FolderA is an incoming shared folder and there are no delete permissions. - * Thus moving /FolderA/test.txt to /test.txt should fail already on that check - * - * @expectedException \Sabre\DAV\Exception\Forbidden - * @expectedExceptionMessage FolderA/test.txt cannot be deleted - */ - public function testMoveSrcNotDeletable() { - $fileInfoFolderATestTXT = $this->getMockBuilder('\OCP\Files\FileInfo') - ->disableOriginalConstructor() - ->getMock(); - $fileInfoFolderATestTXT->expects($this->once()) - ->method('isDeletable') - ->willReturn(false); - - $this->view->expects($this->once()) - ->method('getFileInfo') - ->with('FolderA/test.txt') - ->willReturn($fileInfoFolderATestTXT); - - $this->plugin->checkMove('FolderA/test.txt', 'test.txt'); - } - - public function testMoveSrcDeletable() { - $fileInfoFolderATestTXT = $this->getMockBuilder('\OCP\Files\FileInfo') - ->disableOriginalConstructor() - ->getMock(); - $fileInfoFolderATestTXT->expects($this->once()) - ->method('isDeletable') - ->willReturn(true); - - $this->view->expects($this->once()) - ->method('getFileInfo') - ->with('FolderA/test.txt') - ->willReturn($fileInfoFolderATestTXT); - - $this->plugin->checkMove('FolderA/test.txt', 'test.txt'); - } - - /** - * @expectedException \Sabre\DAV\Exception\NotFound - * @expectedExceptionMessage FolderA/test.txt does not exist - */ - public function testMoveSrcNotExist() { - $this->view->expects($this->once()) - ->method('getFileInfo') - ->with('FolderA/test.txt') - ->willReturn(false); - - $this->plugin->checkMove('FolderA/test.txt', 'test.txt'); - } -} diff --git a/apps/dav/tests/unit/connector/sabre/filesreportplugin.php b/apps/dav/tests/unit/connector/sabre/filesreportplugin.php deleted file mode 100644 index 87973ef0071..00000000000 --- a/apps/dav/tests/unit/connector/sabre/filesreportplugin.php +++ /dev/null @@ -1,603 +0,0 @@ -<?php -/** - * @author Joas Schilling <nickvergessen@owncloud.com> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\Connector\Sabre; - -use OCA\DAV\Connector\Sabre\FilesReportPlugin as FilesReportPluginImplementation; -use Sabre\DAV\Exception\NotFound; -use OCP\SystemTag\ISystemTagObjectMapper; -use OC\Files\View; -use OCP\Files\Folder; -use OCP\IGroupManager; -use OCP\SystemTag\ISystemTagManager; - -class FilesReportPlugin extends \Test\TestCase { - /** @var \Sabre\DAV\Server|\PHPUnit_Framework_MockObject_MockObject */ - private $server; - - /** @var \Sabre\DAV\Tree|\PHPUnit_Framework_MockObject_MockObject */ - private $tree; - - /** @var ISystemTagObjectMapper|\PHPUnit_Framework_MockObject_MockObject */ - private $tagMapper; - - /** @var ISystemTagManager|\PHPUnit_Framework_MockObject_MockObject */ - private $tagManager; - - /** @var \OCP\IUserSession */ - private $userSession; - - /** @var FilesReportPluginImplementation */ - private $plugin; - - /** @var View|\PHPUnit_Framework_MockObject_MockObject **/ - private $view; - - /** @var IGroupManager|\PHPUnit_Framework_MockObject_MockObject **/ - private $groupManager; - - /** @var Folder|\PHPUnit_Framework_MockObject_MockObject **/ - private $userFolder; - - public function setUp() { - parent::setUp(); - $this->tree = $this->getMockBuilder('\Sabre\DAV\Tree') - ->disableOriginalConstructor() - ->getMock(); - - $this->view = $this->getMockBuilder('\OC\Files\View') - ->disableOriginalConstructor() - ->getMock(); - - $this->server = $this->getMockBuilder('\Sabre\DAV\Server') - ->setConstructorArgs([$this->tree]) - ->setMethods(['getRequestUri']) - ->getMock(); - - $this->groupManager = $this->getMockBuilder('\OCP\IGroupManager') - ->disableOriginalConstructor() - ->getMock(); - - $this->userFolder = $this->getMockBuilder('\OCP\Files\Folder') - ->disableOriginalConstructor() - ->getMock(); - - $this->tagManager = $this->getMock('\OCP\SystemTag\ISystemTagManager'); - $this->tagMapper = $this->getMock('\OCP\SystemTag\ISystemTagObjectMapper'); - $this->userSession = $this->getMock('\OCP\IUserSession'); - - $user = $this->getMock('\OCP\IUser'); - $user->expects($this->any()) - ->method('getUID') - ->will($this->returnValue('testuser')); - $this->userSession->expects($this->any()) - ->method('getUser') - ->will($this->returnValue($user)); - - $this->plugin = new FilesReportPluginImplementation( - $this->tree, - $this->view, - $this->tagManager, - $this->tagMapper, - $this->userSession, - $this->groupManager, - $this->userFolder - ); - } - - /** - * @expectedException \Sabre\DAV\Exception\ReportNotSupported - */ - public function testOnReportInvalidNode() { - $path = 'totally/unrelated/13'; - - $this->tree->expects($this->any()) - ->method('getNodeForPath') - ->with('/' . $path) - ->will($this->returnValue($this->getMock('\Sabre\DAV\INode'))); - - $this->server->expects($this->any()) - ->method('getRequestUri') - ->will($this->returnValue($path)); - $this->plugin->initialize($this->server); - - $this->plugin->onReport(FilesReportPluginImplementation::REPORT_NAME, [], '/' . $path); - } - - /** - * @expectedException \Sabre\DAV\Exception\ReportNotSupported - */ - public function testOnReportInvalidReportName() { - $path = 'test'; - - $this->tree->expects($this->any()) - ->method('getNodeForPath') - ->with('/' . $path) - ->will($this->returnValue($this->getMock('\Sabre\DAV\INode'))); - - $this->server->expects($this->any()) - ->method('getRequestUri') - ->will($this->returnValue($path)); - $this->plugin->initialize($this->server); - - $this->plugin->onReport('{whoever}whatever', [], '/' . $path); - } - - public function testOnReport() { - $path = 'test'; - - $parameters = [ - [ - 'name' => '{DAV:}prop', - 'value' => [ - ['name' => '{DAV:}getcontentlength', 'value' => ''], - ['name' => '{http://owncloud.org/ns}size', 'value' => ''], - ], - ], - [ - 'name' => '{http://owncloud.org/ns}filter-rules', - 'value' => [ - ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'], - ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'], - ], - ], - ]; - - $this->groupManager->expects($this->any()) - ->method('isAdmin') - ->will($this->returnValue(true)); - - $this->tagMapper->expects($this->at(0)) - ->method('getObjectIdsForTags') - ->with('123', 'files') - ->will($this->returnValue(['111', '222'])); - $this->tagMapper->expects($this->at(1)) - ->method('getObjectIdsForTags') - ->with('456', 'files') - ->will($this->returnValue(['111', '222', '333'])); - - $reportTargetNode = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Directory') - ->disableOriginalConstructor() - ->getMock(); - - $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface') - ->disableOriginalConstructor() - ->getMock(); - - $response->expects($this->once()) - ->method('setHeader') - ->with('Content-Type', 'application/xml; charset=utf-8'); - - $response->expects($this->once()) - ->method('setStatus') - ->with(207); - - $response->expects($this->once()) - ->method('setBody'); - - $this->tree->expects($this->any()) - ->method('getNodeForPath') - ->with('/' . $path) - ->will($this->returnValue($reportTargetNode)); - - $filesNode1 = $this->getMockBuilder('\OCP\Files\Folder') - ->disableOriginalConstructor() - ->getMock(); - $filesNode2 = $this->getMockBuilder('\OCP\Files\File') - ->disableOriginalConstructor() - ->getMock(); - - $this->userFolder->expects($this->at(0)) - ->method('getById') - ->with('111') - ->will($this->returnValue([$filesNode1])); - $this->userFolder->expects($this->at(1)) - ->method('getById') - ->with('222') - ->will($this->returnValue([$filesNode2])); - - $this->server->expects($this->any()) - ->method('getRequestUri') - ->will($this->returnValue($path)); - $this->server->httpResponse = $response; - $this->plugin->initialize($this->server); - - $this->plugin->onReport(FilesReportPluginImplementation::REPORT_NAME, $parameters, '/' . $path); - } - - public function testFindNodesByFileIdsRoot() { - $filesNode1 = $this->getMockBuilder('\OCP\Files\Folder') - ->disableOriginalConstructor() - ->getMock(); - $filesNode1->expects($this->once()) - ->method('getName') - ->will($this->returnValue('first node')); - - $filesNode2 = $this->getMockBuilder('\OCP\Files\File') - ->disableOriginalConstructor() - ->getMock(); - $filesNode2->expects($this->once()) - ->method('getName') - ->will($this->returnValue('second node')); - - $reportTargetNode = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Directory') - ->disableOriginalConstructor() - ->getMock(); - $reportTargetNode->expects($this->any()) - ->method('getPath') - ->will($this->returnValue('/')); - - $this->userFolder->expects($this->at(0)) - ->method('getById') - ->with('111') - ->will($this->returnValue([$filesNode1])); - $this->userFolder->expects($this->at(1)) - ->method('getById') - ->with('222') - ->will($this->returnValue([$filesNode2])); - - /** @var \OCA\DAV\Connector\Sabre\Directory|\PHPUnit_Framework_MockObject_MockObject $reportTargetNode */ - $result = $this->plugin->findNodesByFileIds($reportTargetNode, ['111', '222']); - - $this->assertCount(2, $result); - $this->assertInstanceOf('\OCA\DAV\Connector\Sabre\Directory', $result[0]); - $this->assertEquals('first node', $result[0]->getName()); - $this->assertInstanceOf('\OCA\DAV\Connector\Sabre\File', $result[1]); - $this->assertEquals('second node', $result[1]->getName()); - } - - public function testFindNodesByFileIdsSubDir() { - $filesNode1 = $this->getMockBuilder('\OCP\Files\Folder') - ->disableOriginalConstructor() - ->getMock(); - $filesNode1->expects($this->once()) - ->method('getName') - ->will($this->returnValue('first node')); - - $filesNode2 = $this->getMockBuilder('\OCP\Files\File') - ->disableOriginalConstructor() - ->getMock(); - $filesNode2->expects($this->once()) - ->method('getName') - ->will($this->returnValue('second node')); - - $reportTargetNode = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Directory') - ->disableOriginalConstructor() - ->getMock(); - $reportTargetNode->expects($this->any()) - ->method('getPath') - ->will($this->returnValue('/sub1/sub2')); - - - $subNode = $this->getMockBuilder('\OCP\Files\Folder') - ->disableOriginalConstructor() - ->getMock(); - - $this->userFolder->expects($this->at(0)) - ->method('get') - ->with('/sub1/sub2') - ->will($this->returnValue($subNode)); - - $subNode->expects($this->at(0)) - ->method('getById') - ->with('111') - ->will($this->returnValue([$filesNode1])); - $subNode->expects($this->at(1)) - ->method('getById') - ->with('222') - ->will($this->returnValue([$filesNode2])); - - /** @var \OCA\DAV\Connector\Sabre\Directory|\PHPUnit_Framework_MockObject_MockObject $reportTargetNode */ - $result = $this->plugin->findNodesByFileIds($reportTargetNode, ['111', '222']); - - $this->assertCount(2, $result); - $this->assertInstanceOf('\OCA\DAV\Connector\Sabre\Directory', $result[0]); - $this->assertEquals('first node', $result[0]->getName()); - $this->assertInstanceOf('\OCA\DAV\Connector\Sabre\File', $result[1]); - $this->assertEquals('second node', $result[1]->getName()); - } - - public function testPrepareResponses() { - $requestedProps = ['{DAV:}getcontentlength', '{http://owncloud.org/ns}fileid', '{DAV:}resourcetype']; - - $node1 = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Directory') - ->disableOriginalConstructor() - ->getMock(); - $node2 = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\File') - ->disableOriginalConstructor() - ->getMock(); - - $node1->expects($this->once()) - ->method('getInternalFileId') - ->will($this->returnValue('111')); - $node2->expects($this->once()) - ->method('getInternalFileId') - ->will($this->returnValue('222')); - $node2->expects($this->once()) - ->method('getSize') - ->will($this->returnValue(1024)); - - $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\FilesPlugin($this->tree, $this->view)); - $this->plugin->initialize($this->server); - $responses = $this->plugin->prepareResponses($requestedProps, [$node1, $node2]); - - $this->assertCount(2, $responses); - - $this->assertEquals(200, $responses[0]->getHttpStatus()); - $this->assertEquals(200, $responses[1]->getHttpStatus()); - - $props1 = $responses[0]->getResponseProperties(); - $this->assertEquals('111', $props1[200]['{http://owncloud.org/ns}fileid']); - $this->assertNull($props1[404]['{DAV:}getcontentlength']); - $this->assertInstanceOf('\Sabre\DAV\Xml\Property\ResourceType', $props1[200]['{DAV:}resourcetype']); - $resourceType1 = $props1[200]['{DAV:}resourcetype']->getValue(); - $this->assertEquals('{DAV:}collection', $resourceType1[0]); - - $props2 = $responses[1]->getResponseProperties(); - $this->assertEquals('1024', $props2[200]['{DAV:}getcontentlength']); - $this->assertEquals('222', $props2[200]['{http://owncloud.org/ns}fileid']); - $this->assertInstanceOf('\Sabre\DAV\Xml\Property\ResourceType', $props2[200]['{DAV:}resourcetype']); - $this->assertCount(0, $props2[200]['{DAV:}resourcetype']->getValue()); - } - - public function testProcessFilterRulesSingle() { - $this->groupManager->expects($this->any()) - ->method('isAdmin') - ->will($this->returnValue(true)); - - $this->tagMapper->expects($this->exactly(1)) - ->method('getObjectIdsForTags') - ->withConsecutive( - ['123', 'files'] - ) - ->willReturnMap([ - ['123', 'files', 0, '', ['111', '222']], - ]); - - $rules = [ - ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'], - ]; - - $this->assertEquals(['111', '222'], $this->invokePrivate($this->plugin, 'processFilterRules', [$rules])); - } - - public function testProcessFilterRulesAndCondition() { - $this->groupManager->expects($this->any()) - ->method('isAdmin') - ->will($this->returnValue(true)); - - $this->tagMapper->expects($this->exactly(2)) - ->method('getObjectIdsForTags') - ->withConsecutive( - ['123', 'files'], - ['456', 'files'] - ) - ->willReturnMap([ - ['123', 'files', 0, '', ['111', '222']], - ['456', 'files', 0, '', ['222', '333']], - ]); - - $rules = [ - ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'], - ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'], - ]; - - $this->assertEquals(['222'], array_values($this->invokePrivate($this->plugin, 'processFilterRules', [$rules]))); - } - - public function testProcessFilterRulesAndConditionWithOneEmptyResult() { - $this->groupManager->expects($this->any()) - ->method('isAdmin') - ->will($this->returnValue(true)); - - $this->tagMapper->expects($this->exactly(2)) - ->method('getObjectIdsForTags') - ->withConsecutive( - ['123', 'files'], - ['456', 'files'] - ) - ->willReturnMap([ - ['123', 'files', 0, '', ['111', '222']], - ['456', 'files', 0, '', []], - ]); - - $rules = [ - ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'], - ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'], - ]; - - $this->assertEquals([], array_values($this->invokePrivate($this->plugin, 'processFilterRules', [$rules]))); - } - - public function testProcessFilterRulesAndConditionWithFirstEmptyResult() { - $this->groupManager->expects($this->any()) - ->method('isAdmin') - ->will($this->returnValue(true)); - - $this->tagMapper->expects($this->exactly(1)) - ->method('getObjectIdsForTags') - ->withConsecutive( - ['123', 'files'], - ['456', 'files'] - ) - ->willReturnMap([ - ['123', 'files', 0, '', []], - ['456', 'files', 0, '', ['111', '222']], - ]); - - $rules = [ - ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'], - ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'], - ]; - - $this->assertEquals([], array_values($this->invokePrivate($this->plugin, 'processFilterRules', [$rules]))); - } - - public function testProcessFilterRulesAndConditionWithEmptyMidResult() { - $this->groupManager->expects($this->any()) - ->method('isAdmin') - ->will($this->returnValue(true)); - - $this->tagMapper->expects($this->exactly(2)) - ->method('getObjectIdsForTags') - ->withConsecutive( - ['123', 'files'], - ['456', 'files'], - ['789', 'files'] - ) - ->willReturnMap([ - ['123', 'files', 0, '', ['111', '222']], - ['456', 'files', 0, '', ['333']], - ['789', 'files', 0, '', ['111', '222']], - ]); - - $rules = [ - ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'], - ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'], - ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '789'], - ]; - - $this->assertEquals([], array_values($this->invokePrivate($this->plugin, 'processFilterRules', [$rules]))); - } - - public function testProcessFilterRulesInvisibleTagAsAdmin() { - $this->groupManager->expects($this->any()) - ->method('isAdmin') - ->will($this->returnValue(true)); - - $tag1 = $this->getMock('\OCP\SystemTag\ISystemTag'); - $tag1->expects($this->any()) - ->method('getId') - ->will($this->returnValue('123')); - $tag1->expects($this->any()) - ->method('isUserVisible') - ->will($this->returnValue(true)); - - $tag2 = $this->getMock('\OCP\SystemTag\ISystemTag'); - $tag2->expects($this->any()) - ->method('getId') - ->will($this->returnValue('123')); - $tag2->expects($this->any()) - ->method('isUserVisible') - ->will($this->returnValue(false)); - - // no need to fetch tags to check permissions - $this->tagManager->expects($this->never()) - ->method('getTagsByIds'); - - $this->tagMapper->expects($this->at(0)) - ->method('getObjectIdsForTags') - ->with('123') - ->will($this->returnValue(['111', '222'])); - $this->tagMapper->expects($this->at(1)) - ->method('getObjectIdsForTags') - ->with('456') - ->will($this->returnValue(['222', '333'])); - - $rules = [ - ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'], - ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'], - ]; - - $this->assertEquals(['222'], array_values($this->invokePrivate($this->plugin, 'processFilterRules', [$rules]))); - } - - /** - * @expectedException \OCP\SystemTag\TagNotFoundException - */ - public function testProcessFilterRulesInvisibleTagAsUser() { - $this->groupManager->expects($this->any()) - ->method('isAdmin') - ->will($this->returnValue(false)); - - $tag1 = $this->getMock('\OCP\SystemTag\ISystemTag'); - $tag1->expects($this->any()) - ->method('getId') - ->will($this->returnValue('123')); - $tag1->expects($this->any()) - ->method('isUserVisible') - ->will($this->returnValue(true)); - - $tag2 = $this->getMock('\OCP\SystemTag\ISystemTag'); - $tag2->expects($this->any()) - ->method('getId') - ->will($this->returnValue('123')); - $tag2->expects($this->any()) - ->method('isUserVisible') - ->will($this->returnValue(false)); // invisible - - $this->tagManager->expects($this->once()) - ->method('getTagsByIds') - ->with(['123', '456']) - ->will($this->returnValue([$tag1, $tag2])); - - $rules = [ - ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'], - ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'], - ]; - - $this->invokePrivate($this->plugin, 'processFilterRules', [$rules]); - } - - public function testProcessFilterRulesVisibleTagAsUser() { - $this->groupManager->expects($this->any()) - ->method('isAdmin') - ->will($this->returnValue(false)); - - $tag1 = $this->getMock('\OCP\SystemTag\ISystemTag'); - $tag1->expects($this->any()) - ->method('getId') - ->will($this->returnValue('123')); - $tag1->expects($this->any()) - ->method('isUserVisible') - ->will($this->returnValue(true)); - - $tag2 = $this->getMock('\OCP\SystemTag\ISystemTag'); - $tag2->expects($this->any()) - ->method('getId') - ->will($this->returnValue('123')); - $tag2->expects($this->any()) - ->method('isUserVisible') - ->will($this->returnValue(true)); - - $this->tagManager->expects($this->once()) - ->method('getTagsByIds') - ->with(['123', '456']) - ->will($this->returnValue([$tag1, $tag2])); - - $this->tagMapper->expects($this->at(0)) - ->method('getObjectIdsForTags') - ->with('123') - ->will($this->returnValue(['111', '222'])); - $this->tagMapper->expects($this->at(1)) - ->method('getObjectIdsForTags') - ->with('456') - ->will($this->returnValue(['222', '333'])); - - $rules = [ - ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '123'], - ['name' => '{http://owncloud.org/ns}systemtag', 'value' => '456'], - ]; - - $this->assertEquals(['222'], array_values($this->invokePrivate($this->plugin, 'processFilterRules', [$rules]))); - } -} diff --git a/apps/dav/tests/unit/connector/sabre/node.php b/apps/dav/tests/unit/connector/sabre/node.php deleted file mode 100644 index cde8e746dc3..00000000000 --- a/apps/dav/tests/unit/connector/sabre/node.php +++ /dev/null @@ -1,130 +0,0 @@ -<?php -/** - * @author Joas Schilling <nickvergessen@owncloud.com> - * @author Robin Appelman <icewind@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\Connector\Sabre; - -class Node extends \Test\TestCase { - public function davPermissionsProvider() { - return array( - array(\OCP\Constants::PERMISSION_ALL, 'file', false, false, 'RDNVW'), - array(\OCP\Constants::PERMISSION_ALL, 'dir', false, false, 'RDNVCK'), - array(\OCP\Constants::PERMISSION_ALL, 'file', true, false, 'SRDNVW'), - array(\OCP\Constants::PERMISSION_ALL, 'file', true, true, 'SRMDNVW'), - array(\OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_SHARE, 'file', true, false, 'SDNVW'), - array(\OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_UPDATE, 'file', false, false, 'RD'), - array(\OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_DELETE, 'file', false, false, 'RNVW'), - array(\OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE, 'file', false, false, 'RDNVW'), - array(\OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE, 'dir', false, false, 'RDNV'), - ); - } - - /** - * @dataProvider davPermissionsProvider - */ - public function testDavPermissions($permissions, $type, $shared, $mounted, $expected) { - $info = $this->getMockBuilder('\OC\Files\FileInfo') - ->disableOriginalConstructor() - ->setMethods(array('getPermissions', 'isShared', 'isMounted', 'getType')) - ->getMock(); - $info->expects($this->any()) - ->method('getPermissions') - ->will($this->returnValue($permissions)); - $info->expects($this->any()) - ->method('isShared') - ->will($this->returnValue($shared)); - $info->expects($this->any()) - ->method('isMounted') - ->will($this->returnValue($mounted)); - $info->expects($this->any()) - ->method('getType') - ->will($this->returnValue($type)); - $view = $this->getMock('\OC\Files\View'); - - $node = new \OCA\DAV\Connector\Sabre\File($view, $info); - $this->assertEquals($expected, $node->getDavPermissions()); - } - - public function sharePermissionsProvider() { - return [ - [\OCP\Files\FileInfo::TYPE_FILE, 1, 0], - [\OCP\Files\FileInfo::TYPE_FILE, 3, 0], - [\OCP\Files\FileInfo::TYPE_FILE, 5, 0], - [\OCP\Files\FileInfo::TYPE_FILE, 7, 0], - [\OCP\Files\FileInfo::TYPE_FILE, 9, 0], - [\OCP\Files\FileInfo::TYPE_FILE, 11, 0], - [\OCP\Files\FileInfo::TYPE_FILE, 13, 0], - [\OCP\Files\FileInfo::TYPE_FILE, 15, 0], - [\OCP\Files\FileInfo::TYPE_FILE, 17, 17], - [\OCP\Files\FileInfo::TYPE_FILE, 19, 19], - [\OCP\Files\FileInfo::TYPE_FILE, 21, 17], - [\OCP\Files\FileInfo::TYPE_FILE, 23, 19], - [\OCP\Files\FileInfo::TYPE_FILE, 25, 17], - [\OCP\Files\FileInfo::TYPE_FILE, 27, 19], - [\OCP\Files\FileInfo::TYPE_FILE, 29, 17], - [\OCP\Files\FileInfo::TYPE_FILE, 30, 0], - [\OCP\Files\FileInfo::TYPE_FILE, 31, 19], - [\OCP\Files\FileInfo::TYPE_FOLDER, 1, 0], - [\OCP\Files\FileInfo::TYPE_FOLDER, 3, 0], - [\OCP\Files\FileInfo::TYPE_FOLDER, 5, 0], - [\OCP\Files\FileInfo::TYPE_FOLDER, 7, 0], - [\OCP\Files\FileInfo::TYPE_FOLDER, 9, 0], - [\OCP\Files\FileInfo::TYPE_FOLDER, 11, 0], - [\OCP\Files\FileInfo::TYPE_FOLDER, 13, 0], - [\OCP\Files\FileInfo::TYPE_FOLDER, 15, 0], - [\OCP\Files\FileInfo::TYPE_FOLDER, 17, 17], - [\OCP\Files\FileInfo::TYPE_FOLDER, 19, 19], - [\OCP\Files\FileInfo::TYPE_FOLDER, 21, 21], - [\OCP\Files\FileInfo::TYPE_FOLDER, 23, 23], - [\OCP\Files\FileInfo::TYPE_FOLDER, 25, 25], - [\OCP\Files\FileInfo::TYPE_FOLDER, 27, 27], - [\OCP\Files\FileInfo::TYPE_FOLDER, 29, 29], - [\OCP\Files\FileInfo::TYPE_FOLDER, 30, 0], - [\OCP\Files\FileInfo::TYPE_FOLDER, 31, 31], - ]; - } - - /** - * @dataProvider sharePermissionsProvider - */ - public function testSharePermissions($type, $permissions, $expected) { - $storage = $this->getMock('\OCP\Files\Storage'); - $storage->method('getPermissions')->willReturn($permissions); - - $mountpoint = $this->getMock('\OCP\Files\Mount\IMountPoint'); - $mountpoint->method('getMountPoint')->willReturn('myPath'); - - $info = $this->getMockBuilder('\OC\Files\FileInfo') - ->disableOriginalConstructor() - ->setMethods(['getStorage', 'getType', 'getMountPoint']) - ->getMock(); - - $info->method('getStorage')->willReturn($storage); - $info->method('getType')->willReturn($type); - $info->method('getMountPoint')->willReturn($mountpoint); - - $view = $this->getMock('\OC\Files\View'); - - $node = new \OCA\DAV\Connector\Sabre\File($view, $info); - $this->assertEquals($expected, $node->getSharePermissions()); - } -} diff --git a/apps/dav/tests/unit/connector/sabre/objecttree.php b/apps/dav/tests/unit/connector/sabre/objecttree.php deleted file mode 100644 index e5e858ef17b..00000000000 --- a/apps/dav/tests/unit/connector/sabre/objecttree.php +++ /dev/null @@ -1,355 +0,0 @@ -<?php -/** - * @author Joas Schilling <nickvergessen@owncloud.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <icewind@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\Connector\Sabre; - - -use OC\Files\FileInfo; -use OC\Files\Storage\Temporary; - -class TestDoubleFileView extends \OC\Files\View { - - public function __construct($updatables, $deletables, $canRename = true) { - $this->updatables = $updatables; - $this->deletables = $deletables; - $this->canRename = $canRename; - } - - public function isUpdatable($path) { - return $this->updatables[$path]; - } - - public function isCreatable($path) { - return $this->updatables[$path]; - } - - public function isDeletable($path) { - return $this->deletables[$path]; - } - - public function rename($path1, $path2) { - return $this->canRename; - } - - public function getRelativePath($path) { - return $path; - } -} - -/** - * Class ObjectTree - * - * @group DB - * - * @package OCA\DAV\Tests\Unit\Connector\Sabre - */ -class ObjectTree extends \Test\TestCase { - - /** - * @dataProvider moveFailedProvider - * @expectedException \Sabre\DAV\Exception\Forbidden - */ - public function testMoveFailed($source, $destination, $updatables, $deletables) { - $this->moveTest($source, $destination, $updatables, $deletables); - } - - /** - * @dataProvider moveSuccessProvider - */ - public function testMoveSuccess($source, $destination, $updatables, $deletables) { - $this->moveTest($source, $destination, $updatables, $deletables); - $this->assertTrue(true); - } - - /** - * @dataProvider moveFailedInvalidCharsProvider - * @expectedException \OCA\DAV\Connector\Sabre\Exception\InvalidPath - */ - public function testMoveFailedInvalidChars($source, $destination, $updatables, $deletables) { - $this->moveTest($source, $destination, $updatables, $deletables); - } - - function moveFailedInvalidCharsProvider() { - return array( - array('a/b', 'a/*', array('a' => true, 'a/b' => true, 'a/c*' => false), array()), - ); - } - - function moveFailedProvider() { - return array( - array('a/b', 'a/c', array('a' => false, 'a/b' => false, 'a/c' => false), array()), - array('a/b', 'b/b', array('a' => false, 'a/b' => false, 'b' => false, 'b/b' => false), array()), - array('a/b', 'b/b', array('a' => false, 'a/b' => true, 'b' => false, 'b/b' => false), array()), - array('a/b', 'b/b', array('a' => true, 'a/b' => true, 'b' => false, 'b/b' => false), array()), - array('a/b', 'b/b', array('a' => true, 'a/b' => true, 'b' => true, 'b/b' => false), array('a/b' => false)), - array('a/b', 'a/c', array('a' => false, 'a/b' => true, 'a/c' => false), array()), - ); - } - - function moveSuccessProvider() { - return array( - array('a/b', 'b/b', array('a' => true, 'a/b' => true, 'b' => true, 'b/b' => false), array('a/b' => true)), - // older files with special chars can still be renamed to valid names - array('a/b*', 'b/b', array('a' => true, 'a/b*' => true, 'b' => true, 'b/b' => false), array('a/b*' => true)), - ); - } - - /** - * @param $source - * @param $destination - * @param $updatables - */ - private function moveTest($source, $destination, $updatables, $deletables) { - $view = new TestDoubleFileView($updatables, $deletables); - - $info = new FileInfo('', null, null, array(), null); - - $rootDir = new \OCA\DAV\Connector\Sabre\Directory($view, $info); - $objectTree = $this->getMock('\OCA\DAV\Connector\Sabre\ObjectTree', - array('nodeExists', 'getNodeForPath'), - array($rootDir, $view)); - - $objectTree->expects($this->once()) - ->method('getNodeForPath') - ->with($this->identicalTo($source)) - ->will($this->returnValue(false)); - - /** @var $objectTree \OCA\DAV\Connector\Sabre\ObjectTree */ - $mountManager = \OC\Files\Filesystem::getMountManager(); - $objectTree->init($rootDir, $view, $mountManager); - $objectTree->move($source, $destination); - } - - /** - * @dataProvider nodeForPathProvider - */ - public function testGetNodeForPath( - $inputFileName, - $fileInfoQueryPath, - $outputFileName, - $type, - $enableChunkingHeader - ) { - - if ($enableChunkingHeader) { - $_SERVER['HTTP_OC_CHUNKED'] = true; - } - - $rootNode = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Directory') - ->disableOriginalConstructor() - ->getMock(); - $mountManager = $this->getMock('\OC\Files\Mount\Manager'); - $view = $this->getMock('\OC\Files\View'); - $fileInfo = $this->getMock('\OCP\Files\FileInfo'); - $fileInfo->expects($this->once()) - ->method('getType') - ->will($this->returnValue($type)); - $fileInfo->expects($this->once()) - ->method('getName') - ->will($this->returnValue($outputFileName)); - - $view->expects($this->once()) - ->method('getFileInfo') - ->with($fileInfoQueryPath) - ->will($this->returnValue($fileInfo)); - - $tree = new \OCA\DAV\Connector\Sabre\ObjectTree(); - $tree->init($rootNode, $view, $mountManager); - - $node = $tree->getNodeForPath($inputFileName); - - $this->assertNotNull($node); - $this->assertEquals($outputFileName, $node->getName()); - - if ($type === 'file') { - $this->assertTrue($node instanceof \OCA\DAV\Connector\Sabre\File); - } else { - $this->assertTrue($node instanceof \OCA\DAV\Connector\Sabre\Directory); - } - - unset($_SERVER['HTTP_OC_CHUNKED']); - } - - function nodeForPathProvider() { - return array( - // regular file - array( - 'regularfile.txt', - 'regularfile.txt', - 'regularfile.txt', - 'file', - false - ), - // regular directory - array( - 'regulardir', - 'regulardir', - 'regulardir', - 'dir', - false - ), - // regular file with chunking - array( - 'regularfile.txt', - 'regularfile.txt', - 'regularfile.txt', - 'file', - true - ), - // regular directory with chunking - array( - 'regulardir', - 'regulardir', - 'regulardir', - 'dir', - true - ), - // file with chunky file name - array( - 'regularfile.txt-chunking-123566789-10-1', - 'regularfile.txt', - 'regularfile.txt', - 'file', - true - ), - // regular file in subdir - array( - 'subdir/regularfile.txt', - 'subdir/regularfile.txt', - 'regularfile.txt', - 'file', - false - ), - // regular directory in subdir - array( - 'subdir/regulardir', - 'subdir/regulardir', - 'regulardir', - 'dir', - false - ), - // file with chunky file name in subdir - array( - 'subdir/regularfile.txt-chunking-123566789-10-1', - 'subdir/regularfile.txt', - 'regularfile.txt', - 'file', - true - ), - ); - } - - /** - * @expectedException \OCA\DAV\Connector\Sabre\Exception\InvalidPath - */ - public function testGetNodeForPathInvalidPath() { - $path = '/foo\bar'; - - - $storage = new Temporary([]); - - $view = $this->getMock('\OC\Files\View', ['resolvePath']); - $view->expects($this->once()) - ->method('resolvePath') - ->will($this->returnCallback(function($path) use ($storage){ - return [$storage, ltrim($path, '/')]; - })); - - $rootNode = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Directory') - ->disableOriginalConstructor() - ->getMock(); - $mountManager = $this->getMock('\OC\Files\Mount\Manager'); - - $tree = new \OCA\DAV\Connector\Sabre\ObjectTree(); - $tree->init($rootNode, $view, $mountManager); - - $tree->getNodeForPath($path); - } - - public function testGetNodeForPathRoot() { - $path = '/'; - - - $storage = new Temporary([]); - - $view = $this->getMock('\OC\Files\View', ['resolvePath']); - $view->expects($this->any()) - ->method('resolvePath') - ->will($this->returnCallback(function ($path) use ($storage) { - return [$storage, ltrim($path, '/')]; - })); - - $rootNode = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Directory') - ->disableOriginalConstructor() - ->getMock(); - $mountManager = $this->getMock('\OC\Files\Mount\Manager'); - - $tree = new \OCA\DAV\Connector\Sabre\ObjectTree(); - $tree->init($rootNode, $view, $mountManager); - - $this->assertInstanceOf('\Sabre\DAV\INode', $tree->getNodeForPath($path)); - } - - /** - * @expectedException \Sabre\DAV\Exception\Forbidden - * @expectedExceptionMessage Could not copy directory nameOfSourceNode, target exists - */ - public function testFailingMove() { - $source = 'a/b'; - $destination = 'b/b'; - $updatables = array('a' => true, 'a/b' => true, 'b' => true, 'b/b' => false); - $deletables = array('a/b' => true); - - $view = new TestDoubleFileView($updatables, $deletables); - - $info = new FileInfo('', null, null, array(), null); - - $rootDir = new \OCA\DAV\Connector\Sabre\Directory($view, $info); - $objectTree = $this->getMock('\OCA\DAV\Connector\Sabre\ObjectTree', - array('nodeExists', 'getNodeForPath'), - array($rootDir, $view)); - - $sourceNode = $this->getMockBuilder('\Sabre\DAV\ICollection') - ->disableOriginalConstructor() - ->getMock(); - $sourceNode->expects($this->once()) - ->method('getName') - ->will($this->returnValue('nameOfSourceNode')); - - $objectTree->expects($this->once()) - ->method('nodeExists') - ->with($this->identicalTo($destination)) - ->will($this->returnValue(true)); - $objectTree->expects($this->once()) - ->method('getNodeForPath') - ->with($this->identicalTo($source)) - ->will($this->returnValue($sourceNode)); - - /** @var $objectTree \OCA\DAV\Connector\Sabre\ObjectTree */ - $mountManager = \OC\Files\Filesystem::getMountManager(); - $objectTree->init($rootDir, $view, $mountManager); - $objectTree->move($source, $destination); - } -} diff --git a/apps/dav/tests/unit/connector/sabre/principal.php b/apps/dav/tests/unit/connector/sabre/principal.php deleted file mode 100644 index 1747885240a..00000000000 --- a/apps/dav/tests/unit/connector/sabre/principal.php +++ /dev/null @@ -1,258 +0,0 @@ -<?php -/** - * @author Lukas Reschke <lukas@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\Connector\Sabre; - -use OCP\IGroupManager; -use \Sabre\DAV\PropPatch; -use OCP\IUserManager; -use Test\TestCase; - -class Principal extends TestCase { - /** @var IUserManager | \PHPUnit_Framework_MockObject_MockObject */ - private $userManager; - /** @var \OCA\DAV\Connector\Sabre\Principal */ - private $connector; - /** @var IGroupManager | \PHPUnit_Framework_MockObject_MockObject */ - private $groupManager; - - public function setUp() { - $this->userManager = $this->getMockBuilder('\OCP\IUserManager') - ->disableOriginalConstructor()->getMock(); - $this->groupManager = $this->getMockBuilder('\OCP\IGroupManager') - ->disableOriginalConstructor()->getMock(); - - $this->connector = new \OCA\DAV\Connector\Sabre\Principal( - $this->userManager, - $this->groupManager); - parent::setUp(); - } - - public function testGetPrincipalsByPrefixWithoutPrefix() { - $response = $this->connector->getPrincipalsByPrefix(''); - $this->assertSame([], $response); - } - - public function testGetPrincipalsByPrefixWithUsers() { - $fooUser = $this->getMockBuilder('\OC\User\User') - ->disableOriginalConstructor()->getMock(); - $fooUser - ->expects($this->exactly(1)) - ->method('getUID') - ->will($this->returnValue('foo')); - $fooUser - ->expects($this->exactly(1)) - ->method('getDisplayName') - ->will($this->returnValue('Dr. Foo-Bar')); - $fooUser - ->expects($this->exactly(1)) - ->method('getEMailAddress') - ->will($this->returnValue('')); - $barUser = $this->getMockBuilder('\OC\User\User') - ->disableOriginalConstructor()->getMock(); - $barUser - ->expects($this->exactly(1)) - ->method('getUID') - ->will($this->returnValue('bar')); - $barUser - ->expects($this->exactly(1)) - ->method('getEMailAddress') - ->will($this->returnValue('bar@owncloud.org')); - $this->userManager - ->expects($this->once()) - ->method('search') - ->with('') - ->will($this->returnValue([$fooUser, $barUser])); - - $expectedResponse = [ - 0 => [ - 'uri' => 'principals/users/foo', - '{DAV:}displayname' => 'Dr. Foo-Bar' - ], - 1 => [ - 'uri' => 'principals/users/bar', - '{DAV:}displayname' => 'bar', - '{http://sabredav.org/ns}email-address' => 'bar@owncloud.org' - ] - ]; - $response = $this->connector->getPrincipalsByPrefix('principals/users'); - $this->assertSame($expectedResponse, $response); - } - - public function testGetPrincipalsByPrefixEmpty() { - $this->userManager - ->expects($this->once()) - ->method('search') - ->with('') - ->will($this->returnValue([])); - - $response = $this->connector->getPrincipalsByPrefix('principals/users'); - $this->assertSame([], $response); - } - - public function testGetPrincipalsByPathWithoutMail() { - $fooUser = $this->getMockBuilder('\OC\User\User') - ->disableOriginalConstructor()->getMock(); - $fooUser - ->expects($this->exactly(1)) - ->method('getUID') - ->will($this->returnValue('foo')); - $this->userManager - ->expects($this->once()) - ->method('get') - ->with('foo') - ->will($this->returnValue($fooUser)); - - $expectedResponse = [ - 'uri' => 'principals/users/foo', - '{DAV:}displayname' => 'foo' - ]; - $response = $this->connector->getPrincipalByPath('principals/users/foo'); - $this->assertSame($expectedResponse, $response); - } - - public function testGetPrincipalsByPathWithMail() { - $fooUser = $this->getMockBuilder('\OC\User\User') - ->disableOriginalConstructor()->getMock(); - $fooUser - ->expects($this->exactly(1)) - ->method('getEMailAddress') - ->will($this->returnValue('foo@owncloud.org')); - $fooUser - ->expects($this->exactly(1)) - ->method('getUID') - ->will($this->returnValue('foo')); - $this->userManager - ->expects($this->once()) - ->method('get') - ->with('foo') - ->will($this->returnValue($fooUser)); - - $expectedResponse = [ - 'uri' => 'principals/users/foo', - '{DAV:}displayname' => 'foo', - '{http://sabredav.org/ns}email-address' => 'foo@owncloud.org' - ]; - $response = $this->connector->getPrincipalByPath('principals/users/foo'); - $this->assertSame($expectedResponse, $response); - } - - public function testGetPrincipalsByPathEmpty() { - $this->userManager - ->expects($this->once()) - ->method('get') - ->with('foo') - ->will($this->returnValue(null)); - - $response = $this->connector->getPrincipalByPath('principals/users/foo'); - $this->assertSame(null, $response); - } - - public function testGetGroupMemberSet() { - $fooUser = $this->getMockBuilder('\OC\User\User') - ->disableOriginalConstructor()->getMock(); - $fooUser - ->expects($this->exactly(1)) - ->method('getUID') - ->will($this->returnValue('foo')); - $this->userManager - ->expects($this->once()) - ->method('get') - ->with('foo') - ->will($this->returnValue($fooUser)); - - $response = $this->connector->getGroupMemberSet('principals/users/foo'); - $this->assertSame(['principals/users/foo'], $response); - } - - /** - * @expectedException \Sabre\DAV\Exception - * @expectedExceptionMessage Principal not found - */ - public function testGetGroupMemberSetEmpty() { - $this->userManager - ->expects($this->once()) - ->method('get') - ->with('foo') - ->will($this->returnValue(null)); - - $this->connector->getGroupMemberSet('principals/users/foo'); - } - - public function testGetGroupMembership() { - $fooUser = $this->getMockBuilder('\OC\User\User') - ->disableOriginalConstructor()->getMock(); - $group = $this->getMockBuilder('\OCP\IGroup') - ->disableOriginalConstructor()->getMock(); - $group->expects($this->once()) - ->method('getGID') - ->willReturn('group1'); - $this->userManager - ->expects($this->once()) - ->method('get') - ->with('foo') - ->willReturn($fooUser); - $this->groupManager - ->expects($this->once()) - ->method('getUserGroups') - ->willReturn([ - $group - ]); - - $expectedResponse = [ - 'principals/groups/group1' - ]; - $response = $this->connector->getGroupMembership('principals/users/foo'); - $this->assertSame($expectedResponse, $response); - } - - /** - * @expectedException \Sabre\DAV\Exception - * @expectedExceptionMessage Principal not found - */ - public function testGetGroupMembershipEmpty() { - $this->userManager - ->expects($this->once()) - ->method('get') - ->with('foo') - ->will($this->returnValue(null)); - - $this->connector->getGroupMembership('principals/users/foo'); - } - - /** - * @expectedException \Sabre\DAV\Exception - * @expectedExceptionMessage Setting members of the group is not supported yet - */ - public function testSetGroupMembership() { - $this->connector->setGroupMemberSet('principals/users/foo', ['foo']); - } - - public function testUpdatePrincipal() { - $this->assertSame(0, $this->connector->updatePrincipal('foo', new PropPatch(array()))); - } - - public function testSearchPrincipals() { - $this->assertSame([], $this->connector->searchPrincipals('principals/users', [])); - } -} diff --git a/apps/dav/tests/unit/connector/sabre/quotaplugin.php b/apps/dav/tests/unit/connector/sabre/quotaplugin.php deleted file mode 100644 index b5a8bfef31c..00000000000 --- a/apps/dav/tests/unit/connector/sabre/quotaplugin.php +++ /dev/null @@ -1,223 +0,0 @@ -<?php -/** - * @author Robin Appelman <icewind@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace OCA\DAV\Tests\Unit\Connector\Sabre; -/** - * Copyright (c) 2013 Thomas Müller <thomas.mueller@tmit.eu> - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. - */ -class QuotaPlugin extends \Test\TestCase { - - /** - * @var \Sabre\DAV\Server - */ - private $server; - - /** - * @var \OCA\DAV\Connector\Sabre\QuotaPlugin - */ - private $plugin; - - private function init($quota, $checkedPath = '') { - $view = $this->buildFileViewMock($quota, $checkedPath); - $this->server = new \Sabre\DAV\Server(); - $this->plugin = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\QuotaPlugin') - ->setConstructorArgs([$view]) - ->setMethods(['getFileChunking']) - ->getMock(); - $this->plugin->initialize($this->server); - } - - /** - * @dataProvider lengthProvider - */ - public function testLength($expected, $headers) { - $this->init(0); - $this->plugin->expects($this->never()) - ->method('getFileChunking'); - $this->server->httpRequest = new \Sabre\HTTP\Request(null, null, $headers); - $length = $this->plugin->getLength(); - $this->assertEquals($expected, $length); - } - - /** - * @dataProvider quotaOkayProvider - */ - public function testCheckQuota($quota, $headers) { - $this->init($quota); - $this->plugin->expects($this->never()) - ->method('getFileChunking'); - - $this->server->httpRequest = new \Sabre\HTTP\Request(null, null, $headers); - $result = $this->plugin->checkQuota(''); - $this->assertTrue($result); - } - - /** - * @expectedException \Sabre\DAV\Exception\InsufficientStorage - * @dataProvider quotaExceededProvider - */ - public function testCheckExceededQuota($quota, $headers) { - $this->init($quota); - $this->plugin->expects($this->never()) - ->method('getFileChunking'); - - $this->server->httpRequest = new \Sabre\HTTP\Request(null, null, $headers); - $this->plugin->checkQuota(''); - } - - /** - * @dataProvider quotaOkayProvider - */ - public function testCheckQuotaOnPath($quota, $headers) { - $this->init($quota, 'sub/test.txt'); - $this->plugin->expects($this->never()) - ->method('getFileChunking'); - - $this->server->httpRequest = new \Sabre\HTTP\Request(null, null, $headers); - $result = $this->plugin->checkQuota('/sub/test.txt'); - $this->assertTrue($result); - } - - public function quotaOkayProvider() { - return array( - array(1024, array()), - array(1024, array('X-EXPECTED-ENTITY-LENGTH' => '1024')), - array(1024, array('CONTENT-LENGTH' => '512')), - array(1024, array('OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512')), - // \OCP\Files\FileInfo::SPACE-UNKNOWN = -2 - array(-2, array()), - array(-2, array('X-EXPECTED-ENTITY-LENGTH' => '1024')), - array(-2, array('CONTENT-LENGTH' => '512')), - array(-2, array('OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512')), - ); - } - - public function quotaExceededProvider() { - return array( - array(1023, array('X-EXPECTED-ENTITY-LENGTH' => '1024')), - array(511, array('CONTENT-LENGTH' => '512')), - array(2047, array('OC-TOTAL-LENGTH' => '2048', 'CONTENT-LENGTH' => '1024')), - ); - } - - public function lengthProvider() { - return array( - array(null, array()), - array(1024, array('X-EXPECTED-ENTITY-LENGTH' => '1024')), - array(512, array('CONTENT-LENGTH' => '512')), - array(2048, array('OC-TOTAL-LENGTH' => '2048', 'CONTENT-LENGTH' => '1024')), - array(4096, array('OC-TOTAL-LENGTH' => '2048', 'X-EXPECTED-ENTITY-LENGTH' => '4096')), - ); - } - - public function quotaChunkedOkProvider() { - return array( - array(1024, 0, array('X-EXPECTED-ENTITY-LENGTH' => '1024')), - array(1024, 0, array('CONTENT-LENGTH' => '512')), - array(1024, 0, array('OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512')), - // with existing chunks (allowed size = total length - chunk total size) - array(400, 128, array('X-EXPECTED-ENTITY-LENGTH' => '512')), - array(400, 128, array('CONTENT-LENGTH' => '512')), - array(400, 128, array('OC-TOTAL-LENGTH' => '512', 'CONTENT-LENGTH' => '500')), - // \OCP\Files\FileInfo::SPACE-UNKNOWN = -2 - array(-2, 0, array('X-EXPECTED-ENTITY-LENGTH' => '1024')), - array(-2, 0, array('CONTENT-LENGTH' => '512')), - array(-2, 0, array('OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512')), - array(-2, 128, array('X-EXPECTED-ENTITY-LENGTH' => '1024')), - array(-2, 128, array('CONTENT-LENGTH' => '512')), - array(-2, 128, array('OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512')), - ); - } - - /** - * @dataProvider quotaChunkedOkProvider - */ - public function testCheckQuotaChunkedOk($quota, $chunkTotalSize, $headers) { - $this->init($quota, 'sub/test.txt'); - - $mockChunking = $this->getMockBuilder('\OC_FileChunking') - ->disableOriginalConstructor() - ->getMock(); - $mockChunking->expects($this->once()) - ->method('getCurrentSize') - ->will($this->returnValue($chunkTotalSize)); - - $this->plugin->expects($this->once()) - ->method('getFileChunking') - ->will($this->returnValue($mockChunking)); - - $headers['OC-CHUNKED'] = 1; - $this->server->httpRequest = new \Sabre\HTTP\Request(null, null, $headers); - $result = $this->plugin->checkQuota('/sub/test.txt-chunking-12345-3-1'); - $this->assertTrue($result); - } - - public function quotaChunkedFailProvider() { - return array( - array(400, 0, array('X-EXPECTED-ENTITY-LENGTH' => '1024')), - array(400, 0, array('CONTENT-LENGTH' => '512')), - array(400, 0, array('OC-TOTAL-LENGTH' => '1024', 'CONTENT-LENGTH' => '512')), - // with existing chunks (allowed size = total length - chunk total size) - array(380, 128, array('X-EXPECTED-ENTITY-LENGTH' => '512')), - array(380, 128, array('CONTENT-LENGTH' => '512')), - array(380, 128, array('OC-TOTAL-LENGTH' => '512', 'CONTENT-LENGTH' => '500')), - ); - } - - /** - * @dataProvider quotaChunkedFailProvider - * @expectedException \Sabre\DAV\Exception\InsufficientStorage - */ - public function testCheckQuotaChunkedFail($quota, $chunkTotalSize, $headers) { - $this->init($quota, 'sub/test.txt'); - - $mockChunking = $this->getMockBuilder('\OC_FileChunking') - ->disableOriginalConstructor() - ->getMock(); - $mockChunking->expects($this->once()) - ->method('getCurrentSize') - ->will($this->returnValue($chunkTotalSize)); - - $this->plugin->expects($this->once()) - ->method('getFileChunking') - ->will($this->returnValue($mockChunking)); - - $headers['OC-CHUNKED'] = 1; - $this->server->httpRequest = new \Sabre\HTTP\Request(null, null, $headers); - $this->plugin->checkQuota('/sub/test.txt-chunking-12345-3-1'); - } - - private function buildFileViewMock($quota, $checkedPath) { - // mock filesysten - $view = $this->getMock('\OC\Files\View', array('free_space'), array(), '', false); - $view->expects($this->any()) - ->method('free_space') - ->with($this->identicalTo($checkedPath)) - ->will($this->returnValue($quota)); - - return $view; - } - -} diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/downloadtest.php b/apps/dav/tests/unit/connector/sabre/requesttest/downloadtest.php deleted file mode 100644 index 3d047399a1f..00000000000 --- a/apps/dav/tests/unit/connector/sabre/requesttest/downloadtest.php +++ /dev/null @@ -1,73 +0,0 @@ -<?php -/** - * @author Robin Appelman <icewind@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest; - -use OCP\AppFramework\Http; -use OCP\Lock\ILockingProvider; - -/** - * Class DownloadTest - * - * @group DB - * - * @package OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest - */ -class DownloadTest extends RequestTest { - public function testDownload() { - $user = $this->getUniqueID(); - $view = $this->setupUser($user, 'pass'); - - $view->file_put_contents('foo.txt', 'bar'); - - $response = $this->request($view, $user, 'pass', 'GET', '/foo.txt'); - $this->assertEquals(Http::STATUS_OK, $response->getStatus()); - $this->assertEquals(stream_get_contents($response->getBody()), 'bar'); - } - - /** - * @expectedException \OCA\DAV\Connector\Sabre\Exception\FileLocked - */ - public function testDownloadWriteLocked() { - $user = $this->getUniqueID(); - $view = $this->setupUser($user, 'pass'); - - $view->file_put_contents('foo.txt', 'bar'); - - $view->lockFile('/foo.txt', ILockingProvider::LOCK_EXCLUSIVE); - - $this->request($view, $user, 'pass', 'GET', '/foo.txt', 'asd'); - } - - public function testDownloadReadLocked() { - $user = $this->getUniqueID(); - $view = $this->setupUser($user, 'pass'); - - $view->file_put_contents('foo.txt', 'bar'); - - $view->lockFile('/foo.txt', ILockingProvider::LOCK_SHARED); - - $response = $this->request($view, $user, 'pass', 'GET', '/foo.txt', 'asd'); - $this->assertEquals(Http::STATUS_OK, $response->getStatus()); - $this->assertEquals(stream_get_contents($response->getBody()), 'bar'); - } -} diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/encryptionuploadtest.php b/apps/dav/tests/unit/connector/sabre/requesttest/encryptionuploadtest.php deleted file mode 100644 index c5c6d0da0c2..00000000000 --- a/apps/dav/tests/unit/connector/sabre/requesttest/encryptionuploadtest.php +++ /dev/null @@ -1,46 +0,0 @@ -<?php -/** - * @author Robin Appelman <icewind@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest; - -use OC\Files\View; -use Test\Traits\EncryptionTrait; - -/** - * Class EncryptionUploadTest - * - * @group DB - * - * @package OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest - */ -class EncryptionUploadTest extends UploadTest { - use EncryptionTrait; - - protected function setupUser($name, $password) { - $this->createUser($name, $password); - $tmpFolder = \OC::$server->getTempManager()->getTemporaryFolder(); - $this->registerMount($name, '\OC\Files\Storage\Local', '/' . $name, ['datadir' => $tmpFolder]); - $this->setupForUser($name, $password); - $this->loginWithEncryption($name); - return new View('/' . $name . '/files'); - } -} diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/exceptionplugin.php b/apps/dav/tests/unit/connector/sabre/requesttest/exceptionplugin.php deleted file mode 100644 index a6a0f9d3b86..00000000000 --- a/apps/dav/tests/unit/connector/sabre/requesttest/exceptionplugin.php +++ /dev/null @@ -1,46 +0,0 @@ -<?php -/** - * @author Robin Appelman <icewind@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest; - -use Sabre\DAV\Exception; - -class ExceptionPlugin extends \OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin { - /** - * @var \Exception[] - */ - protected $exceptions = []; - - public function logException(\Exception $ex) { - $exceptionClass = get_class($ex); - if (!isset($this->nonFatalExceptions[$exceptionClass])) { - $this->exceptions[] = $ex; - } - } - - /** - * @return \Exception[] - */ - public function getExceptions() { - return $this->exceptions; - } -} diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/partfileinrootupload.php b/apps/dav/tests/unit/connector/sabre/requesttest/partfileinrootupload.php deleted file mode 100644 index 52790c5b00b..00000000000 --- a/apps/dav/tests/unit/connector/sabre/requesttest/partfileinrootupload.php +++ /dev/null @@ -1,56 +0,0 @@ -<?php -/** - * @author Robin Appelman <icewind@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest; - -use OC\Files\View; -use Test\Traits\EncryptionTrait; - -/** - * Class EncryptionUploadTest - * - * @group DB - * - * @package OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest - */ -class PartFileInRootUpload extends UploadTest { - protected function setUp() { - $config = \OC::$server->getConfig(); - $mockConfig = $this->getMock('\OCP\IConfig'); - $mockConfig->expects($this->any()) - ->method('getSystemValue') - ->will($this->returnCallback(function ($key, $default) use ($config) { - if ($key === 'part_file_in_storage') { - return false; - } else { - return $config->getSystemValue($key, $default); - } - })); - $this->overwriteService('AllConfig', $mockConfig); - parent::setUp(); - } - - protected function tearDown() { - $this->restoreService('AllConfig'); - return parent::tearDown(); - } -} diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/sapi.php b/apps/dav/tests/unit/connector/sabre/requesttest/sapi.php deleted file mode 100644 index 6407d9bc28b..00000000000 --- a/apps/dav/tests/unit/connector/sabre/requesttest/sapi.php +++ /dev/null @@ -1,75 +0,0 @@ -<?php -/** - * @author Robin Appelman <icewind@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest; - -use Sabre\HTTP\Request; -use Sabre\HTTP\Response; - -class Sapi { - /** - * @var \Sabre\HTTP\Request - */ - private $request; - - /** - * @var \Sabre\HTTP\Response - */ - private $response; - - /** - * This static method will create a new Request object, based on the - * current PHP request. - * - * @return \Sabre\HTTP\Request - */ - public function getRequest() { - return $this->request; - } - - public function __construct(Request $request) { - $this->request = $request; - } - - /** - * @param \Sabre\HTTP\Response $response - * @return void - */ - public function sendResponse(Response $response) { - // we need to copy the body since we close the source stream - $copyStream = fopen('php://temp', 'r+'); - if (is_string($response->getBody())) { - fwrite($copyStream, $response->getBody()); - } else if (is_resource($response->getBody())) { - stream_copy_to_stream($response->getBody(), $copyStream); - } - rewind($copyStream); - $this->response = new Response($response->getStatus(), $response->getHeaders(), $copyStream); - } - - /** - * @return \Sabre\HTTP\Response - */ - public function getResponse() { - return $this->response; - } -} diff --git a/apps/dav/tests/unit/connector/sabre/requesttest/uploadtest.php b/apps/dav/tests/unit/connector/sabre/requesttest/uploadtest.php deleted file mode 100644 index ae30268e366..00000000000 --- a/apps/dav/tests/unit/connector/sabre/requesttest/uploadtest.php +++ /dev/null @@ -1,211 +0,0 @@ -<?php -/** - * @author Robin Appelman <icewind@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest; - -use OC\Connector\Sabre\Exception\FileLocked; -use OCP\AppFramework\Http; -use OCP\Lock\ILockingProvider; - -/** - * Class UploadTest - * - * @group DB - * - * @package OCA\DAV\Tests\Unit\Connector\Sabre\RequestTest - */ -class UploadTest extends RequestTest { - public function testBasicUpload() { - $user = $this->getUniqueID(); - $view = $this->setupUser($user, 'pass'); - - $this->assertFalse($view->file_exists('foo.txt')); - $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt', 'asd'); - - $this->assertEquals(Http::STATUS_CREATED, $response->getStatus()); - $this->assertTrue($view->file_exists('foo.txt')); - $this->assertEquals('asd', $view->file_get_contents('foo.txt')); - - $info = $view->getFileInfo('foo.txt'); - $this->assertInstanceOf('\OC\Files\FileInfo', $info); - $this->assertEquals(3, $info->getSize()); - } - - public function testUploadOverWrite() { - $user = $this->getUniqueID(); - $view = $this->setupUser($user, 'pass'); - - $view->file_put_contents('foo.txt', 'foobar'); - - $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt', 'asd'); - - $this->assertEquals(Http::STATUS_NO_CONTENT, $response->getStatus()); - $this->assertEquals('asd', $view->file_get_contents('foo.txt')); - - $info = $view->getFileInfo('foo.txt'); - $this->assertInstanceOf('\OC\Files\FileInfo', $info); - $this->assertEquals(3, $info->getSize()); - } - - /** - * @expectedException \OCA\DAV\Connector\Sabre\Exception\FileLocked - */ - public function testUploadOverWriteReadLocked() { - $user = $this->getUniqueID(); - $view = $this->setupUser($user, 'pass'); - - $view->file_put_contents('foo.txt', 'bar'); - - $view->lockFile('/foo.txt', ILockingProvider::LOCK_SHARED); - - $this->request($view, $user, 'pass', 'PUT', '/foo.txt', 'asd'); - } - - /** - * @expectedException \OCA\DAV\Connector\Sabre\Exception\FileLocked - */ - public function testUploadOverWriteWriteLocked() { - $user = $this->getUniqueID(); - $view = $this->setupUser($user, 'pass'); - - $view->file_put_contents('foo.txt', 'bar'); - - $view->lockFile('/foo.txt', ILockingProvider::LOCK_EXCLUSIVE); - - $this->request($view, $user, 'pass', 'PUT', '/foo.txt', 'asd'); - } - - public function testChunkedUpload() { - $user = $this->getUniqueID(); - $view = $this->setupUser($user, 'pass'); - - $this->assertFalse($view->file_exists('foo.txt')); - $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-0', 'asd', ['OC-Chunked' => '1']); - - $this->assertEquals(201, $response->getStatus()); - $this->assertFalse($view->file_exists('foo.txt')); - - $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-1', 'bar', ['OC-Chunked' => '1']); - - $this->assertEquals(Http::STATUS_CREATED, $response->getStatus()); - $this->assertTrue($view->file_exists('foo.txt')); - - $this->assertEquals('asdbar', $view->file_get_contents('foo.txt')); - - $info = $view->getFileInfo('foo.txt'); - $this->assertInstanceOf('\OC\Files\FileInfo', $info); - $this->assertEquals(6, $info->getSize()); - } - - public function testChunkedUploadOverWrite() { - $user = $this->getUniqueID(); - $view = $this->setupUser($user, 'pass'); - - $view->file_put_contents('foo.txt', 'bar'); - $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-0', 'asd', ['OC-Chunked' => '1']); - - $this->assertEquals(Http::STATUS_CREATED, $response->getStatus()); - $this->assertEquals('bar', $view->file_get_contents('foo.txt')); - - $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-1', 'bar', ['OC-Chunked' => '1']); - - $this->assertEquals(Http::STATUS_CREATED, $response->getStatus()); - - $this->assertEquals('asdbar', $view->file_get_contents('foo.txt')); - - $info = $view->getFileInfo('foo.txt'); - $this->assertInstanceOf('\OC\Files\FileInfo', $info); - $this->assertEquals(6, $info->getSize()); - } - - public function testChunkedUploadOutOfOrder() { - $user = $this->getUniqueID(); - $view = $this->setupUser($user, 'pass'); - - $this->assertFalse($view->file_exists('foo.txt')); - $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-1', 'bar', ['OC-Chunked' => '1']); - - $this->assertEquals(Http::STATUS_CREATED, $response->getStatus()); - $this->assertFalse($view->file_exists('foo.txt')); - - $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-0', 'asd', ['OC-Chunked' => '1']); - - $this->assertEquals(201, $response->getStatus()); - $this->assertTrue($view->file_exists('foo.txt')); - - $this->assertEquals('asdbar', $view->file_get_contents('foo.txt')); - - $info = $view->getFileInfo('foo.txt'); - $this->assertInstanceOf('\OC\Files\FileInfo', $info); - $this->assertEquals(6, $info->getSize()); - } - - /** - * @expectedException \OCA\DAV\Connector\Sabre\Exception\FileLocked - */ - public function testChunkedUploadOutOfOrderReadLocked() { - $user = $this->getUniqueID(); - $view = $this->setupUser($user, 'pass'); - - $this->assertFalse($view->file_exists('foo.txt')); - - $view->lockFile('/foo.txt', ILockingProvider::LOCK_SHARED); - - try { - $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-1', 'bar', ['OC-Chunked' => '1']); - } catch (\OCA\DAV\Connector\Sabre\Exception\FileLocked $e) { - $this->fail('Didn\'t expect locked error for the first chunk on read lock'); - return; - } - - $this->assertEquals(Http::STATUS_CREATED, $response->getStatus()); - $this->assertFalse($view->file_exists('foo.txt')); - - // last chunk should trigger the locked error since it tries to assemble - $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-0', 'asd', ['OC-Chunked' => '1']); - } - - /** - * @expectedException \OCA\DAV\Connector\Sabre\Exception\FileLocked - */ - public function testChunkedUploadOutOfOrderWriteLocked() { - $user = $this->getUniqueID(); - $view = $this->setupUser($user, 'pass'); - - $this->assertFalse($view->file_exists('foo.txt')); - - $view->lockFile('/foo.txt', ILockingProvider::LOCK_EXCLUSIVE); - - try { - $response = $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-1', 'bar', ['OC-Chunked' => '1']); - } catch (\OCA\DAV\Connector\Sabre\Exception\FileLocked $e) { - $this->fail('Didn\'t expect locked error for the first chunk on write lock'); // maybe forbid this in the future for write locks only? - return; - } - - $this->assertEquals(Http::STATUS_CREATED, $response->getStatus()); - $this->assertFalse($view->file_exists('foo.txt')); - - // last chunk should trigger the locked error since it tries to assemble - $this->request($view, $user, 'pass', 'PUT', '/foo.txt-chunking-123-2-0', 'asd', ['OC-Chunked' => '1']); - } -} diff --git a/apps/dav/tests/unit/connector/sabre/sharesplugin.php b/apps/dav/tests/unit/connector/sabre/sharesplugin.php deleted file mode 100644 index 42f1b539916..00000000000 --- a/apps/dav/tests/unit/connector/sabre/sharesplugin.php +++ /dev/null @@ -1,259 +0,0 @@ -<?php -/** - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace OCA\DAV\Tests\Unit\Connector\Sabre; - -class SharesPlugin extends \Test\TestCase { - - const SHARETYPES_PROPERTYNAME = \OCA\DAV\Connector\Sabre\SharesPlugin::SHARETYPES_PROPERTYNAME; - - /** - * @var \Sabre\DAV\Server - */ - private $server; - - /** - * @var \Sabre\DAV\Tree - */ - private $tree; - - /** - * @var \OCP\Share\IManager - */ - private $shareManager; - - /** - * @var \OCP\Files\Folder - */ - private $userFolder; - - /** - * @var \OCA\DAV\Connector\Sabre\SharesPlugin - */ - private $plugin; - - public function setUp() { - parent::setUp(); - $this->server = new \Sabre\DAV\Server(); - $this->tree = $this->getMockBuilder('\Sabre\DAV\Tree') - ->disableOriginalConstructor() - ->getMock(); - $this->shareManager = $this->getMock('\OCP\Share\IManager'); - $user = $this->getMock('\OCP\IUser'); - $user->expects($this->once()) - ->method('getUID') - ->will($this->returnValue('user1')); - $userSession = $this->getMock('\OCP\IUserSession'); - $userSession->expects($this->once()) - ->method('getUser') - ->will($this->returnValue($user)); - - $this->userFolder = $this->getMock('\OCP\Files\Folder'); - - $this->plugin = new \OCA\DAV\Connector\Sabre\SharesPlugin( - $this->tree, - $userSession, - $this->userFolder, - $this->shareManager - ); - $this->plugin->initialize($this->server); - } - - /** - * @dataProvider sharesGetPropertiesDataProvider - */ - public function testGetProperties($shareTypes) { - $sabreNode = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Node') - ->disableOriginalConstructor() - ->getMock(); - $sabreNode->expects($this->any()) - ->method('getId') - ->will($this->returnValue(123)); - $sabreNode->expects($this->once()) - ->method('getPath') - ->will($this->returnValue('/subdir')); - - // node API nodes - $node = $this->getMock('\OCP\Files\Folder'); - - $this->userFolder->expects($this->once()) - ->method('get') - ->with('/subdir') - ->will($this->returnValue($node)); - - $this->shareManager->expects($this->any()) - ->method('getSharesBy') - ->with( - $this->equalTo('user1'), - $this->anything(), - $this->anything(), - $this->equalTo(false), - $this->equalTo(1) - ) - ->will($this->returnCallback(function($userId, $requestedShareType, $node, $flag, $limit) use ($shareTypes){ - if (in_array($requestedShareType, $shareTypes)) { - return ['dummyshare']; - } - return []; - })); - - $propFind = new \Sabre\DAV\PropFind( - '/dummyPath', - [self::SHARETYPES_PROPERTYNAME], - 0 - ); - - $this->plugin->handleGetProperties( - $propFind, - $sabreNode - ); - - $result = $propFind->getResultForMultiStatus(); - - $this->assertEmpty($result[404]); - unset($result[404]); - $this->assertEquals($shareTypes, $result[200][self::SHARETYPES_PROPERTYNAME]->getShareTypes()); - } - - /** - * @dataProvider sharesGetPropertiesDataProvider - */ - public function testPreloadThenGetProperties($shareTypes) { - $sabreNode1 = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\File') - ->disableOriginalConstructor() - ->getMock(); - $sabreNode1->expects($this->any()) - ->method('getId') - ->will($this->returnValue(111)); - $sabreNode1->expects($this->never()) - ->method('getPath'); - $sabreNode2 = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\File') - ->disableOriginalConstructor() - ->getMock(); - $sabreNode2->expects($this->any()) - ->method('getId') - ->will($this->returnValue(222)); - $sabreNode2->expects($this->never()) - ->method('getPath'); - - $sabreNode = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Directory') - ->disableOriginalConstructor() - ->getMock(); - $sabreNode->expects($this->any()) - ->method('getId') - ->will($this->returnValue(123)); - // never, because we use getDirectoryListing from the Node API instead - $sabreNode->expects($this->never()) - ->method('getChildren'); - $sabreNode->expects($this->any()) - ->method('getPath') - ->will($this->returnValue('/subdir')); - - // node API nodes - $node = $this->getMock('\OCP\Files\Folder'); - $node->expects($this->any()) - ->method('getId') - ->will($this->returnValue(123)); - $node1 = $this->getMock('\OCP\Files\File'); - $node1->expects($this->any()) - ->method('getId') - ->will($this->returnValue(111)); - $node2 = $this->getMock('\OCP\Files\File'); - $node2->expects($this->any()) - ->method('getId') - ->will($this->returnValue(222)); - $node->expects($this->once()) - ->method('getDirectoryListing') - ->will($this->returnValue([$node1, $node2])); - - $this->userFolder->expects($this->once()) - ->method('get') - ->with('/subdir') - ->will($this->returnValue($node)); - - $this->shareManager->expects($this->any()) - ->method('getSharesBy') - ->with( - $this->equalTo('user1'), - $this->anything(), - $this->anything(), - $this->equalTo(false), - $this->equalTo(1) - ) - ->will($this->returnCallback(function($userId, $requestedShareType, $node, $flag, $limit) use ($shareTypes){ - if ($node->getId() === 111 && in_array($requestedShareType, $shareTypes)) { - return ['dummyshare']; - } - - return []; - })); - - // simulate sabre recursive PROPFIND traversal - $propFindRoot = new \Sabre\DAV\PropFind( - '/subdir', - [self::SHARETYPES_PROPERTYNAME], - 1 - ); - $propFind1 = new \Sabre\DAV\PropFind( - '/subdir/test.txt', - [self::SHARETYPES_PROPERTYNAME], - 0 - ); - $propFind2 = new \Sabre\DAV\PropFind( - '/subdir/test2.txt', - [self::SHARETYPES_PROPERTYNAME], - 0 - ); - - $this->plugin->handleGetProperties( - $propFindRoot, - $sabreNode - ); - $this->plugin->handleGetProperties( - $propFind1, - $sabreNode1 - ); - $this->plugin->handleGetProperties( - $propFind2, - $sabreNode2 - ); - - $result = $propFind1->getResultForMultiStatus(); - - $this->assertEmpty($result[404]); - unset($result[404]); - $this->assertEquals($shareTypes, $result[200][self::SHARETYPES_PROPERTYNAME]->getShareTypes()); - } - - function sharesGetPropertiesDataProvider() { - return [ - [[]], - [[\OCP\Share::SHARE_TYPE_USER]], - [[\OCP\Share::SHARE_TYPE_GROUP]], - [[\OCP\Share::SHARE_TYPE_LINK]], - [[\OCP\Share::SHARE_TYPE_REMOTE]], - [[\OCP\Share::SHARE_TYPE_USER, \OCP\Share::SHARE_TYPE_GROUP]], - [[\OCP\Share::SHARE_TYPE_USER, \OCP\Share::SHARE_TYPE_GROUP, \OCP\Share::SHARE_TYPE_LINK]], - [[\OCP\Share::SHARE_TYPE_USER, \OCP\Share::SHARE_TYPE_LINK]], - [[\OCP\Share::SHARE_TYPE_GROUP, \OCP\Share::SHARE_TYPE_LINK]], - [[\OCP\Share::SHARE_TYPE_USER, \OCP\Share::SHARE_TYPE_REMOTE]], - ]; - } -} diff --git a/apps/dav/tests/unit/connector/sabre/tagsplugin.php b/apps/dav/tests/unit/connector/sabre/tagsplugin.php deleted file mode 100644 index 95ba002e393..00000000000 --- a/apps/dav/tests/unit/connector/sabre/tagsplugin.php +++ /dev/null @@ -1,417 +0,0 @@ -<?php -/** - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace OCA\DAV\Tests\Unit\Connector\Sabre; - -/** - * Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com> - * This file is licensed under the Affero General Public License version 3 or - * later. - * See the COPYING-README file. - */ -class TagsPlugin extends \Test\TestCase { - - const TAGS_PROPERTYNAME = \OCA\DAV\Connector\Sabre\TagsPlugin::TAGS_PROPERTYNAME; - const FAVORITE_PROPERTYNAME = \OCA\DAV\Connector\Sabre\TagsPlugin::FAVORITE_PROPERTYNAME; - const TAG_FAVORITE = \OCA\DAV\Connector\Sabre\TagsPlugin::TAG_FAVORITE; - - /** - * @var \Sabre\DAV\Server - */ - private $server; - - /** - * @var \Sabre\DAV\Tree - */ - private $tree; - - /** - * @var \OCP\ITagManager - */ - private $tagManager; - - /** - * @var \OCP\ITags - */ - private $tagger; - - /** - * @var \OCA\DAV\Connector\Sabre\TagsPlugin - */ - private $plugin; - - public function setUp() { - parent::setUp(); - $this->server = new \Sabre\DAV\Server(); - $this->tree = $this->getMockBuilder('\Sabre\DAV\Tree') - ->disableOriginalConstructor() - ->getMock(); - $this->tagger = $this->getMock('\OCP\ITags'); - $this->tagManager = $this->getMock('\OCP\ITagManager'); - $this->tagManager->expects($this->any()) - ->method('load') - ->with('files') - ->will($this->returnValue($this->tagger)); - $this->plugin = new \OCA\DAV\Connector\Sabre\TagsPlugin($this->tree, $this->tagManager); - $this->plugin->initialize($this->server); - } - - /** - * @dataProvider tagsGetPropertiesDataProvider - */ - public function testGetProperties($tags, $requestedProperties, $expectedProperties) { - $node = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Node') - ->disableOriginalConstructor() - ->getMock(); - $node->expects($this->any()) - ->method('getId') - ->will($this->returnValue(123)); - - $expectedCallCount = 0; - if (count($requestedProperties) > 0) { - $expectedCallCount = 1; - } - - $this->tagger->expects($this->exactly($expectedCallCount)) - ->method('getTagsForObjects') - ->with($this->equalTo(array(123))) - ->will($this->returnValue(array(123 => $tags))); - - $propFind = new \Sabre\DAV\PropFind( - '/dummyPath', - $requestedProperties, - 0 - ); - - $this->plugin->handleGetProperties( - $propFind, - $node - ); - - $result = $propFind->getResultForMultiStatus(); - - $this->assertEmpty($result[404]); - unset($result[404]); - $this->assertEquals($expectedProperties, $result); - } - - /** - * @dataProvider tagsGetPropertiesDataProvider - */ - public function testPreloadThenGetProperties($tags, $requestedProperties, $expectedProperties) { - $node1 = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\File') - ->disableOriginalConstructor() - ->getMock(); - $node1->expects($this->any()) - ->method('getId') - ->will($this->returnValue(111)); - $node2 = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\File') - ->disableOriginalConstructor() - ->getMock(); - $node2->expects($this->any()) - ->method('getId') - ->will($this->returnValue(222)); - - $expectedCallCount = 0; - if (count($requestedProperties) > 0) { - // this guarantees that getTagsForObjects - // is only called once and then the tags - // are cached - $expectedCallCount = 1; - } - - $node = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Directory') - ->disableOriginalConstructor() - ->getMock(); - $node->expects($this->any()) - ->method('getId') - ->will($this->returnValue(123)); - $node->expects($this->exactly($expectedCallCount)) - ->method('getChildren') - ->will($this->returnValue(array($node1, $node2))); - - $this->tagger->expects($this->exactly($expectedCallCount)) - ->method('getTagsForObjects') - ->with($this->equalTo(array(123, 111, 222))) - ->will($this->returnValue( - array( - 111 => $tags, - 123 => $tags - ) - )); - - // simulate sabre recursive PROPFIND traversal - $propFindRoot = new \Sabre\DAV\PropFind( - '/subdir', - $requestedProperties, - 1 - ); - $propFind1 = new \Sabre\DAV\PropFind( - '/subdir/test.txt', - $requestedProperties, - 0 - ); - $propFind2 = new \Sabre\DAV\PropFind( - '/subdir/test2.txt', - $requestedProperties, - 0 - ); - - $this->plugin->handleGetProperties( - $propFindRoot, - $node - ); - $this->plugin->handleGetProperties( - $propFind1, - $node1 - ); - $this->plugin->handleGetProperties( - $propFind2, - $node2 - ); - - $result = $propFind1->getResultForMultiStatus(); - - $this->assertEmpty($result[404]); - unset($result[404]); - $this->assertEquals($expectedProperties, $result); - } - - function tagsGetPropertiesDataProvider() { - return array( - // request both, receive both - array( - array('tag1', 'tag2', self::TAG_FAVORITE), - array(self::TAGS_PROPERTYNAME, self::FAVORITE_PROPERTYNAME), - array( - 200 => array( - self::TAGS_PROPERTYNAME => new \OCA\DAV\Connector\Sabre\TagList(array('tag1', 'tag2')), - self::FAVORITE_PROPERTYNAME => true, - ) - ) - ), - // request tags alone - array( - array('tag1', 'tag2', self::TAG_FAVORITE), - array(self::TAGS_PROPERTYNAME), - array( - 200 => array( - self::TAGS_PROPERTYNAME => new \OCA\DAV\Connector\Sabre\TagList(array('tag1', 'tag2')), - ) - ) - ), - // request fav alone - array( - array('tag1', 'tag2', self::TAG_FAVORITE), - array(self::FAVORITE_PROPERTYNAME), - array( - 200 => array( - self::FAVORITE_PROPERTYNAME => true, - ) - ) - ), - // request none - array( - array('tag1', 'tag2', self::TAG_FAVORITE), - array(), - array( - 200 => array() - ), - ), - // request both with none set, receive both - array( - array(), - array(self::TAGS_PROPERTYNAME, self::FAVORITE_PROPERTYNAME), - array( - 200 => array( - self::TAGS_PROPERTYNAME => new \OCA\DAV\Connector\Sabre\TagList(array()), - self::FAVORITE_PROPERTYNAME => false, - ) - ) - ), - ); - } - - public function testUpdateTags() { - // this test will replace the existing tags "tagremove" with "tag1" and "tag2" - // and keep "tagkeep" - $node = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Node') - ->disableOriginalConstructor() - ->getMock(); - $node->expects($this->any()) - ->method('getId') - ->will($this->returnValue(123)); - - $this->tree->expects($this->any()) - ->method('getNodeForPath') - ->with('/dummypath') - ->will($this->returnValue($node)); - - $this->tagger->expects($this->at(0)) - ->method('getTagsForObjects') - ->with($this->equalTo(array(123))) - ->will($this->returnValue(array(123 => array('tagkeep', 'tagremove', self::TAG_FAVORITE)))); - - // then tag as tag1 and tag2 - $this->tagger->expects($this->at(1)) - ->method('tagAs') - ->with(123, 'tag1'); - $this->tagger->expects($this->at(2)) - ->method('tagAs') - ->with(123, 'tag2'); - - // it will untag tag3 - $this->tagger->expects($this->at(3)) - ->method('unTag') - ->with(123, 'tagremove'); - - // properties to set - $propPatch = new \Sabre\DAV\PropPatch(array( - self::TAGS_PROPERTYNAME => new \OCA\DAV\Connector\Sabre\TagList(array('tag1', 'tag2', 'tagkeep')) - )); - - $this->plugin->handleUpdateProperties( - '/dummypath', - $propPatch - ); - - $propPatch->commit(); - - // all requested properties removed, as they were processed already - $this->assertEmpty($propPatch->getRemainingMutations()); - - $result = $propPatch->getResult(); - $this->assertEquals(200, $result[self::TAGS_PROPERTYNAME]); - $this->assertFalse(isset($result[self::FAVORITE_PROPERTYNAME])); - } - - public function testUpdateTagsFromScratch() { - $node = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Node') - ->disableOriginalConstructor() - ->getMock(); - $node->expects($this->any()) - ->method('getId') - ->will($this->returnValue(123)); - - $this->tree->expects($this->any()) - ->method('getNodeForPath') - ->with('/dummypath') - ->will($this->returnValue($node)); - - $this->tagger->expects($this->at(0)) - ->method('getTagsForObjects') - ->with($this->equalTo(array(123))) - ->will($this->returnValue(array())); - - // then tag as tag1 and tag2 - $this->tagger->expects($this->at(1)) - ->method('tagAs') - ->with(123, 'tag1'); - $this->tagger->expects($this->at(2)) - ->method('tagAs') - ->with(123, 'tag2'); - - // properties to set - $propPatch = new \Sabre\DAV\PropPatch(array( - self::TAGS_PROPERTYNAME => new \OCA\DAV\Connector\Sabre\TagList(array('tag1', 'tag2', 'tagkeep')) - )); - - $this->plugin->handleUpdateProperties( - '/dummypath', - $propPatch - ); - - $propPatch->commit(); - - // all requested properties removed, as they were processed already - $this->assertEmpty($propPatch->getRemainingMutations()); - - $result = $propPatch->getResult(); - $this->assertEquals(200, $result[self::TAGS_PROPERTYNAME]); - $this->assertFalse(false, isset($result[self::FAVORITE_PROPERTYNAME])); - } - - public function testUpdateFav() { - // this test will replace the existing tags "tagremove" with "tag1" and "tag2" - // and keep "tagkeep" - $node = $this->getMockBuilder('\OCA\DAV\Connector\Sabre\Node') - ->disableOriginalConstructor() - ->getMock(); - $node->expects($this->any()) - ->method('getId') - ->will($this->returnValue(123)); - - $this->tree->expects($this->any()) - ->method('getNodeForPath') - ->with('/dummypath') - ->will($this->returnValue($node)); - - // set favorite tag - $this->tagger->expects($this->once()) - ->method('tagAs') - ->with(123, self::TAG_FAVORITE); - - // properties to set - $propPatch = new \Sabre\DAV\PropPatch(array( - self::FAVORITE_PROPERTYNAME => true - )); - - $this->plugin->handleUpdateProperties( - '/dummypath', - $propPatch - ); - - $propPatch->commit(); - - // all requested properties removed, as they were processed already - $this->assertEmpty($propPatch->getRemainingMutations()); - - $result = $propPatch->getResult(); - $this->assertFalse(false, isset($result[self::TAGS_PROPERTYNAME])); - $this->assertEquals(200, isset($result[self::FAVORITE_PROPERTYNAME])); - - // unfavorite now - // set favorite tag - $this->tagger->expects($this->once()) - ->method('unTag') - ->with(123, self::TAG_FAVORITE); - - // properties to set - $propPatch = new \Sabre\DAV\PropPatch(array( - self::FAVORITE_PROPERTYNAME => false - )); - - $this->plugin->handleUpdateProperties( - '/dummypath', - $propPatch - ); - - $propPatch->commit(); - - // all requested properties removed, as they were processed already - $this->assertEmpty($propPatch->getRemainingMutations()); - - $result = $propPatch->getResult(); - $this->assertFalse(false, isset($result[self::TAGS_PROPERTYNAME])); - $this->assertEquals(200, isset($result[self::FAVORITE_PROPERTYNAME])); - } - -} diff --git a/apps/dav/tests/unit/dav/groupprincipaltest.php b/apps/dav/tests/unit/dav/groupprincipaltest.php deleted file mode 100644 index 9d012639310..00000000000 --- a/apps/dav/tests/unit/dav/groupprincipaltest.php +++ /dev/null @@ -1,164 +0,0 @@ -<?php -/** - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace OCA\DAV\Tests\Unit\DAV; - -use OCA\DAV\DAV\GroupPrincipalBackend; -use OCP\IGroupManager; -use PHPUnit_Framework_MockObject_MockObject; -use \Sabre\DAV\PropPatch; - -class GroupPrincipalTest extends \Test\TestCase { - - /** @var IGroupManager | PHPUnit_Framework_MockObject_MockObject */ - private $groupManager; - - /** @var GroupPrincipalBackend */ - private $connector; - - public function setUp() { - $this->groupManager = $this->getMockBuilder('\OCP\IGroupManager') - ->disableOriginalConstructor()->getMock(); - - $this->connector = new GroupPrincipalBackend($this->groupManager); - parent::setUp(); - } - - public function testGetPrincipalsByPrefixWithoutPrefix() { - $response = $this->connector->getPrincipalsByPrefix(''); - $this->assertSame([], $response); - } - - public function testGetPrincipalsByPrefixWithUsers() { - $group1 = $this->mockGroup('foo'); - $group2 = $this->mockGroup('bar'); - $this->groupManager - ->expects($this->once()) - ->method('search') - ->with('') - ->will($this->returnValue([$group1, $group2])); - - $expectedResponse = [ - 0 => [ - 'uri' => 'principals/groups/foo', - '{DAV:}displayname' => 'foo' - ], - 1 => [ - 'uri' => 'principals/groups/bar', - '{DAV:}displayname' => 'bar', - ] - ]; - $response = $this->connector->getPrincipalsByPrefix('principals/groups'); - $this->assertSame($expectedResponse, $response); - } - - public function testGetPrincipalsByPrefixEmpty() { - $this->groupManager - ->expects($this->once()) - ->method('search') - ->with('') - ->will($this->returnValue([])); - - $response = $this->connector->getPrincipalsByPrefix('principals/groups'); - $this->assertSame([], $response); - } - - public function testGetPrincipalsByPathWithoutMail() { - $group1 = $this->mockGroup('foo'); - $this->groupManager - ->expects($this->once()) - ->method('get') - ->with('foo') - ->will($this->returnValue($group1)); - - $expectedResponse = [ - 'uri' => 'principals/groups/foo', - '{DAV:}displayname' => 'foo' - ]; - $response = $this->connector->getPrincipalByPath('principals/groups/foo'); - $this->assertSame($expectedResponse, $response); - } - - public function testGetPrincipalsByPathWithMail() { - $fooUser = $this->mockGroup('foo'); - $this->groupManager - ->expects($this->once()) - ->method('get') - ->with('foo') - ->will($this->returnValue($fooUser)); - - $expectedResponse = [ - 'uri' => 'principals/groups/foo', - '{DAV:}displayname' => 'foo', - ]; - $response = $this->connector->getPrincipalByPath('principals/groups/foo'); - $this->assertSame($expectedResponse, $response); - } - - public function testGetPrincipalsByPathEmpty() { - $this->groupManager - ->expects($this->once()) - ->method('get') - ->with('foo') - ->will($this->returnValue(null)); - - $response = $this->connector->getPrincipalByPath('principals/groups/foo'); - $this->assertSame(null, $response); - } - - public function testGetGroupMemberSet() { - $response = $this->connector->getGroupMemberSet('principals/groups/foo'); - $this->assertSame([], $response); - } - - public function testGetGroupMembership() { - $response = $this->connector->getGroupMembership('principals/groups/foo'); - $this->assertSame([], $response); - } - - /** - * @expectedException \Sabre\DAV\Exception - * @expectedExceptionMessage Setting members of the group is not supported yet - */ - public function testSetGroupMembership() { - $this->connector->setGroupMemberSet('principals/groups/foo', ['foo']); - } - - public function testUpdatePrincipal() { - $this->assertSame(0, $this->connector->updatePrincipal('foo', new PropPatch(array()))); - } - - public function testSearchPrincipals() { - $this->assertSame([], $this->connector->searchPrincipals('principals/groups', [])); - } - - /** - * @return PHPUnit_Framework_MockObject_MockObject - */ - private function mockGroup($gid) { - $fooUser = $this->getMockBuilder('\OC\Group\Group') - ->disableOriginalConstructor()->getMock(); - $fooUser - ->expects($this->exactly(1)) - ->method('getGID') - ->will($this->returnValue($gid)); - return $fooUser; - } -} diff --git a/apps/dav/tests/unit/dav/sharing/plugintest.php b/apps/dav/tests/unit/dav/sharing/plugintest.php deleted file mode 100644 index ce6c96f1bfc..00000000000 --- a/apps/dav/tests/unit/dav/sharing/plugintest.php +++ /dev/null @@ -1,83 +0,0 @@ -<?php -/** - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\DAV; - - -use OCA\DAV\DAV\Sharing\IShareable; -use OCA\DAV\DAV\Sharing\Plugin; -use OCA\DAV\Connector\Sabre\Auth; -use OCP\IRequest; -use Sabre\DAV\Server; -use Sabre\DAV\SimpleCollection; -use Sabre\HTTP\Request; -use Sabre\HTTP\Response; -use Test\TestCase; - -class PluginTest extends TestCase { - - /** @var Plugin */ - private $plugin; - /** @var Server */ - private $server; - /** @var IShareable | \PHPUnit_Framework_MockObject_MockObject */ - private $book; - - public function setUp() { - parent::setUp(); - - /** @var Auth | \PHPUnit_Framework_MockObject_MockObject $authBackend */ - $authBackend = $this->getMockBuilder('OCA\DAV\Connector\Sabre\Auth')->disableOriginalConstructor()->getMock(); - $authBackend->method('isDavAuthenticated')->willReturn(true); - - /** @var IRequest $request */ - $request = $this->getMockBuilder('OCP\IRequest')->disableOriginalConstructor()->getMock(); - $this->plugin = new Plugin($authBackend, $request); - - $root = new SimpleCollection('root'); - $this->server = new \Sabre\DAV\Server($root); - /** @var SimpleCollection $node */ - $this->book = $this->getMockBuilder('OCA\DAV\DAV\Sharing\IShareable')-> - disableOriginalConstructor()-> - getMock(); - $this->book->method('getName')->willReturn('addressbook1.vcf'); - $root->addChild($this->book); - $this->plugin->initialize($this->server); - } - - public function testSharing() { - - $this->book->expects($this->once())->method('updateShares')->with([[ - 'href' => 'principal:principals/admin', - 'commonName' => null, - 'summary' => null, - 'readOnly' => false - ]], ['mailto:wilfredo@example.com']); - - // setup request - $request = new Request(); - $request->addHeader('Content-Type', 'application/xml'); - $request->setUrl('addressbook1.vcf'); - $request->setBody('<?xml version="1.0" encoding="utf-8" ?><CS:share xmlns:D="DAV:" xmlns:CS="http://owncloud.org/ns"><CS:set><D:href>principal:principals/admin</D:href><CS:read-write/></CS:set> <CS:remove><D:href>mailto:wilfredo@example.com</D:href></CS:remove></CS:share>'); - $response = new Response(); - $this->plugin->httpPost($request, $response); - } -} diff --git a/apps/dav/tests/unit/dav/systemprincipalbackendtest.php b/apps/dav/tests/unit/dav/systemprincipalbackendtest.php deleted file mode 100644 index 26717f7509b..00000000000 --- a/apps/dav/tests/unit/dav/systemprincipalbackendtest.php +++ /dev/null @@ -1,131 +0,0 @@ -<?php -/** - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\DAV; - -use OCA\DAV\DAV\SystemPrincipalBackend; -use Test\TestCase; - -class SystemPrincipalBackendTest extends TestCase { - - /** - * @dataProvider providesPrefix - * @param $expected - * @param $prefix - */ - public function testGetPrincipalsByPrefix($expected, $prefix) { - $backend = new SystemPrincipalBackend(); - $result = $backend->getPrincipalsByPrefix($prefix); - $this->assertEquals($expected, $result); - } - - public function providesPrefix() { - return [ - [[], ''], - [[[ - 'uri' => 'principals/system/system', - '{DAV:}displayname' => 'system', - ]], 'principals/system'], - ]; - } - - /** - * @dataProvider providesPath - * @param $expected - * @param $path - */ - public function testGetPrincipalByPath($expected, $path) { - $backend = new SystemPrincipalBackend(); - $result = $backend->getPrincipalByPath($path); - $this->assertEquals($expected, $result); - } - - public function providesPath() { - return [ - [null, ''], - [null, 'principals'], - [null, 'principals/system'], - [[ - 'uri' => 'principals/system/system', - '{DAV:}displayname' => 'system', - ], 'principals/system/system'], - ]; - } - - /** - * @dataProvider providesPrincipalForGetGroupMemberSet - * @expectedException \Sabre\DAV\Exception - * @expectedExceptionMessage Principal not found - * - * @param string $principal - * @throws \Sabre\DAV\Exception - */ - public function testGetGroupMemberSetExceptional($principal) { - $backend = new SystemPrincipalBackend(); - $backend->getGroupMemberSet($principal); - } - - public function providesPrincipalForGetGroupMemberSet() { - return [ - [null], - ['principals/system'], - ]; - } - - /** - * @throws \Sabre\DAV\Exception - */ - public function testGetGroupMemberSet() { - $backend = new SystemPrincipalBackend(); - $result = $backend->getGroupMemberSet('principals/system/system'); - $this->assertEquals(['principals/system/system'], $result); - } - - /** - * @dataProvider providesPrincipalForGetGroupMembership - * @expectedException \Sabre\DAV\Exception - * @expectedExceptionMessage Principal not found - * - * @param string $principal - * @throws \Sabre\DAV\Exception - */ - public function testGetGroupMembershipExceptional($principal) { - $backend = new SystemPrincipalBackend(); - $backend->getGroupMembership($principal); - } - - public function providesPrincipalForGetGroupMembership() { - return [ - ['principals/system/a'], - ]; - } - - /** - * @throws \Sabre\DAV\Exception - */ - public function testGetGroupMembership() { - $backend = new SystemPrincipalBackend(); - $result = $backend->getGroupMembership('principals/system/system'); - $this->assertEquals([], $result); - } - - -} diff --git a/apps/dav/tests/unit/migration/addressbookadaptertest.php b/apps/dav/tests/unit/migration/addressbookadaptertest.php deleted file mode 100644 index e6e57049a93..00000000000 --- a/apps/dav/tests/unit/migration/addressbookadaptertest.php +++ /dev/null @@ -1,129 +0,0 @@ -<?php -/** - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace OCA\DAV\Tests\Unit\Migration; - -use DomainException; -use OCA\Dav\Migration\AddressBookAdapter; -use OCP\IDBConnection; -use Test\TestCase; - -/** - * Class AddressbookAdapterTest - * - * @group DB - * - * @package OCA\DAV\Tests\Unit\Migration - */ -class AddressbookAdapterTest extends TestCase { - - /** @var IDBConnection */ - private $db; - /** @var AddressBookAdapter */ - private $adapter; - /** @var array */ - private $books = []; - /** @var array */ - private $cards = []; - - public function setUp() { - parent::setUp(); - $this->db = \OC::$server->getDatabaseConnection(); - - $manager = new \OC\DB\MDB2SchemaManager($this->db); - $manager->createDbFromStructure(__DIR__ . '/contacts_schema.xml'); - - $this->adapter = new AddressBookAdapter($this->db); - } - - public function tearDown() { - $this->db->dropTable('contacts_addressbooks'); - $this->db->dropTable('contacts_cards'); - parent::tearDown(); - } - - /** - * @expectedException DomainException - */ - public function testOldTablesDoNotExist() { - $adapter = new AddressBookAdapter(\OC::$server->getDatabaseConnection(), 'crazy_table_that_does_no_exist'); - $adapter->setup(); - } - - public function test() { - - // insert test data - $builder = $this->db->getQueryBuilder(); - $builder->insert('contacts_addressbooks') - ->values([ - 'userid' => $builder->createNamedParameter('test-user-666'), - 'displayname' => $builder->createNamedParameter('Display Name'), - 'uri' => $builder->createNamedParameter('contacts'), - 'description' => $builder->createNamedParameter('An address book for testing'), - 'ctag' => $builder->createNamedParameter('112233'), - 'active' => $builder->createNamedParameter('1') - ]) - ->execute(); - $builder = $this->db->getQueryBuilder(); - $builder->insert('contacts_cards') - ->values([ - 'addressbookid' => $builder->createNamedParameter(6666), - 'fullname' => $builder->createNamedParameter('Full Name'), - 'carddata' => $builder->createNamedParameter('datadatadata'), - 'uri' => $builder->createNamedParameter('some-card.vcf'), - 'lastmodified' => $builder->createNamedParameter('112233'), - ]) - ->execute(); - $builder = $this->db->getQueryBuilder(); - $builder->insert('share') - ->values([ - 'share_type' => $builder->createNamedParameter(1), - 'share_with' => $builder->createNamedParameter('user01'), - 'uid_owner' => $builder->createNamedParameter('user02'), - 'item_type' => $builder->createNamedParameter('addressbook'), - 'item_source' => $builder->createNamedParameter(6666), - 'item_target' => $builder->createNamedParameter('Contacts (user02)'), - ]) - ->execute(); - - // test the adapter - $this->adapter->foreachBook('test-user-666', function($row) { - $this->books[] = $row; - }); - $this->assertArrayHasKey('id', $this->books[0]); - $this->assertEquals('test-user-666', $this->books[0]['userid']); - $this->assertEquals('Display Name', $this->books[0]['displayname']); - $this->assertEquals('contacts', $this->books[0]['uri']); - $this->assertEquals('An address book for testing', $this->books[0]['description']); - $this->assertEquals('112233', $this->books[0]['ctag']); - - $this->adapter->foreachCard(6666, function($row) { - $this->cards[]= $row; - }); - $this->assertArrayHasKey('id', $this->cards[0]); - $this->assertEquals(6666, $this->cards[0]['addressbookid']); - - // test getShares - $shares = $this->adapter->getShares(6666); - $this->assertEquals(1, count($shares)); - - } - -} diff --git a/apps/dav/tests/unit/migration/calendar_schema.xml b/apps/dav/tests/unit/migration/calendar_schema.xml deleted file mode 100644 index 6c88b596a3f..00000000000 --- a/apps/dav/tests/unit/migration/calendar_schema.xml +++ /dev/null @@ -1,191 +0,0 @@ -<?xml version="1.0" encoding="ISO-8859-1" ?> -<database> - - <name>*dbname*</name> - <create>true</create> - <overwrite>false</overwrite> - - <charset>utf8</charset> - - <table> - - <name>*dbprefix*clndr_objects</name> - - <declaration> - - <field> - <name>id</name> - <type>integer</type> - <default>0</default> - <notnull>true</notnull> - <autoincrement>1</autoincrement> - <unsigned>true</unsigned> - <length>4</length> - </field> - - <field> - <name>calendarid</name> - <type>integer</type> - <default></default> - <notnull>true</notnull> - <unsigned>true</unsigned> - <length>4</length> - </field> - - <field> - <name>objecttype</name> - <type>text</type> - <default></default> - <notnull>true</notnull> - <length>40</length> - </field> - - <field> - <name>startdate</name> - <type>timestamp</type> - <default>1970-01-01 00:00:00</default> - <notnull>false</notnull> - </field> - - <field> - <name>enddate</name> - <type>timestamp</type> - <default>1970-01-01 00:00:00</default> - <notnull>false</notnull> - </field> - - <field> - <name>repeating</name> - <type>integer</type> - <default></default> - <notnull>false</notnull> - <length>4</length> - </field> - - <field> - <name>summary</name> - <type>text</type> - <default></default> - <notnull>false</notnull> - <length>255</length> - </field> - - <field> - <name>calendardata</name> - <type>clob</type> - <notnull>false</notnull> - </field> - - <field> - <name>uri</name> - <type>text</type> - <default></default> - <notnull>false</notnull> - <length>255</length> - </field> - - <field> - <name>lastmodified</name> - <type>integer</type> - <default></default> - <notnull>false</notnull> - <length>4</length> - </field> - - </declaration> - - </table> - - <table> - - <name>*dbprefix*clndr_calendars</name> - - <declaration> - - <field> - <name>id</name> - <type>integer</type> - <default>0</default> - <notnull>true</notnull> - <autoincrement>1</autoincrement> - <unsigned>true</unsigned> - <length>4</length> - </field> - - <field> - <name>userid</name> - <type>text</type> - <default></default> - <notnull>false</notnull> - <length>255</length> - </field> - - <field> - <name>displayname</name> - <type>text</type> - <default></default> - <notnull>false</notnull> - <length>100</length> - </field> - - <field> - <name>uri</name> - <type>text</type> - <default></default> - <notnull>false</notnull> - <length>255</length> - </field> - - <field> - <name>active</name> - <type>integer</type> - <default>1</default> - <notnull>true</notnull> - <length>4</length> - </field> - - <field> - <name>ctag</name> - <type>integer</type> - <default>0</default> - <notnull>true</notnull> - <unsigned>true</unsigned> - <length>4</length> - </field> - - <field> - <name>calendarorder</name> - <type>integer</type> - <default>0</default> - <notnull>true</notnull> - <unsigned>true</unsigned> - <length>4</length> - </field> - - <field> - <name>calendarcolor</name> - <type>text</type> - <default></default> - <notnull>false</notnull> - <length>10</length> - </field> - - <field> - <name>timezone</name> - <type>clob</type> - <notnull>false</notnull> - </field> - - <field> - <name>components</name> - <type>text</type> - <default></default> - <notnull>false</notnull> - <length>100</length> - </field> - - </declaration> - - </table> - -</database> diff --git a/apps/dav/tests/unit/migration/calendaradaptertest.php b/apps/dav/tests/unit/migration/calendaradaptertest.php deleted file mode 100644 index f92774ef6ad..00000000000 --- a/apps/dav/tests/unit/migration/calendaradaptertest.php +++ /dev/null @@ -1,131 +0,0 @@ -<?php -/** - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace OCA\DAV\Tests\Unit\Migration; - -use DomainException; -use OCA\Dav\Migration\AddressBookAdapter; -use OCA\Dav\Migration\CalendarAdapter; -use OCP\IDBConnection; -use Test\TestCase; - -/** - * Class CalendarAdapterTest - * - * @group DB - * - * @package OCA\DAV\Tests\Unit\Migration - */ -class CalendarAdapterTest extends TestCase { - - /** @var IDBConnection */ - private $db; - /** @var CalendarAdapter */ - private $adapter; - /** @var array */ - private $cals = []; - /** @var array */ - private $calObjs = []; - - public function setUp() { - parent::setUp(); - $this->db = \OC::$server->getDatabaseConnection(); - - $manager = new \OC\DB\MDB2SchemaManager($this->db); - $manager->createDbFromStructure(__DIR__ . '/calendar_schema.xml'); - - $this->adapter = new CalendarAdapter($this->db); - } - - public function tearDown() { - $this->db->dropTable('clndr_calendars'); - $this->db->dropTable('clndr_objects'); - parent::tearDown(); - } - - /** - * @expectedException DomainException - */ - public function testOldTablesDoNotExist() { - $adapter = new AddressBookAdapter(\OC::$server->getDatabaseConnection(), 'crazy_table_that_does_no_exist'); - $adapter->setup(); - } - - public function test() { - - // insert test data - $builder = $this->db->getQueryBuilder(); - $builder->insert('clndr_calendars') - ->values([ - 'userid' => $builder->createNamedParameter('test-user-666'), - 'displayname' => $builder->createNamedParameter('Display Name'), - 'uri' => $builder->createNamedParameter('events'), - 'ctag' => $builder->createNamedParameter('112233'), - 'active' => $builder->createNamedParameter('1') - ]) - ->execute(); - $builder = $this->db->getQueryBuilder(); - $builder->insert('clndr_objects') - ->values([ - 'calendarid' => $builder->createNamedParameter(6666), - 'objecttype' => $builder->createNamedParameter('VEVENT'), - 'startdate' => $builder->createNamedParameter(new \DateTime(), 'datetime'), - 'enddate' => $builder->createNamedParameter(new \DateTime(), 'datetime'), - 'repeating' => $builder->createNamedParameter(0), - 'summary' => $builder->createNamedParameter('Something crazy will happen'), - 'uri' => $builder->createNamedParameter('event.ics'), - 'lastmodified' => $builder->createNamedParameter('112233'), - ]) - ->execute(); - $builder = $this->db->getQueryBuilder(); - $builder->insert('share') - ->values([ - 'share_type' => $builder->createNamedParameter(1), - 'share_with' => $builder->createNamedParameter('user01'), - 'uid_owner' => $builder->createNamedParameter('user02'), - 'item_type' => $builder->createNamedParameter('calendar'), - 'item_source' => $builder->createNamedParameter(6666), - 'item_target' => $builder->createNamedParameter('Contacts (user02)'), - ]) - ->execute(); - - // test the adapter - $this->adapter->foreachCalendar('test-user-666', function($row) { - $this->cals[] = $row; - }); - $this->assertArrayHasKey('id', $this->cals[0]); - $this->assertEquals('test-user-666', $this->cals[0]['userid']); - $this->assertEquals('Display Name', $this->cals[0]['displayname']); - $this->assertEquals('events', $this->cals[0]['uri']); - $this->assertEquals('112233', $this->cals[0]['ctag']); - - $this->adapter->foreachCalendarObject(6666, function($row) { - $this->calObjs[]= $row; - }); - $this->assertArrayHasKey('id', $this->calObjs[0]); - $this->assertEquals(6666, $this->calObjs[0]['calendarid']); - - // test getShares - $shares = $this->adapter->getShares(6666); - $this->assertEquals(1, count($shares)); - - } - -} diff --git a/apps/dav/tests/unit/migration/contacts_schema.xml b/apps/dav/tests/unit/migration/contacts_schema.xml deleted file mode 100644 index 51836a1e0c6..00000000000 --- a/apps/dav/tests/unit/migration/contacts_schema.xml +++ /dev/null @@ -1,151 +0,0 @@ -<?xml version="1.0" encoding="ISO-8859-1" ?> -<database> - - <name>*dbname*</name> - <create>true</create> - <overwrite>false</overwrite> - <charset>utf8</charset> - <table> - - <name>*dbprefix*contacts_addressbooks</name> - - <declaration> - - <field> - <name>id</name> - <type>integer</type> - <default>0</default> - <notnull>true</notnull> - <autoincrement>1</autoincrement> - <unsigned>true</unsigned> - <length>4</length> - </field> - - <field> - <name>userid</name> - <type>text</type> - <default></default> - <notnull>true</notnull> - <length>255</length> - </field> - - <field> - <name>displayname</name> - <type>text</type> - <default></default> - <notnull>false</notnull> - <length>255</length> - </field> - - <field> - <name>uri</name> - <type>text</type> - <default></default> - <notnull>false</notnull> - <length>200</length> - </field> - - <field> - <name>description</name> - <type>text</type> - <notnull>false</notnull> - <length>255</length> - </field> - - <field> - <name>ctag</name> - <type>integer</type> - <default>1</default> - <notnull>true</notnull> - <unsigned>true</unsigned> - <length>4</length> - </field> - - <field> - <name>active</name> - <type>integer</type> - <default>1</default> - <notnull>true</notnull> - <length>4</length> - </field> - - <index> - <name>c_addressbook_userid_index</name> - <field> - <name>userid</name> - <sorting>ascending</sorting> - </field> - </index> - </declaration> - - </table> - - <table> - - <name>*dbprefix*contacts_cards</name> - - <declaration> - - <field> - <name>id</name> - <type>integer</type> - <default>0</default> - <notnull>true</notnull> - <autoincrement>1</autoincrement> - <unsigned>true</unsigned> - <length>4</length> - </field> - - <field> - <name>addressbookid</name> - <type>integer</type> - <default></default> - <notnull>true</notnull> - <unsigned>true</unsigned> - <length>4</length> - </field> - - <field> - <name>fullname</name> - <type>text</type> - <default></default> - <notnull>false</notnull> - <length>255</length> - </field> - - <field> - <name>carddata</name> - <type>clob</type> - <notnull>false</notnull> - </field> - - <field> - <name>uri</name> - <type>text</type> - <default></default> - <notnull>false</notnull> - <length>200</length> - </field> - - <field> - <name>lastmodified</name> - <type>integer</type> - <default></default> - <notnull>false</notnull> - <unsigned>true</unsigned> - <length>4</length> - </field> - - - <index> - <name>c_addressbookid_index</name> - <field> - <name>addressbookid</name> - <sorting>ascending</sorting> - </field> - </index> - </declaration> - - </table> - -</database> diff --git a/apps/dav/tests/unit/migration/migrateaddressbooktest.php b/apps/dav/tests/unit/migration/migrateaddressbooktest.php deleted file mode 100644 index 31cb16265c0..00000000000 --- a/apps/dav/tests/unit/migration/migrateaddressbooktest.php +++ /dev/null @@ -1,81 +0,0 @@ -<?php -/** - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace OCA\DAV\Tests\Unit\Migration; - -use OCA\DAV\CardDAV\CardDavBackend; -use OCA\Dav\Migration\AddressBookAdapter; -use OCP\ILogger; -use Test\TestCase; - -class MigrateAddressbookTest extends TestCase { - - public function testMigration() { - /** @var AddressBookAdapter | \PHPUnit_Framework_MockObject_MockObject $adapter */ - $adapter = $this->mockAdapter([ - ['share_type' => '1', 'share_with' => 'users', 'permissions' => '31'], - ['share_type' => '2', 'share_with' => 'adam', 'permissions' => '1'], - ]); - - /** @var CardDavBackend | \PHPUnit_Framework_MockObject_MockObject $cardDav */ - $cardDav = $this->getMockBuilder('\OCA\Dav\CardDAV\CardDAVBackend')->disableOriginalConstructor()->getMock(); - $cardDav->expects($this->any())->method('createAddressBook')->willReturn(666); - $cardDav->expects($this->any())->method('getAddressBookById')->willReturn([]); - $cardDav->expects($this->once())->method('createAddressBook')->with('principals/users/test01', 'test_contacts'); - $cardDav->expects($this->once())->method('createCard')->with(666, '63f0dd6c-39d5-44be-9d34-34e7a7441fc2.vcf', 'BEGIN:VCARD'); - $cardDav->expects($this->once())->method('updateShares')->with($this->anything(), [ - ['href' => 'principal:principals/groups/users', 'readOnly' => false], - ['href' => 'principal:principals/users/adam', 'readOnly' => true] - ]); - /** @var ILogger $logger */ - $logger = $this->getMockBuilder('\OCP\ILogger')->disableOriginalConstructor()->getMock(); - - $m = new \OCA\Dav\Migration\MigrateAddressbooks($adapter, $cardDav, $logger, null); - $m->migrateForUser('test01'); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject - */ - private function mockAdapter($shares = []) { - $adapter = $this->getMockBuilder('\OCA\Dav\Migration\AddressBookAdapter')->disableOriginalConstructor()->getMock(); - $adapter->expects($this->any())->method('foreachBook')->willReturnCallback(function ($user, \Closure $callBack) { - $callBack([ - 'id' => 0, - 'userid' => $user, - 'displayname' => 'Test Contacts', - 'uri' => 'test_contacts', - 'description' => 'Contacts to test with', - 'ctag' => 1234567890, - 'active' => 1 - ]); - }); - $adapter->expects($this->any())->method('foreachCard')->willReturnCallback(function ($addressBookId, \Closure $callBack) { - $callBack([ - 'userid' => $addressBookId, - 'uri' => '63f0dd6c-39d5-44be-9d34-34e7a7441fc2.vcf', - 'carddata' => 'BEGIN:VCARD' - ]); - }); - $adapter->expects($this->any())->method('getShares')->willReturn($shares); - return $adapter; - } - -} diff --git a/apps/dav/tests/unit/migration/migratecalendartest.php b/apps/dav/tests/unit/migration/migratecalendartest.php deleted file mode 100644 index e62970aef34..00000000000 --- a/apps/dav/tests/unit/migration/migratecalendartest.php +++ /dev/null @@ -1,85 +0,0 @@ -<?php -/** - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace OCA\DAV\Tests\Unit\Migration; - -use OCA\DAV\CalDAV\CalDavBackend; -use OCA\Dav\Migration\CalendarAdapter; -use OCP\ILogger; -use Test\TestCase; - -class MigrateCalendarTest extends TestCase { - - public function testMigration() { - /** @var CalendarAdapter | \PHPUnit_Framework_MockObject_MockObject $adapter */ - $adapter = $this->mockAdapter([ - ['share_type' => '1', 'share_with' => 'users', 'permissions' => '31'], - ['share_type' => '2', 'share_with' => 'adam', 'permissions' => '1'], - ]); - - /** @var CalDavBackend | \PHPUnit_Framework_MockObject_MockObject $cardDav */ - $cardDav = $this->getMockBuilder('\OCA\Dav\CalDAV\CalDAVBackend')->disableOriginalConstructor()->getMock(); - $cardDav->expects($this->any())->method('createCalendar')->willReturn(666); - $cardDav->expects($this->once())->method('createCalendar')->with('principals/users/test01', 'test_contacts'); - $cardDav->expects($this->once())->method('createCalendarObject')->with(666, '63f0dd6c-39d5-44be-9d34-34e7a7441fc2.ics', 'BEGIN:VCARD'); - $cardDav->expects($this->once())->method('updateShares')->with($this->anything(), [ - ['href' => 'principal:principals/groups/users', 'readOnly' => false], - ['href' => 'principal:principals/users/adam', 'readOnly' => true] - ]); - /** @var ILogger $logger */ - $logger = $this->getMockBuilder('\OCP\ILogger')->disableOriginalConstructor()->getMock(); - - $m = new \OCA\Dav\Migration\MigrateCalendars($adapter, $cardDav, $logger, null); - $m->migrateForUser('test01'); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject - */ - private function mockAdapter($shares = [], $calData = 'BEGIN:VCARD') { - $adapter = $this->getMockBuilder('\OCA\Dav\Migration\CalendarAdapter') - ->disableOriginalConstructor() - ->getMock(); - $adapter->expects($this->any())->method('foreachCalendar')->willReturnCallback(function ($user, \Closure $callBack) { - $callBack([ - // calendarorder | calendarcolor | timezone | components - 'id' => 0, - 'userid' => $user, - 'displayname' => 'Test Contacts', - 'uri' => 'test_contacts', - 'ctag' => 1234567890, - 'active' => 1, - 'calendarorder' => '0', - 'calendarcolor' => '#b3dc6c', - 'timezone' => null, - 'components' => 'VEVENT,VTODO,VJOURNAL' - ]); - }); - $adapter->expects($this->any())->method('foreachCalendarObject')->willReturnCallback(function ($addressBookId, \Closure $callBack) use ($calData) { - $callBack([ - 'userid' => $addressBookId, - 'uri' => '63f0dd6c-39d5-44be-9d34-34e7a7441fc2.ics', - 'calendardata' => $calData - ]); - }); - $adapter->expects($this->any())->method('getShares')->willReturn($shares); - return $adapter; - } -} diff --git a/apps/dav/tests/unit/phpunit.xml b/apps/dav/tests/unit/phpunit.xml index 314855d863b..c85d07c6fcb 100644 --- a/apps/dav/tests/unit/phpunit.xml +++ b/apps/dav/tests/unit/phpunit.xml @@ -1,4 +1,8 @@ <?xml version="1.0" encoding="utf-8" ?> +<!-- + - SPDX-FileCopyrightText: 2015-2017 ownCloud, Inc. + - SPDX-License-Identifier: AGPL-3.0-only + --> <phpunit bootstrap="bootstrap.php" verbose="true" timeoutForSmallTests="900" @@ -6,14 +10,14 @@ timeoutForLargeTests="900" > <testsuite name='unit'> - <directory suffix='.php'>.</directory> + <directory suffix='Test.php'>.</directory> </testsuite> <!-- filters for code coverage --> <filter> <whitelist> - <directory suffix=".php">../../dav</directory> + <directory suffix=".php">../../../dav</directory> <exclude> - <directory suffix=".php">../../dav/tests</directory> + <directory suffix=".php">../../../dav/tests</directory> </exclude> </whitelist> </filter> @@ -22,4 +26,3 @@ <log type="coverage-clover" target="./clover.xml"/> </logging> </phpunit> - diff --git a/apps/dav/tests/unit/servertest.php b/apps/dav/tests/unit/servertest.php deleted file mode 100644 index b25da3cc807..00000000000 --- a/apps/dav/tests/unit/servertest.php +++ /dev/null @@ -1,43 +0,0 @@ -<?php -/** - * @author Lukas Reschke <lukas@owncloud.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -namespace OCA\DAV\Tests\Unit; - -use OCA\DAV\Server; -use OCP\IRequest; - -/** - * Class ServerTest - * - * @group DB - * - * @package OCA\DAV\Tests\Unit - */ -class ServerTest extends \Test\TestCase { - - public function test() { - /** @var IRequest $r */ - $r = $this->getMockBuilder('\OCP\IRequest') - ->disableOriginalConstructor()->getMock(); - $s = new Server($r, '/'); - $this->assertNotNull($s->server); - } -}
\ No newline at end of file diff --git a/apps/dav/tests/unit/systemtag/systemtagmappingnode.php b/apps/dav/tests/unit/systemtag/systemtagmappingnode.php deleted file mode 100644 index 7f2ff7d6616..00000000000 --- a/apps/dav/tests/unit/systemtag/systemtagmappingnode.php +++ /dev/null @@ -1,132 +0,0 @@ -<?php -/** - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\SystemTag; - -use Sabre\DAV\Exception\NotFound; -use OC\SystemTag\SystemTag; -use OCP\SystemTag\TagNotFoundException; - -class SystemTagMappingNode extends \Test\TestCase { - - /** - * @var \OCP\SystemTag\ISystemTagManager - */ - private $tagManager; - - /** - * @var \OCP\SystemTag\ISystemTagObjectMapper - */ - private $tagMapper; - - protected function setUp() { - parent::setUp(); - - $this->tagManager = $this->getMock('\OCP\SystemTag\ISystemTagManager'); - $this->tagMapper = $this->getMock('\OCP\SystemTag\ISystemTagObjectMapper'); - } - - public function getMappingNode($isAdmin = true, $tag = null) { - if ($tag === null) { - $tag = new SystemTag(1, 'Test', true, true); - } - return new \OCA\DAV\SystemTag\SystemTagMappingNode( - $tag, - 123, - 'files', - $isAdmin, - $this->tagManager, - $this->tagMapper - ); - } - - public function testGetters() { - $tag = new SystemTag(1, 'Test', true, false); - $node = $this->getMappingNode(true, $tag); - $this->assertEquals('1', $node->getName()); - $this->assertEquals($tag, $node->getSystemTag()); - $this->assertEquals(123, $node->getObjectId()); - $this->assertEquals('files', $node->getObjectType()); - } - - public function adminFlagProvider() { - return [[true], [false]]; - } - - /** - * @dataProvider adminFlagProvider - */ - public function testDeleteTag($isAdmin) { - $this->tagManager->expects($this->never()) - ->method('deleteTags'); - $this->tagMapper->expects($this->once()) - ->method('unassignTags') - ->with(123, 'files', 1); - - $this->getMappingNode($isAdmin)->delete(); - } - - public function tagNodeDeleteProviderPermissionException() { - return [ - [ - // cannot unassign invisible tag - new SystemTag(1, 'Original', false, true), - 'Sabre\DAV\Exception\NotFound', - ], - [ - // cannot unassign non-assignable tag - new SystemTag(1, 'Original', true, false), - 'Sabre\DAV\Exception\Forbidden', - ], - ]; - } - - /** - * @dataProvider tagNodeDeleteProviderPermissionException - */ - public function testDeleteTagExpectedException($tag, $expectedException) { - $this->tagManager->expects($this->never()) - ->method('deleteTags'); - $this->tagMapper->expects($this->never()) - ->method('unassignTags'); - - $thrown = null; - try { - $this->getMappingNode(false, $tag)->delete(); - } catch (\Exception $e) { - $thrown = $e; - } - - $this->assertInstanceOf($expectedException, $thrown); - } - - /** - * @expectedException Sabre\DAV\Exception\NotFound - */ - public function testDeleteTagNotFound() { - $this->tagMapper->expects($this->once()) - ->method('unassignTags') - ->with(123, 'files', 1) - ->will($this->throwException(new TagNotFoundException())); - - $this->getMappingNode()->delete(); - } -} diff --git a/apps/dav/tests/unit/systemtag/systemtagnode.php b/apps/dav/tests/unit/systemtag/systemtagnode.php deleted file mode 100644 index 5184b74e5c8..00000000000 --- a/apps/dav/tests/unit/systemtag/systemtagnode.php +++ /dev/null @@ -1,244 +0,0 @@ -<?php -/** - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\SystemTag; - -use Sabre\DAV\Exception\NotFound; -use Sabre\DAV\Exception\MethodNotAllowed; -use Sabre\DAV\Exception\Conflict; - -use OC\SystemTag\SystemTag; -use OCP\SystemTag\TagNotFoundException; -use OCP\SystemTag\TagAlreadyExistsException; - -class SystemTagNode extends \Test\TestCase { - - /** - * @var \OCP\SystemTag\ISystemTagManager - */ - private $tagManager; - - protected function setUp() { - parent::setUp(); - - $this->tagManager = $this->getMock('\OCP\SystemTag\ISystemTagManager'); - } - - protected function getTagNode($isAdmin = true, $tag = null) { - if ($tag === null) { - $tag = new SystemTag(1, 'Test', true, true); - } - return new \OCA\DAV\SystemTag\SystemTagNode( - $tag, - $isAdmin, - $this->tagManager - ); - } - - public function adminFlagProvider() { - return [[true], [false]]; - } - - /** - * @dataProvider adminFlagProvider - */ - public function testGetters($isAdmin) { - $tag = new SystemTag('1', 'Test', true, true); - $node = $this->getTagNode($isAdmin, $tag); - $this->assertEquals('1', $node->getName()); - $this->assertEquals($tag, $node->getSystemTag()); - } - - /** - * @expectedException Sabre\DAV\Exception\MethodNotAllowed - */ - public function testSetName() { - $this->getTagNode()->setName('2'); - } - - public function tagNodeProvider() { - return [ - // admin - [ - true, - new SystemTag(1, 'Original', true, true), - ['Renamed', true, true] - ], - [ - true, - new SystemTag(1, 'Original', true, true), - ['Original', false, false] - ], - // non-admin - [ - // renaming allowed - false, - new SystemTag(1, 'Original', true, true), - ['Rename', true, true] - ], - ]; - } - - /** - * @dataProvider tagNodeProvider - */ - public function testUpdateTag($isAdmin, $originalTag, $changedArgs) { - $this->tagManager->expects($this->once()) - ->method('updateTag') - ->with(1, $changedArgs[0], $changedArgs[1], $changedArgs[2]); - $this->getTagNode($isAdmin, $originalTag) - ->update($changedArgs[0], $changedArgs[1], $changedArgs[2]); - } - - public function tagNodeProviderPermissionException() { - return [ - [ - // changing permissions not allowed - new SystemTag(1, 'Original', true, true), - ['Original', false, true], - 'Sabre\DAV\Exception\Forbidden', - ], - [ - // changing permissions not allowed - new SystemTag(1, 'Original', true, true), - ['Original', true, false], - 'Sabre\DAV\Exception\Forbidden', - ], - [ - // changing permissions not allowed - new SystemTag(1, 'Original', true, true), - ['Original', false, false], - 'Sabre\DAV\Exception\Forbidden', - ], - [ - // changing non-assignable not allowed - new SystemTag(1, 'Original', true, false), - ['Rename', true, false], - 'Sabre\DAV\Exception\Forbidden', - ], - [ - // changing non-assignable not allowed - new SystemTag(1, 'Original', true, false), - ['Original', true, true], - 'Sabre\DAV\Exception\Forbidden', - ], - [ - // invisible tag does not exist - new SystemTag(1, 'Original', false, false), - ['Rename', false, false], - 'Sabre\DAV\Exception\NotFound', - ], - ]; - } - - /** - * @dataProvider tagNodeProviderPermissionException - */ - public function testUpdateTagPermissionException($originalTag, $changedArgs, $expectedException = null) { - $this->tagManager->expects($this->never()) - ->method('updateTag'); - - $thrown = null; - - try { - $this->getTagNode(false, $originalTag) - ->update($changedArgs[0], $changedArgs[1], $changedArgs[2]); - } catch (\Exception $e) { - $thrown = $e; - } - - $this->assertInstanceOf($expectedException, $thrown); - } - - /** - * @expectedException Sabre\DAV\Exception\Conflict - */ - public function testUpdateTagAlreadyExists() { - $this->tagManager->expects($this->once()) - ->method('updateTag') - ->with(1, 'Renamed', false, true) - ->will($this->throwException(new TagAlreadyExistsException())); - $this->getTagNode()->update('Renamed', false, true); - } - - /** - * @expectedException Sabre\DAV\Exception\NotFound - */ - public function testUpdateTagNotFound() { - $this->tagManager->expects($this->once()) - ->method('updateTag') - ->with(1, 'Renamed', false, true) - ->will($this->throwException(new TagNotFoundException())); - $this->getTagNode()->update('Renamed', false, true); - } - - /** - * @dataProvider adminFlagProvider - */ - public function testDeleteTag($isAdmin) { - $this->tagManager->expects($this->once()) - ->method('deleteTags') - ->with('1'); - $this->getTagNode($isAdmin)->delete(); - } - - public function tagNodeDeleteProviderPermissionException() { - return [ - [ - // cannot delete invisible tag - new SystemTag(1, 'Original', false, true), - 'Sabre\DAV\Exception\NotFound', - ], - [ - // cannot delete non-assignable tag - new SystemTag(1, 'Original', true, false), - 'Sabre\DAV\Exception\Forbidden', - ], - ]; - } - - /** - * @dataProvider tagNodeDeleteProviderPermissionException - */ - public function testDeleteTagPermissionException($tag, $expectedException) { - $this->tagManager->expects($this->never()) - ->method('deleteTags'); - - try { - $this->getTagNode(false, $tag)->delete(); - } catch (\Exception $e) { - $thrown = $e; - } - - $this->assertInstanceOf($expectedException, $thrown); - } - - /** - * @expectedException Sabre\DAV\Exception\NotFound - */ - public function testDeleteTagNotFound() { - $this->tagManager->expects($this->once()) - ->method('deleteTags') - ->with('1') - ->will($this->throwException(new TagNotFoundException())); - $this->getTagNode()->delete(); - } -} diff --git a/apps/dav/tests/unit/systemtag/systemtagplugin.php b/apps/dav/tests/unit/systemtag/systemtagplugin.php deleted file mode 100644 index 4466533f1e0..00000000000 --- a/apps/dav/tests/unit/systemtag/systemtagplugin.php +++ /dev/null @@ -1,608 +0,0 @@ -<?php -/** - * @author Arthur Schiwon <blizzz@owncloud.com> - * @author Lukas Reschke <lukas@owncloud.com> - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\SystemTag; - -use OC\SystemTag\SystemTag; -use OCP\IGroupManager; -use OCP\IUserSession; -use OCP\SystemTag\TagAlreadyExistsException; - -class SystemTagPlugin extends \Test\TestCase { - - const ID_PROPERTYNAME = \OCA\DAV\SystemTag\SystemTagPlugin::ID_PROPERTYNAME; - const DISPLAYNAME_PROPERTYNAME = \OCA\DAV\SystemTag\SystemTagPlugin::DISPLAYNAME_PROPERTYNAME; - const USERVISIBLE_PROPERTYNAME = \OCA\DAV\SystemTag\SystemTagPlugin::USERVISIBLE_PROPERTYNAME; - const USERASSIGNABLE_PROPERTYNAME = \OCA\DAV\SystemTag\SystemTagPlugin::USERASSIGNABLE_PROPERTYNAME; - - /** - * @var \Sabre\DAV\Server - */ - private $server; - - /** - * @var \Sabre\DAV\Tree - */ - private $tree; - - /** - * @var \OCP\SystemTag\ISystemTagManager - */ - private $tagManager; - - /** - * @var IGroupManager - */ - private $groupManager; - - /** - * @var IUserSession - */ - private $userSession; - - /** - * @var \OCA\DAV\SystemTag\SystemTagPlugin - */ - private $plugin; - - public function setUp() { - parent::setUp(); - $this->tree = $this->getMockBuilder('\Sabre\DAV\Tree') - ->disableOriginalConstructor() - ->getMock(); - - $this->server = new \Sabre\DAV\Server($this->tree); - - $this->tagManager = $this->getMock('\OCP\SystemTag\ISystemTagManager'); - $this->groupManager = $this->getMock('\OCP\IGroupManager'); - $this->userSession = $this->getMock('\OCP\IUserSession'); - - $this->plugin = new \OCA\DAV\SystemTag\SystemTagPlugin( - $this->tagManager, - $this->groupManager, - $this->userSession - ); - $this->plugin->initialize($this->server); - } - - public function testGetProperties() { - $systemTag = new SystemTag(1, 'Test', true, true); - $requestedProperties = [ - self::ID_PROPERTYNAME, - self::DISPLAYNAME_PROPERTYNAME, - self::USERVISIBLE_PROPERTYNAME, - self::USERASSIGNABLE_PROPERTYNAME - ]; - $expectedProperties = [ - 200 => [ - self::ID_PROPERTYNAME => '1', - self::DISPLAYNAME_PROPERTYNAME => 'Test', - self::USERVISIBLE_PROPERTYNAME => 'true', - self::USERASSIGNABLE_PROPERTYNAME => 'true', - ] - ]; - - $node = $this->getMockBuilder('\OCA\DAV\SystemTag\SystemTagNode') - ->disableOriginalConstructor() - ->getMock(); - $node->expects($this->any()) - ->method('getSystemTag') - ->will($this->returnValue($systemTag)); - - $this->tree->expects($this->any()) - ->method('getNodeForPath') - ->with('/systemtag/1') - ->will($this->returnValue($node)); - - $propFind = new \Sabre\DAV\PropFind( - '/systemtag/1', - $requestedProperties, - 0 - ); - - $this->plugin->handleGetProperties( - $propFind, - $node - ); - - $result = $propFind->getResultForMultiStatus(); - - $this->assertEmpty($result[404]); - unset($result[404]); - $this->assertEquals($expectedProperties, $result); - } - - public function testUpdateProperties() { - $systemTag = new SystemTag(1, 'Test', true, false); - $node = $this->getMockBuilder('\OCA\DAV\SystemTag\SystemTagNode') - ->disableOriginalConstructor() - ->getMock(); - $node->expects($this->any()) - ->method('getSystemTag') - ->will($this->returnValue($systemTag)); - - $this->tree->expects($this->any()) - ->method('getNodeForPath') - ->with('/systemtag/1') - ->will($this->returnValue($node)); - - $node->expects($this->once()) - ->method('update') - ->with('Test changed', false, true); - - // properties to set - $propPatch = new \Sabre\DAV\PropPatch(array( - self::DISPLAYNAME_PROPERTYNAME => 'Test changed', - self::USERVISIBLE_PROPERTYNAME => 'false', - self::USERASSIGNABLE_PROPERTYNAME => 'true', - )); - - $this->plugin->handleUpdateProperties( - '/systemtag/1', - $propPatch - ); - - $propPatch->commit(); - - // all requested properties removed, as they were processed already - $this->assertEmpty($propPatch->getRemainingMutations()); - - $result = $propPatch->getResult(); - $this->assertEquals(200, $result[self::DISPLAYNAME_PROPERTYNAME]); - $this->assertEquals(200, $result[self::USERASSIGNABLE_PROPERTYNAME]); - $this->assertEquals(200, $result[self::USERVISIBLE_PROPERTYNAME]); - } - - /** - * @expectedException \Sabre\DAV\Exception\BadRequest - * @expectedExceptionMessage Not sufficient permissions - */ - public function testCreateNotAssignableTagAsRegularUser() { - $user = $this->getMock('\OCP\IUser'); - $user->expects($this->once()) - ->method('getUID') - ->willReturn('admin'); - $this->userSession - ->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(true); - $this->userSession - ->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $this->groupManager - ->expects($this->once()) - ->method('isAdmin') - ->with('admin') - ->willReturn(false); - - $requestData = json_encode([ - 'name' => 'Test', - 'userVisible' => true, - 'userAssignable' => false, - ]); - - $node = $this->getMockBuilder('\OCA\DAV\SystemTag\SystemTagsByIdCollection') - ->disableOriginalConstructor() - ->getMock(); - $this->tagManager->expects($this->never()) - ->method('createTag'); - - $this->tree->expects($this->any()) - ->method('getNodeForPath') - ->with('/systemtags') - ->will($this->returnValue($node)); - - $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface') - ->disableOriginalConstructor() - ->getMock(); - $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface') - ->disableOriginalConstructor() - ->getMock(); - - $request->expects($this->once()) - ->method('getPath') - ->will($this->returnValue('/systemtags')); - - $request->expects($this->once()) - ->method('getBodyAsString') - ->will($this->returnValue($requestData)); - - $request->expects($this->once()) - ->method('getHeader') - ->with('Content-Type') - ->will($this->returnValue('application/json')); - - $this->plugin->httpPost($request, $response); - } - - /** - * @expectedException \Sabre\DAV\Exception\BadRequest - * @expectedExceptionMessage Not sufficient permissions - */ - public function testCreateInvisibleTagAsRegularUser() { - $user = $this->getMock('\OCP\IUser'); - $user->expects($this->once()) - ->method('getUID') - ->willReturn('admin'); - $this->userSession - ->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(true); - $this->userSession - ->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $this->groupManager - ->expects($this->once()) - ->method('isAdmin') - ->with('admin') - ->willReturn(false); - - $requestData = json_encode([ - 'name' => 'Test', - 'userVisible' => false, - 'userAssignable' => true, - ]); - - $node = $this->getMockBuilder('\OCA\DAV\SystemTag\SystemTagsByIdCollection') - ->disableOriginalConstructor() - ->getMock(); - $this->tagManager->expects($this->never()) - ->method('createTag'); - - $this->tree->expects($this->any()) - ->method('getNodeForPath') - ->with('/systemtags') - ->will($this->returnValue($node)); - - $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface') - ->disableOriginalConstructor() - ->getMock(); - $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface') - ->disableOriginalConstructor() - ->getMock(); - - $request->expects($this->once()) - ->method('getPath') - ->will($this->returnValue('/systemtags')); - - $request->expects($this->once()) - ->method('getBodyAsString') - ->will($this->returnValue($requestData)); - - $request->expects($this->once()) - ->method('getHeader') - ->with('Content-Type') - ->will($this->returnValue('application/json')); - - $this->plugin->httpPost($request, $response); - } - - public function testCreateTagInByIdCollectionAsRegularUser() { - $systemTag = new SystemTag(1, 'Test', true, false); - - $requestData = json_encode([ - 'name' => 'Test', - 'userVisible' => true, - 'userAssignable' => true, - ]); - - $node = $this->getMockBuilder('\OCA\DAV\SystemTag\SystemTagsByIdCollection') - ->disableOriginalConstructor() - ->getMock(); - $this->tagManager->expects($this->once()) - ->method('createTag') - ->with('Test', true, true) - ->will($this->returnValue($systemTag)); - - $this->tree->expects($this->any()) - ->method('getNodeForPath') - ->with('/systemtags') - ->will($this->returnValue($node)); - - $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface') - ->disableOriginalConstructor() - ->getMock(); - $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface') - ->disableOriginalConstructor() - ->getMock(); - - $request->expects($this->once()) - ->method('getPath') - ->will($this->returnValue('/systemtags')); - - $request->expects($this->once()) - ->method('getBodyAsString') - ->will($this->returnValue($requestData)); - - $request->expects($this->once()) - ->method('getHeader') - ->with('Content-Type') - ->will($this->returnValue('application/json')); - - $request->expects($this->once()) - ->method('getUrl') - ->will($this->returnValue('http://example.com/dav/systemtags')); - - $response->expects($this->once()) - ->method('setHeader') - ->with('Content-Location', 'http://example.com/dav/systemtags/1'); - - $this->plugin->httpPost($request, $response); - } - - public function testCreateTagInByIdCollection() { - $user = $this->getMock('\OCP\IUser'); - $user->expects($this->once()) - ->method('getUID') - ->willReturn('admin'); - $this->userSession - ->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(true); - $this->userSession - ->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $this->groupManager - ->expects($this->once()) - ->method('isAdmin') - ->with('admin') - ->willReturn(true); - - $systemTag = new SystemTag(1, 'Test', true, false); - - $requestData = json_encode([ - 'name' => 'Test', - 'userVisible' => true, - 'userAssignable' => false, - ]); - - $node = $this->getMockBuilder('\OCA\DAV\SystemTag\SystemTagsByIdCollection') - ->disableOriginalConstructor() - ->getMock(); - $this->tagManager->expects($this->once()) - ->method('createTag') - ->with('Test', true, false) - ->will($this->returnValue($systemTag)); - - $this->tree->expects($this->any()) - ->method('getNodeForPath') - ->with('/systemtags') - ->will($this->returnValue($node)); - - $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface') - ->disableOriginalConstructor() - ->getMock(); - $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface') - ->disableOriginalConstructor() - ->getMock(); - - $request->expects($this->once()) - ->method('getPath') - ->will($this->returnValue('/systemtags')); - - $request->expects($this->once()) - ->method('getBodyAsString') - ->will($this->returnValue($requestData)); - - $request->expects($this->once()) - ->method('getHeader') - ->with('Content-Type') - ->will($this->returnValue('application/json')); - - $request->expects($this->once()) - ->method('getUrl') - ->will($this->returnValue('http://example.com/dav/systemtags')); - - $response->expects($this->once()) - ->method('setHeader') - ->with('Content-Location', 'http://example.com/dav/systemtags/1'); - - $this->plugin->httpPost($request, $response); - } - - public function nodeClassProvider() { - return [ - ['\OCA\DAV\SystemTag\SystemTagsByIdCollection'], - ['\OCA\DAV\SystemTag\SystemTagsObjectMappingCollection'], - ]; - } - - public function testCreateTagInMappingCollection() { - $user = $this->getMock('\OCP\IUser'); - $user->expects($this->once()) - ->method('getUID') - ->willReturn('admin'); - $this->userSession - ->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(true); - $this->userSession - ->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $this->groupManager - ->expects($this->once()) - ->method('isAdmin') - ->with('admin') - ->willReturn(true); - - $systemTag = new SystemTag(1, 'Test', true, false); - - $requestData = json_encode([ - 'name' => 'Test', - 'userVisible' => true, - 'userAssignable' => false, - ]); - - $node = $this->getMockBuilder('\OCA\DAV\SystemTag\SystemTagsObjectMappingCollection') - ->disableOriginalConstructor() - ->getMock(); - - $this->tagManager->expects($this->once()) - ->method('createTag') - ->with('Test', true, false) - ->will($this->returnValue($systemTag)); - - $this->tree->expects($this->any()) - ->method('getNodeForPath') - ->with('/systemtags-relations/files/12') - ->will($this->returnValue($node)); - - $node->expects($this->once()) - ->method('createFile') - ->with(1); - - $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface') - ->disableOriginalConstructor() - ->getMock(); - $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface') - ->disableOriginalConstructor() - ->getMock(); - - $request->expects($this->once()) - ->method('getPath') - ->will($this->returnValue('/systemtags-relations/files/12')); - - $request->expects($this->once()) - ->method('getBodyAsString') - ->will($this->returnValue($requestData)); - - $request->expects($this->once()) - ->method('getHeader') - ->with('Content-Type') - ->will($this->returnValue('application/json')); - - $request->expects($this->once()) - ->method('getBaseUrl') - ->will($this->returnValue('http://example.com/dav/')); - - $response->expects($this->once()) - ->method('setHeader') - ->with('Content-Location', 'http://example.com/dav/systemtags/1'); - - $this->plugin->httpPost($request, $response); - } - - /** - * @expectedException \Sabre\DAV\Exception\NotFound - */ - public function testCreateTagToUnknownNode() { - $systemTag = new SystemTag(1, 'Test', true, false); - - $node = $this->getMockBuilder('\OCA\DAV\SystemTag\SystemTagsObjectMappingCollection') - ->disableOriginalConstructor() - ->getMock(); - - $this->tree->expects($this->any()) - ->method('getNodeForPath') - ->will($this->throwException(new \Sabre\DAV\Exception\NotFound())); - - $this->tagManager->expects($this->never()) - ->method('createTag'); - - $node->expects($this->never()) - ->method('createFile'); - - $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface') - ->disableOriginalConstructor() - ->getMock(); - $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface') - ->disableOriginalConstructor() - ->getMock(); - - $request->expects($this->once()) - ->method('getPath') - ->will($this->returnValue('/systemtags-relations/files/12')); - - $this->plugin->httpPost($request, $response); - } - - /** - * @dataProvider nodeClassProvider - * @expectedException \Sabre\DAV\Exception\Conflict - */ - public function testCreateTagConflict($nodeClass) { - $user = $this->getMock('\OCP\IUser'); - $user->expects($this->once()) - ->method('getUID') - ->willReturn('admin'); - $this->userSession - ->expects($this->once()) - ->method('isLoggedIn') - ->willReturn(true); - $this->userSession - ->expects($this->once()) - ->method('getUser') - ->willReturn($user); - $this->groupManager - ->expects($this->once()) - ->method('isAdmin') - ->with('admin') - ->willReturn(true); - - $requestData = json_encode([ - 'name' => 'Test', - 'userVisible' => true, - 'userAssignable' => false, - ]); - - $node = $this->getMockBuilder($nodeClass) - ->disableOriginalConstructor() - ->getMock(); - $this->tagManager->expects($this->once()) - ->method('createTag') - ->with('Test', true, false) - ->will($this->throwException(new TagAlreadyExistsException('Tag already exists'))); - - $this->tree->expects($this->any()) - ->method('getNodeForPath') - ->with('/systemtags') - ->will($this->returnValue($node)); - - $request = $this->getMockBuilder('Sabre\HTTP\RequestInterface') - ->disableOriginalConstructor() - ->getMock(); - $response = $this->getMockBuilder('Sabre\HTTP\ResponseInterface') - ->disableOriginalConstructor() - ->getMock(); - - $request->expects($this->once()) - ->method('getPath') - ->will($this->returnValue('/systemtags')); - - $request->expects($this->once()) - ->method('getBodyAsString') - ->will($this->returnValue($requestData)); - - $request->expects($this->once()) - ->method('getHeader') - ->with('Content-Type') - ->will($this->returnValue('application/json')); - - $this->plugin->httpPost($request, $response); - } - -} diff --git a/apps/dav/tests/unit/systemtag/systemtagsbyidcollection.php b/apps/dav/tests/unit/systemtag/systemtagsbyidcollection.php deleted file mode 100644 index a2bf571ab68..00000000000 --- a/apps/dav/tests/unit/systemtag/systemtagsbyidcollection.php +++ /dev/null @@ -1,244 +0,0 @@ -<?php -/** - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\SystemTag; - - -use OC\SystemTag\SystemTag; -use OCP\SystemTag\TagNotFoundException; - -class SystemTagsByIdCollection extends \Test\TestCase { - - /** - * @var \OCP\SystemTag\ISystemTagManager - */ - private $tagManager; - - protected function setUp() { - parent::setUp(); - - $this->tagManager = $this->getMock('\OCP\SystemTag\ISystemTagManager'); - } - - public function getNode($isAdmin = true) { - $user = $this->getMock('\OCP\IUser'); - $user->expects($this->any()) - ->method('getUID') - ->will($this->returnValue('testuser')); - $userSession = $this->getMock('\OCP\IUserSession'); - $userSession->expects($this->any()) - ->method('getUser') - ->will($this->returnValue($user)); - $groupManager = $this->getMock('\OCP\IGroupManager'); - $groupManager->expects($this->any()) - ->method('isAdmin') - ->with('testuser') - ->will($this->returnValue($isAdmin)); - return new \OCA\DAV\SystemTag\SystemTagsByIdCollection( - $this->tagManager, - $userSession, - $groupManager - ); - } - - public function adminFlagProvider() { - return [[true], [false]]; - } - - /** - * @expectedException Sabre\DAV\Exception\Forbidden - */ - public function testForbiddenCreateFile() { - $this->getNode()->createFile('555'); - } - - /** - * @expectedException Sabre\DAV\Exception\Forbidden - */ - public function testForbiddenCreateDirectory() { - $this->getNode()->createDirectory('789'); - } - - public function getChildProvider() { - return [ - [ - true, - true, - ], - [ - true, - false, - ], - [ - false, - true, - ], - ]; - } - - /** - * @dataProvider getChildProvider - */ - public function testGetChild($isAdmin, $userVisible) { - $tag = new SystemTag(123, 'Test', $userVisible, false); - - $this->tagManager->expects($this->once()) - ->method('getTagsByIds') - ->with(['123']) - ->will($this->returnValue([$tag])); - - $childNode = $this->getNode($isAdmin)->getChild('123'); - - $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagNode', $childNode); - $this->assertEquals('123', $childNode->getName()); - $this->assertEquals($tag, $childNode->getSystemTag()); - } - - /** - * @expectedException Sabre\DAV\Exception\BadRequest - */ - public function testGetChildInvalidName() { - $this->tagManager->expects($this->once()) - ->method('getTagsByIds') - ->with(['invalid']) - ->will($this->throwException(new \InvalidArgumentException())); - - $this->getNode()->getChild('invalid'); - } - - /** - * @expectedException Sabre\DAV\Exception\NotFound - */ - public function testGetChildNotFound() { - $this->tagManager->expects($this->once()) - ->method('getTagsByIds') - ->with(['444']) - ->will($this->throwException(new TagNotFoundException())); - - $this->getNode()->getChild('444'); - } - - /** - * @expectedException Sabre\DAV\Exception\NotFound - */ - public function testGetChildUserNotVisible() { - $tag = new SystemTag(123, 'Test', false, false); - - $this->tagManager->expects($this->once()) - ->method('getTagsByIds') - ->with(['123']) - ->will($this->returnValue([$tag])); - - $this->getNode(false)->getChild('123'); - } - - public function testGetChildrenAdmin() { - $tag1 = new SystemTag(123, 'One', true, false); - $tag2 = new SystemTag(456, 'Two', true, true); - - $this->tagManager->expects($this->once()) - ->method('getAllTags') - ->with(null) - ->will($this->returnValue([$tag1, $tag2])); - - $children = $this->getNode(true)->getChildren(); - - $this->assertCount(2, $children); - - $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagNode', $children[0]); - $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagNode', $children[1]); - $this->assertEquals($tag1, $children[0]->getSystemTag()); - $this->assertEquals($tag2, $children[1]->getSystemTag()); - } - - public function testGetChildrenNonAdmin() { - $tag1 = new SystemTag(123, 'One', true, false); - $tag2 = new SystemTag(456, 'Two', true, true); - - $this->tagManager->expects($this->once()) - ->method('getAllTags') - ->with(true) - ->will($this->returnValue([$tag1, $tag2])); - - $children = $this->getNode(false)->getChildren(); - - $this->assertCount(2, $children); - - $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagNode', $children[0]); - $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagNode', $children[1]); - $this->assertEquals($tag1, $children[0]->getSystemTag()); - $this->assertEquals($tag2, $children[1]->getSystemTag()); - } - - public function testGetChildrenEmpty() { - $this->tagManager->expects($this->once()) - ->method('getAllTags') - ->with(null) - ->will($this->returnValue([])); - $this->assertCount(0, $this->getNode()->getChildren()); - } - - public function childExistsProvider() { - return [ - // admins, always visible - [true, true, true], - [true, false, true], - // non-admins, depends on flag - [false, true, true], - [false, false, false], - ]; - } - - /** - * @dataProvider childExistsProvider - */ - public function testChildExists($isAdmin, $userVisible, $expectedResult) { - $tag = new SystemTag(123, 'One', $userVisible, false); - - $this->tagManager->expects($this->once()) - ->method('getTagsByIds') - ->with(['123']) - ->will($this->returnValue([$tag])); - - $this->assertEquals($expectedResult, $this->getNode($isAdmin)->childExists('123')); - } - - public function testChildExistsNotFound() { - $this->tagManager->expects($this->once()) - ->method('getTagsByIds') - ->with(['123']) - ->will($this->throwException(new TagNotFoundException())); - - $this->assertFalse($this->getNode()->childExists('123')); - } - - /** - * @expectedException Sabre\DAV\Exception\BadRequest - */ - public function testChildExistsBadRequest() { - $this->tagManager->expects($this->once()) - ->method('getTagsByIds') - ->with(['invalid']) - ->will($this->throwException(new \InvalidArgumentException())); - - $this->getNode()->childExists('invalid'); - } -} diff --git a/apps/dav/tests/unit/systemtag/systemtagsobjectmappingcollection.php b/apps/dav/tests/unit/systemtag/systemtagsobjectmappingcollection.php deleted file mode 100644 index df97acd846b..00000000000 --- a/apps/dav/tests/unit/systemtag/systemtagsobjectmappingcollection.php +++ /dev/null @@ -1,381 +0,0 @@ -<?php -/** - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\SystemTag; - - -use OC\SystemTag\SystemTag; -use OCP\SystemTag\TagNotFoundException; - -class SystemTagsObjectMappingCollection extends \Test\TestCase { - - /** - * @var \OCP\SystemTag\ISystemTagManager - */ - private $tagManager; - - /** - * @var \OCP\SystemTag\ISystemTagMapper - */ - private $tagMapper; - - protected function setUp() { - parent::setUp(); - - $this->tagManager = $this->getMock('\OCP\SystemTag\ISystemTagManager'); - $this->tagMapper = $this->getMock('\OCP\SystemTag\ISystemTagObjectMapper'); - } - - public function getNode($isAdmin = true) { - return new \OCA\DAV\SystemTag\SystemTagsObjectMappingCollection ( - 111, - 'files', - $isAdmin, - $this->tagManager, - $this->tagMapper - ); - } - - public function testAssignTagAsAdmin() { - $this->tagManager->expects($this->never()) - ->method('getTagsByIds'); - $this->tagMapper->expects($this->once()) - ->method('assignTags') - ->with(111, 'files', '555'); - - $this->getNode(true)->createFile('555'); - } - - public function testAssignTagAsUser() { - $tag = new SystemTag('1', 'Test', true, true); - - $this->tagManager->expects($this->once()) - ->method('getTagsByIds') - ->with('555') - ->will($this->returnValue([$tag])); - $this->tagMapper->expects($this->once()) - ->method('assignTags') - ->with(111, 'files', '555'); - - $this->getNode(false)->createFile('555'); - } - - public function permissionsProvider() { - return [ - // invisible, tag does not exist for user - [false, true, '\Sabre\DAV\Exception\PreconditionFailed'], - // visible but static, cannot assign tag - [true, false, '\Sabre\DAV\Exception\Forbidden'], - ]; - } - - /** - * @dataProvider permissionsProvider - */ - public function testAssignTagAsUserNoPermission($userVisible, $userAssignable, $expectedException) { - $tag = new SystemTag('1', 'Test', $userVisible, $userAssignable); - - $this->tagManager->expects($this->once()) - ->method('getTagsByIds') - ->with('555') - ->will($this->returnValue([$tag])); - $this->tagMapper->expects($this->never()) - ->method('assignTags'); - - $thrown = null; - try { - $this->getNode(false)->createFile('555'); - } catch (\Exception $e) { - $thrown = $e; - } - - $this->assertInstanceOf($expectedException, $thrown); - } - - /** - * @expectedException Sabre\DAV\Exception\PreconditionFailed - */ - public function testAssignTagNotFound() { - $this->tagMapper->expects($this->once()) - ->method('assignTags') - ->with(111, 'files', '555') - ->will($this->throwException(new TagNotFoundException())); - - $this->getNode()->createFile('555'); - } - - /** - * @expectedException Sabre\DAV\Exception\Forbidden - */ - public function testForbiddenCreateDirectory() { - $this->getNode()->createDirectory('789'); - } - - public function getChildProvider() { - return [ - [ - true, - true, - ], - [ - true, - false, - ], - [ - false, - true, - ], - ]; - } - - /** - * @dataProvider getChildProvider - */ - public function testGetChild($isAdmin, $userVisible) { - $tag = new SystemTag(555, 'TheTag', $userVisible, false); - - $this->tagMapper->expects($this->once()) - ->method('haveTag') - ->with([111], 'files', '555', true) - ->will($this->returnValue(true)); - - $this->tagManager->expects($this->once()) - ->method('getTagsByIds') - ->with(['555']) - ->will($this->returnValue(['555' => $tag])); - - $childNode = $this->getNode($isAdmin)->getChild('555'); - - $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagNode', $childNode); - $this->assertEquals('555', $childNode->getName()); - } - - /** - * @expectedException \Sabre\DAV\Exception\NotFound - */ - public function testGetChildUserNonVisible() { - $tag = new SystemTag(555, 'TheTag', false, false); - - $this->tagMapper->expects($this->once()) - ->method('haveTag') - ->with([111], 'files', '555', true) - ->will($this->returnValue(true)); - - $this->tagManager->expects($this->once()) - ->method('getTagsByIds') - ->with(['555']) - ->will($this->returnValue(['555' => $tag])); - - $this->getNode(false)->getChild('555'); - } - - /** - * @expectedException Sabre\DAV\Exception\NotFound - */ - public function testGetChildRelationNotFound() { - $this->tagMapper->expects($this->once()) - ->method('haveTag') - ->with([111], 'files', '777') - ->will($this->returnValue(false)); - - $this->getNode()->getChild('777'); - } - - /** - * @expectedException Sabre\DAV\Exception\BadRequest - */ - public function testGetChildInvalidId() { - $this->tagMapper->expects($this->once()) - ->method('haveTag') - ->with([111], 'files', 'badid') - ->will($this->throwException(new \InvalidArgumentException())); - - $this->getNode()->getChild('badid'); - } - - /** - * @expectedException Sabre\DAV\Exception\NotFound - */ - public function testGetChildTagDoesNotExist() { - $this->tagMapper->expects($this->once()) - ->method('haveTag') - ->with([111], 'files', '777') - ->will($this->throwException(new TagNotFoundException())); - - $this->getNode()->getChild('777'); - } - - public function testGetChildrenAsAdmin() { - $tag1 = new SystemTag(555, 'TagOne', true, false); - $tag2 = new SystemTag(556, 'TagTwo', true, true); - $tag3 = new SystemTag(557, 'InvisibleTag', false, true); - - $this->tagMapper->expects($this->once()) - ->method('getTagIdsForObjects') - ->with([111], 'files') - ->will($this->returnValue(['111' => ['555', '556', '557']])); - - $this->tagManager->expects($this->once()) - ->method('getTagsByIds') - ->with(['555', '556', '557']) - ->will($this->returnValue(['555' => $tag1, '556' => $tag2, '557' => $tag3])); - - $children = $this->getNode(true)->getChildren(); - - $this->assertCount(3, $children); - - $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagMappingNode', $children[0]); - $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagMappingNode', $children[1]); - $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagMappingNode', $children[2]); - - $this->assertEquals(111, $children[0]->getObjectId()); - $this->assertEquals('files', $children[0]->getObjectType()); - $this->assertEquals($tag1, $children[0]->getSystemTag()); - - $this->assertEquals(111, $children[1]->getObjectId()); - $this->assertEquals('files', $children[1]->getObjectType()); - $this->assertEquals($tag2, $children[1]->getSystemTag()); - - $this->assertEquals(111, $children[2]->getObjectId()); - $this->assertEquals('files', $children[2]->getObjectType()); - $this->assertEquals($tag3, $children[2]->getSystemTag()); - } - - public function testGetChildrenAsUser() { - $tag1 = new SystemTag(555, 'TagOne', true, false); - $tag2 = new SystemTag(556, 'TagTwo', true, true); - $tag3 = new SystemTag(557, 'InvisibleTag', false, true); - - $this->tagMapper->expects($this->once()) - ->method('getTagIdsForObjects') - ->with([111], 'files') - ->will($this->returnValue(['111' => ['555', '556', '557']])); - - $this->tagManager->expects($this->once()) - ->method('getTagsByIds') - ->with(['555', '556', '557']) - ->will($this->returnValue(['555' => $tag1, '556' => $tag2, '557' => $tag3])); - - $children = $this->getNode(false)->getChildren(); - - $this->assertCount(2, $children); - - $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagMappingNode', $children[0]); - $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagMappingNode', $children[1]); - - $this->assertEquals(111, $children[0]->getObjectId()); - $this->assertEquals('files', $children[0]->getObjectType()); - $this->assertEquals($tag1, $children[0]->getSystemTag()); - - $this->assertEquals(111, $children[1]->getObjectId()); - $this->assertEquals('files', $children[1]->getObjectType()); - $this->assertEquals($tag2, $children[1]->getSystemTag()); - } - - public function testChildExistsAsAdmin() { - $this->tagMapper->expects($this->once()) - ->method('haveTag') - ->with([111], 'files', '555') - ->will($this->returnValue(true)); - - $this->assertTrue($this->getNode(true)->childExists('555')); - } - - public function testChildExistsWithVisibleTagAsUser() { - $tag = new SystemTag(555, 'TagOne', true, false); - - $this->tagMapper->expects($this->once()) - ->method('haveTag') - ->with([111], 'files', '555') - ->will($this->returnValue(true)); - - $this->tagManager->expects($this->once()) - ->method('getTagsByIds') - ->with('555') - ->will($this->returnValue([$tag])); - - $this->assertTrue($this->getNode(false)->childExists('555')); - } - - public function testChildExistsWithInvisibleTagAsUser() { - $tag = new SystemTag(555, 'TagOne', false, false); - - $this->tagMapper->expects($this->once()) - ->method('haveTag') - ->with([111], 'files', '555') - ->will($this->returnValue(true)); - - $this->tagManager->expects($this->once()) - ->method('getTagsByIds') - ->with('555') - ->will($this->returnValue([$tag])); - - $this->assertFalse($this->getNode(false)->childExists('555')); - } - - public function testChildExistsNotFound() { - $this->tagMapper->expects($this->once()) - ->method('haveTag') - ->with([111], 'files', '555') - ->will($this->returnValue(false)); - - $this->assertFalse($this->getNode()->childExists('555')); - } - - public function testChildExistsTagNotFound() { - $this->tagMapper->expects($this->once()) - ->method('haveTag') - ->with([111], 'files', '555') - ->will($this->throwException(new TagNotFoundException())); - - $this->assertFalse($this->getNode()->childExists('555')); - } - - /** - * @expectedException Sabre\DAV\Exception\BadRequest - */ - public function testChildExistsInvalidId() { - $this->tagMapper->expects($this->once()) - ->method('haveTag') - ->with([111], 'files', '555') - ->will($this->throwException(new \InvalidArgumentException())); - - $this->getNode()->childExists('555'); - } - - /** - * @expectedException Sabre\DAV\Exception\Forbidden - */ - public function testDelete() { - $this->getNode()->delete(); - } - - /** - * @expectedException Sabre\DAV\Exception\Forbidden - */ - public function testSetName() { - $this->getNode()->setName('somethingelse'); - } - - public function testGetName() { - $this->assertEquals('111', $this->getNode()->getName()); - } -} diff --git a/apps/dav/tests/unit/systemtag/systemtagsobjecttypecollection.php b/apps/dav/tests/unit/systemtag/systemtagsobjecttypecollection.php deleted file mode 100644 index 1d4264f94f9..00000000000 --- a/apps/dav/tests/unit/systemtag/systemtagsobjecttypecollection.php +++ /dev/null @@ -1,160 +0,0 @@ -<?php -/** - * @author Vincent Petry <pvince81@owncloud.com> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace OCA\DAV\Tests\Unit\SystemTag; - -class SystemTagsObjectTypeCollection extends \Test\TestCase { - - /** - * @var \OCA\DAV\SystemTag\SystemTagsObjectTypeCollection - */ - private $node; - - /** - * @var \OCP\SystemTag\ISystemTagManager - */ - private $tagManager; - - /** - * @var \OCP\SystemTag\ISystemTagMapper - */ - private $tagMapper; - - /** - * @var \OCP\Files\Folder - */ - private $userFolder; - - protected function setUp() { - parent::setUp(); - - $this->tagManager = $this->getMock('\OCP\SystemTag\ISystemTagManager'); - $this->tagMapper = $this->getMock('\OCP\SystemTag\ISystemTagObjectMapper'); - - $user = $this->getMock('\OCP\IUser'); - $user->expects($this->any()) - ->method('getUID') - ->will($this->returnValue('testuser')); - $userSession = $this->getMock('\OCP\IUserSession'); - $userSession->expects($this->any()) - ->method('getUser') - ->will($this->returnValue($user)); - $groupManager = $this->getMock('\OCP\IGroupManager'); - $groupManager->expects($this->any()) - ->method('isAdmin') - ->with('testuser') - ->will($this->returnValue(true)); - - $this->userFolder = $this->getMock('\OCP\Files\Folder'); - - $fileRoot = $this->getMock('\OCP\Files\IRootFolder'); - $fileRoot->expects($this->any()) - ->method('getUserfolder') - ->with('testuser') - ->will($this->returnValue($this->userFolder)); - - $this->node = new \OCA\DAV\SystemTag\SystemTagsObjectTypeCollection( - 'files', - $this->tagManager, - $this->tagMapper, - $userSession, - $groupManager, - $fileRoot - ); - } - - /** - * @expectedException Sabre\DAV\Exception\Forbidden - */ - public function testForbiddenCreateFile() { - $this->node->createFile('555'); - } - - /** - * @expectedException Sabre\DAV\Exception\Forbidden - */ - public function testForbiddenCreateDirectory() { - $this->node->createDirectory('789'); - } - - public function testGetChild() { - $this->userFolder->expects($this->once()) - ->method('getById') - ->with('555') - ->will($this->returnValue([true])); - $childNode = $this->node->getChild('555'); - - $this->assertInstanceOf('\OCA\DAV\SystemTag\SystemTagsObjectMappingCollection', $childNode); - $this->assertEquals('555', $childNode->getName()); - } - - /** - * @expectedException Sabre\DAV\Exception\NotFound - */ - public function testGetChildWithoutAccess() { - $this->userFolder->expects($this->once()) - ->method('getById') - ->with('555') - ->will($this->returnValue([])); - $this->node->getChild('555'); - } - - /** - * @expectedException Sabre\DAV\Exception\MethodNotAllowed - */ - public function testGetChildren() { - $this->node->getChildren(); - } - - public function testChildExists() { - $this->userFolder->expects($this->once()) - ->method('getById') - ->with('123') - ->will($this->returnValue([true])); - $this->assertTrue($this->node->childExists('123')); - } - - public function testChildExistsWithoutAccess() { - $this->userFolder->expects($this->once()) - ->method('getById') - ->with('555') - ->will($this->returnValue([])); - $this->assertFalse($this->node->childExists('555')); - } - - /** - * @expectedException Sabre\DAV\Exception\Forbidden - */ - public function testDelete() { - $this->node->delete(); - } - - /** - * @expectedException Sabre\DAV\Exception\Forbidden - */ - public function testSetName() { - $this->node->setName('somethingelse'); - } - - public function testGetName() { - $this->assertEquals('files', $this->node->getName()); - } -} diff --git a/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-1.ics b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-1.ics new file mode 100644 index 00000000000..e76ac3c9b2f --- /dev/null +++ b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-1.ics @@ -0,0 +1,17 @@ +BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +BEGIN:VEVENT +CREATED:20240507T105946Z +LAST-MODIFIED:20240507T121113Z +DTSTAMP:20240507T121113Z +UID:07514c7b-1014-425c-b1b8-2c35ab0eea1d +SUMMARY:Event A +RRULE:FREQ=YEARLY +DTSTART;TZID=Europe/Berlin:20240101T101500 +DTEND;TZID=Europe/Berlin:20240101T111500 +TRANSP:OPAQUE +X-MOZ-GENERATION:4 +SEQUENCE:2 +END:VEVENT +END:VCALENDAR diff --git a/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-2.ics b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-2.ics new file mode 100644 index 00000000000..fe948321d51 --- /dev/null +++ b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-2.ics @@ -0,0 +1,17 @@ +BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +BEGIN:VEVENT +CREATED:20240507T110122Z +LAST-MODIFIED:20240507T121120Z +DTSTAMP:20240507T121120Z +UID:67cf8134-ff10-49a7-913d-acfeda463db6 +SUMMARY:Event B +RRULE:FREQ=YEARLY +DTSTART;TZID=Europe/Berlin:20240101T123000 +DTEND;TZID=Europe/Berlin:20240101T133000 +TRANSP:OPAQUE +X-MOZ-GENERATION:4 +SEQUENCE:2 +END:VEVENT +END:VCALENDAR diff --git a/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-3.ics b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-3.ics new file mode 100644 index 00000000000..de7765b28d2 --- /dev/null +++ b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-3.ics @@ -0,0 +1,17 @@ +BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +BEGIN:VEVENT +CREATED:20240507T120352Z +LAST-MODIFIED:20240507T121128Z +DTSTAMP:20240507T121128Z +UID:59090ca1-e52b-447f-8e08-491d1da729fa +SUMMARY:Event C +RRULE:FREQ=YEARLY +DTSTART;TZID=Europe/Berlin:20240101T151000 +DTEND;TZID=Europe/Berlin:20240101T161000 +TRANSP:OPAQUE +X-MOZ-GENERATION:2 +SEQUENCE:1 +END:VEVENT +END:VCALENDAR diff --git a/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-4.ics b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-4.ics new file mode 100644 index 00000000000..b4d2f752c0a --- /dev/null +++ b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-4.ics @@ -0,0 +1,17 @@ +BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +BEGIN:VEVENT +CREATED:20240507T120414Z +LAST-MODIFIED:20240507T121134Z +DTSTAMP:20240507T121134Z +UID:b1814d32-9adf-4518-8535-37f2c037f423 +SUMMARY:Event D +RRULE:FREQ=YEARLY +DTSTART;TZID=Europe/Berlin:20240101T164500 +DTEND;TZID=Europe/Berlin:20240101T171500 +TRANSP:OPAQUE +SEQUENCE:2 +X-MOZ-GENERATION:3 +END:VEVENT +END:VCALENDAR diff --git a/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-5.ics b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-5.ics new file mode 100644 index 00000000000..1cd8b7ebf13 --- /dev/null +++ b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-5.ics @@ -0,0 +1,14 @@ +BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +BEGIN:VEVENT +CREATED:20240507T122221Z +LAST-MODIFIED:20240507T122237Z +DTSTAMP:20240507T122237Z +UID:19c4e049-0b09-4101-a2ad-061a837e6a5e +SUMMARY:Cake Tasting +DTSTART;TZID=Europe/Berlin:20240509T151500 +DTEND;TZID=Europe/Berlin:20240509T171500 +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR diff --git a/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-6.ics b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-6.ics new file mode 100644 index 00000000000..6c24d534281 --- /dev/null +++ b/apps/dav/tests/unit/test_fixtures/caldav-search-limit-timerange-6.ics @@ -0,0 +1,15 @@ +BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +BEGIN:VEVENT +CREATED:20240507T122246Z +LAST-MODIFIED:20240507T175258Z +DTSTAMP:20240507T175258Z +UID:60a7d310-aa7b-4974-8a8a-ff9339367e1d +SUMMARY:Pasta Day +DTSTART;TZID=Europe/Berlin:20240514T123000 +DTEND;TZID=Europe/Berlin:20240514T133000 +TRANSP:OPAQUE +X-MOZ-GENERATION:2 +END:VEVENT +END:VCALENDAR diff --git a/apps/dav/tests/unit/test_fixtures/caldav-search-missing-start-1.ics b/apps/dav/tests/unit/test_fixtures/caldav-search-missing-start-1.ics new file mode 100644 index 00000000000..a7865eaf5ef --- /dev/null +++ b/apps/dav/tests/unit/test_fixtures/caldav-search-missing-start-1.ics @@ -0,0 +1,14 @@ +BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +BEGIN:VEVENT +CREATED:20240507T122246Z +LAST-MODIFIED:20240507T175258Z +DTSTAMP:20240507T175258Z +UID:39e1b04f-d1cc-4622-bf97-11c38e070f43 +SUMMARY:Missing DTSTART 1 +DTEND;TZID=Europe/Berlin:20240514T133000 +TRANSP:OPAQUE +X-MOZ-GENERATION:2 +END:VEVENT +END:VCALENDAR diff --git a/apps/dav/tests/unit/test_fixtures/caldav-search-missing-start-2.ics b/apps/dav/tests/unit/test_fixtures/caldav-search-missing-start-2.ics new file mode 100644 index 00000000000..4a33f2b1c8a --- /dev/null +++ b/apps/dav/tests/unit/test_fixtures/caldav-search-missing-start-2.ics @@ -0,0 +1,14 @@ +BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +BEGIN:VEVENT +CREATED:20240507T122246Z +LAST-MODIFIED:20240507T175258Z +DTSTAMP:20240507T175258Z +UID:12413feb-4b8c-4e95-ae7f-9ec4f42f3348 +SUMMARY:Missing DTSTART 2 +DTEND;TZID=Europe/Berlin:20240514T133000 +TRANSP:OPAQUE +X-MOZ-GENERATION:2 +END:VEVENT +END:VCALENDAR diff --git a/apps/dav/tests/unit/test_fixtures/example-event-default-expected.ics b/apps/dav/tests/unit/test_fixtures/example-event-default-expected.ics new file mode 100644 index 00000000000..09606ca5ee4 --- /dev/null +++ b/apps/dav/tests/unit/test_fixtures/example-event-default-expected.ics @@ -0,0 +1,20 @@ +BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Sabre//Sabre VObject 4.5.6//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+UID:RANDOM-UID
+DTSTAMP:20250121T000000Z
+SUMMARY:Example event - open me!
+DTSTART:20250128T100000Z
+DTEND:20250128T110000Z
+DESCRIPTION:Welcome to Nextcloud Calendar!\n\nThis is a sample event - expl
+ ore the flexibility of planning with Nextcloud Calendar by making any edit
+ s you want!\n\nWith Nextcloud Calendar\, you can:\n- Create\, edit\, and m
+ anage events effortlessly.\n- Create multiple calendars and share them wit
+ h teammates\, friends\, or family.\n- Check availability and display your
+ busy times to others.\n- Seamlessly integrate with apps and devices via Ca
+ lDAV.\n- Customize your experience: schedule recurring events\, adjust not
+ ifications and other settings.
+END:VEVENT
+END:VCALENDAR
diff --git a/apps/dav/tests/unit/test_fixtures/example-event-default-expected.ics.license b/apps/dav/tests/unit/test_fixtures/example-event-default-expected.ics.license new file mode 100644 index 00000000000..23e2d6b1908 --- /dev/null +++ b/apps/dav/tests/unit/test_fixtures/example-event-default-expected.ics.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/tests/unit/test_fixtures/example-event-expected.ics b/apps/dav/tests/unit/test_fixtures/example-event-expected.ics new file mode 100644 index 00000000000..f9dfc37718e --- /dev/null +++ b/apps/dav/tests/unit/test_fixtures/example-event-expected.ics @@ -0,0 +1,18 @@ +BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//IDN nextcloud.com//Calendar app 5.2.0-dev.1//EN
+BEGIN:VEVENT
+CREATED:20250128T091147Z
+DTSTAMP:20250128T091507Z
+LAST-MODIFIED:20250128T091507Z
+SEQUENCE:2
+STATUS:CONFIRMED
+SUMMARY:Welcome!
+DESCRIPTION:Welcome!!!
+LOCATION:Test
+UID:RANDOM-UID
+DTSTART:20250128T100000Z
+DTEND:20250128T110000Z
+END:VEVENT
+END:VCALENDAR
diff --git a/apps/dav/tests/unit/test_fixtures/example-event-expected.ics.license b/apps/dav/tests/unit/test_fixtures/example-event-expected.ics.license new file mode 100644 index 00000000000..23e2d6b1908 --- /dev/null +++ b/apps/dav/tests/unit/test_fixtures/example-event-expected.ics.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/tests/unit/test_fixtures/example-event-with-attendees.ics b/apps/dav/tests/unit/test_fixtures/example-event-with-attendees.ics new file mode 100644 index 00000000000..8018552f2a5 --- /dev/null +++ b/apps/dav/tests/unit/test_fixtures/example-event-with-attendees.ics @@ -0,0 +1,21 @@ +BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//IDN nextcloud.com//Calendar app 5.2.0-dev.1//EN
+BEGIN:VEVENT
+CREATED:20250128T091147Z
+DTSTAMP:20250128T091507Z
+LAST-MODIFIED:20250128T091507Z
+SEQUENCE:2
+UID:3b4df6a8-84df-43d5-baf9-377b43390b70
+DTSTART;VALUE=DATE:20250130
+DTEND;VALUE=DATE:20250131
+STATUS:CONFIRMED
+SUMMARY:Welcome!
+DESCRIPTION:Welcome!!!
+LOCATION:Test
+ATTENDEE;CN=user a;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICI
+ PANT;RSVP=TRUE;LANGUAGE=en;SCHEDULE-STATUS=1.1:mailto:usera@imap.localhost
+ORGANIZER;CN=Admin Account:mailto:admin@imap.localhost
+END:VEVENT
+END:VCALENDAR
diff --git a/apps/dav/tests/unit/test_fixtures/example-event-with-attendees.ics.license b/apps/dav/tests/unit/test_fixtures/example-event-with-attendees.ics.license new file mode 100644 index 00000000000..23e2d6b1908 --- /dev/null +++ b/apps/dav/tests/unit/test_fixtures/example-event-with-attendees.ics.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/dav/tests/unit/test_fixtures/example-event.ics b/apps/dav/tests/unit/test_fixtures/example-event.ics new file mode 100644 index 00000000000..6fc1848ea52 --- /dev/null +++ b/apps/dav/tests/unit/test_fixtures/example-event.ics @@ -0,0 +1,18 @@ +BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//IDN nextcloud.com//Calendar app 5.2.0-dev.1//EN
+BEGIN:VEVENT
+CREATED:20250128T091147Z
+DTSTAMP:20250128T091507Z
+LAST-MODIFIED:20250128T091507Z
+SEQUENCE:2
+UID:3b4df6a8-84df-43d5-baf9-377b43390b70
+STATUS:CONFIRMED
+SUMMARY:Welcome!
+DESCRIPTION:Welcome!!!
+LOCATION:Test
+DTSTART:20250204T100000Z
+DTEND:20250204T110000Z
+END:VEVENT
+END:VCALENDAR
diff --git a/apps/dav/tests/unit/test_fixtures/example-event.ics.license b/apps/dav/tests/unit/test_fixtures/example-event.ics.license new file mode 100644 index 00000000000..23e2d6b1908 --- /dev/null +++ b/apps/dav/tests/unit/test_fixtures/example-event.ics.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +SPDX-License-Identifier: AGPL-3.0-or-later |