diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php
index b42750c25e..bd63dee13b 100644
--- a/apps/dav/composer/composer/autoload_classmap.php
+++ b/apps/dav/composer/composer/autoload_classmap.php
@@ -210,6 +210,8 @@ return array(
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningNode.php',
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php',
'OCA\\DAV\\RootCollection' => $baseDir . '/../lib/RootCollection.php',
+ 'OCA\\DAV\\Search\\ContactsSearchProvider' => $baseDir . '/../lib/Search/ContactsSearchProvider.php',
+ 'OCA\\DAV\\Search\\ContactsSearchResultEntry' => $baseDir . '/../lib/Search/ContactsSearchResultEntry.php',
'OCA\\DAV\\Server' => $baseDir . '/../lib/Server.php',
'OCA\\DAV\\Settings\\CalDAVSettings' => $baseDir . '/../lib/Settings/CalDAVSettings.php',
'OCA\\DAV\\Storage\\PublicOwnerWrapper' => $baseDir . '/../lib/Storage/PublicOwnerWrapper.php',
diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php
index 2d579289f0..a664c86f5f 100644
--- a/apps/dav/composer/composer/autoload_static.php
+++ b/apps/dav/composer/composer/autoload_static.php
@@ -225,6 +225,8 @@ class ComposerStaticInitDAV
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningNode.php',
'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php',
'OCA\\DAV\\RootCollection' => __DIR__ . '/..' . '/../lib/RootCollection.php',
+ 'OCA\\DAV\\Search\\ContactsSearchProvider' => __DIR__ . '/..' . '/../lib/Search/ContactsSearchProvider.php',
+ 'OCA\\DAV\\Search\\ContactsSearchResultEntry' => __DIR__ . '/..' . '/../lib/Search/ContactsSearchResultEntry.php',
'OCA\\DAV\\Server' => __DIR__ . '/..' . '/../lib/Server.php',
'OCA\\DAV\\Settings\\CalDAVSettings' => __DIR__ . '/..' . '/../lib/Settings/CalDAVSettings.php',
'OCA\\DAV\\Storage\\PublicOwnerWrapper' => __DIR__ . '/..' . '/../lib/Storage/PublicOwnerWrapper.php',
diff --git a/apps/dav/lib/AppInfo/Application.php b/apps/dav/lib/AppInfo/Application.php
index bf1e614633..6f2f7b2915 100644
--- a/apps/dav/lib/AppInfo/Application.php
+++ b/apps/dav/lib/AppInfo/Application.php
@@ -54,6 +54,7 @@ use OCA\DAV\CardDAV\ContactsManager;
use OCA\DAV\CardDAV\PhotoCache;
use OCA\DAV\CardDAV\SyncService;
use OCA\DAV\HookManager;
+use OCA\DAV\Search\ContactsSearchProvider;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
@@ -96,6 +97,11 @@ class Application extends App implements IBootstrap {
* Register capabilities
*/
$context->registerCapability(Capabilities::class);
+
+ /*
+ * Register Search Providers
+ */
+ $context->registerSearchProvider(ContactsSearchProvider::class);
}
public function boot(IBootContext $context): void {
diff --git a/apps/dav/lib/CardDAV/CardDavBackend.php b/apps/dav/lib/CardDAV/CardDavBackend.php
index 9d602025c7..3b3474cdd0 100644
--- a/apps/dav/lib/CardDAV/CardDavBackend.php
+++ b/apps/dav/lib/CardDAV/CardDavBackend.php
@@ -951,7 +951,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
}
/**
- * search contact
+ * Search contacts in a specific address-book
*
* @param int $addressBookId
* @param string $pattern which should match within the $searchProperties
@@ -962,11 +962,55 @@ class CardDavBackend implements BackendInterface, SyncSupport {
* - 'offset' - Set the offset for the limited search results
* @return array an array of contacts which are arrays of key-value-pairs
*/
- public function search($addressBookId, $pattern, $searchProperties, $options = []) {
+ public function search($addressBookId, $pattern, $searchProperties, $options = []): array {
+ return $this->searchByAddressBookIds([$addressBookId], $pattern, $searchProperties, $options);
+ }
+
+ /**
+ * Search contacts in all address-books accessible by a user
+ *
+ * @param string $principalUri
+ * @param string $pattern
+ * @param array $searchProperties
+ * @param array $options
+ * @return array
+ */
+ public function searchPrincipalUri(string $principalUri,
+ string $pattern,
+ array $searchProperties,
+ array $options = []): array {
+ $addressBookIds = array_map(static function ($row):int {
+ return (int) $row['id'];
+ }, $this->getAddressBooksForUser($principalUri));
+
+ return $this->searchByAddressBookIds($addressBookIds, $pattern, $searchProperties, $options);
+ }
+
+ /**
+ * @param array $addressBookIds
+ * @param string $pattern
+ * @param array $searchProperties
+ * @param array $options
+ * @return array
+ */
+ private function searchByAddressBookIds(array $addressBookIds,
+ string $pattern,
+ array $searchProperties,
+ array $options = []): array {
$escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
$query2 = $this->db->getQueryBuilder();
- $or = $query2->expr()->orX();
+
+ $addressBookOr = $query2->expr()->orX();
+ foreach ($addressBookIds as $addressBookId) {
+ $addressBookOr->add($query2->expr()->eq('cp.addressbookid', $query2->createNamedParameter($addressBookId)));
+ }
+
+ if ($addressBookOr->count() === 0) {
+ return [];
+ }
+
+ $propertyOr = $query2->expr()->orX();
foreach ($searchProperties as $property) {
if ($escapePattern) {
if ($property === 'EMAIL' && strpos($pattern, ' ') !== false) {
@@ -980,17 +1024,17 @@ class CardDavBackend implements BackendInterface, SyncSupport {
}
}
- $or->add($query2->expr()->eq('cp.name', $query2->createNamedParameter($property)));
+ $propertyOr->add($query2->expr()->eq('cp.name', $query2->createNamedParameter($property)));
}
- if ($or->count() === 0) {
+ if ($propertyOr->count() === 0) {
return [];
}
$query2->selectDistinct('cp.cardid')
->from($this->dbCardsPropertiesTable, 'cp')
- ->andWhere($query2->expr()->eq('cp.addressbookid', $query2->createNamedParameter($addressBookId)))
- ->andWhere($or);
+ ->andWhere($addressBookOr)
+ ->andWhere($propertyOr);
// No need for like when the pattern is empty
if ('' !== $pattern) {
@@ -1016,7 +1060,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
}, $matches);
$query = $this->db->getQueryBuilder();
- $query->select('c.carddata', 'c.uri')
+ $query->select('c.addressbookid', 'c.carddata', 'c.uri')
->from($this->dbCardsTable, 'c')
->where($query->expr()->in('c.id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));
@@ -1026,6 +1070,7 @@ class CardDavBackend implements BackendInterface, SyncSupport {
$result->closeCursor();
return array_map(function ($array) {
+ $array['addressbookid'] = (int) $array['addressbookid'];
$modified = false;
$array['carddata'] = $this->readBlob($array['carddata'], $modified);
if ($modified) {
diff --git a/apps/dav/lib/Search/ContactsSearchProvider.php b/apps/dav/lib/Search/ContactsSearchProvider.php
new file mode 100644
index 0000000000..656b484c2b
--- /dev/null
+++ b/apps/dav/lib/Search/ContactsSearchProvider.php
@@ -0,0 +1,189 @@
+
+ *
+ * @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
+ *
+ */
+namespace OCA\DAV\Search;
+
+use OCA\DAV\CardDAV\CardDavBackend;
+use OCP\App\IAppManager;
+use OCP\IL10N;
+use OCP\IURLGenerator;
+use OCP\IUser;
+use OCP\Search\IProvider;
+use OCP\Search\ISearchQuery;
+use OCP\Search\SearchResult;
+use Sabre\VObject\Component\VCard;
+use Sabre\VObject\Reader;
+
+class ContactsSearchProvider implements IProvider {
+
+ /** @var IAppManager */
+ private $appManager;
+
+ /** @var IL10N */
+ private $l10n;
+
+ /** @var IURLGenerator */
+ private $urlGenerator;
+
+ /** @var CardDavBackend */
+ private $backend;
+
+ /**
+ * @var string[]
+ */
+ private static $searchProperties = [
+ 'N',
+ 'FN',
+ 'NICKNAME',
+ 'EMAIL',
+ 'ADR',
+ ];
+
+ /**
+ * ContactsSearchProvider constructor.
+ *
+ * @param IAppManager $appManager
+ * @param IL10N $l10n
+ * @param IURLGenerator $urlGenerator
+ * @param CardDavBackend $backend
+ */
+ public function __construct(IAppManager $appManager,
+ IL10N $l10n,
+ IURLGenerator $urlGenerator,
+ CardDavBackend $backend) {
+ $this->appManager = $appManager;
+ $this->l10n = $l10n;
+ $this->urlGenerator = $urlGenerator;
+ $this->backend = $backend;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getId(): string {
+ return 'contacts-dav';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getName(): string {
+ return $this->l10n->t('Contacts');
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function search(IUser $user, ISearchQuery $query): SearchResult {
+ if (!$this->appManager->isEnabledForUser('contacts', $user)) {
+ return SearchResult::complete($this->getName(), []);
+ }
+
+ $principalUri = 'principals/users/' . $user->getUID();
+ $addressBooks = $this->backend->getAddressBooksForUser($principalUri);
+ $addressBooksById = [];
+ foreach ($addressBooks as $addressBook) {
+ $addressBooksById[(int) $addressBook['id']] = $addressBook;
+ }
+
+ $searchResults = $this->backend->searchPrincipalUri(
+ $principalUri,
+ $query->getTerm(),
+ self::$searchProperties,
+ [
+ 'limit' => $query->getLimit(),
+ 'offset' => $query->getCursor(),
+ ]
+ );
+ $formattedResults = \array_map(function (array $contactRow) use ($addressBooksById):ContactsSearchResultEntry {
+ $addressBook = $addressBooksById[$contactRow['addressbookid']];
+
+ /** @var VCard $vCard */
+ $vCard = Reader::read($contactRow['carddata']);
+ $thumbnailUrl = '';
+ if ($vCard->PHOTO) {
+ $thumbnailUrl = $this->getDavUrlForContact($addressBook['principaluri'], $addressBook['uri'], $contactRow['uri']) . '?photo';
+ }
+
+ $title = (string)$vCard->FN;
+ $subline = $this->generateSubline($vCard);
+ $resourceUrl = $this->getDeepLinkToContactsApp($addressBook['uri'], (string) $vCard->UID);
+
+ return new ContactsSearchResultEntry($thumbnailUrl, $title, $subline, $resourceUrl, 'icon-contacts-dark', true);
+ }, $searchResults);
+
+ return SearchResult::paginated(
+ $this->getName(),
+ $formattedResults,
+ $query->getCursor() + count($formattedResults)
+ );
+ }
+
+ /**
+ * @param string $principalUri
+ * @param string $addressBookUri
+ * @param string $contactsUri
+ * @return string
+ */
+ protected function getDavUrlForContact(string $principalUri,
+ string $addressBookUri,
+ string $contactsUri): string {
+ [, $principalType, $principalId] = explode('/', $principalUri, 3);
+
+ return $this->urlGenerator->getAbsoluteURL(
+ $this->urlGenerator->linkTo('', 'remote.php') . '/dav/addressbooks/'
+ . $principalType . '/'
+ . $principalId . '/'
+ . $addressBookUri . '/'
+ . $contactsUri
+ );
+ }
+
+ /**
+ * @param string $addressBookUri
+ * @param string $contactUid
+ * @return string
+ */
+ protected function getDeepLinkToContactsApp(string $addressBookUri,
+ string $contactUid): string {
+ return $this->urlGenerator->getAbsoluteURL(
+ $this->urlGenerator->linkToRoute('contacts.contacts.direct', [
+ 'contact' => $contactUid . '~' . $addressBookUri
+ ])
+ );
+ }
+
+ /**
+ * @param VCard $vCard
+ * @return string
+ */
+ protected function generateSubline(VCard $vCard): string {
+ $emailAddresses = $vCard->select('EMAIL');
+ if (!is_array($emailAddresses) || empty($emailAddresses)) {
+ return '';
+ }
+
+ return (string)$emailAddresses[0];
+ }
+}
diff --git a/apps/dav/lib/Search/ContactsSearchResultEntry.php b/apps/dav/lib/Search/ContactsSearchResultEntry.php
new file mode 100644
index 0000000000..698fc1b3f4
--- /dev/null
+++ b/apps/dav/lib/Search/ContactsSearchResultEntry.php
@@ -0,0 +1,30 @@
+
+ *
+ * @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
+ *
+ */
+namespace OCA\DAV\Search;
+
+use OCP\Search\ASearchResultEntry;
+
+class ContactsSearchResultEntry extends ASearchResultEntry {
+}
diff --git a/apps/dav/tests/unit/Search/ContactsSearchProviderTest.php b/apps/dav/tests/unit/Search/ContactsSearchProviderTest.php
new file mode 100644
index 0000000000..d89d8dd169
--- /dev/null
+++ b/apps/dav/tests/unit/Search/ContactsSearchProviderTest.php
@@ -0,0 +1,275 @@
+
+ *
+ * @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
+ *
+ */
+namespace OCA\DAV\Tests\unit;
+
+use OCA\DAV\CardDAV\CardDavBackend;
+use OCA\DAV\Search\ContactsSearchProvider;
+use OCA\DAV\Search\ContactsSearchResultEntry;
+use OCP\App\IAppManager;
+use OCP\IL10N;
+use OCP\IURLGenerator;
+use OCP\IUser;
+use OCP\Search\ISearchQuery;
+use OCP\Search\SearchResult;
+use Sabre\VObject\Reader;
+use Test\TestCase;
+
+class ContactsSearchProviderTest extends TestCase {
+
+ /** @var IAppManager|\PHPUnit\Framework\MockObject\MockObject */
+ private $appManager;
+
+ /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */
+ private $l10n;
+
+ /** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
+ private $urlGenerator;
+
+ /** @var CardDavBackend|\PHPUnit\Framework\MockObject\MockObject */
+ private $backend;
+
+ /** @var ContactsSearchProvider */
+ private $provider;
+
+ 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:FN of Test'.PHP_EOL.
+ 'N:Test;;;;'.PHP_EOL.
+ 'EMAIL:forrestgump@example.com'.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.
+ 'PHOTO;ENCODING=b;TYPE=image/jpeg:'.PHP_EOL.
+ 'UID:Test2'.PHP_EOL.
+ 'FN:FN of Test2'.PHP_EOL.
+ 'N:Test2;;;;'.PHP_EOL.
+ 'END:VCARD';
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->appManager = $this->createMock(IAppManager::class);
+ $this->l10n = $this->createMock(IL10N::class);
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+ $this->backend = $this->createMock(CardDavBackend::class);
+
+ $this->provider = new ContactsSearchProvider(
+ $this->appManager,
+ $this->l10n,
+ $this->urlGenerator,
+ $this->backend
+ );
+ }
+
+ public function testGetId(): void {
+ $this->assertEquals('contacts-dav', $this->provider->getId());
+ }
+
+ public function testGetName(): void {
+ $this->l10n->expects($this->exactly(1))
+ ->method('t')
+ ->with('Contacts')
+ ->willReturnArgument(0);
+
+ $this->assertEquals('Contacts', $this->provider->getName());
+ }
+
+ public function testSearchAppDisabled(): void {
+ $user = $this->createMock(IUser::class);
+ $query = $this->createMock(ISearchQuery::class);
+ $this->appManager->expects($this->once())
+ ->method('isEnabledForUser')
+ ->with('contacts', $user)
+ ->willReturn(false);
+ $this->l10n->expects($this->exactly(1))
+ ->method('t')
+ ->with('Contacts')
+ ->willReturnArgument(0);
+ $this->backend->expects($this->never())
+ ->method('getAddressBooksForUser');
+ $this->backend->expects($this->never())
+ ->method('searchPrincipalUri');
+
+ $actual = $this->provider->search($user, $query);
+ $data = $actual->jsonSerialize();
+ $this->assertInstanceOf(SearchResult::class, $actual);
+ $this->assertEquals('Contacts', $data['name']);
+ $this->assertEmpty($data['entries']);
+ $this->assertFalse($data['isPaginated']);
+ $this->assertNull($data['cursor']);
+ }
+
+ public function testSearch(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('john.doe');
+ $query = $this->createMock(ISearchQuery::class);
+ $query->method('getTerm')->willReturn('search term');
+ $query->method('getLimit')->willReturn(5);
+ $query->method('getCursor')->willReturn(20);
+ $this->appManager->expects($this->once())
+ ->method('isEnabledForUser')
+ ->with('contacts', $user)
+ ->willReturn(true);
+ $this->l10n->expects($this->exactly(1))
+ ->method('t')
+ ->with('Contacts')
+ ->willReturnArgument(0);
+
+ $this->backend->expects($this->once())
+ ->method('getAddressBooksForUser')
+ ->with('principals/users/john.doe')
+ ->willReturn([
+ [
+ 'id' => 99,
+ 'principaluri' => 'principals/users/john.doe',
+ 'uri' => 'addressbook-uri-99',
+ ], [
+ 'id' => 123,
+ 'principaluri' => 'principals/users/john.doe',
+ 'uri' => 'addressbook-uri-123',
+ ]
+ ]);
+ $this->backend->expects($this->once())
+ ->method('searchPrincipalUri')
+ ->with('principals/users/john.doe', 'search term',
+ ['N', 'FN', 'NICKNAME', 'EMAIL', 'ADR'],
+ ['limit' => 5, 'offset' => 20])
+ ->willReturn([
+ [
+ 'addressbookid' => 99,
+ 'uri' => 'vcard0.vcf',
+ 'carddata' => $this->vcardTest0,
+ ],
+ [
+ 'addressbookid' => 123,
+ 'uri' => 'vcard1.vcf',
+ 'carddata' => $this->vcardTest1,
+ ],
+ ]);
+
+ $provider = $this->getMockBuilder(ContactsSearchProvider::class)
+ ->setConstructorArgs([
+ $this->appManager,
+ $this->l10n,
+ $this->urlGenerator,
+ $this->backend,
+ ])
+ ->setMethods([
+ 'getDavUrlForContact',
+ 'getDeepLinkToContactsApp',
+ 'generateSubline',
+ ])
+ ->getMock();
+
+ $provider->expects($this->once())
+ ->method('getDavUrlForContact')
+ ->with('principals/users/john.doe', 'addressbook-uri-123', 'vcard1.vcf')
+ ->willReturn('absolute-thumbnail-url');
+
+ $provider->expects($this->exactly(2))
+ ->method('generateSubline')
+ ->willReturn('subline');
+ $provider->expects($this->exactly(2))
+ ->method('getDeepLinkToContactsApp')
+ ->withConsecutive(
+ ['addressbook-uri-99', 'Test'],
+ ['addressbook-uri-123', 'Test2']
+ )
+ ->willReturn('deep-link-to-contacts');
+
+ $actual = $provider->search($user, $query);
+ $data = $actual->jsonSerialize();
+ $this->assertInstanceOf(SearchResult::class, $actual);
+ $this->assertEquals('Contacts', $data['name']);
+ $this->assertCount(2, $data['entries']);
+ $this->assertTrue($data['isPaginated']);
+ $this->assertEquals(22, $data['cursor']);
+
+ $result0 = $data['entries'][0];
+ $result0Data = $result0->jsonSerialize();
+ $result1 = $data['entries'][1];
+ $result1Data = $result1->jsonSerialize();
+
+ $this->assertInstanceOf(ContactsSearchResultEntry::class, $result0);
+ $this->assertEquals('', $result0Data['thumbnailUrl']);
+ $this->assertEquals('FN of Test', $result0Data['title']);
+ $this->assertEquals('subline', $result0Data['subline']);
+ $this->assertEquals('deep-link-to-contacts', $result0Data['resourceUrl']);
+ $this->assertEquals('icon-contacts-dark', $result0Data['iconClass']);
+ $this->assertTrue($result0Data['rounded']);
+
+ $this->assertInstanceOf(ContactsSearchResultEntry::class, $result0);
+ $this->assertEquals('absolute-thumbnail-url?photo', $result1Data['thumbnailUrl']);
+ $this->assertEquals('FN of Test2', $result1Data['title']);
+ $this->assertEquals('subline', $result1Data['subline']);
+ $this->assertEquals('deep-link-to-contacts', $result1Data['resourceUrl']);
+ $this->assertEquals('icon-contacts-dark', $result1Data['iconClass']);
+ $this->assertTrue($result1Data['rounded']);
+ }
+
+ public function testGetDavUrlForContact(): void {
+ $this->urlGenerator->expects($this->once())
+ ->method('linkTo')
+ ->with('', 'remote.php')
+ ->willReturn('link-to-remote.php');
+ $this->urlGenerator->expects($this->once())
+ ->method('getAbsoluteURL')
+ ->with('link-to-remote.php/dav/addressbooks/users/john.doe/foo/bar.vcf')
+ ->willReturn('absolute-url-link-to-remote.php/dav/addressbooks/users/john.doe/foo/bar.vcf');
+
+ $actual = self::invokePrivate($this->provider, 'getDavUrlForContact', ['principals/users/john.doe', 'foo', 'bar.vcf']);
+
+ $this->assertEquals('absolute-url-link-to-remote.php/dav/addressbooks/users/john.doe/foo/bar.vcf', $actual);
+ }
+
+ public function testGetDeepLinkToContactsApp(): void {
+ $this->urlGenerator->expects($this->once())
+ ->method('linkToRoute')
+ ->with('contacts.contacts.direct', ['contact' => 'uid123~uri-john.doe'])
+ ->willReturn('link-to-route-contacts.contacts.direct/direct/uid123~uri-john.doe');
+ $this->urlGenerator->expects($this->once())
+ ->method('getAbsoluteURL')
+ ->with('link-to-route-contacts.contacts.direct/direct/uid123~uri-john.doe')
+ ->willReturn('absolute-url-link-to-route-contacts.contacts.direct/direct/uid123~uri-john.doe');
+
+ $actual = self::invokePrivate($this->provider, 'getDeepLinkToContactsApp', ['uri-john.doe', 'uid123']);
+ $this->assertEquals('absolute-url-link-to-route-contacts.contacts.direct/direct/uid123~uri-john.doe', $actual);
+ }
+
+ public function testGenerateSubline(): void {
+ $vCard0 = Reader::read($this->vcardTest0);
+ $vCard1 = Reader::read($this->vcardTest1);
+
+ $actual1 = self::invokePrivate($this->provider, 'generateSubline', [$vCard0]);
+ $actual2 = self::invokePrivate($this->provider, 'generateSubline', [$vCard1]);
+
+ $this->assertEquals('forrestgump@example.com', $actual1);
+ $this->assertEquals('', $actual2);
+ }
+}