Browse Source

Add Event and Task Backends for Unified Search

Signed-off-by: Georg Ehrke <developer@georgehrke.com>
tags/v20.0.0beta1
Georg Ehrke 3 years ago
parent
commit
900617e7d7
No account linked to committer's email address

+ 5
- 0
apps/dav/composer/composer/autoload_classmap.php View File

@@ -210,8 +210,13 @@ 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\\ACalendarSearchProvider' => $baseDir . '/../lib/Search/ACalendarSearchProvider.php',
'OCA\\DAV\\Search\\ContactsSearchProvider' => $baseDir . '/../lib/Search/ContactsSearchProvider.php',
'OCA\\DAV\\Search\\ContactsSearchResultEntry' => $baseDir . '/../lib/Search/ContactsSearchResultEntry.php',
'OCA\\DAV\\Search\\EventsSearchProvider' => $baseDir . '/../lib/Search/EventsSearchProvider.php',
'OCA\\DAV\\Search\\EventsSearchResultEntry' => $baseDir . '/../lib/Search/EventsSearchResultEntry.php',
'OCA\\DAV\\Search\\TasksSearchProvider' => $baseDir . '/../lib/Search/TasksSearchProvider.php',
'OCA\\DAV\\Search\\TasksSearchResultEntry' => $baseDir . '/../lib/Search/TasksSearchResultEntry.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',

+ 5
- 0
apps/dav/composer/composer/autoload_static.php View File

@@ -225,8 +225,13 @@ 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\\ACalendarSearchProvider' => __DIR__ . '/..' . '/../lib/Search/ACalendarSearchProvider.php',
'OCA\\DAV\\Search\\ContactsSearchProvider' => __DIR__ . '/..' . '/../lib/Search/ContactsSearchProvider.php',
'OCA\\DAV\\Search\\ContactsSearchResultEntry' => __DIR__ . '/..' . '/../lib/Search/ContactsSearchResultEntry.php',
'OCA\\DAV\\Search\\EventsSearchProvider' => __DIR__ . '/..' . '/../lib/Search/EventsSearchProvider.php',
'OCA\\DAV\\Search\\EventsSearchResultEntry' => __DIR__ . '/..' . '/../lib/Search/EventsSearchResultEntry.php',
'OCA\\DAV\\Search\\TasksSearchProvider' => __DIR__ . '/..' . '/../lib/Search/TasksSearchProvider.php',
'OCA\\DAV\\Search\\TasksSearchResultEntry' => __DIR__ . '/..' . '/../lib/Search/TasksSearchResultEntry.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',

+ 4
- 0
apps/dav/lib/AppInfo/Application.php View File

@@ -55,6 +55,8 @@ use OCA\DAV\CardDAV\PhotoCache;
use OCA\DAV\CardDAV\SyncService;
use OCA\DAV\HookManager;
use OCA\DAV\Search\ContactsSearchProvider;
use OCA\DAV\Search\EventsSearchProvider;
use OCA\DAV\Search\TasksSearchProvider;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
@@ -102,6 +104,8 @@ class Application extends App implements IBootstrap {
* Register Search Providers
*/
$context->registerSearchProvider(ContactsSearchProvider::class);
$context->registerSearchProvider(EventsSearchProvider::class);
$context->registerSearchProvider(TasksSearchProvider::class);
}

public function boot(IBootContext $context): void {

+ 118
- 0
apps/dav/lib/CalDAV/CalDavBackend.php View File

@@ -1669,6 +1669,124 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
];
}

/**
* @param string $principalUri
* @param string $pattern
* @param array $componentTypes
* @param array $searchProperties
* @param array $searchParameters
* @param array $options
* @return array
*/
public function searchPrincipalUri(string $principalUri,
string $pattern,
array $componentTypes,
array $searchProperties,
array $searchParameters,
array $options = []): array {
$escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;

$calendarObjectIdQuery = $this->db->getQueryBuilder();
$calendarOr = $calendarObjectIdQuery->expr()->orX();
$searchOr = $calendarObjectIdQuery->expr()->orX();

// Fetch calendars and subscription
$calendars = $this->getCalendarsForUser($principalUri);
$subscriptions = $this->getSubscriptionsForUser($principalUri);
foreach ($calendars as $calendar) {
$calendarAnd = $calendarObjectIdQuery->expr()->andX();
$calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$calendar['id'])));
$calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));

// If it's shared, limit search to public events
if ($calendar['principaluri'] !== $calendar['{http://owncloud.org/ns}owner-principal']) {
$calendarAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
}

$calendarOr->add($calendarAnd);
}
foreach ($subscriptions as $subscription) {
$subscriptionAnd = $calendarObjectIdQuery->expr()->andX();
$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$subscription['id'])));
$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));

// If it's shared, limit search to public events
if ($subscription['principaluri'] !== $subscription['{http://owncloud.org/ns}owner-principal']) {
$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
}

$calendarOr->add($subscriptionAnd);
}

foreach ($searchProperties as $property) {
$propertyAnd = $calendarObjectIdQuery->expr()->andX();
$propertyAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)));
$propertyAnd->add($calendarObjectIdQuery->expr()->isNull('cob.parameter'));

$searchOr->add($propertyAnd);
}
foreach ($searchParameters as $property => $parameter) {
$parameterAnd = $calendarObjectIdQuery->expr()->andX();
$parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)));
$parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.parameter', $calendarObjectIdQuery->createNamedParameter($parameter, IQueryBuilder::PARAM_STR)));

$searchOr->add($parameterAnd);
}

if ($calendarOr->count() === 0) {
return [];
}
if ($searchOr->count() === 0) {
return [];
}

$calendarObjectIdQuery->selectDistinct('cob.objectid')
->from($this->dbObjectPropertiesTable, 'cob')
->leftJoin('cob', 'calendarobjects', 'co', $calendarObjectIdQuery->expr()->eq('co.id', 'cob.objectid'))
->andWhere($calendarObjectIdQuery->expr()->in('co.componenttype', $calendarObjectIdQuery->createNamedParameter($componentTypes, IQueryBuilder::PARAM_STR_ARRAY)))
->andWhere($calendarOr)
->andWhere($searchOr);

if ('' !== $pattern) {
if (!$escapePattern) {
$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter($pattern)));
} else {
$calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
}
}

if (isset($options['limit'])) {
$calendarObjectIdQuery->setMaxResults($options['limit']);
}
if (isset($options['offset'])) {
$calendarObjectIdQuery->setFirstResult($options['offset']);
}

$result = $calendarObjectIdQuery->execute();
$matches = $result->fetchAll();
$result->closeCursor();
$matches = array_map(static function (array $match):int {
return (int) $match['objectid'];
}, $matches);

