diff options
Diffstat (limited to 'apps/dav/tests/unit/CardDAV')
-rw-r--r-- | apps/dav/tests/unit/CardDAV/Activity/BackendTest.php | 483 | ||||
-rw-r--r-- | apps/dav/tests/unit/CardDAV/AddressBookImplTest.php | 208 | ||||
-rw-r--r-- | apps/dav/tests/unit/CardDAV/AddressBookTest.php | 150 | ||||
-rw-r--r-- | apps/dav/tests/unit/CardDAV/BirthdayServiceTest.php | 254 | ||||
-rw-r--r-- | apps/dav/tests/unit/CardDAV/CardDavBackendTest.php | 491 | ||||
-rw-r--r-- | apps/dav/tests/unit/CardDAV/ContactsManagerTest.php | 45 | ||||
-rw-r--r-- | apps/dav/tests/unit/CardDAV/ConverterTest.php | 195 | ||||
-rw-r--r-- | apps/dav/tests/unit/CardDAV/ImageExportPluginTest.php | 114 | ||||
-rw-r--r-- | apps/dav/tests/unit/CardDAV/Security/CardDavRateLimitingPluginTest.php | 146 | ||||
-rw-r--r-- | apps/dav/tests/unit/CardDAV/Sharing/PluginTest.php | 58 | ||||
-rw-r--r-- | apps/dav/tests/unit/CardDAV/SyncServiceTest.php | 571 | ||||
-rw-r--r-- | apps/dav/tests/unit/CardDAV/SystemAddressBookTest.php | 428 | ||||
-rw-r--r-- | apps/dav/tests/unit/CardDAV/Validation/CardDavValidatePluginTest.php | 73 |
13 files changed, 2305 insertions, 911 deletions
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 index 5b28e6b025b..74699cf3925 100644 --- a/apps/dav/tests/unit/CardDAV/AddressBookImplTest.php +++ b/apps/dav/tests/unit/CardDAV/AddressBookImplTest.php @@ -1,63 +1,31 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Björn Schießle <bjoern@schiessle.org> - * @author call-me-matt <nextcloud@matthiasheinisch.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @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\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 { - - /** @var AddressBookImpl */ - private $addressBookImpl; - - /** @var array */ - private $addressBookInfo; - - /** @var AddressBook | \PHPUnit\Framework\MockObject\MockObject */ - private $addressBook; - - /** @var IURLGenerator | \PHPUnit\Framework\MockObject\MockObject */ - private $urlGenerator; - - /** @var CardDavBackend | \PHPUnit\Framework\MockObject\MockObject */ - private $backend; - - /** @var VCard | \PHPUnit\Framework\MockObject\MockObject */ - private $vCard; + 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(); @@ -68,34 +36,34 @@ class AddressBookImplTest extends TestCase { 'principaluri' => 'principals/system/system', '{DAV:}displayname' => 'display name', ]; - $this->addressBook = $this->getMockBuilder(AddressBook::class) - ->disableOriginalConstructor()->getMock(); - $this->backend = $this->getMockBuilder(CardDavBackend::class) - ->disableOriginalConstructor()->getMock(); + $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->urlGenerator, + $this->propertyMapper, + null ); } - public function testGetKey() { + public function testGetKey(): void { $this->assertSame($this->addressBookInfo['id'], $this->addressBookImpl->getKey()); } - public function testGetDisplayName() { + public function testGetDisplayName(): void { $this->assertSame($this->addressBookInfo['{DAV:}displayname'], $this->addressBookImpl->getDisplayName()); } - public function testSearch() { - - /** @var \PHPUnit\Framework\MockObject\MockObject | AddressBookImpl $addressBookImpl */ + public function testSearch(): void { + /** @var MockObject&AddressBookImpl $addressBookImpl */ $addressBookImpl = $this->getMockBuilder(AddressBookImpl::class) ->setConstructorArgs( [ @@ -103,9 +71,11 @@ class AddressBookImplTest extends TestCase { $this->addressBookInfo, $this->backend, $this->urlGenerator, + $this->propertyMapper, + null ] ) - ->setMethods(['vCard2Array', 'readCard']) + ->onlyMethods(['vCard2Array', 'readCard']) ->getMock(); $pattern = 'pattern'; @@ -123,25 +93,21 @@ class AddressBookImplTest extends TestCase { $addressBookImpl->expects($this->exactly(2))->method('readCard') ->willReturn($this->vCard); $addressBookImpl->expects($this->exactly(2))->method('vCard2Array') - ->withConsecutive( - ['foo.vcf', $this->vCard], - ['bar.vcf', $this->vCard] - )->willReturn('vCard'); + ->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)); } - /** - * @dataProvider dataTestCreate - * - * @param array $properties - */ - public function testCreate($properties) { + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestCreate')] + public function testCreate(array $properties): void { $uid = 'uid'; - /** @var \PHPUnit\Framework\MockObject\MockObject | AddressBookImpl $addressBookImpl */ + /** @var MockObject&AddressBookImpl $addressBookImpl */ $addressBookImpl = $this->getMockBuilder(AddressBookImpl::class) ->setConstructorArgs( [ @@ -149,16 +115,27 @@ class AddressBookImplTest extends TestCase { $this->addressBookInfo, $this->backend, $this->urlGenerator, + $this->propertyMapper, + null ] ) - ->setMethods(['vCard2Array', 'createUid', 'createEmptyVCard']) + ->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(count($properties))) + $this->vCard->expects($this->exactly($expectedProperties)) ->method('createProperty'); $this->backend->expects($this->once())->method('createCard'); $this->backend->expects($this->never())->method('updateCard'); @@ -169,19 +146,20 @@ class AddressBookImplTest extends TestCase { $this->assertTrue($addressBookImpl->createOrUpdate($properties)); } - public function dataTestCreate() { + public static function dataTestCreate(): array { return [ [[]], - [['FN' => 'John Doe']] + [['FN' => 'John Doe']], + [['FN' => 'John Doe', 'EMAIL' => ['john@doe.cloud', 'john.doe@example.org']]], ]; } - public function testUpdate() { + public function testUpdate(): void { $uid = 'uid'; $uri = 'bla.vcf'; $properties = ['URI' => $uri, 'UID' => $uid, 'FN' => 'John Doe']; - /** @var \PHPUnit\Framework\MockObject\MockObject | AddressBookImpl $addressBookImpl */ + /** @var MockObject&AddressBookImpl $addressBookImpl */ $addressBookImpl = $this->getMockBuilder(AddressBookImpl::class) ->setConstructorArgs( [ @@ -189,9 +167,11 @@ class AddressBookImplTest extends TestCase { $this->addressBookInfo, $this->backend, $this->urlGenerator, + $this->propertyMapper, + null ] ) - ->setMethods(['vCard2Array', 'createUid', 'createEmptyVCard', 'readCard']) + ->onlyMethods(['vCard2Array', 'createUid', 'createEmptyVCard', 'readCard']) ->getMock(); $addressBookImpl->expects($this->never())->method('createUid'); @@ -201,7 +181,7 @@ class AddressBookImplTest extends TestCase { ->willReturn(['carddata' => 'data']); $addressBookImpl->expects($this->once())->method('readCard') ->with('data')->willReturn($this->vCard); - $this->vCard->expects($this->exactly(count($properties)-1)) + $this->vCard->expects($this->exactly(count($properties) - 1)) ->method('createProperty'); $this->backend->expects($this->never())->method('createCard'); $this->backend->expects($this->once())->method('updateCard'); @@ -211,14 +191,14 @@ class AddressBookImplTest extends TestCase { $this->assertTrue($addressBookImpl->createOrUpdate($properties)); } - public function testUpdateWithTypes() { + 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'); + $textProperty = $vCard->createProperty('KEY', 'value'); - /** @var \PHPUnit\Framework\MockObject\MockObject | AddressBookImpl $addressBookImpl */ + /** @var MockObject&AddressBookImpl $addressBookImpl */ $addressBookImpl = $this->getMockBuilder(AddressBookImpl::class) ->setConstructorArgs( [ @@ -226,9 +206,11 @@ class AddressBookImplTest extends TestCase { $this->addressBookInfo, $this->backend, $this->urlGenerator, + $this->propertyMapper, + null ] ) - ->setMethods(['vCard2Array', 'createUid', 'createEmptyVCard', 'readCard']) + ->onlyMethods(['vCard2Array', 'createUid', 'createEmptyVCard', 'readCard']) ->getMock(); $this->backend->expects($this->once())->method('getCard') @@ -237,7 +219,7 @@ class AddressBookImplTest extends TestCase { $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)) + $this->vCard->expects($this->exactly(count($properties) - 1)) ->method('createProperty'); $this->vCard->expects($this->once())->method('remove') ->with('ADR'); @@ -246,13 +228,8 @@ class AddressBookImplTest extends TestCase { $addressBookImpl->createOrUpdate($properties); } - /** - * @dataProvider dataTestGetPermissions - * - * @param array $permissions - * @param int $expected - */ - public function testGetPermissions($permissions, $expected) { + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestGetPermissions')] + public function testGetPermissions(array $permissions, int $expected): void { $this->addressBook->expects($this->once())->method('getACL') ->willReturn($permissions); @@ -261,21 +238,22 @@ class AddressBookImplTest extends TestCase { ); } - public function dataTestGetPermissions() { + public static function dataTestGetPermissions(): array { 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], + [[['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() { + public function testDelete(): void { $cardId = 1; $cardUri = 'cardUri'; $this->backend->expects($this->once())->method('getCardUri') @@ -287,7 +265,7 @@ class AddressBookImplTest extends TestCase { $this->assertTrue($this->addressBookImpl->delete($cardId)); } - public function testReadCard() { + public function testReadCard(): void { $vCard = new VCard(); $vCard->add(new Text($vCard, 'UID', 'uid')); $vCardSerialized = $vCard->serialize(); @@ -298,8 +276,8 @@ class AddressBookImplTest extends TestCase { $this->assertSame($vCardSerialized, $resultSerialized); } - public function testCreateUid() { - /** @var \PHPUnit\Framework\MockObject\MockObject | AddressBookImpl $addressBookImpl */ + public function testCreateUid(): void { + /** @var MockObject&AddressBookImpl $addressBookImpl */ $addressBookImpl = $this->getMockBuilder(AddressBookImpl::class) ->setConstructorArgs( [ @@ -307,13 +285,19 @@ class AddressBookImplTest extends TestCase { $this->addressBookInfo, $this->backend, $this->urlGenerator, + $this->propertyMapper, + null ] ) - ->setMethods(['getUid']) + ->onlyMethods(['getUid']) ->getMock(); - $addressBookImpl->expects($this->at(0))->method('getUid')->willReturn('uid0'); - $addressBookImpl->expects($this->at(1))->method('getUid')->willReturn('uid1'); + $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') @@ -328,7 +312,7 @@ class AddressBookImplTest extends TestCase { ); } - public function testCreateEmptyVCard() { + public function testCreateEmptyVCard(): void { $uid = 'uid'; $expectedVCard = new VCard(); $expectedVCard->UID = $uid; @@ -340,7 +324,7 @@ class AddressBookImplTest extends TestCase { $this->assertSame($expectedVCardSerialized, $resultSerialized); } - public function testVCard2Array() { + public function testVCard2Array(): void { $vCard = new VCard(); $vCard->add($vCard->createProperty('FN', 'Full Name')); @@ -407,7 +391,7 @@ class AddressBookImplTest extends TestCase { ], $array); } - public function testVCard2ArrayWithTypes() { + public function testVCard2ArrayWithTypes(): void { $vCard = new VCard(); $vCard->add($vCard->createProperty('FN', 'Full Name')); @@ -499,7 +483,9 @@ class AddressBookImplTest extends TestCase { $this->addressBook, $addressBookInfo, $this->backend, - $this->urlGenerator + $this->urlGenerator, + $this->propertyMapper, + null ); $this->assertTrue($addressBookImpl->isSystemAddressBook()); @@ -518,7 +504,9 @@ class AddressBookImplTest extends TestCase { $this->addressBook, $addressBookInfo, $this->backend, - $this->urlGenerator + $this->urlGenerator, + $this->propertyMapper, + 'user2' ); $this->assertFalse($addressBookImpl->isSystemAddressBook()); @@ -538,7 +526,9 @@ class AddressBookImplTest extends TestCase { $this->addressBook, $addressBookInfo, $this->backend, - $this->urlGenerator + $this->urlGenerator, + $this->propertyMapper, + 'user2' ); $this->assertFalse($addressBookImpl->isSystemAddressBook()); diff --git a/apps/dav/tests/unit/CardDAV/AddressBookTest.php b/apps/dav/tests/unit/CardDAV/AddressBookTest.php index cddbd5164dc..cf28b7b8a8e 100644 --- a/apps/dav/tests/unit/CardDAV/AddressBookTest.php +++ b/apps/dav/tests/unit/CardDAV/AddressBookTest.php @@ -1,120 +1,144 @@ <?php + +declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @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\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 testDelete() { - /** @var \PHPUnit\Framework\MockObject\MockObject | CardDavBackend $backend */ - $backend = $this->getMockBuilder(CardDavBackend::class)->disableOriginalConstructor()->getMock(); + 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'] ]); - $calendarInfo = [ + $addressBookInfo = [ '{http://owncloud.org/ns}owner-principal' => 'user1', '{DAV:}displayname' => 'Test address book', 'principaluri' => 'user2', 'id' => 666, 'uri' => 'default', ]; - $l = $this->createMock(IL10N::class); - $c = new AddressBook($backend, $calendarInfo, $l); - $c->delete(); + $l10n = $this->createMock(IL10N::class); + $logger = $this->createMock(LoggerInterface::class); + $addressBook = new AddressBook($backend, $addressBookInfo, $l10n); + $addressBook->delete(); } - public function testDeleteFromGroup() { - $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + public function testDeleteFromGroup(): void { + $this->expectException(Forbidden::class); - /** @var \PHPUnit\Framework\MockObject\MockObject | CardDavBackend $backend */ - $backend = $this->getMockBuilder(CardDavBackend::class)->disableOriginalConstructor()->getMock(); + /** @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'] ]); - $calendarInfo = [ + $addressBookInfo = [ '{http://owncloud.org/ns}owner-principal' => 'user1', '{DAV:}displayname' => 'Test address book', 'principaluri' => 'user2', 'id' => 666, 'uri' => 'default', ]; - $l = $this->createMock(IL10N::class); - $c = new AddressBook($backend, $calendarInfo, $l); - $c->delete(); + $l10n = $this->createMock(IL10N::class); + $logger = $this->createMock(LoggerInterface::class); + $addressBook = new AddressBook($backend, $addressBookInfo, $l10n); + $addressBook->delete(); } - public function testPropPatch() { - $this->expectException(\Sabre\DAV\Exception\Forbidden::class); - - /** @var \PHPUnit\Framework\MockObject\MockObject | CardDavBackend $backend */ - $backend = $this->getMockBuilder(CardDavBackend::class)->disableOriginalConstructor()->getMock(); - $calendarInfo = [ + 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', ]; - $l = $this->createMock(IL10N::class); - $c = new AddressBook($backend, $calendarInfo, $l); - $c->propPatch(new PropPatch([])); + $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'])); } - /** - * @dataProvider providesReadOnlyInfo - */ - public function testAcl($expectsWrite, $readOnlyValue, $hasOwnerSet) { - /** @var \PHPUnit\Framework\MockObject\MockObject | CardDavBackend $backend */ - $backend = $this->getMockBuilder(CardDavBackend::class)->disableOriginalConstructor()->getMock(); + 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); - $calendarInfo = [ + $addressBookInfo = [ '{DAV:}displayname' => 'Test address book', 'principaluri' => 'user2', 'id' => 666, 'uri' => 'default' ]; if (!is_null($readOnlyValue)) { - $calendarInfo['{http://owncloud.org/ns}read-only'] = $readOnlyValue; + $addressBookInfo['{http://owncloud.org/ns}read-only'] = $readOnlyValue; } if ($hasOwnerSet) { - $calendarInfo['{http://owncloud.org/ns}owner-principal'] = 'user1'; + $addressBookInfo['{http://owncloud.org/ns}owner-principal'] = 'user1'; } - $l = $this->createMock(IL10N::class); - $c = new AddressBook($backend, $calendarInfo, $l); - $acl = $c->getACL(); - $childAcl = $c->getChildACL(); + $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', @@ -124,6 +148,10 @@ class AddressBookTest extends TestCase { 'privilege' => '{DAV:}write', 'principal' => $hasOwnerSet ? 'user1' : 'user2', 'protected' => true + ], [ + 'privilege' => '{DAV:}write-properties', + 'principal' => $hasOwnerSet ? 'user1' : 'user2', + 'protected' => true ]]; if ($hasOwnerSet) { $expectedAcl[] = [ @@ -143,7 +171,7 @@ class AddressBookTest extends TestCase { $this->assertEquals($expectedAcl, $childAcl); } - public function providesReadOnlyInfo() { + public static function providesReadOnlyInfo(): array { return [ 'read-only property not set' => [true, null, true], 'read-only property is false' => [true, false, true], diff --git a/apps/dav/tests/unit/CardDAV/BirthdayServiceTest.php b/apps/dav/tests/unit/CardDAV/BirthdayServiceTest.php index 5d1323ba8b9..6908dfd17bc 100644 --- a/apps/dav/tests/unit/CardDAV/BirthdayServiceTest.php +++ b/apps/dav/tests/unit/CardDAV/BirthdayServiceTest.php @@ -1,30 +1,11 @@ <?php + +declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @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\CardDAV; use OCA\DAV\CalDAV\BirthdayService; @@ -34,26 +15,19 @@ 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 { - - /** @var BirthdayService */ - private $service; - /** @var CalDavBackend | \PHPUnit\Framework\MockObject\MockObject */ - private $calDav; - /** @var CardDavBackend | \PHPUnit\Framework\MockObject\MockObject */ - private $cardDav; - /** @var GroupPrincipalBackend | \PHPUnit\Framework\MockObject\MockObject */ - private $groupPrincipalBackend; - /** @var IConfig | \PHPUnit\Framework\MockObject\MockObject */ - private $config; - /** @var IDBConnection | \PHPUnit\Framework\MockObject\MockObject */ - private $dbConnection; - /** @var IL10N | \PHPUnit\Framework\MockObject\MockObject */ - private $l10n; + 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(); @@ -71,23 +45,15 @@ class BirthdayServiceTest extends TestCase { return vsprintf($string, $args); }); - $this->service = new BirthdayService($this->calDav,$this->cardDav, + $this->service = new BirthdayService($this->calDav, $this->cardDav, $this->groupPrincipalBackend, $this->config, $this->dbConnection, $this->l10n); } - /** - * @dataProvider providesVCards - * @param string $expectedSummary - * @param string $expectedDTStart - * @param string $expectedFieldType - * @param string $expectedUnknownYear - * @param string $expectedOriginalYear - * @param string | null $data - */ - public function testBuildBirthdayFromContact($expectedSummary, $expectedDTStart, $expectedFieldType, $expectedUnknownYear, $expectedOriginalYear, $data, $fieldType, $prefix, $supports4Bytes) { + #[\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); + $cal = $this->service->buildDateFromContact($data, $fieldType, $prefix, $configuredReminder); if ($expectedSummary === null) { $this->assertNull($cal); @@ -95,7 +61,7 @@ class BirthdayServiceTest extends TestCase { $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('FREQ=YEARLY', $cal->VEVENT->RRULE->getValue()); + $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()); @@ -105,11 +71,16 @@ class BirthdayServiceTest extends TestCase { $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() { + public function testOnCardDeleteGloballyDisabled(): void { $this->config->expects($this->once()) ->method('getAppValue') ->with('dav', 'generateBirthdayCalendar', 'yes') @@ -120,7 +91,7 @@ class BirthdayServiceTest extends TestCase { $this->service->onCardDeleted(666, 'gump.vcf'); } - public function testOnCardDeleteUserDisabled() { + public function testOnCardDeleteUserDisabled(): void { $this->config->expects($this->once()) ->method('getAppValue') ->with('dav', 'generateBirthdayCalendar', 'yes') @@ -144,7 +115,7 @@ class BirthdayServiceTest extends TestCase { $this->service->onCardDeleted(666, 'gump.vcf'); } - public function testOnCardDeleted() { + public function testOnCardDeleted(): void { $this->config->expects($this->once()) ->method('getAppValue') ->with('dav', 'generateBirthdayCalendar', 'yes') @@ -166,15 +137,23 @@ class BirthdayServiceTest extends TestCase { ->willReturn([ 'id' => 1234 ]); - $this->calDav->expects($this->at(1))->method('deleteCalendarObject')->with(1234, 'default-gump.vcf.ics'); - $this->calDav->expects($this->at(2))->method('deleteCalendarObject')->with(1234, 'default-gump.vcf-death.ics'); - $this->calDav->expects($this->at(3))->method('deleteCalendarObject')->with(1234, 'default-gump.vcf-anniversary.ics'); + $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() { + public function testOnCardChangedGloballyDisabled(): void { $this->config->expects($this->once()) ->method('getAppValue') ->with('dav', 'generateBirthdayCalendar', 'yes') @@ -183,14 +162,14 @@ class BirthdayServiceTest extends TestCase { $this->cardDav->expects($this->never())->method('getAddressBookById'); $service = $this->getMockBuilder(BirthdayService::class) - ->setMethods(['buildDateFromContact', 'birthdayEvenChanged']) + ->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() { + public function testOnCardChangedUserDisabled(): void { $this->config->expects($this->once()) ->method('getAppValue') ->with('dav', 'generateBirthdayCalendar', 'yes') @@ -210,28 +189,28 @@ class BirthdayServiceTest extends TestCase { $this->cardDav->expects($this->once())->method('getShares')->willReturn([]); $this->calDav->expects($this->never())->method('getCalendarByUri'); - /** @var BirthdayService | \PHPUnit\Framework\MockObject\MockObject $service */ + /** @var BirthdayService&MockObject $service */ $service = $this->getMockBuilder(BirthdayService::class) - ->setMethods(['buildDateFromContact', 'birthdayEvenChanged']) + ->onlyMethods(['buildDateFromContact', 'birthdayEvenChanged']) ->setConstructorArgs([$this->calDav, $this->cardDav, $this->groupPrincipalBackend, $this->config, $this->dbConnection, $this->l10n]) ->getMock(); $service->onCardChanged(666, 'gump.vcf', ''); } - /** - * @dataProvider providesCardChanges - */ - public function testOnCardChanged($expectedOp) { + #[\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->once()) + $this->config->expects($this->exactly(2)) ->method('getUserValue') - ->with('user01', 'dav', 'generateBirthdayCalendar', 'yes') - ->willReturn('yes'); + ->willReturnMap([ + ['user01', 'dav', 'generateBirthdayCalendar', 'yes', 'yes'], + ['user01', 'dav', 'birthdayCalendarReminderOffset', 'PT9H', 'PT9H'], + ]); $this->cardDav->expects($this->once())->method('getAddressBookById') ->with(666) @@ -246,31 +225,45 @@ class BirthdayServiceTest extends TestCase { ]); $this->cardDav->expects($this->once())->method('getShares')->willReturn([]); - /** @var BirthdayService | \PHPUnit\Framework\MockObject\MockObject $service */ + /** @var BirthdayService&MockObject $service */ $service = $this->getMockBuilder(BirthdayService::class) - ->setMethods(['buildDateFromContact', 'birthdayEvenChanged']) + ->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); - $this->calDav->expects($this->exactly(3))->method('deleteCalendarObject')->withConsecutive( + + $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); - $this->calDav->expects($this->exactly(3))->method('createCalendarObject')->withConsecutive( + + $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(); @@ -279,28 +272,30 @@ class BirthdayServiceTest extends TestCase { $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' => '']); - $this->calDav->expects($this->exactly(3))->method('updateCalendarObject')->withConsecutive( + + $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', ''); } - /** - * @dataProvider providesBirthday - * @param $expected - * @param $old - * @param $new - */ - public function testBirthdayEvenChanged($expected, $old, $new) { + #[\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() { + public function testGetAllAffectedPrincipals(): void { $this->cardDav->expects($this->once())->method('getShares')->willReturn([ [ '{http://owncloud.org/ns}group-share' => false, @@ -339,7 +334,7 @@ class BirthdayServiceTest extends TestCase { ], $users); } - public function testBirthdayCalendarHasComponentEvent() { + public function testBirthdayCalendarHasComponentEvent(): void { $this->calDav->expects($this->once()) ->method('createCalendar') ->with('principal001', 'contact_birthdays', [ @@ -350,33 +345,33 @@ class BirthdayServiceTest extends TestCase { $this->service->ensureCalendarExists('principal001'); } - public function testResetForUser() { - $this->calDav->expects($this->at(0)) + public function testResetForUser(): void { + $this->calDav->expects($this->once()) ->method('getCalendarByUri') ->with('principals/users/user123', 'contact_birthdays') ->willReturn(['id' => 42]); - $this->calDav->expects($this->at(1)) + $this->calDav->expects($this->once()) ->method('getCalendarObjects') ->with(42, 0) ->willReturn([['uri' => '1.ics'], ['uri' => '2.ics'], ['uri' => '3.ics']]); - $this->calDav->expects($this->at(2)) - ->method('deleteCalendarObject') - ->with(42, '1.ics', 0); - - $this->calDav->expects($this->at(3)) - ->method('deleteCalendarObject') - ->with(42, '2.ics', 0); - - $this->calDav->expects($this->at(4)) + $calls = [ + [42, '1.ics', 0], + [42, '2.ics', 0], + [42, '3.ics', 0], + ]; + $this->calDav->expects($this->exactly(count($calls))) ->method('deleteCalendarObject') - ->with(42, '3.ics', 0); + ->willReturnCallback(function ($calendarId, $objectUri, $calendarType) use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, [$calendarId, $objectUri, $calendarType]); + }); $this->service->resetForUser('user123'); } - public function providesBirthday() { + public static function providesBirthday(): array { return [ [true, '', @@ -393,7 +388,7 @@ class BirthdayServiceTest extends TestCase { ]; } - public function providesCardChanges() { + public static function providesCardChanges(): array { return[ ['delete'], ['create'], @@ -401,31 +396,38 @@ class BirthdayServiceTest extends TestCase { ]; } - public function providesVCards() { + public static function providesVCards(): array { return [ - // $expectedSummary, $expectedDTStart, $expectedFieldType, $expectedUnknownYear, $expectedOriginalYear, $data, $fieldType, $prefix, $supports4Byte - [null, null, null, null, null, 'yasfewf', '', '', true], - [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, "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, "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], - ['🎂 12345 (1900)', '19700101', 'BDAY', '0', '1900', "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], - ['🎂 12345 (1900)', '19701231', 'BDAY', '0', '1900', "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], - ['Death of 12345 (1900)', '19701231', 'DEATHDATE', '0', '1900', "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], - ['Death of 12345 (1900)', '19701231', 'DEATHDATE', '0', '1900', "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], - ['💍 12345 (1900)', '19701231', 'ANNIVERSARY', '0', '1900', "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], - ['12345 (⚭1900)', '19701231', 'ANNIVERSARY', '0', '1900', "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], - ['🎂 12345', '19701231', 'BDAY', '1', 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], - ['🎂 12345', '19701231', 'BDAY', '1', 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, "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, "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], - ['🎂 12345 (900)', '19701231', 'BDAY', '0', '900', "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], - ['12345 (*1900)', '19700101', 'BDAY', '0', '1900', "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], - ['12345 (*1900)', '19701231', 'BDAY', '0', '1900', "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], - ['12345 *', '19701231', 'BDAY', '1', 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], - ['12345 *', '19701231', 'BDAY', '1', 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, "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, "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], - ['12345 (*900)', '19701231', 'BDAY', '0', '900', "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], + // $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 index a8c7a781724..c5eafa0764a 100644 --- a/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php +++ b/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php @@ -1,59 +1,42 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arne Hamann <kontakt+github@arne.email> - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @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\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 Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\EventDispatcher\GenericEvent; use Test\TestCase; +use function time; /** * Class CardDavBackendTest @@ -63,85 +46,72 @@ use Test\TestCase; * @package OCA\DAV\Tests\unit\CardDAV */ class CardDavBackendTest extends TestCase { - - /** @var CardDavBackend */ - private $backend; - - /** @var Principal | \PHPUnit\Framework\MockObject\MockObject */ - private $principal; - - /** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */ - private $userManager; - - /** @var IGroupManager|\PHPUnit\Framework\MockObject\MockObject */ - private $groupManager; - - /** @var EventDispatcherInterface|\PHPUnit\Framework\MockObject\MockObject */ - private $legacyDispatcher; - - /** @var IEventDispatcher|\PHPUnit\Framework\MockObject\MockObject */ - private $dispatcher; - - /** @var IDBConnection */ - private $db; - - /** @var string */ - private $dbCardsTable = 'cards'; - - /** @var string */ - private $dbCardsPropertiesTable = 'cards_properties'; + 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'; + 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(IConfig::class), + $this->createMock(KnownUserService::class), + $this->config, + $this->createMock(IFactory::class) ]) - ->setMethods(['getPrincipalByPath', 'getGroupMembership']) + ->onlyMethods(['getPrincipalByPath', 'getGroupMembership', 'findByUri']) ->getMock(); $this->principal->method('getPrincipalByPath') ->willReturn([ @@ -152,23 +122,39 @@ class CardDavBackendTest extends TestCase { ->withAnyParameters() ->willReturn([self::UNIT_TEST_GROUP]); $this->dispatcher = $this->createMock(IEventDispatcher::class); - $this->legacyDispatcher = $this->createMock(EventDispatcherInterface::class); - - $this->db = \OC::$server->getDatabaseConnection(); - $this->backend = new CardDavBackend($this->db, $this->principal, $this->userManager, $this->groupManager, $this->dispatcher, $this->legacyDispatcher); + $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')->execute(); + $query->delete('cards_properties')->executeStatement(); $query = $this->db->getQueryBuilder(); - $query->delete('cards')->execute(); + $query->delete('cards')->executeStatement(); - $this->tearDown(); + $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 { - parent::tearDown(); - if (is_null($this->backend)) { return; } @@ -180,10 +166,11 @@ class CardDavBackendTest extends TestCase { foreach ($books as $book) { $this->backend->deleteAddressBook($book['id']); } - } - public function testAddressBookOperations() { + parent::tearDown(); + } + public function testAddressBookOperations(): void { // create a new address book $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); @@ -193,7 +180,7 @@ class CardDavBackendTest extends TestCase { $this->assertEquals('Example', $books[0]['{DAV:}displayname']); $this->assertEquals('User\'s displayname', $books[0]['{http://nextcloud.com/ns}owner-displayname']); - // update it's display name + // update its display name $patch = new PropPatch([ '{DAV:}displayname' => 'Unit test', '{urn:ietf:params:xml:ns:carddav}addressbook-description' => 'Addressbook used for unit testing' @@ -211,14 +198,16 @@ class CardDavBackendTest extends TestCase { $this->assertEquals(0, count($books)); } - public function testAddressBookSharing() { + 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); @@ -242,12 +231,12 @@ class CardDavBackendTest extends TestCase { $this->assertEquals(0, count($books)); } - public function testCardOperations() { - - /** @var CardDavBackend | \PHPUnit\Framework\MockObject\MockObject $backend */ + public function testCardOperations(): void { + /** @var CardDavBackend&MockObject $backend */ $backend = $this->getMockBuilder(CardDavBackend::class) - ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->groupManager, $this->dispatcher, $this->legacyDispatcher]) - ->setMethods(['updateProperties', 'purgeProperties'])->getMock(); + ->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', []); @@ -257,17 +246,21 @@ class CardDavBackendTest extends TestCase { $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, $this->vcardTest0); - $backend->expects($this->at(1))->method('updateProperties')->with($bookId, $uri, $this->vcardTest1); + $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->legacyDispatcher->expects($this->at(0)) - ->method('dispatch') - ->with('\OCA\DAV\CardDAV\CardDavBackend::createCard', $this->callback(function (GenericEvent $e) use ($bookId, $uri) { - return $e->getArgument('addressBookId') === $bookId && - $e->getArgument('cardUri') === $uri && - $e->getArgument('cardData') === $this->vcardTest0; - })); + $this->dispatcher + ->expects($this->exactly(3)) + ->method('dispatchTyped'); // create a card $backend->createCard($bookId, $uri, $this->vcardTest0); @@ -287,28 +280,11 @@ class CardDavBackendTest extends TestCase { $this->assertArrayHasKey('size', $card); $this->assertEquals($this->vcardTest0, $card['carddata']); - // Expect event - $this->legacyDispatcher->expects($this->at(0)) - ->method('dispatch') - ->with('\OCA\DAV\CardDAV\CardDavBackend::updateCard', $this->callback(function (GenericEvent $e) use ($bookId, $uri) { - return $e->getArgument('addressBookId') === $bookId && - $e->getArgument('cardUri') === $uri && - $e->getArgument('cardData') === $this->vcardTest1; - })); - // update the card $backend->updateCard($bookId, $uri, $this->vcardTest1); $card = $backend->getCard($bookId, $uri); $this->assertEquals($this->vcardTest1, $card['carddata']); - // Expect event - $this->legacyDispatcher->expects($this->at(0)) - ->method('dispatch') - ->with('\OCA\DAV\CardDAV\CardDavBackend::deleteCard', $this->callback(function (GenericEvent $e) use ($bookId, $uri) { - return $e->getArgument('addressBookId') === $bookId && - $e->getArgument('cardUri') === $uri; - })); - // delete the card $backend->expects($this->once())->method('purgeProperties')->with($bookId, $card['id']); $backend->deleteCard($bookId, $uri); @@ -316,10 +292,11 @@ class CardDavBackendTest extends TestCase { $this->assertEquals(0, count($cards)); } - public function testMultiCard() { + public function testMultiCard(): void { $this->backend = $this->getMockBuilder(CardDavBackend::class) - ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->groupManager, $this->dispatcher, $this->legacyDispatcher]) - ->setMethods(['updateProperties'])->getMock(); + ->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', []); @@ -358,7 +335,7 @@ class CardDavBackendTest extends TestCase { $this->assertArrayHasKey('lastmodified', $card); $this->assertArrayHasKey('etag', $card); $this->assertArrayHasKey('size', $card); - $this->assertEquals($this->{ 'vcardTest'.($index + 1) }, $card['carddata']); + $this->assertEquals($this->{ 'vcardTest' . ($index + 1) }, $card['carddata']); } // delete the card @@ -369,10 +346,11 @@ class CardDavBackendTest extends TestCase { $this->assertEquals(0, count($cards)); } - public function testMultipleUIDOnDifferentAddressbooks() { + public function testMultipleUIDOnDifferentAddressbooks(): void { $this->backend = $this->getMockBuilder(CardDavBackend::class) - ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->groupManager, $this->dispatcher, $this->legacyDispatcher]) - ->setMethods(['updateProperties'])->getMock(); + ->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', []); @@ -391,10 +369,11 @@ class CardDavBackendTest extends TestCase { $this->backend->createCard($bookId1, $uri1, $this->vcardTest0); } - public function testMultipleUIDDenied() { + public function testMultipleUIDDenied(): void { $this->backend = $this->getMockBuilder(CardDavBackend::class) - ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->groupManager, $this->dispatcher, $this->legacyDispatcher]) - ->setMethods(['updateProperties'])->getMock(); + ->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', []); @@ -412,10 +391,11 @@ class CardDavBackendTest extends TestCase { $test = $this->backend->createCard($bookId, $uri1, $this->vcardTest0); } - public function testNoValidUID() { + public function testNoValidUID(): void { $this->backend = $this->getMockBuilder(CardDavBackend::class) - ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->groupManager, $this->dispatcher, $this->legacyDispatcher]) - ->setMethods(['updateProperties'])->getMock(); + ->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', []); @@ -429,10 +409,10 @@ class CardDavBackendTest extends TestCase { $test = $this->backend->createCard($bookId, $uri1, $this->vcardTestNoUID); } - public function testDeleteWithoutCard() { + public function testDeleteWithoutCard(): void { $this->backend = $this->getMockBuilder(CardDavBackend::class) - ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->groupManager, $this->dispatcher, $this->legacyDispatcher]) - ->setMethods([ + ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend, $this->config]) + ->onlyMethods([ 'getCardId', 'addChange', 'purgeProperties', @@ -453,12 +433,17 @@ class CardDavBackendTest extends TestCase { ->method('getCardId') ->with($bookId, $uri) ->willThrowException(new \InvalidArgumentException()); - $this->backend->expects($this->exactly(2)) + + $calls = [ + [$bookId, $uri, 1], + [$bookId, $uri, 3], + ]; + $this->backend->expects($this->exactly(count($calls))) ->method('addChange') - ->withConsecutive( - [$bookId, $uri, 1], - [$bookId, $uri, 3] - ); + ->willReturnCallback(function () use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + }); $this->backend->expects($this->never()) ->method('purgeProperties'); @@ -469,10 +454,11 @@ class CardDavBackendTest extends TestCase { $this->assertTrue($this->backend->deleteCard($bookId, $uri)); } - public function testSyncSupport() { + public function testSyncSupport(): void { $this->backend = $this->getMockBuilder(CardDavBackend::class) - ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->groupManager, $this->dispatcher, $this->legacyDispatcher]) - ->setMethods(['updateProperties'])->getMock(); + ->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', []); @@ -493,14 +479,16 @@ class CardDavBackendTest extends TestCase { $this->assertEquals($uri0, $changes['added'][0]); } - public function testSharing() { + 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); @@ -531,14 +519,14 @@ class CardDavBackendTest extends TestCase { $this->assertEquals(0, count($books)); } - public function testUpdateProperties() { + public function testUpdateProperties(): void { $bookId = 42; $cardUri = 'card-uri'; $cardId = 2; $backend = $this->getMockBuilder(CardDavBackend::class) - ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->groupManager, $this->dispatcher, $this->legacyDispatcher]) - ->setMethods(['getCardId'])->getMock(); + ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend, $this->config]) + ->onlyMethods(['getCardId'])->getMock(); $backend->expects($this->any())->method('getCardId')->willReturn($cardId); @@ -550,7 +538,8 @@ class CardDavBackendTest extends TestCase { $query = $this->db->getQueryBuilder(); $query->select('*') - ->from('cards_properties'); + ->from('cards_properties') + ->orderBy('name'); $qResult = $query->execute(); $result = $qResult->fetchAll(); @@ -558,13 +547,13 @@ class CardDavBackendTest extends TestCase { $this->assertSame(2, count($result)); - $this->assertSame('UID', $result[0]['name']); - $this->assertSame($cardUri, $result[0]['value']); + $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('FN', $result[1]['name']); - $this->assertSame('John Doe', $result[1]['value']); + $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']); @@ -589,7 +578,7 @@ class CardDavBackendTest extends TestCase { $this->assertSame($cardId, (int)$result[0]['cardid']); } - public function testPurgeProperties() { + public function testPurgeProperties(): void { $query = $this->db->getQueryBuilder(); $query->insert('cards_properties') ->values( @@ -627,11 +616,11 @@ class CardDavBackendTest extends TestCase { $qResult->closeCursor(); $this->assertSame(1, count($result)); - $this->assertSame(1 ,(int)$result[0]['addressbookid']); - $this->assertSame(2 ,(int)$result[0]['cardid']); + $this->assertSame(1, (int)$result[0]['addressbookid']); + $this->assertSame(2, (int)$result[0]['cardid']); } - public function testGetCardId() { + public function testGetCardId(): void { $query = $this->db->getQueryBuilder(); $query->insert('cards') @@ -653,21 +642,14 @@ class CardDavBackendTest extends TestCase { } - public function testGetCardIdFailed() { + public function testGetCardIdFailed(): void { $this->expectException(\InvalidArgumentException::class); $this->invokePrivate($this->backend, 'getCardId', [1, 'uri']); } - /** - * @dataProvider dataTestSearch - * - * @param string $pattern - * @param array $properties - * @param array $options - * @param array $expected - */ - public function testSearch($pattern, $properties, $options, $expected) { + #[\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(); @@ -686,20 +668,21 @@ class CardDavBackendTest extends TestCase { $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), - ] - ); + ->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( [ @@ -711,6 +694,7 @@ class CardDavBackendTest extends TestCase { ] ); $query->execute(); + $query = $this->db->getQueryBuilder(); $query->insert($this->dbCardsPropertiesTable) ->values( [ @@ -722,6 +706,7 @@ class CardDavBackendTest extends TestCase { ] ); $query->execute(); + $query = $this->db->getQueryBuilder(); $query->insert($this->dbCardsPropertiesTable) ->values( [ @@ -733,6 +718,7 @@ class CardDavBackendTest extends TestCase { ] ); $query->execute(); + $query = $this->db->getQueryBuilder(); $query->insert($this->dbCardsPropertiesTable) ->values( [ @@ -744,6 +730,7 @@ class CardDavBackendTest extends TestCase { ] ); $query->execute(); + $query = $this->db->getQueryBuilder(); $query->insert($this->dbCardsPropertiesTable) ->values( [ @@ -773,7 +760,7 @@ class CardDavBackendTest extends TestCase { $this->assertSame(count($expected), count($found)); } - public function dataTestSearch() { + public static function dataTestSearch(): array { return [ ['John', ['FN'], [], [['uri0', 'John Doe'], ['uri1', 'John M. Doe']]], ['M. Doe', ['FN'], [], [['uri1', 'John M. Doe']]], @@ -787,19 +774,19 @@ class CardDavBackendTest extends TestCase { ]; } - public function testGetCardUri() { + 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), - ] - ); + ->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(); @@ -808,26 +795,26 @@ class CardDavBackendTest extends TestCase { } - public function testGetCardUriFailed() { + public function testGetCardUriFailed(): void { $this->expectException(\InvalidArgumentException::class); $this->backend->getCardUri(1); } - public function testGetContact() { + 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), - ] - ); + ->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(); } @@ -845,11 +832,11 @@ class CardDavBackendTest extends TestCase { $this->assertEmpty($result); } - public function testGetContactFail() { + public function testGetContactFail(): void { $this->assertEmpty($this->backend->getContact(0, 'uri')); } - public function testCollectCardProperties() { + public function testCollectCardProperties(): void { $query = $this->db->getQueryBuilder(); $query->insert($this->dbCardsPropertiesTable) ->values( @@ -861,9 +848,71 @@ class CardDavBackendTest extends TestCase { 'preferred' => $query->createNamedParameter(0) ] ) - ->execute(); + ->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 index 63c1413eec0..bdd826f671b 100644 --- a/apps/dav/tests/unit/CardDAV/ContactsManagerTest.php +++ b/apps/dav/tests/unit/CardDAV/ContactsManagerTest.php @@ -1,52 +1,37 @@ <?php + +declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @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\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() { - /** @var IManager | \PHPUnit\Framework\MockObject\MockObject $cm */ - $cm = $this->getMockBuilder(IManager::class)->disableOriginalConstructor()->getMock(); + public function test(): void { + /** @var IManager&MockObject $cm */ + $cm = $this->createMock(IManager::class); $cm->expects($this->exactly(2))->method('registerAddressBook'); - $urlGenerator = $this->getMockBuilder(IURLGenerator::class)->disableOriginalConstructor()->getMock(); - /** @var CardDavBackend | \PHPUnit\Framework\MockObject\MockObject $backEnd */ - $backEnd = $this->getMockBuilder(CardDavBackend::class)->disableOriginalConstructor()->getMock(); + $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); + $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 index aef5cf8ef1c..00519b82766 100644 --- a/apps/dav/tests/unit/CardDAV/ConverterTest.php +++ b/apps/dav/tests/unit/CardDAV/ConverterTest.php @@ -1,103 +1,90 @@ <?php + +declare(strict_types=1); + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @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\CardDAV; -use OC\Accounts\AccountManager; 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 { - - /** @var AccountManager|\PHPUnit\Framework\MockObject\MockObject */ - private $accountManager; + 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(AccountManager::class); + $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) { - $accountManager = $this->getMockBuilder(AccountManager::class) - ->disableOriginalConstructor()->getMock(); - $accountManager->expects($this->any())->method('getUser')->willReturn( - [ - IAccountManager::PROPERTY_DISPLAYNAME => - [ - 'value' => $user->getDisplayName(), - 'scope' => AccountManager::VISIBILITY_CONTACTS_ONLY, - ], - IAccountManager::PROPERTY_ADDRESS => - [ - 'value' => '', - 'scope' => AccountManager::VISIBILITY_PRIVATE, - ], - IAccountManager::PROPERTY_WEBSITE => - [ - 'value' => '', - 'scope' => AccountManager::VISIBILITY_PRIVATE, - ], - IAccountManager::PROPERTY_EMAIL => - [ - 'value' => $user->getEMailAddress(), - 'scope' => AccountManager::VISIBILITY_CONTACTS_ONLY, - ], - IAccountManager::PROPERTY_AVATAR => - [ - 'scope' => AccountManager::VISIBILITY_CONTACTS_ONLY - ], - IAccountManager::PROPERTY_PHONE => - [ - 'value' => '', - 'scope' => AccountManager::VISIBILITY_PRIVATE, - ], - IAccountManager::PROPERTY_TWITTER => - [ - 'value' => '', - 'scope' => AccountManager::VISIBILITY_PRIVATE, - ], - ] - ); + $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; } - /** - * @dataProvider providesNewUsers - */ - public function testCreation($expectedVCard, $displayName = null, $eMailAddress = null, $cloudId = null) { - $user = $this->getUserMock($displayName, $eMailAddress, $cloudId); + #[\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); + $converter = new Converter($accountManager, $this->userManager, $this->urlGenerator, $this->logger); $vCard = $converter->createCardFromUser($user); if ($expectedVCard !== null) { $this->assertInstanceOf('Sabre\VObject\Component\VCard', $vCard); @@ -108,7 +95,30 @@ class ConverterTest extends TestCase { } } - protected function compareData($expected, $data) { + 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) { @@ -123,7 +133,7 @@ class ConverterTest extends TestCase { } } - public function providesNewUsers() { + public static function providesNewUsers(): array { return [ [ null @@ -150,8 +160,8 @@ class ConverterTest extends TestCase { 'fn' => 'Dr. Foo Bar', 'photo' => 'MTIzNDU2Nzg5', ], - "Dr. Foo Bar", - "foo@bar.net", + 'Dr. Foo Bar', + 'foo@bar.net', 'foo@cloud.net' ], [ @@ -160,9 +170,9 @@ class ConverterTest extends TestCase { 'fn' => 'Dr. Foo Bar', 'photo' => 'MTIzNDU2Nzg5', ], - "Dr. Foo Bar", + 'Dr. Foo Bar', null, - "foo@cloud.net" + 'foo@cloud.net' ], [ [ @@ -177,19 +187,15 @@ class ConverterTest extends TestCase { ]; } - /** - * @dataProvider providesNames - * @param $expected - * @param $fullName - */ - public function testNameSplitter($expected, $fullName) { - $converter = new Converter($this->accountManager); + #[\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 function providesNames() { + public static function providesNames(): array { return [ ['Sauron;;;;', 'Sauron'], ['Baggins;Bilbo;;;', 'Bilbo Baggins'], @@ -198,16 +204,13 @@ class ConverterTest extends TestCase { } /** - * @param $displayName - * @param $eMailAddress - * @param $cloudId - * @return IUser | \PHPUnit\Framework\MockObject\MockObject + * @return IUser&MockObject */ - protected function getUserMock($displayName, $eMailAddress, $cloudId) { - $image0 = $this->getMockBuilder(IImage::class)->disableOriginalConstructor()->getMock(); + 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->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock(); + $user = $this->createMock(IUser::class); $user->method('getUID')->willReturn('12345'); $user->method('getDisplayName')->willReturn($displayName); $user->method('getEMailAddress')->willReturn($eMailAddress); diff --git a/apps/dav/tests/unit/CardDAV/ImageExportPluginTest.php b/apps/dav/tests/unit/CardDAV/ImageExportPluginTest.php index 47606a45241..d47f53bddcd 100644 --- a/apps/dav/tests/unit/CardDAV/ImageExportPluginTest.php +++ b/apps/dav/tests/unit/CardDAV/ImageExportPluginTest.php @@ -1,36 +1,20 @@ <?php + +declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Jacob Neplokh <me@jacobneplokh.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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\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; @@ -40,19 +24,12 @@ use Sabre\HTTP\ResponseInterface; use Test\TestCase; class ImageExportPluginTest extends TestCase { - - /** @var ResponseInterface|\PHPUnit\Framework\MockObject\MockObject */ - private $response; - /** @var RequestInterface|\PHPUnit\Framework\MockObject\MockObject */ - private $request; - /** @var ImageExportPlugin|\PHPUnit\Framework\MockObject\MockObject */ - private $plugin; - /** @var Server */ - private $server; - /** @var Tree|\PHPUnit\Framework\MockObject\MockObject */ - private $tree; - /** @var PhotoCache|\PHPUnit\Framework\MockObject\MockObject */ - private $cache; + 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(); @@ -64,24 +41,18 @@ class ImageExportPluginTest extends TestCase { $this->server->tree = $this->tree; $this->cache = $this->createMock(PhotoCache::class); - $this->plugin = $this->getMockBuilder(ImageExportPlugin::class) - ->setMethods(['getPhoto']) - ->setConstructorArgs([$this->cache]) - ->getMock(); + $this->plugin = new ImageExportPlugin($this->cache); $this->plugin->initialize($this->server); } - /** - * @dataProvider providesQueryParams - * @param $param - */ - public function testQueryParams($param) { + #[\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 function providesQueryParams() { + public static function providesQueryParams(): array { return [ [[]], [['1']], @@ -89,7 +60,7 @@ class ImageExportPluginTest extends TestCase { ]; } - public function testNoCard() { + public function testNoCard(): void { $this->request->method('getQueryParameters') ->willReturn([ 'photo' @@ -106,7 +77,7 @@ class ImageExportPluginTest extends TestCase { $this->assertTrue($result); } - public function dataTestCard() { + public static function dataTestCard(): array { return [ [null, false], [null, true], @@ -115,13 +86,8 @@ class ImageExportPluginTest extends TestCase { ]; } - /** - * @dataProvider dataTestCard - * - * @param $size - * @param bool $photo - */ - public function testCard($size, $photo) { + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestCard')] + public function testCard(?int $size, bool $photo): void { $query = ['photo' => null]; if ($size !== null) { $query['size'] = $size; @@ -151,16 +117,6 @@ class ImageExportPluginTest extends TestCase { $this->fail(); }); - $this->response->expects($this->at(0)) - ->method('setHeader') - ->with('Cache-Control', 'private, max-age=3600, must-revalidate'); - $this->response->expects($this->at(1)) - ->method('setHeader') - ->with('Etag', '"myEtag"'); - $this->response->expects($this->at(2)) - ->method('setHeader') - ->with('Pragma', 'public'); - $size = $size === null ? -1 : $size; if ($photo) { @@ -174,12 +130,18 @@ class ImageExportPluginTest extends TestCase { ->with(1, 'card', $size, $card) ->willReturn($file); - $this->response->expects($this->at(3)) - ->method('setHeader') - ->with('Content-Type', 'image/jpeg'); - $this->response->expects($this->at(4)) + $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') - ->with('Content-Disposition', 'attachment; filename=card.jpg'); + ->willReturnCallback(function () use (&$setHeaderCalls): void { + $expected = array_shift($setHeaderCalls); + $this->assertEquals($expected, func_get_args()); + }); $this->response->expects($this->once()) ->method('setStatus') @@ -188,12 +150,22 @@ class ImageExportPluginTest extends TestCase { ->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(404); + ->with(Http::STATUS_NO_CONTENT); } $result = $this->plugin->httpGet($this->request, $this->response); 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 index 472826ac2f7..1e934a69a53 100644 --- a/apps/dav/tests/unit/CardDAV/Sharing/PluginTest.php +++ b/apps/dav/tests/unit/CardDAV/Sharing/PluginTest.php @@ -1,30 +1,11 @@ <?php + +declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @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\CardDAV\Sharing; use OCA\DAV\Connector\Sabre\Auth; @@ -32,6 +13,7 @@ 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; @@ -39,36 +21,30 @@ 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; + private Plugin $plugin; + private Server $server; + private IShareable&MockObject $book; protected function setUp(): void { parent::setUp(); - /** @var Auth | \PHPUnit\Framework\MockObject\MockObject $authBackend */ - $authBackend = $this->getMockBuilder(Auth::class)->disableOriginalConstructor()->getMock(); - $authBackend->method('isDavAuthenticated')->willReturn(true); - - /** @var IRequest $request */ - $request = $this->getMockBuilder(IRequest::class)->disableOriginalConstructor()->getMock(); + $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->getMockBuilder(IShareable::class)->disableOriginalConstructor()->getMock(); - $this->book->method('getName')->willReturn('addressbook1.vcf'); + $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() { + public function testSharing(): void { $this->book->expects($this->once())->method('updateShares')->with([[ 'href' => 'principal:principals/admin', 'commonName' => null, diff --git a/apps/dav/tests/unit/CardDAV/SyncServiceTest.php b/apps/dav/tests/unit/CardDAV/SyncServiceTest.php index eb8186807c6..77caed336f4 100644 --- a/apps/dav/tests/unit/CardDAV/SyncServiceTest.php +++ b/apps/dav/tests/unit/CardDAV/SyncServiceTest.php @@ -1,112 +1,335 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@tcit.fr> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @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\CardDAV; -use OC\Accounts\AccountManager; +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\Accounts\IAccountManager; -use OCP\ILogger; +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 { - public function testEmptySync() { - $backend = $this->getBackendMock(0, 0, 0); - $ss = $this->getSyncServiceMock($backend, []); - $return = $ss->syncRemoteAddressBook('', 'system', 'system', '1234567890', null, '1', 'principals/system/system', []); - $this->assertEquals('sync-token-1', $return); + 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 testSyncWithNewElement() { - $backend = $this->getBackendMock(1, 0, 0); - $backend->method('getCard')->willReturn(false); + 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); - $ss = $this->getSyncServiceMock($backend, ['0' => [200 => '']]); - $return = $ss->syncRemoteAddressBook('', 'system', 'system', '1234567890', null, '1', 'principals/system/system', []); - $this->assertEquals('sync-token-1', $return); + $token = $this->service->syncRemoteAddressBook( + '', + 'system', + 'system', + '1234567890', + null, + '1', + 'principals/system/system', + [] + )[0]; + + $this->assertEquals('http://sabre.io/ns/sync/1', $token); } - public function testSyncWithUpdatedElement() { - $backend = $this->getBackendMock(0, 1, 0); - $backend->method('getCard')->willReturn(true); + 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>'; - $ss = $this->getSyncServiceMock($backend, ['0' => [200 => '']]); - $return = $ss->syncRemoteAddressBook('', 'system', 'system', '1234567890', null, '1', 'principals/system/system', []); - $this->assertEquals('sync-token-1', $return); + $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 testSyncWithDeletedElement() { - $backend = $this->getBackendMock(0, 0, 1); + 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]; - $ss = $this->getSyncServiceMock($backend, ['0' => [404 => '']]); - $return = $ss->syncRemoteAddressBook('', 'system', 'system', '1234567890', null, '1', 'principals/system/system', []); - $this->assertEquals('sync-token-1', $return); + $this->assertEquals('http://sabre.io/ns/sync/3', $token); } - public function testEnsureSystemAddressBookExists() { - /** @var CardDavBackend | \PHPUnit\Framework\MockObject\MockObject $backend */ - $backend = $this->getMockBuilder(CardDavBackend::class)->disableOriginalConstructor()->getMock(); + 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->at(0))->method('getAddressBooksByUri')->willReturn(null); - $backend->expects($this->at(1))->method('getAddressBooksByUri')->willReturn([]); - - /** @var IUserManager $userManager */ - $userManager = $this->getMockBuilder(IUserManager::class)->disableOriginalConstructor()->getMock(); - $logger = $this->getMockBuilder(ILogger::class)->disableOriginalConstructor()->getMock(); - $accountManager = $this->getMockBuilder(AccountManager::class)->disableOriginalConstructor()->getMock(); - $ss = new SyncService($backend, $userManager, $logger, $accountManager); + $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 function dataActivatedUsers() { + public static function dataActivatedUsers(): array { return [ [true, 1, 1, 1], [false, 0, 0, 3], ]; } - /** - * @dataProvider dataActivatedUsers - * - * @param boolean $activated - * @param integer $createCalls - * @param integer $updateCalls - * @param integer $deleteCalls - * @return void - */ - public function testUpdateAndDeleteUser($activated, $createCalls, $updateCalls, $deleteCalls) { - /** @var CardDavBackend | \PHPUnit\Framework\MockObject\MockObject $backend */ + #[\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(ILogger::class)->disableOriginalConstructor()->getMock(); + $logger = $this->getMockBuilder(LoggerInterface::class)->disableOriginalConstructor()->getMock(); $backend->expects($this->exactly($createCalls))->method('createCard'); $backend->expects($this->exactly($updateCalls))->method('updateCard'); @@ -120,57 +343,23 @@ class SyncServiceTest extends TestCase { ->with('principals/system/system', 'system') ->willReturn(['id' => -1]); - /** @var IUserManager | \PHPUnit\Framework\MockObject\MockObject $userManager */ - $userManager = $this->getMockBuilder(IUserManager::class)->disableOriginalConstructor()->getMock(); - - /** @var IUser | \PHPUnit\Framework\MockObject\MockObject $user */ - $user = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock(); + $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); - $accountManager = $this->getMockBuilder(AccountManager::class)->disableOriginalConstructor()->getMock(); - $accountManager->expects($this->any())->method('getUser') - ->willReturn([ - IAccountManager::PROPERTY_DISPLAYNAME => - [ - 'value' => $user->getDisplayName(), - 'scope' => AccountManager::VISIBILITY_CONTACTS_ONLY, - ], - IAccountManager::PROPERTY_ADDRESS => - [ - 'value' => '', - 'scope' => AccountManager::VISIBILITY_PRIVATE, - ], - IAccountManager::PROPERTY_WEBSITE => - [ - 'value' => '', - 'scope' => AccountManager::VISIBILITY_PRIVATE, - ], - IAccountManager::PROPERTY_EMAIL => - [ - 'value' => $user->getEMailAddress(), - 'scope' => AccountManager::VISIBILITY_CONTACTS_ONLY, - ], - IAccountManager::PROPERTY_AVATAR => - [ - 'scope' => AccountManager::VISIBILITY_CONTACTS_ONLY - ], - IAccountManager::PROPERTY_PHONE => - [ - 'value' => '', - 'scope' => AccountManager::VISIBILITY_PRIVATE, - ], - IAccountManager::PROPERTY_TWITTER => - [ - 'value' => '', - 'scope' => AccountManager::VISIBILITY_PRIVATE, - ], - ] - ); + $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, $logger, $accountManager); + $ss = new SyncService($backend, $userManager, $dbConnection, $logger, $converter, $clientService, $config); $ss->updateUser($user); $ss->updateUser($user); @@ -178,44 +367,114 @@ class SyncServiceTest extends TestCase { $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(CardDavBackend::class) - ->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; + 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', + [] + ); } - /** - * @param $backend - * @param $response - * @return SyncService|\PHPUnit\Framework\MockObject\MockObject - */ - private function getSyncServiceMock($backend, $response) { - $userManager = $this->getMockBuilder(IUserManager::class)->disableOriginalConstructor()->getMock(); - $logger = $this->getMockBuilder(ILogger::class)->disableOriginalConstructor()->getMock(); - $accountManager = $this->getMockBuilder(AccountManager::class)->disableOriginalConstructor()->getMock(); - /** @var SyncService | \PHPUnit\Framework\MockObject\MockObject $ss */ - $ss = $this->getMockBuilder(SyncService::class) - ->setMethods(['ensureSystemAddressBookExists', 'requestSyncReport', 'download', 'getCertPath']) - ->setConstructorArgs([$backend, $userManager, $logger, $accountManager]) - ->getMock(); - $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' => [] - ]); - $ss->method('getCertPath')->willReturn(''); - return $ss; + 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); + + } + +} |