Signed-off-by: Georg Ehrke <developer@georgehrke.com>pull/22020/head
parent
c124b485f1
commit
900617e7d7
@ -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]; |
||||
} |
||||
} |
@ -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'); |
||||
} |
||||
} |
@ -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 { |
||||
} |
@ -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 ''; |
||||
} |
||||
} |
@ -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 { |
||||
} |
@ -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'], |
||||
]; |
||||
} |
||||
} |
@ -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…
Reference in new issue