$query = $this->db->getQueryBuilder();
$query->select('calendardata', 'uri', 'calendarid', 'calendartype')
->from('calendarobjects')
->where($query->expr()->in('id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));

$result = $query->execute();
$calendarObjects = $result->fetchAll();
$result->closeCursor();

return array_map(function (array $array): array {
$array['calendarid'] = (int)$array['calendarid'];
$array['calendartype'] = (int)$array['calendartype'];
$array['calendardata'] = $this->readBlob($array['calendardata']);

return $array;
}, $calendarObjects);
}

/**
* Searches through all of a users calendars and calendar objects to find
* an object with a specific UID.

+ 138
- 0
apps/dav/lib/Search/ACalendarSearchProvider.php View File

@@ -0,0 +1,138 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2020, Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\DAV\Search;

use OCA\DAV\CalDAV\CalDavBackend;
use OCP\App\IAppManager;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Search\IProvider;
use Sabre\VObject\Component;
use Sabre\VObject\Reader;

/**
* Class ACalendarSearchProvider
*
* @package OCA\DAV\Search
*/
abstract class ACalendarSearchProvider implements IProvider {

/** @var IAppManager */
protected $appManager;

/** @var IL10N */
protected $l10n;

/** @var IURLGenerator */
protected $urlGenerator;

/** @var CalDavBackend */
protected $backend;

/**
* ACalendarSearchProvider constructor.
*
* @param IAppManager $appManager
* @param IL10N $l10n
* @param IURLGenerator $urlGenerator
* @param CalDavBackend $backend
*/
public function __construct(IAppManager $appManager,
IL10N $l10n,
IURLGenerator $urlGenerator,
CalDavBackend $backend) {
$this->appManager = $appManager;
$this->l10n = $l10n;
$this->urlGenerator = $urlGenerator;
$this->backend = $backend;
}

/**
* Get an associative array of calendars
* calendarId => calendar
*
* @param string $principalUri
* @return array
*/
protected function getSortedCalendars(string $principalUri): array {
$calendars = $this->backend->getCalendarsForUser($principalUri);
$calendarsById = [];
foreach ($calendars as $calendar) {
$calendarsById[(int) $calendar['id']] = $calendar;
}

return $calendarsById;
}

/**
* Get an associative array of subscriptions
* subscriptionId => subscription
*
* @param string $principalUri
* @return array
*/
protected function getSortedSubscriptions(string $principalUri): array {
$subscriptions = $this->backend->getSubscriptionsForUser($principalUri);
$subscriptionsById = [];
foreach ($subscriptions as $subscription) {
$subscriptionsById[(int) $subscription['id']] = $subscription;
}

return $subscriptionsById;
}

/**
* Returns the primary VEvent / VJournal / VTodo component
* If it's a component with recurrence-ids, it will return
* the primary component
*
* TODO: It would be a nice enhancement to show recurrence-exceptions
* as individual search-results.
* For now we will just display the primary element of a recurrence-set.
*
* @param string $calendarData
* @param string $componentName
* @return Component
*/
protected function getPrimaryComponent(string $calendarData, string $componentName): Component {
$vCalendar = Reader::read($calendarData, Reader::OPTION_FORGIVING);

$components = $vCalendar->select($componentName);
if (count($components) === 1) {
return $components[0];
}

// If it's a recurrence-set, take the primary element
foreach ($components as $component) {
/** @var Component $component */
if (!$component->{'RECURRENCE-ID'}) {
return $component;
}
}

// In case of error, just fallback to the first element in the set
return $components[0];
}
}

+ 231
- 0
apps/dav/lib/Search/EventsSearchProvider.php View File

@@ -0,0 +1,231 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2020, Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\DAV\Search;

use OCA\DAV\CalDAV\CalDavBackend;
use OCP\IUser;
use OCP\Search\ISearchQuery;
use OCP\Search\SearchResult;
use Sabre\VObject\Component;
use Sabre\VObject\DateTimeParser;
use Sabre\VObject\Property;

/**
* Class EventsSearchProvider
*
* @package OCA\DAV\Search
*/
class EventsSearchProvider extends ACalendarSearchProvider {

/**
* @var string[]
*/
private static $searchProperties = [
'SUMMARY',
'LOCATION',
'DESCRIPTION',
'ATTENDEE',
'ORGANIZER',
'CATEGORIES',
];

/**
* @var string[]
*/
private static $searchParameters = [
'ATTENDEE' => ['CN'],
'ORGANIZER' => ['CN'],
];

/**
* @var string
*/
private static $componentType = 'VEVENT';

/**
* @inheritDoc
*/
public function getId(): string {
return 'calendar-dav';
}

/**
* @inheritDoc
*/
public function getName(): string {
return $this->l10n->t('Events');
}

/**
* @inheritDoc
*/
public function search(IUser $user,
ISearchQuery $query): SearchResult {
if (!$this->appManager->isEnabledForUser('calendar', $user)) {
return SearchResult::complete($this->getName(), []);
}

$principalUri = 'principals/users/' . $user->getUID();
$calendarsById = $this->getSortedCalendars($principalUri);
$subscriptionsById = $this->getSortedSubscriptions($principalUri);

$searchResults = $this->backend->searchPrincipalUri(
$principalUri,
$query->getTerm(),
[self::$componentType],
self::$searchProperties,
self::$searchParameters,
[
'limit' => $query->getLimit(),
'offset' => $query->getCursor(),
]
);
$formattedResults = \array_map(function (array $eventRow) use ($calendarsById, $subscriptionsById):EventsSearchResultEntry {
$component = $this->getPrimaryComponent($eventRow['calendardata'], self::$componentType);
$title = (string)($component->SUMMARY ?? $this->l10n->t('Untitled event'));
$subline = $this->generateSubline($component);

if ($eventRow['calendartype'] === CalDavBackend::CALENDAR_TYPE_CALENDAR) {
$calendar = $calendarsById[$eventRow['calendarid']];
} else {
$calendar = $subscriptionsById[$eventRow['calendarid']];
}
$resourceUrl = $this->getDeepLinkToCalendarApp($calendar['principaluri'], $calendar['uri'], $eventRow['uri']);

return new EventsSearchResultEntry('', $title, $subline, $resourceUrl, 'icon-calendar-dark', false);
}, $searchResults);

return SearchResult::paginated(
$this->getName(),
$formattedResults,
$query->getCursor() + count($formattedResults)
);
}

/**
* @param string $principalUri
* @param string $calendarUri
* @param string $calendarObjectUri
* @return string
*/
protected function getDeepLinkToCalendarApp(string $principalUri,
string $calendarUri,
string $calendarObjectUri): string {
$davUrl = $this->getDavUrlForCalendarObject($principalUri, $calendarUri, $calendarObjectUri);
// This route will automatically figure out what recurrence-id to open
return $this->urlGenerator->getAbsoluteURL(
$this->urlGenerator->linkToRoute('calendar.view.index')
. 'edit/'
. base64_encode($davUrl)
);
}

/**
* @param string $principalUri
* @param string $calendarUri
* @param string $calendarObjectUri
* @return string
*/
protected function getDavUrlForCalendarObject(string $principalUri,
string $calendarUri,
string $calendarObjectUri): string {
[,, $principalId] = explode('/', $principalUri, 3);

return $this->urlGenerator->linkTo('', 'remote.php') . '/dav/calendars/'
. $principalId . '/'
. $calendarUri . '/'
. $calendarObjectUri;
}

/**
* @param Component $eventComponent
* @return string
*/
protected function generateSubline(Component $eventComponent): string {
$dtStart = $eventComponent->DTSTART;
$dtEnd = $this->getDTEndForEvent($eventComponent);
$isAllDayEvent = $dtStart instanceof Property\ICalendar\Date;
$startDateTime = new \DateTime($dtStart->getDateTime()->format(\DateTime::ATOM));
$endDateTime = new \DateTime($dtEnd->getDateTime()->format(\DateTime::ATOM));

if ($isAllDayEvent) {
$endDateTime->modify('-1 day');
if ($this->isDayEqual($startDateTime, $endDateTime)) {
return $this->l10n->l('date', $startDateTime, ['width' => 'medium']);
}

$formattedStart = $this->l10n->l('date', $startDateTime, ['width' => 'medium']);
$formattedEnd = $this->l10n->l('date', $endDateTime, ['width' => 'medium']);
return "$formattedStart - $formattedEnd";
}

$formattedStartDate = $this->l10n->l('date', $startDateTime, ['width' => 'medium']);
$formattedEndDate = $this->l10n->l('date', $endDateTime, ['width' => 'medium']);
$formattedStartTime = $this->l10n->l('time', $startDateTime, ['width' => 'short']);
$formattedEndTime = $this->l10n->l('time', $endDateTime, ['width' => 'short']);

if ($this->isDayEqual($startDateTime, $endDateTime)) {
return "$formattedStartDate $formattedStartTime - $formattedEndTime";
}

return "$formattedStartDate $formattedStartTime - $formattedEndDate $formattedEndTime";
}

/**
* @param Component $eventComponent
* @return Property
*/
protected function getDTEndForEvent(Component $eventComponent):Property {
if (isset($eventComponent->DTEND)) {
$end = $eventComponent->DTEND;
} elseif (isset($eventComponent->DURATION)) {
$isFloating = $eventComponent->DTSTART->isFloating();
$end = clone $eventComponent->DTSTART;
$endDateTime = $end->getDateTime();
$endDateTime = $endDateTime->add(DateTimeParser::parse($eventComponent->DURATION->getValue()));
$end->setDateTime($endDateTime, $isFloating);
} elseif (!$eventComponent->DTSTART->hasTime()) {
$isFloating = $eventComponent->DTSTART->isFloating();
$end = clone $eventComponent->DTSTART;
$endDateTime = $end->getDateTime();
$endDateTime = $endDateTime->modify('+1 day');
$end->setDateTime($endDateTime, $isFloating);
} else {
$end = clone $eventComponent->DTSTART;
}

return $end;
}

/**
* @param \DateTime $dtStart
* @param \DateTime $dtEnd
* @return bool
*/
protected function isDayEqual(\DateTime $dtStart,
\DateTime $dtEnd) {
return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
}
}

+ 30
- 0
apps/dav/lib/Search/EventsSearchResultEntry.php View File

@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2020, Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\DAV\Search;

use OCP\Search\ASearchResultEntry;

class EventsSearchResultEntry extends ASearchResultEntry {
}

+ 160
- 0
apps/dav/lib/Search/TasksSearchProvider.php View File

@@ -0,0 +1,160 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2020, Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\DAV\Search;

use OCA\DAV\CalDAV\CalDavBackend;
use OCP\IUser;
use OCP\Search\ISearchQuery;
use OCP\Search\SearchResult;
use Sabre\VObject\Component;

/**
* Class TasksSearchProvider
*
* @package OCA\DAV\Search
*/
class TasksSearchProvider extends ACalendarSearchProvider {

/**
* @var string[]
*/
private static $searchProperties = [
'SUMMARY',
'DESCRIPTION',
'CATEGORIES',
];

/**
* @var string[]
*/
private static $searchParameters = [];

/**
* @var string
*/
private static $componentType = 'VTODO';

/**
* @inheritDoc
*/
public function getId(): string {
return 'tasks-dav';
}

/**
* @inheritDoc
*/
public function getName(): string {
return $this->l10n->t('Tasks');
}

/**
* @inheritDoc
*/
public function search(IUser $user,
ISearchQuery $query): SearchResult {
if (!$this->appManager->isEnabledForUser('tasks', $user)) {
return SearchResult::complete($this->getName(), []);
}

$principalUri = 'principals/users/' . $user->getUID();
$calendarsById = $this->getSortedCalendars($principalUri);
$subscriptionsById = $this->getSortedSubscriptions($principalUri);

$searchResults = $this->backend->searchPrincipalUri(
$principalUri,
$query->getTerm(),
[self::$componentType],
self::$searchProperties,
self::$searchParameters,
[
'limit' => $query->getLimit(),
'offset' => $query->getCursor(),
]
);
$formattedResults = \array_map(function (array $taskRow) use ($calendarsById, $subscriptionsById):TasksSearchResultEntry {
$component = $this->getPrimaryComponent($taskRow['calendardata'], self::$componentType);
$title = (string)($component->SUMMARY ?? $this->l10n->t('Untitled task'));
$subline = $this->generateSubline($component);

if ($taskRow['calendartype'] === CalDavBackend::CALENDAR_TYPE_CALENDAR) {
$calendar = $calendarsById[$taskRow['calendarid']];
} else {
$calendar = $subscriptionsById[$taskRow['calendarid']];
}
$resourceUrl = $this->getDeepLinkToTasksApp($calendar['uri'], $taskRow['uri']);

return new TasksSearchResultEntry('', $title, $subline, $resourceUrl, 'icon-checkmark', false);
}, $searchResults);

return SearchResult::paginated(
$this->getName(),
$formattedResults,
$query->getCursor() + count($formattedResults)
);
}

/**
* @param string $calendarUri
* @param string $taskUri
* @return string
*/
protected function getDeepLinkToTasksApp(string $calendarUri,
string $taskUri): string {
return $this->urlGenerator->getAbsoluteURL(
$this->urlGenerator->linkToRoute('tasks.page.index')
. '#/calendars/'
. $calendarUri
. '/tasks/'
. $taskUri
);
}

/**
* @param Component $taskComponent
* @return string
*/
protected function generateSubline(Component $taskComponent): string {
if ($taskComponent->COMPLETED) {
$completedDateTime = new \DateTime($taskComponent->COMPLETED->getDateTime()->format(\DateTime::ATOM));
$formattedDate = $this->l10n->l('date', $completedDateTime, ['width' => 'medium']);
return $this->l10n->t('Completed on %s', [$formattedDate]);
}

if ($taskComponent->DUE) {
$dueDateTime = new \DateTime($taskComponent->DUE->getDateTime()->format(\DateTime::ATOM));
$formattedDate = $this->l10n->l('date', $dueDateTime, ['width' => 'medium']);

if ($taskComponent->DUE->hasTime()) {
$formattedTime = $this->l10n->l('time', $dueDateTime, ['width' => 'short']);
return $this->l10n->t('Due on %s by %s', [$formattedDate, $formattedTime]);
}

return $this->l10n->t('Due on %s', [$formattedDate]);
}

return '';
}
}

+ 30
- 0
apps/dav/lib/Search/TasksSearchResultEntry.php View File

@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2020, Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\DAV\Search;

use OCP\Search\ASearchResultEntry;

class TasksSearchResultEntry extends ASearchResultEntry {
}

+ 7
- 2
apps/dav/tests/unit/CalDAV/AbstractCalDavBackend.php View File

@@ -121,7 +121,12 @@ abstract class AbstractCalDavBackend extends TestCase {
$this->principal->expects($this->any())->method('getGroupMembership')
->withAnyParameters()
->willReturn([self::UNIT_TEST_GROUP, self::UNIT_TEST_GROUP2]);
$calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
$this->cleanupForPrincipal(self::UNIT_TEST_USER);
$this->cleanupForPrincipal(self::UNIT_TEST_USER1);
}

private function cleanupForPrincipal($principal): void {
$calendars = $this->backend->getCalendarsForUser($principal);
foreach ($calendars as $calendar) {
$this->dispatcher->expects($this->at(0))
->method('dispatch')
@@ -129,7 +134,7 @@ abstract class AbstractCalDavBackend extends TestCase {

$this->backend->deleteCalendar($calendar['id']);
}
$subscriptions = $this->backend->getSubscriptionsForUser(self::UNIT_TEST_USER);
$subscriptions = $this->backend->getSubscriptionsForUser($principal);
foreach ($subscriptions as $subscription) {
$this->backend->deleteSubscription($subscription['id']);
}

+ 288
- 10
apps/dav/tests/unit/CalDAV/CalDavBackendTest.php View File

@@ -605,21 +605,21 @@ DTSTART;TZID=Europe/Warsaw:20170325T150000
DTEND;TZID=Europe/Warsaw:20170325T160000
TRANSP:OPAQUE
DESCRIPTION:Magiczna treść uzyskana za pomocą magicznego proszku.\n\nę
żźćńłóÓŻŹĆŁĘ€śśśŚŚ\n \,\,))))))))\;\,\n
żźćńłóÓŻŹĆŁĘ€śśśŚŚ\n \,\,))))))))\;\,\n
__))))))))))))))\,\n \\|/ -\\(((((''''((((((((.\n -*-==///
///(('' . `))))))\,\n /|\\ ))| o \;-. '(((((
\,(\,\n ( `| / ) \;))))'
///(('' . `))))))\,\n /|\\ ))| o \;-. '(((((
\,(\,\n ( `| / ) \;))))'
\,_))^\;(~\n | | | \,))((((_ _____-
-----~~~-. %\,\;(\;(>'\;'~\n o_)\; \; )))(((` ~---
~ `:: \\ %%~~)(v\;(`('~\n \; ''''````
`: `:::|\\\,__\,%% )\;`'\; ~\n | _
) / `:|`----' `-'\n ______/\\/~ |
`: `:::|\\\,__\,%% )\;`'\; ~\n | _
) / `:|`----' `-'\n ______/\\/~ |
/ /\n /~\;\;.____/\;\;' / ___--\
,-( `\;\;\;/\n / // _\;______\;'------~~~~~ /\;\;/\\ /\n
// | | / \; \\\;\;\,\\\n (<_ | \;
/'\,/-----' _>\n \\_| ||_
//~\;~~~~~~~~~\n `\\_| (\,~~ -Tua Xiong\n
\\~\\\n
,-( `\;\;\;/\n / // _\;______\;'------~~~~~ /\;\;/\\ /\n
// | | / \; \\\;\;\,\\\n (<_ | \;
/'\,/-----' _>\n \\_| ||_
//~\;~~~~~~~~~\n `\\_| (\,~~ -Tua Xiong\n
\\~\\\n
~~\n\n
SEQUENCE:1
X-MOZ-GENERATION:1
@@ -999,4 +999,282 @@ EOD;
$this->assertEquals($calendarInfoUser['id'], $calendarInfoUser1['id']);
$this->assertEquals($calendarInfoUser['uri'], $calendarInfoUser1['uri']);
}

public function testSearchPrincipal(): void {
$myPublic = <<<EOD
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//dmfs.org//mimedir.icalendar//EN
BEGIN:VTIMEZONE
TZID:Europe/Berlin
X-LIC-LOCATION:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID=Europe/Berlin:20160419T130000
SUMMARY:My Test (public)
CLASS:PUBLIC
TRANSP:OPAQUE
STATUS:CONFIRMED
DTEND;TZID=Europe/Berlin:20160419T140000
LAST-MODIFIED:20160419T074202Z
DTSTAMP:20160419T074202Z
CREATED:20160419T074202Z
UID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310-1
END:VEVENT
END:VCALENDAR
EOD;
$myPrivate = <<<EOD
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//dmfs.org//mimedir.icalendar//EN
BEGIN:VTIMEZONE
TZID:Europe/Berlin
X-LIC-LOCATION:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID=Europe/Berlin:20160419T130000
SUMMARY:My Test (private)
CLASS:PRIVATE
TRANSP:OPAQUE
STATUS:CONFIRMED
DTEND;TZID=Europe/Berlin:20160419T140000
LAST-MODIFIED:20160419T074202Z
DTSTAMP:20160419T074202Z
CREATED:20160419T074202Z
UID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310-2
END:VEVENT
END:VCALENDAR
EOD;
$myConfidential = <<<EOD
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//dmfs.org//mimedir.icalendar//EN
BEGIN:VTIMEZONE
TZID:Europe/Berlin
X-LIC-LOCATION:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID=Europe/Berlin:20160419T130000
SUMMARY:My Test (confidential)
CLASS:CONFIDENTIAL
TRANSP:OPAQUE
STATUS:CONFIRMED
DTEND;TZID=Europe/Berlin:20160419T140000
LAST-MODIFIED:20160419T074202Z
DTSTAMP:20160419T074202Z
CREATED:20160419T074202Z
UID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310-3
END:VEVENT
END:VCALENDAR
EOD;

$sharerPublic = <<<EOD
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//dmfs.org//mimedir.icalendar//EN
BEGIN:VTIMEZONE
TZID:Europe/Berlin
X-LIC-LOCATION:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID=Europe/Berlin:20160419T130000
SUMMARY:Sharer Test (public)
CLASS:PUBLIC
TRANSP:OPAQUE
STATUS:CONFIRMED
DTEND;TZID=Europe/Berlin:20160419T140000
LAST-MODIFIED:20160419T074202Z
DTSTAMP:20160419T074202Z
CREATED:20160419T074202Z
UID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310-4
END:VEVENT
END:VCALENDAR
EOD;
$sharerPrivate = <<<EOD
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//dmfs.org//mimedir.icalendar//EN
BEGIN:VTIMEZONE
TZID:Europe/Berlin
X-LIC-LOCATION:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID=Europe/Berlin:20160419T130000
SUMMARY:Sharer Test (private)
CLASS:PRIVATE
TRANSP:OPAQUE
STATUS:CONFIRMED
DTEND;TZID=Europe/Berlin:20160419T140000
LAST-MODIFIED:20160419T074202Z
DTSTAMP:20160419T074202Z
CREATED:20160419T074202Z
UID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310-5
END:VEVENT
END:VCALENDAR
EOD;
$sharerConfidential = <<<EOD
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//dmfs.org//mimedir.icalendar//EN
BEGIN:VTIMEZONE
TZID:Europe/Berlin
X-LIC-LOCATION:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID=Europe/Berlin:20160419T130000
SUMMARY:Sharer Test (confidential)
CLASS:CONFIDENTIAL
TRANSP:OPAQUE
STATUS:CONFIRMED
DTEND;TZID=Europe/Berlin:20160419T140000
LAST-MODIFIED:20160419T074202Z
DTSTAMP:20160419T074202Z
CREATED:20160419T074202Z
UID:2e468c48-7860-492e-bc52-92fa0daeeccf.1461051722310-6
END:VEVENT
END:VCALENDAR
EOD;

$l10n = $this->createMock(IL10N::class);
$l10n
->expects($this->any())
->method('t')
->willReturnCallback(function ($text, $parameters = []) {
return vsprintf($text, $parameters);
});
$config = $this->createMock(IConfig::class);
$this->userManager->expects($this->any())
->method('userExists')
->willReturn(true);
$this->groupManager->expects($this->any())
->method('groupExists')
->willReturn(true);

$me = self::UNIT_TEST_USER;
$sharer = self::UNIT_TEST_USER1;
$this->backend->createCalendar($me, 'calendar-uri-me', []);
$this->backend->createCalendar($sharer, 'calendar-uri-sharer', []);

$myCalendars = $this->backend->getCalendarsForUser($me);
$this->assertCount(1, $myCalendars);

$sharerCalendars = $this->backend->getCalendarsForUser($sharer);
$this->assertCount(1, $sharerCalendars);
$sharerCalendar = new Calendar($this->backend, $sharerCalendars[0], $l10n, $config);
$this->backend->updateShares($sharerCalendar, [
[
'href' => 'principal:' . $me,
'readOnly' => false,
],
], []);

$this->assertCount(2, $this->backend->getCalendarsForUser($me));

$this->backend->createCalendarObject($myCalendars[0]['id'], 'event0.ics', $myPublic);
$this->backend->createCalendarObject($myCalendars[0]['id'], 'event1.ics', $myPrivate);
$this->backend->createCalendarObject($myCalendars[0]['id'], 'event2.ics', $myConfidential);

$this->backend->createCalendarObject($sharerCalendars[0]['id'], 'event3.ics', $sharerPublic);
$this->backend->createCalendarObject($sharerCalendars[0]['id'], 'event4.ics', $sharerPrivate);
$this->backend->createCalendarObject($sharerCalendars[0]['id'], 'event5.ics', $sharerConfidential);

$mySearchResults = $this->backend->searchPrincipalUri($me, 'Test', ['VEVENT'], ['SUMMARY'], []);
$sharerSearchResults = $this->backend->searchPrincipalUri($sharer, 'Test', ['VEVENT'], ['SUMMARY'], []);

$this->assertCount(4, $mySearchResults);
$this->assertCount(3, $sharerSearchResults);

$this->assertEquals($myPublic, $mySearchResults[0]['calendardata']);
$this->assertEquals($myPrivate, $mySearchResults[1]['calendardata']);
$this->assertEquals($myConfidential, $mySearchResults[2]['calendardata']);
$this->assertEquals($sharerPublic, $mySearchResults[3]['calendardata']);

$this->assertEquals($sharerPublic, $sharerSearchResults[0]['calendardata']);
$this->assertEquals($sharerPrivate, $sharerSearchResults[1]['calendardata']);
$this->assertEquals($sharerConfidential, $sharerSearchResults[2]['calendardata']);
}
}

+ 1
- 1
apps/dav/tests/unit/Search/ContactsSearchProviderTest.php View File

@@ -224,7 +224,7 @@ class ContactsSearchProviderTest extends TestCase {
$this->assertEquals('icon-contacts-dark', $result0Data['iconClass']);
$this->assertTrue($result0Data['rounded']);

$this->assertInstanceOf(ContactsSearchResultEntry::class, $result0);
$this->assertInstanceOf(ContactsSearchResultEntry::class, $result1);
$this->assertEquals('absolute-thumbnail-url?photo', $result1Data['thumbnailUrl']);
$this->assertEquals('FN of Test2', $result1Data['title']);
$this->assertEquals('subline', $result1Data['subline']);

+ 473
- 0
apps/dav/tests/unit/Search/EventsSearchProviderTest.php View File

@@ -0,0 +1,473 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2020, Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\DAV\Tests\unit\Search;

use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\Search\EventsSearchProvider;
use OCA\DAV\Search\EventsSearchResultEntry;
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 EventsSearchProviderTest 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 CalDavBackend|\PHPUnit\Framework\MockObject\MockObject */
private $backend;

/** @var EventsSearchProvider */
private $provider;

// NO SUMMARY
private $vEvent0 = 'BEGIN:VCALENDAR'.PHP_EOL.
'VERSION:2.0'.PHP_EOL.
'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN'.PHP_EOL.
'CALSCALE:GREGORIAN'.PHP_EOL.
'BEGIN:VEVENT'.PHP_EOL.
'CREATED:20161004T144433Z'.PHP_EOL.
'UID:85560E76-1B0D-47E1-A735-21625767FCA4'.PHP_EOL.
'DTEND;VALUE=DATE:20161008'.PHP_EOL.
'TRANSP:TRANSPARENT'.PHP_EOL.
'DTSTART;VALUE=DATE:20161005'.PHP_EOL.
'DTSTAMP:20161004T144437Z'.PHP_EOL.
'SEQUENCE:0'.PHP_EOL.
'END:VEVENT'.PHP_EOL.
'END:VCALENDAR';

// TIMED SAME DAY
private $vEvent1 = 'BEGIN:VCALENDAR'.PHP_EOL.
'VERSION:2.0'.PHP_EOL.
'PRODID:-//Tests//'.PHP_EOL.
'CALSCALE:GREGORIAN'.PHP_EOL.
'BEGIN:VTIMEZONE'.PHP_EOL.
'TZID:Europe/Berlin'.PHP_EOL.
'BEGIN:DAYLIGHT'.PHP_EOL.
'TZOFFSETFROM:+0100'.PHP_EOL.
'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU'.PHP_EOL.
'DTSTART:19810329T020000'.PHP_EOL.
'TZNAME:GMT+2'.PHP_EOL.
'TZOFFSETTO:+0200'.PHP_EOL.
'END:DAYLIGHT'.PHP_EOL.
'BEGIN:STANDARD'.PHP_EOL.
'TZOFFSETFROM:+0200'.PHP_EOL.
'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU'.PHP_EOL.
'DTSTART:19961027T030000'.PHP_EOL.
'TZNAME:GMT+1'.PHP_EOL.
'TZOFFSETTO:+0100'.PHP_EOL.
'END:STANDARD'.PHP_EOL.
'END:VTIMEZONE'.PHP_EOL.
'BEGIN:VEVENT'.PHP_EOL.
'CREATED:20160809T163629Z'.PHP_EOL.
'UID:0AD16F58-01B3-463B-A215-FD09FC729A02'.PHP_EOL.
'DTEND;TZID=Europe/Berlin:20160816T100000'.PHP_EOL.
'TRANSP:OPAQUE'.PHP_EOL.
'SUMMARY:Test Europe Berlin'.PHP_EOL.
'DTSTART;TZID=Europe/Berlin:20160816T090000'.PHP_EOL.
'DTSTAMP:20160809T163632Z'.PHP_EOL.
'SEQUENCE:0'.PHP_EOL.
'END:VEVENT'.PHP_EOL.
'END:VCALENDAR';

// TIMED DIFFERENT DAY
private $vEvent2 = 'BEGIN:VCALENDAR'.PHP_EOL.
'VERSION:2.0'.PHP_EOL.
'PRODID:-//Tests//'.PHP_EOL.
'CALSCALE:GREGORIAN'.PHP_EOL.
'BEGIN:VTIMEZONE'.PHP_EOL.
'TZID:Europe/Berlin'.PHP_EOL.
'BEGIN:DAYLIGHT'.PHP_EOL.
'TZOFFSETFROM:+0100'.PHP_EOL.
'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU'.PHP_EOL.
'DTSTART:19810329T020000'.PHP_EOL.
'TZNAME:GMT+2'.PHP_EOL.
'TZOFFSETTO:+0200'.PHP_EOL.
'END:DAYLIGHT'.PHP_EOL.
'BEGIN:STANDARD'.PHP_EOL.
'TZOFFSETFROM:+0200'.PHP_EOL.
'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU'.PHP_EOL.
'DTSTART:19961027T030000'.PHP_EOL.
'TZNAME:GMT+1'.PHP_EOL.
'TZOFFSETTO:+0100'.PHP_EOL.
'END:STANDARD'.PHP_EOL.
'END:VTIMEZONE'.PHP_EOL.
'BEGIN:VEVENT'.PHP_EOL.
'CREATED:20160809T163629Z'.PHP_EOL.
'UID:0AD16F58-01B3-463B-A215-FD09FC729A02'.PHP_EOL.
'DTEND;TZID=Europe/Berlin:20160817T100000'.PHP_EOL.
'TRANSP:OPAQUE'.PHP_EOL.
'SUMMARY:Test Europe Berlin'.PHP_EOL.
'DTSTART;TZID=Europe/Berlin:20160816T090000'.PHP_EOL.
'DTSTAMP:20160809T163632Z'.PHP_EOL.
'SEQUENCE:0'.PHP_EOL.
'END:VEVENT'.PHP_EOL.
'END:VCALENDAR';

// ALL-DAY ONE-DAY
private $vEvent3 = 'BEGIN:VCALENDAR'.PHP_EOL.
'VERSION:2.0'.PHP_EOL.
'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN'.PHP_EOL.
'CALSCALE:GREGORIAN'.PHP_EOL.
'BEGIN:VEVENT'.PHP_EOL.
'CREATED:20161004T144433Z'.PHP_EOL.
'UID:85560E76-1B0D-47E1-A735-21625767FCA4'.PHP_EOL.
'DTEND;VALUE=DATE:20161006'.PHP_EOL.
'TRANSP:TRANSPARENT'.PHP_EOL.
'DTSTART;VALUE=DATE:20161005'.PHP_EOL.
'DTSTAMP:20161004T144437Z'.PHP_EOL.
'SEQUENCE:0'.PHP_EOL.
'END:VEVENT'.PHP_EOL.
'END:VCALENDAR';

// ALL-DAY MULTIPLE DAYS
private $vEvent4 = 'BEGIN:VCALENDAR'.PHP_EOL.
'VERSION:2.0'.PHP_EOL.
'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN'.PHP_EOL.
'CALSCALE:GREGORIAN'.PHP_EOL.
'BEGIN:VEVENT'.PHP_EOL.
'CREATED:20161004T144433Z'.PHP_EOL.
'UID:85560E76-1B0D-47E1-A735-21625767FCA4'.PHP_EOL.
'DTEND;VALUE=DATE:20161008'.PHP_EOL.
'TRANSP:TRANSPARENT'.PHP_EOL.
'DTSTART;VALUE=DATE:20161005'.PHP_EOL.
'DTSTAMP:20161004T144437Z'.PHP_EOL.
'SEQUENCE:0'.PHP_EOL.
'END:VEVENT'.PHP_EOL.
'END:VCALENDAR';

// DURATION
private $vEvent5 = 'BEGIN:VCALENDAR'.PHP_EOL.
'VERSION:2.0'.PHP_EOL.
'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN'.PHP_EOL.
'CALSCALE:GREGORIAN'.PHP_EOL.
'BEGIN:VEVENT'.PHP_EOL.
'CREATED:20161004T144433Z'.PHP_EOL.
'UID:85560E76-1B0D-47E1-A735-21625767FCA4'.PHP_EOL.
'DURATION:P5D'.PHP_EOL.
'TRANSP:TRANSPARENT'.PHP_EOL.
'DTSTART;VALUE=DATE:20161005'.PHP_EOL.
'DTSTAMP:20161004T144437Z'.PHP_EOL.
'SEQUENCE:0'.PHP_EOL.
'END:VEVENT'.PHP_EOL.
'END:VCALENDAR';

// NO DTEND - DATE
private $vEvent6 = 'BEGIN:VCALENDAR'.PHP_EOL.
'VERSION:2.0'.PHP_EOL.
'PRODID:-//Apple Inc.//Mac OS X 10.11.6//EN'.PHP_EOL.
'CALSCALE:GREGORIAN'.PHP_EOL.
'BEGIN:VEVENT'.PHP_EOL.
'CREATED:20161004T144433Z'.PHP_EOL.
'UID:85560E76-1B0D-47E1-A735-21625767FCA4'.PHP_EOL.
'TRANSP:TRANSPARENT'.PHP_EOL.
'DTSTART;VALUE=DATE:20161005'.PHP_EOL.
'DTSTAMP:20161004T144437Z'.PHP_EOL.
'SEQUENCE:0'.PHP_EOL.
'END:VEVENT'.PHP_EOL.
'END:VCALENDAR';

// NO DTEND - DATE-TIME
private $vEvent7 = 'BEGIN:VCALENDAR'.PHP_EOL.
'VERSION:2.0'.PHP_EOL.
'PRODID:-//Tests//'.PHP_EOL.
'CALSCALE:GREGORIAN'.PHP_EOL.
'BEGIN:VTIMEZONE'.PHP_EOL.
'TZID:Europe/Berlin'.PHP_EOL.
'BEGIN:DAYLIGHT'.PHP_EOL.
'TZOFFSETFROM:+0100'.PHP_EOL.
'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU'.PHP_EOL.
'DTSTART:19810329T020000'.PHP_EOL.
'TZNAME:GMT+2'.PHP_EOL.
'TZOFFSETTO:+0200'.PHP_EOL.
'END:DAYLIGHT'.PHP_EOL.
'BEGIN:STANDARD'.PHP_EOL.
'TZOFFSETFROM:+0200'.PHP_EOL.
'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU'.PHP_EOL.
'DTSTART:19961027T030000'.PHP_EOL.
'TZNAME:GMT+1'.PHP_EOL.
'TZOFFSETTO:+0100'.PHP_EOL.
'END:STANDARD'.PHP_EOL.
'END:VTIMEZONE'.PHP_EOL.
'BEGIN:VEVENT'.PHP_EOL.
'CREATED:20160809T163629Z'.PHP_EOL.
'UID:0AD16F58-01B3-463B-A215-FD09FC729A02'.PHP_EOL.
'TRANSP:OPAQUE'.PHP_EOL.
'SUMMARY:Test Europe Berlin'.PHP_EOL.
'DTSTART;TZID=Europe/Berlin:20160816T090000'.PHP_EOL.
'DTSTAMP:20160809T163632Z'.PHP_EOL.
'SEQUENCE:0'.PHP_EOL.
'END:VEVENT'.PHP_EOL.
'END:VCALENDAR';

protected function setUp(): void {
parent::setUp();

$this->appManager = $this->createMock(IAppManager::class);
$this->l10n = $this->createMock(IL10N::class);
$this->urlGenerator = $this->createMock(IURLGenerator::class);
$this->backend = $this->createMock(CalDavBackend::class);

$this->provider = new EventsSearchProvider(
$this->appManager,
$this->l10n,
$this->urlGenerator,
$this->backend
);
}

public function testGetId(): void {
$this->assertEquals('calendar-dav', $this->provider->getId());
}

public function testGetName(): void {
$this->l10n->expects($this->exactly(1))
->method('t')
->with('Events')
->willReturnArgument(0);

$this->assertEquals('Events', $this->provider->getName());
}

public function testSearchAppDisabled(): void {
$user = $this->createMock(IUser::class);
$query = $this->createMock(ISearchQuery::class);
$this->appManager->expects($this->once())
->method('isEnabledForUser')
->with('calendar', $user)
->willReturn(false);
$this->l10n->expects($this->exactly(1))
->method('t')
->willReturnArgument(0);
$this->backend->expects($this->never())
->method('getCalendarsForUser');
$this->backend->expects($this->never())
->method('getSubscriptionsForUser');
$this->backend->expects($this->never())
->method('searchPrincipalUri');

$actual = $this->provider->search($user, $query);
$data = $actual->jsonSerialize();
$this->assertInstanceOf(SearchResult::class, $actual);
$this->assertEquals('Events', $data['name']);
$this->assertEmpty($data['entries']);
$this->assertFalse($data['isPaginated']);
$this->assertNull($data['cursor']);
}

public function testSearch(): void {
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('john.doe');
$query = $this->createMock(ISearchQuery::class);
$query->method('getTerm')->willReturn('search term');
$query->method('getLimit')->willReturn(5);
$query->method('getCursor')->willReturn(20);
$this->appManager->expects($this->once())
->method('isEnabledForUser')
->with('calendar', $user)
->willReturn(true);
$this->l10n->method('t')->willReturnArgument(0);

$this->backend->expects($this->once())
->method('getCalendarsForUser')
->with('principals/users/john.doe')
->willReturn([
[
'id' => 99,
'principaluri' => 'principals/users/john.doe',
'uri' => 'calendar-uri-99',
], [
'id' => 123,
'principaluri' => 'principals/users/john.doe',
'uri' => 'calendar-uri-123',
]
]);
$this->backend->expects($this->once())
->method('getSubscriptionsForUser')
->with('principals/users/john.doe')
->willReturn([
[
'id' => 1337,
'principaluri' => 'principals/users/john.doe',
'uri' => 'subscription-uri-1337',
]
]);
$this->backend->expects($this->once())
->method('searchPrincipalUri')
->with('principals/users/john.doe', 'search term', ['VEVENT'],
['SUMMARY', 'LOCATION', 'DESCRIPTION', 'ATTENDEE', 'ORGANIZER', 'CATEGORIES'],
['ATTENDEE' => ['CN'], 'ORGANIZER' => ['CN']],
['limit' => 5, 'offset' => 20])
->willReturn([
[
'calendarid' => 99,
'calendartype' => CalDavBackend::CALENDAR_TYPE_CALENDAR,
'uri' => 'event0.ics',
'calendardata' => $this->vEvent0,
],
[
'calendarid' => 123,
'calendartype' => CalDavBackend::CALENDAR_TYPE_CALENDAR,
'uri' => 'event1.ics',
'calendardata' => $this->vEvent1,
],
[
'calendarid' => 1337,
'calendartype' => CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION,
'uri' => 'event2.ics',
'calendardata' => $this->vEvent2,
]
]);

$provider = $this->getMockBuilder(EventsSearchProvider::class)
->setConstructorArgs([
$this->appManager,
$this->l10n,
$this->urlGenerator,
$this->backend,
])
->setMethods([
'getDeepLinkToCalendarApp',
'generateSubline',
])
->getMock();

$provider->expects($this->exactly(3))
->method('generateSubline')
->willReturn('subline');
$provider->expects($this->exactly(3))
->method('getDeepLinkToCalendarApp')
->withConsecutive(
['principals/users/john.doe', 'calendar-uri-99', 'event0.ics'],
['principals/users/john.doe', 'calendar-uri-123', 'event1.ics'],
['principals/users/john.doe', 'subscription-uri-1337', 'event2.ics']
)
->willReturn('deep-link-to-calendar');

$actual = $provider->search($user, $query);
$data = $actual->jsonSerialize();
$this->assertInstanceOf(SearchResult::class, $actual);
$this->assertEquals('Events', $data['name']);
$this->assertCount(3, $data['entries']);
$this->assertTrue($data['isPaginated']);
$this->assertEquals(23, $data['cursor']);

$result0 = $data['entries'][0];
$result0Data = $result0->jsonSerialize();
$result1 = $data['entries'][1];
$result1Data = $result1->jsonSerialize();
$result2 = $data['entries'][2];
$result2Data = $result2->jsonSerialize();

$this->assertInstanceOf(EventsSearchResultEntry::class, $result0);
$this->assertEmpty($result0Data['thumbnailUrl']);
$this->assertEquals('Untitled event', $result0Data['title']);
$this->assertEquals('subline', $result0Data['subline']);
$this->assertEquals('deep-link-to-calendar', $result0Data['resourceUrl']);
$this->assertEquals('icon-calendar-dark', $result0Data['iconClass']);
$this->assertFalse($result0Data['rounded']);

$this->assertInstanceOf(EventsSearchResultEntry::class, $result1);
$this->assertEmpty($result1Data['thumbnailUrl']);
$this->assertEquals('Test Europe Berlin', $result1Data['title']);
$this->assertEquals('subline', $result1Data['subline']);
$this->assertEquals('deep-link-to-calendar', $result1Data['resourceUrl']);
$this->assertEquals('icon-calendar-dark', $result1Data['iconClass']);
$this->assertFalse($result1Data['rounded']);

$this->assertInstanceOf(EventsSearchResultEntry::class, $result2);
$this->assertEmpty($result2Data['thumbnailUrl']);
$this->assertEquals('Test Europe Berlin', $result2Data['title']);
$this->assertEquals('subline', $result2Data['subline']);
$this->assertEquals('deep-link-to-calendar', $result2Data['resourceUrl']);
$this->assertEquals('icon-calendar-dark', $result2Data['iconClass']);
$this->assertFalse($result2Data['rounded']);
}

public function testGetDeepLinkToCalendarApp(): void {
$this->urlGenerator->expects($this->at(0))
->method('linkTo')
->with('', 'remote.php')
->willReturn('link-to-remote.php');
$this->urlGenerator->expects($this->at(1))
->method('linkToRoute')
->with('calendar.view.index')
->willReturn('link-to-route-calendar/');
$this->urlGenerator->expects($this->at(2))
->method('getAbsoluteURL')
->with('link-to-route-calendar/edit/bGluay10by1yZW1vdGUucGhwL2Rhdi9jYWxlbmRhcnMvam9obi5kb2UvZm9vL2Jhci5pY3M=')
->willReturn('absolute-url-to-route');

$actual = self::invokePrivate($this->provider, 'getDeepLinkToCalendarApp', ['principals/users/john.doe', 'foo', 'bar.ics']);

$this->assertEquals('absolute-url-to-route', $actual);
}

/**
* @param string $ics
* @param string $expectedSubline
*
* @dataProvider generateSublineDataProvider
*/
public function testGenerateSubline(string $ics, string $expectedSubline): void {
$vCalendar = Reader::read($ics, Reader::OPTION_FORGIVING);
$eventComponent = $vCalendar->VEVENT;

$this->l10n->method('l')
->willReturnCallback(static function (string $type, \DateTime $date, $_):string {
if ($type === 'time') {
return $date->format('H:i');
}

return $date->format('m-d');
});

$actual = self::invokePrivate($this->provider, 'generateSubline', [$eventComponent]);
$this->assertEquals($expectedSubline, $actual);
}

public function generateSublineDataProvider(): array {
return [
[$this->vEvent1, '08-16 09:00 - 10:00'],
[$this->vEvent2, '08-16 09:00 - 08-17 10:00'],
[$this->vEvent3, '10-05'],
[$this->vEvent4, '10-05 - 10-07'],
[$this->vEvent5, '10-05 - 10-09'],
[$this->vEvent6, '10-05'],
[$this->vEvent7, '08-16 09:00 - 09:00'],
];
}
}

+ 344
- 0
apps/dav/tests/unit/Search/TasksSearchProviderTest.php View File

@@ -0,0 +1,344 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2020, Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\DAV\Tests\unit\Search;

use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\Search\TasksSearchProvider;
use OCA\DAV\Search\TasksSearchResultEntry;
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 TasksSearchProviderTest 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 CalDavBackend|\PHPUnit\Framework\MockObject\MockObject */
private $backend;

/** @var TasksSearchProvider */
private $provider;

// NO DUE NOR COMPLETED NOR SUMMARY
private $vTodo0 = 'BEGIN:VCALENDAR'.PHP_EOL.
'PRODID:TEST'.PHP_EOL.
'VERSION:2.0'.PHP_EOL.
'BEGIN:VTODO'.PHP_EOL.
'UID:20070313T123432Z-456553@example.com'.PHP_EOL.
'DTSTAMP:20070313T123432Z'.PHP_EOL.
'STATUS:NEEDS-ACTION'.PHP_EOL.
'END:VTODO'.PHP_EOL.
'END:VCALENDAR';

// DUE AND COMPLETED
private $vTodo1 = 'BEGIN:VCALENDAR'.PHP_EOL.
'PRODID:TEST'.PHP_EOL.
'VERSION:2.0'.PHP_EOL.
'BEGIN:VTODO'.PHP_EOL.
'UID:20070313T123432Z-456553@example.com'.PHP_EOL.
'DTSTAMP:20070313T123432Z'.PHP_EOL.
'COMPLETED:20070707T100000Z'.PHP_EOL.
'DUE;VALUE=DATE:20070501'.PHP_EOL.
'SUMMARY:Task title'.PHP_EOL.
'STATUS:NEEDS-ACTION'.PHP_EOL.
'END:VTODO'.PHP_EOL.
'END:VCALENDAR';

// COMPLETED ONLY
private $vTodo2 = 'BEGIN:VCALENDAR'.PHP_EOL.
'PRODID:TEST'.PHP_EOL.
'VERSION:2.0'.PHP_EOL.
'BEGIN:VTODO'.PHP_EOL.
'UID:20070313T123432Z-456553@example.com'.PHP_EOL.
'DTSTAMP:20070313T123432Z'.PHP_EOL.
'COMPLETED:20070707T100000Z'.PHP_EOL.
'SUMMARY:Task title'.PHP_EOL.
'STATUS:NEEDS-ACTION'.PHP_EOL.
'END:VTODO'.PHP_EOL.
'END:VCALENDAR';

// DUE DATE
private $vTodo3 = 'BEGIN:VCALENDAR'.PHP_EOL.
'PRODID:TEST'.PHP_EOL.
'VERSION:2.0'.PHP_EOL.
'BEGIN:VTODO'.PHP_EOL.
'UID:20070313T123432Z-456553@example.com'.PHP_EOL.
'DTSTAMP:20070313T123432Z'.PHP_EOL.
'DUE;VALUE=DATE:20070501'.PHP_EOL.
'SUMMARY:Task title'.PHP_EOL.
'STATUS:NEEDS-ACTION'.PHP_EOL.
'END:VTODO'.PHP_EOL.
'END:VCALENDAR';

// DUE DATETIME
private $vTodo4 = 'BEGIN:VCALENDAR'.PHP_EOL.
'PRODID:TEST'.PHP_EOL.
'VERSION:2.0'.PHP_EOL.
'BEGIN:VTODO'.PHP_EOL.
'UID:20070313T123432Z-456553@example.com'.PHP_EOL.
'DTSTAMP:20070313T123432Z'.PHP_EOL.
'DUE:20070709T130000Z'.PHP_EOL.
'SUMMARY:Task title'.PHP_EOL.
'STATUS:NEEDS-ACTION'.PHP_EOL.
'END:VTODO'.PHP_EOL.
'END:VCALENDAR';

protected function setUp(): void {
parent::setUp();

$this->appManager = $this->createMock(IAppManager::class);
$this->l10n = $this->createMock(IL10N::class);
$this->urlGenerator = $this->createMock(IURLGenerator::class);
$this->backend = $this->createMock(CalDavBackend::class);

$this->provider = new TasksSearchProvider(
$this->appManager,
$this->l10n,
$this->urlGenerator,
$this->backend
);
}

public function testGetId(): void {
$this->assertEquals('tasks-dav', $this->provider->getId());
}

public function testGetName(): void {
$this->l10n->expects($this->exactly(1))
->method('t')
->with('Tasks')
->willReturnArgument(0);

$this->assertEquals('Tasks', $this->provider->getName());
}

public function testSearchAppDisabled(): void {
$user = $this->createMock(IUser::class);
$query = $this->createMock(ISearchQuery::class);
$this->appManager->expects($this->once())
->method('isEnabledForUser')
->with('tasks', $user)
->willReturn(false);
$this->l10n->expects($this->exactly(1))
->method('t')
->willReturnArgument(0);
$this->backend->expects($this->never())
->method('getCalendarsForUser');
$this->backend->expects($this->never())
->method('getSubscriptionsForUser');
$this->backend->expects($this->never())
->method('searchPrincipalUri');

$actual = $this->provider->search($user, $query);
$data = $actual->jsonSerialize();
$this->assertInstanceOf(SearchResult::class, $actual);
$this->assertEquals('Tasks', $data['name']);
$this->assertEmpty($data['entries']);
$this->assertFalse($data['isPaginated']);
$this->assertNull($data['cursor']);
}

public function testSearch(): void {
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('john.doe');
$query = $this->createMock(ISearchQuery::class);
$query->method('getTerm')->willReturn('search term');
$query->method('getLimit')->willReturn(5);
$query->method('getCursor')->willReturn(20);
$this->appManager->expects($this->once())
->method('isEnabledForUser')
->with('tasks', $user)
->willReturn(true);
$this->l10n->method('t')->willReturnArgument(0);

$this->backend->expects($this->once())
->method('getCalendarsForUser')
->with('principals/users/john.doe')
->willReturn([
[
'id' => 99,
'principaluri' => 'principals/users/john.doe',
'uri' => 'calendar-uri-99',
], [
'id' => 123,
'principaluri' => 'principals/users/john.doe',
'uri' => 'calendar-uri-123',
]
]);
$this->backend->expects($this->once())
->method('getSubscriptionsForUser')
->with('principals/users/john.doe')
->willReturn([
[
'id' => 1337,
'principaluri' => 'principals/users/john.doe',
'uri' => 'subscription-uri-1337',
]
]);
$this->backend->expects($this->once())
->method('searchPrincipalUri')
->with('principals/users/john.doe', 'search term', ['VTODO'],
['SUMMARY', 'DESCRIPTION', 'CATEGORIES'],
[],
['limit' => 5, 'offset' => 20])
->willReturn([
[
'calendarid' => 99,
'calendartype' => CalDavBackend::CALENDAR_TYPE_CALENDAR,
'uri' => 'todo0.ics',
'calendardata' => $this->vTodo0,
],
[
'calendarid' => 123,
'calendartype' => CalDavBackend::CALENDAR_TYPE_CALENDAR,
'uri' => 'todo1.ics',
'calendardata' => $this->vTodo1,
],
[
'calendarid' => 1337,
'calendartype' => CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION,
'uri' => 'todo2.ics',
'calendardata' => $this->vTodo2,
]
]);

$provider = $this->getMockBuilder(TasksSearchProvider::class)
->setConstructorArgs([
$this->appManager,
$this->l10n,
$this->urlGenerator,
$this->backend,
])
->setMethods([
'getDeepLinkToTasksApp',
'generateSubline',
])
->getMock();

$provider->expects($this->exactly(3))
->method('generateSubline')
->willReturn('subline');
$provider->expects($this->exactly(3))
->method('getDeepLinkToTasksApp')
->withConsecutive(
['calendar-uri-99', 'todo0.ics'],
['calendar-uri-123', 'todo1.ics'],
['subscription-uri-1337', 'todo2.ics']
)
->willReturn('deep-link-to-tasks');

$actual = $provider->search($user, $query);
$data = $actual->jsonSerialize();
$this->assertInstanceOf(SearchResult::class, $actual);
$this->assertEquals('Tasks', $data['name']);
$this->assertCount(3, $data['entries']);
$this->assertTrue($data['isPaginated']);
$this->assertEquals(23, $data['cursor']);

$result0 = $data['entries'][0];
$result0Data = $result0->jsonSerialize();
$result1 = $data['entries'][1];
$result1Data = $result1->jsonSerialize();
$result2 = $data['entries'][2];
$result2Data = $result2->jsonSerialize();

$this->assertInstanceOf(TasksSearchResultEntry::class, $result0);
$this->assertEmpty($result0Data['thumbnailUrl']);
$this->assertEquals('Untitled task', $result0Data['title']);
$this->assertEquals('subline', $result0Data['subline']);
$this->assertEquals('deep-link-to-tasks', $result0Data['resourceUrl']);
$this->assertEquals('icon-checkmark', $result0Data['iconClass']);
$this->assertFalse($result0Data['rounded']);

$this->assertInstanceOf(TasksSearchResultEntry::class, $result1);
$this->assertEmpty($result1Data['thumbnailUrl']);
$this->assertEquals('Task title', $result1Data['title']);
$this->assertEquals('subline', $result1Data['subline']);
$this->assertEquals('deep-link-to-tasks', $result1Data['resourceUrl']);
$this->assertEquals('icon-checkmark', $result1Data['iconClass']);
$this->assertFalse($result1Data['rounded']);

$this->assertInstanceOf(TasksSearchResultEntry::class, $result2);
$this->assertEmpty($result2Data['thumbnailUrl']);
$this->assertEquals('Task title', $result2Data['title']);
$this->assertEquals('subline', $result2Data['subline']);
$this->assertEquals('deep-link-to-tasks', $result2Data['resourceUrl']);
$this->assertEquals('icon-checkmark', $result2Data['iconClass']);
$this->assertFalse($result2Data['rounded']);
}

public function testGetDeepLinkToTasksApp(): void {
$this->urlGenerator->expects($this->once())
->method('linkToRoute')
->with('tasks.page.index')
->willReturn('link-to-route-tasks.index');
$this->urlGenerator->expects($this->once())
->method('getAbsoluteURL')
->with('link-to-route-tasks.index#/calendars/uri-john.doe/tasks/task-uri.ics')
->willReturn('absolute-url-link-to-route-tasks.index#/calendars/uri-john.doe/tasks/task-uri.ics');

$actual = self::invokePrivate($this->provider, 'getDeepLinkToTasksApp', ['uri-john.doe', 'task-uri.ics']);
$this->assertEquals('absolute-url-link-to-route-tasks.index#/calendars/uri-john.doe/tasks/task-uri.ics', $actual);
}

/**
* @param string $ics
* @param string $expectedSubline
*
* @dataProvider generateSublineDataProvider
*/
public function testGenerateSubline(string $ics, string $expectedSubline): void {
$vCalendar = Reader::read($ics, Reader::OPTION_FORGIVING);
$taskComponent = $vCalendar->VTODO;

$this->l10n->method('t')->willReturnArgument(0);
$this->l10n->method('l')->willReturnArgument('');

$actual = self::invokePrivate($this->provider, 'generateSubline', [$taskComponent]);
$this->assertEquals($expectedSubline, $actual);
}

public function generateSublineDataProvider(): array {
return [
[$this->vTodo0, ''],
[$this->vTodo1, 'Completed on %s'],
[$this->vTodo2, 'Completed on %s'],
[$this->vTodo3, 'Due on %s'],
[$this->vTodo4, 'Due on %s by %s'],
];
}
}

Loading…
Cancel
Save