From e1073cf442613ac92878c8ded30a33db35b30e14 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Mon, 9 May 2016 10:02:07 +0200 Subject: [PATCH] Notificacations for simple @-mentioning in comments (WIP) notify user when mentioned in comments Fix doc, and create absolute URL for as notification link. PSR-4 compatibility changes also move notification creation to comments app Do not notify yourself unit test for controller and application smaller fixes - translatable app name - remove doubles in mention array - micro perf optimization - display name: special label for deleted users, keep user id for users that could not be fetched from userManager Comment Notification-Listener Unit Test fix email adresses remove notification when triggering comment was deleted add and adjust tests add missing @license tags simplify NotificationsController registration appinfo simplification, php docs make string easier to translate adjust test replace dispatcher-based listeners with a registration method and interface safer to not pass optional data parameter to setSubject for marking as processed. ID and mention suffices Signed-off-by: Arthur Schiwon update comment Signed-off-by: Arthur Schiwon --- apps/comments/appinfo/app.php | 31 +- apps/comments/appinfo/routes.php | 28 + apps/comments/lib/AppInfo/Application.php | 35 ++ .../comments/lib/Controller/Notifications.php | 138 +++++ apps/comments/lib/EventHandler.php | 78 +++ apps/comments/lib/Notification/Listener.php | 130 +++++ apps/comments/lib/Notification/Notifier.php | 115 ++++ .../tests/Unit/AppInfo/ApplicationTest.php | 65 +++ .../Unit/Controller/NotificationsTest.php | 162 ++++++ apps/comments/tests/Unit/EventHandlerTest.php | 152 ++++++ .../tests/Unit/Notification/ListenerTest.php | 360 +++++++++++++ .../tests/Unit/Notification/NotifierTest.php | 499 ++++++++++++++++++ .../DependencyInjection/DIContainer.php | 4 + lib/private/Comments/Manager.php | 79 ++- lib/public/Comments/ICommentsEventHandler.php | 39 ++ lib/public/Comments/ICommentsManager.php | 9 + tests/lib/Comments/FakeManager.php | 2 + tests/lib/Comments/ManagerTest.php | 44 +- 18 files changed, 1934 insertions(+), 36 deletions(-) create mode 100644 apps/comments/appinfo/routes.php create mode 100644 apps/comments/lib/AppInfo/Application.php create mode 100644 apps/comments/lib/Controller/Notifications.php create mode 100644 apps/comments/lib/EventHandler.php create mode 100644 apps/comments/lib/Notification/Listener.php create mode 100644 apps/comments/lib/Notification/Notifier.php create mode 100644 apps/comments/tests/Unit/AppInfo/ApplicationTest.php create mode 100644 apps/comments/tests/Unit/Controller/NotificationsTest.php create mode 100644 apps/comments/tests/Unit/EventHandlerTest.php create mode 100644 apps/comments/tests/Unit/Notification/ListenerTest.php create mode 100644 apps/comments/tests/Unit/Notification/NotifierTest.php create mode 100644 lib/public/Comments/ICommentsEventHandler.php diff --git a/apps/comments/appinfo/app.php b/apps/comments/appinfo/app.php index df41bdfa32d..771b35d9c6a 100644 --- a/apps/comments/appinfo/app.php +++ b/apps/comments/appinfo/app.php @@ -41,22 +41,33 @@ $activityManager = \OC::$server->getActivityManager(); $activityManager->registerExtension(function() { $application = new \OCP\AppFramework\App('comments'); /** @var \OCA\Comments\Activity\Extension $extension */ - $extension = $application->getContainer()->query('OCA\Comments\Activity\Extension'); + $extension = $application->getContainer()->query(\OCA\Comments\Activity\Extension::class); return $extension; }); -$managerListener = function(\OCP\Comments\CommentsEvent $event) use ($activityManager) { - $application = new \OCP\AppFramework\App('comments'); - /** @var \OCA\Comments\Activity\Listener $listener */ - $listener = $application->getContainer()->query('OCA\Comments\Activity\Listener'); - $listener->commentEvent($event); -}; - -$eventDispatcher->addListener(\OCP\Comments\CommentsEvent::EVENT_ADD, $managerListener); - $eventDispatcher->addListener(\OCP\Comments\CommentsEntityEvent::EVENT_ENTITY, function(\OCP\Comments\CommentsEntityEvent $event) { $event->addEntityCollection('files', function($name) { $nodes = \OC::$server->getUserFolder()->getById(intval($name)); return !empty($nodes); }); }); + +$notificationManager = \OC::$server->getNotificationManager(); +$notificationManager->registerNotifier( + function() { + $application = new \OCP\AppFramework\App('comments'); + return $application->getContainer()->query(\OCA\Comments\Notification\Notifier::class); + }, + function () { + $l = \OC::$server->getL10N('comments'); + return ['id' => 'comments', 'name' => $l->t('Comments')]; + } +); + +$commentsManager = \OC::$server->getCommentsManager(); +$commentsManager->registerEventHandler(function () { + $application = new \OCP\AppFramework\App('comments'); + /** @var \OCA\Comments\Activity\Extension $extension */ + $handler = $application->getContainer()->query(\OCA\Comments\EventHandler::class); + return $handler; +}); diff --git a/apps/comments/appinfo/routes.php b/apps/comments/appinfo/routes.php new file mode 100644 index 00000000000..ab751ddb2a2 --- /dev/null +++ b/apps/comments/appinfo/routes.php @@ -0,0 +1,28 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +use \OCA\Comments\AppInfo\Application; + +$application = new Application(); +$application->registerRoutes($this, ['routes' => [ + ['name' => 'Notifications#view', 'url' => '/notifications/view/{id}', 'verb' => 'GET'], +]]); diff --git a/apps/comments/lib/AppInfo/Application.php b/apps/comments/lib/AppInfo/Application.php new file mode 100644 index 00000000000..f168779cd0d --- /dev/null +++ b/apps/comments/lib/AppInfo/Application.php @@ -0,0 +1,35 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 along with this library. If not, see . + */ + +namespace OCA\Comments\AppInfo; + +use OCA\Comments\Controller\Notifications; +use OCP\AppFramework\App; + +class Application extends App { + + public function __construct (array $urlParams = array()) { + parent::__construct('comments', $urlParams); + $container = $this->getContainer(); + + $container->registerAlias('NotificationsController', Notifications::class); + } +} diff --git a/apps/comments/lib/Controller/Notifications.php b/apps/comments/lib/Controller/Notifications.php new file mode 100644 index 00000000000..f76f1605345 --- /dev/null +++ b/apps/comments/lib/Controller/Notifications.php @@ -0,0 +1,138 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 along with this library. If not, see . + * + */ + +namespace OCA\Comments\Controller; + +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\NotFoundResponse; +use OCP\AppFramework\Http\RedirectResponse; +use OCP\AppFramework\Http\Response; +use OCP\Comments\IComment; +use OCP\Comments\ICommentsManager; +use OCP\Files\Folder; +use OCP\IRequest; +use OCP\IURLGenerator; +use OCP\IUserSession; +use OCP\Notification\IManager; + +/** + * Class Notifications + * + * @package OCA\Comments\Controller + */ +class Notifications extends Controller { + /** @var Folder */ + protected $folder; + + /** @var ICommentsManager */ + protected $commentsManager; + + /** @var IURLGenerator */ + protected $urlGenerator; + + /** @var IManager */ + protected $notificationManager; + + /** @var IUserSession */ + protected $userSession; + + /** + * Notifications constructor. + * + * @param string $appName + * @param IRequest $request + * @param ICommentsManager $commentsManager + * @param Folder $folder + * @param IURLGenerator $urlGenerator + * @param IManager $notificationManager + * @param IUserSession $userSession + */ + public function __construct( + $appName, + IRequest $request, + ICommentsManager $commentsManager, + Folder $folder, + IURLGenerator $urlGenerator, + IManager $notificationManager, + IUserSession $userSession + ) { + parent::__construct($appName, $request); + $this->commentsManager = $commentsManager; + $this->folder = $folder; + $this->urlGenerator = $urlGenerator; + $this->notificationManager = $notificationManager; + $this->userSession = $userSession; + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * + * @param string $id the comment ID + * @return Response + */ + public function view($id) { + try { + $comment = $this->commentsManager->get($id); + if($comment->getObjectType() !== 'files') { + return new NotFoundResponse(); + } + $files = $this->folder->getById($comment->getObjectId()); + if(count($files) === 0) { + $this->markProcessed($comment); + return new NotFoundResponse(); + } + + $dir = $this->folder->getRelativePath($files[0]->getParent()->getPath()); + $url = $this->urlGenerator->linkToRouteAbsolute( + 'files.view.index', + [ + 'dir' => $dir, + 'scrollto' => $files[0]->getName() + ] + ); + + $this->markProcessed($comment); + + return new RedirectResponse($url); + } catch (\Exception $e) { + return new NotFoundResponse(); + } + } + + /** + * Marks the notification about a comment as processed + * @param IComment $comment + */ + protected function markProcessed(IComment $comment) { + $user = $this->userSession->getUser(); + if(is_null($user)) { + return; + } + $notification = $this->notificationManager->createNotification(); + $notification->setApp('comments') + ->setObject('comment', $comment->getId()) + ->setSubject('mention') + ->setUser($user->getUID()); + $this->notificationManager->markProcessed($notification); + } +} diff --git a/apps/comments/lib/EventHandler.php b/apps/comments/lib/EventHandler.php new file mode 100644 index 00000000000..a32bb8ffa71 --- /dev/null +++ b/apps/comments/lib/EventHandler.php @@ -0,0 +1,78 @@ + + * + * @author Arthur Schiwon + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +namespace OCA\Comments; + +use OCA\Comments\Activity\Listener as ActivityListener; +use OCA\Comments\AppInfo\Application; +use OCA\Comments\Notification\Listener as NotificationListener; +use OCP\Comments\CommentsEvent; +use OCP\Comments\ICommentsEventHandler; + +/** + * Class EventHandler + * + * @package OCA\Comments + */ +class EventHandler implements ICommentsEventHandler { + + /** @var Application */ + protected $app; + + public function __construct(Application $app) { + $this->app = $app; + } + + /** + * @param CommentsEvent $event + */ + public function handle(CommentsEvent $event) { + if($event->getComment()->getObjectType() !== 'files') { + // this is a 'files'-specific Handler + return; + } + + if( $event->getEvent() === CommentsEvent::EVENT_ADD + && $event instanceof CommentsEvent + ) { + $this->onAdd($event); + return; + } + } + + /** + * @param CommentsEvent $event + */ + private function onAdd(CommentsEvent $event) { + $c = $this->app->getContainer(); + + /** @var NotificationListener $notificationListener */ + $notificationListener = $c->query(NotificationListener::class); + $notificationListener->evaluate($event); + + /** @var ActivityListener $listener */ + $activityListener = $c->query(ActivityListener::class); + $activityListener->commentEvent($event); + } +} diff --git a/apps/comments/lib/Notification/Listener.php b/apps/comments/lib/Notification/Listener.php new file mode 100644 index 00000000000..5e979fd9bf0 --- /dev/null +++ b/apps/comments/lib/Notification/Listener.php @@ -0,0 +1,130 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\Comments\Notification; + +use OCP\Comments\CommentsEvent; +use OCP\Comments\IComment; +use OCP\IURLGenerator; +use OCP\IUserManager; +use OCP\Notification\IManager; + +class Listener { + /** @var IManager */ + protected $notificationManager; + + /** @var IUserManager */ + protected $userManager; + + /** @var IURLGenerator */ + protected $urlGenerator; + + /** + * Listener constructor. + * + * @param IManager $notificationManager + * @param IUserManager $userManager + * @param IURLGenerator $urlGenerator + */ + public function __construct( + IManager $notificationManager, + IUserManager $userManager, + IURLGenerator $urlGenerator + ) { + + $this->notificationManager = $notificationManager; + $this->userManager = $userManager; + $this->urlGenerator = $urlGenerator; + } + + /** + * @param CommentsEvent $event + */ + public function evaluate(CommentsEvent $event) { + $comment = $event->getComment(); + + if($comment->getObjectType() !== 'files') { + // comments App serves files only, other object types/apps need to + // register their own ICommentsEventHandler and trigger notifications + return; + } + + $mentions = $this->extractMentions($comment->getMessage()); + if(empty($mentions)) { + // no one to notify + return; + } + + $notification = $this->instantiateNotification($comment); + + foreach($mentions as $mention) { + $user = substr($mention, 1); // @username → username + if( ($comment->getActorType() === 'users' && $user === $comment->getActorId()) + || !$this->userManager->userExists($user) + ) { + // do not notify unknown users or yourself + continue; + } + + $notification->setUser($user); + if($event->getEvent() === CommentsEvent::EVENT_DELETE) { + $this->notificationManager->markProcessed($notification); + } else { + $this->notificationManager->notify($notification); + } + } + } + + /** + * creates a notification instance and fills it with comment data + * + * @param IComment $comment + * @return \OCP\Notification\INotification + */ + public function instantiateNotification(IComment $comment) { + $notification = $this->notificationManager->createNotification(); + $notification + ->setApp('comments') + ->setObject('comment', $comment->getId()) + ->setSubject('mention', [ $comment->getObjectType(), $comment->getObjectId() ]) + ->setDateTime($comment->getCreationDateTime()) + ->setLink($this->urlGenerator->linkToRouteAbsolute( + 'comments.Notifications.view', + ['id' => $comment->getId()]) + ); + + return $notification; + } + + /** + * extracts @-mentions out of a message body. + * + * @param string $message + * @return string[] containing the mentions, e.g. ['@alice', '@bob'] + */ + public function extractMentions($message) { + $ok = preg_match_all('/\B@[a-z0-9_\-@\.\']+/i', $message, $mentions); + if(!$ok || !isset($mentions[0]) || !is_array($mentions[0])) { + return []; + } + return array_unique($mentions[0]); + } +} diff --git a/apps/comments/lib/Notification/Notifier.php b/apps/comments/lib/Notification/Notifier.php new file mode 100644 index 00000000000..df05f297301 --- /dev/null +++ b/apps/comments/lib/Notification/Notifier.php @@ -0,0 +1,115 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\Comments\Notification; + +use OCP\Comments\ICommentsManager; +use OCP\Comments\NotFoundException; +use OCP\Files\Folder; +use OCP\IUserManager; +use OCP\L10N\IFactory; +use OCP\Notification\INotification; +use OCP\Notification\INotifier; + +class Notifier implements INotifier { + + /** @var IFactory */ + protected $l10nFactory; + + /** @var Folder */ + protected $userFolder; + + /** @var ICommentsManager */ + protected $commentsManager; + + /** @var IUserManager */ + protected $userManager; + + public function __construct( + IFactory $l10nFactory, + Folder $userFolder, + ICommentsManager $commentsManager, + IUserManager $userManager + ) { + $this->l10nFactory = $l10nFactory; + $this->userFolder = $userFolder; + $this->commentsManager = $commentsManager; + $this->userManager = $userManager; + } + + /** + * @param INotification $notification + * @param string $languageCode The code of the language that should be used to prepare the notification + * @return INotification + * @throws \InvalidArgumentException When the notification was not prepared by a notifier + */ + public function prepare(INotification $notification, $languageCode) { + if($notification->getApp() !== 'comments') { + throw new \InvalidArgumentException(); + } + try { + $comment = $this->commentsManager->get($notification->getObjectId()); + } catch(NotFoundException $e) { + // needs to be converted to InvalidArgumentException, otherwise none Notifications will be shown at all + throw new \InvalidArgumentException('Comment not found', 0, $e); + } + $l = $this->l10nFactory->get('comments', $languageCode); + $displayName = $comment->getActorId(); + $isDeletedActor = $comment->getActorType() === ICommentsManager::DELETED_USER; + if($comment->getActorType() === 'users') { + $commenter = $this->userManager->get($comment->getActorId()); + if(!is_null($commenter)) { + $displayName = $commenter->getDisplayName(); + } + } + switch($notification->getSubject()) { + case 'mention': + $parameters = $notification->getSubjectParameters(); + if($parameters[0] !== 'files') { + throw new \InvalidArgumentException('Unsupported comment object'); + } + $nodes = $this->userFolder->getById($parameters[1]); + if(empty($nodes)) { + throw new \InvalidArgumentException('Cannot resolve file id to Node instance'); + } + $fileName = $nodes[0]->getName(); + if($isDeletedActor) { + $subject = (string) $l->t( + 'You were mentioned in a comment on "%s" by a now deleted user.', + [ $fileName ] + ); + } else { + $subject = (string) $l->t( + 'You were mentioned in a comment on "%s" by %s.', + [ $fileName, $displayName ] + ); + } + $notification->setParsedSubject($subject); + + return $notification; + break; + + default: + throw new \InvalidArgumentException('Invalid subject'); + } + + } +} diff --git a/apps/comments/tests/Unit/AppInfo/ApplicationTest.php b/apps/comments/tests/Unit/AppInfo/ApplicationTest.php new file mode 100644 index 00000000000..c11d7857496 --- /dev/null +++ b/apps/comments/tests/Unit/AppInfo/ApplicationTest.php @@ -0,0 +1,65 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\Comments\Tests\Unit\AppInfo; + +use OCA\Comments\AppInfo\Application; +use Test\TestCase; + +/** + * Class ApplicationTest + * + * @group DB + * + * @package OCA\Comments\Tests\Unit\AppInfo + */ +class ApplicationTest extends TestCase { + protected function setUp() { + parent::setUp(); + \OC::$server->getUserManager()->createUser('dummy', '456'); + \OC::$server->getUserSession()->setUser(\OC::$server->getUserManager()->get('dummy')); + } + + protected function tearDown() { + \OC::$server->getUserManager()->get('dummy')->delete(); + parent::tearDown(); + } + + public function test() { + $app = new Application(); + $c = $app->getContainer(); + + // assert service instances in the container are properly setup + $s = $c->query('NotificationsController'); + $this->assertInstanceOf('OCA\Comments\Controller\Notifications', $s); + + $services = [ + 'OCA\Comments\Activity\Extension', + 'OCA\Comments\Activity\Listener', + 'OCA\Comments\Notification\Listener' + ]; + + foreach($services as $service) { + $s = $c->query($service); + $this->assertInstanceOf($service, $s); + } + } +} diff --git a/apps/comments/tests/Unit/Controller/NotificationsTest.php b/apps/comments/tests/Unit/Controller/NotificationsTest.php new file mode 100644 index 00000000000..5ab39788213 --- /dev/null +++ b/apps/comments/tests/Unit/Controller/NotificationsTest.php @@ -0,0 +1,162 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\Comments\Tests\Unit\Controller; + +use OCA\Comments\Controller\Notifications; +use OCP\Comments\NotFoundException; +use Test\TestCase; + +class NotificationsTest extends TestCase { + /** @var \OCA\Comments\Controller\Notifications */ + protected $notificationsController; + + /** @var \OCP\Comments\ICommentsManager|\PHPUnit_Framework_MockObject_MockObject */ + protected $commentsManager; + + /** @var \OCP\Files\Folder|\PHPUnit_Framework_MockObject_MockObject */ + protected $folder; + + /** @var \OCP\IUserSession|\PHPUnit_Framework_MockObject_MockObject */ + protected $session; + + /** @var \OCP\Notification\IManager|\PHPUnit_Framework_MockObject_MockObject */ + protected $notificationManager; + + protected function setUp() { + parent::setUp(); + + $this->commentsManager = $this->getMockBuilder('\OCP\Comments\ICommentsManager')->getMock(); + $this->folder = $this->getMockBuilder('\OCP\Files\Folder')->getMock(); + $this->session = $this->getMockBuilder('\OCP\IUserSession')->getMock(); + $this->notificationManager = $this->getMockBuilder('\OCP\Notification\IManager')->getMock(); + + $this->notificationsController = new Notifications( + 'comments', + $this->getMockBuilder('\OCP\IRequest')->getMock(), + $this->commentsManager, + $this->folder, + $this->getMockBuilder('\OCP\IURLGenerator')->getMock(), + $this->notificationManager, + $this->session + ); + } + + public function testViewSuccess() { + $comment = $this->getMockBuilder('\OCP\Comments\IComment')->getMock(); + $comment->expects($this->any()) + ->method('getObjectType') + ->will($this->returnValue('files')); + + $this->commentsManager->expects($this->any()) + ->method('get') + ->with('42') + ->will($this->returnValue($comment)); + + $file = $this->getMockBuilder('\OCP\Files\Node')->getMock(); + $file->expects($this->once()) + ->method('getParent') + ->will($this->returnValue($this->getMockBuilder('\OCP\Files\Folder')->getMock())); + + $this->folder->expects($this->once()) + ->method('getById') + ->will($this->returnValue([$file])); + + $this->session->expects($this->once()) + ->method('getUser') + ->will($this->returnValue($this->getMockBuilder('\OCP\IUser')->getMock())); + + $notification = $this->getMockBuilder('\OCP\Notification\INotification')->getMock(); + $notification->expects($this->any()) + ->method($this->anything()) + ->will($this->returnValue($notification)); + + $this->notificationManager->expects($this->once()) + ->method('createNotification') + ->will($this->returnValue($notification)); + $this->notificationManager->expects($this->once()) + ->method('markProcessed') + ->with($notification); + + $response = $this->notificationsController->view('42'); + $this->assertInstanceOf('\OCP\AppFramework\Http\RedirectResponse', $response); + } + + public function testViewInvalidComment() { + $this->commentsManager->expects($this->any()) + ->method('get') + ->with('42') + ->will($this->throwException(new NotFoundException())); + + $file = $this->getMockBuilder('\OCP\Files\Node')->getMock(); + $file->expects($this->never()) + ->method('getParent'); + + $this->folder->expects($this->never()) + ->method('getById'); + + $this->session->expects($this->never()) + ->method('getUser'); + + $this->notificationManager->expects($this->never()) + ->method('createNotification'); + $this->notificationManager->expects($this->never()) + ->method('markProcessed'); + + $response = $this->notificationsController->view('42'); + $this->assertInstanceOf('\OCP\AppFramework\Http\NotFoundResponse', $response); + } + + public function testViewNoFile() { + $comment = $this->getMockBuilder('\OCP\Comments\IComment')->getMock(); + $comment->expects($this->any()) + ->method('getObjectType') + ->will($this->returnValue('files')); + + $this->commentsManager->expects($this->any()) + ->method('get') + ->with('42') + ->will($this->returnValue($comment)); + + $this->folder->expects($this->once()) + ->method('getById') + ->will($this->returnValue([])); + + $this->session->expects($this->once()) + ->method('getUser') + ->will($this->returnValue($this->getMockBuilder('\OCP\IUser')->getMock())); + + $notification = $this->getMockBuilder('\OCP\Notification\INotification')->getMock(); + $notification->expects($this->any()) + ->method($this->anything()) + ->will($this->returnValue($notification)); + + $this->notificationManager->expects($this->once()) + ->method('createNotification') + ->will($this->returnValue($notification)); + $this->notificationManager->expects($this->once()) + ->method('markProcessed') + ->with($notification); + + $response = $this->notificationsController->view('42'); + $this->assertInstanceOf('\OCP\AppFramework\Http\NotFoundResponse', $response); + } +} diff --git a/apps/comments/tests/Unit/EventHandlerTest.php b/apps/comments/tests/Unit/EventHandlerTest.php new file mode 100644 index 00000000000..7a630329974 --- /dev/null +++ b/apps/comments/tests/Unit/EventHandlerTest.php @@ -0,0 +1,152 @@ + + * + * @author Arthur Schiwon + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +namespace OCA\Comments\Tests\Unit\Notification; + +use OCA\Comments\AppInfo\Application; +use OCA\Comments\EventHandler; +use OCP\Comments\CommentsEvent; +use OCP\Comments\IComment; +use OCA\Comments\Activity\Listener as ActivityListener; +use OCA\Comments\Notification\Listener as NotificationListener; +use OCP\IContainer; +use Test\TestCase; + +class EventHandlerTest extends TestCase { + /** @var EventHandler */ + protected $eventHandler; + + /** @var Application|\PHPUnit_Framework_MockObject_MockObject */ + protected $app; + + protected function setUp() { + parent::setUp(); + + $this->app = $this->getMockBuilder(Application::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->eventHandler = new EventHandler($this->app); + } + + public function testNotFiles() { + /** @var IComment|\PHPUnit_Framework_MockObject_MockObject $comment */ + $comment = $this->getMockBuilder(IComment::class)->getMock(); + $comment->expects($this->once()) + ->method('getObjectType') + ->willReturn('smiles'); + + /** @var CommentsEvent|\PHPUnit_Framework_MockObject_MockObject $event */ + $event = $this->getMockBuilder(CommentsEvent::class) + ->disableOriginalConstructor() + ->getMock(); + $event->expects($this->once()) + ->method('getComment') + ->willReturn($comment); + $event->expects($this->never()) + ->method('getEvent'); + + $this->eventHandler->handle($event); + } + + public function notHandledProvider() { + return [ + [CommentsEvent::EVENT_DELETE], + [CommentsEvent::EVENT_UPDATE] + ]; + } + + /** + * @dataProvider notHandledProvider + * @param $eventType + */ + public function testNotHandled($eventType) { + /** @var IComment|\PHPUnit_Framework_MockObject_MockObject $comment */ + $comment = $this->getMockBuilder(IComment::class)->getMock(); + $comment->expects($this->once()) + ->method('getObjectType') + ->willReturn('files'); + + /** @var CommentsEvent|\PHPUnit_Framework_MockObject_MockObject $event */ + $event = $this->getMockBuilder(CommentsEvent::class) + ->disableOriginalConstructor() + ->getMock(); + $event->expects($this->once()) + ->method('getComment') + ->willReturn($comment); + $event->expects($this->once()) + ->method('getEvent') + ->willReturn($eventType); + + // further processing does not happen, because $event methods cannot be + // access more than once. + $this->eventHandler->handle($event); + } + + public function testHandled() { + /** @var IComment|\PHPUnit_Framework_MockObject_MockObject $comment */ + $comment = $this->getMockBuilder(IComment::class)->getMock(); + $comment->expects($this->once()) + ->method('getObjectType') + ->willReturn('files'); + + /** @var CommentsEvent|\PHPUnit_Framework_MockObject_MockObject $event */ + $event = $this->getMockBuilder(CommentsEvent::class) + ->disableOriginalConstructor() + ->getMock(); + $event->expects($this->atLeastOnce()) + ->method('getComment') + ->willReturn($comment); + $event->expects($this->atLeastOnce()) + ->method('getEvent') + ->willReturn(CommentsEvent::EVENT_ADD); + + $notificationListener = $this->getMockBuilder(NotificationListener::class) + ->disableOriginalConstructor() + ->getMock(); + $notificationListener->expects($this->once()) + ->method('evaluate') + ->with($event); + + $activityListener = $this->getMockBuilder(ActivityListener::class) + ->disableOriginalConstructor() + ->getMock(); + $activityListener->expects($this->once()) + ->method('commentEvent') + ->with($event); + + /** @var IContainer|\PHPUnit_Framework_MockObject_MockObject $c */ + $c = $this->getMockBuilder(IContainer::class)->getMock(); + $c->expects($this->exactly(2)) + ->method('query') + ->withConsecutive([NotificationListener::class], [ActivityListener::class]) + ->willReturnOnConsecutiveCalls($notificationListener, $activityListener); + + $this->app->expects($this->once()) + ->method('getContainer') + ->willReturn($c); + + $this->eventHandler->handle($event); + } + +} diff --git a/apps/comments/tests/Unit/Notification/ListenerTest.php b/apps/comments/tests/Unit/Notification/ListenerTest.php new file mode 100644 index 00000000000..98a0375e49a --- /dev/null +++ b/apps/comments/tests/Unit/Notification/ListenerTest.php @@ -0,0 +1,360 @@ + + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\Comments\Tests\Unit\Notification; + +use OCA\Comments\Notification\Listener; +use OCP\Comments\CommentsEvent; +use OCP\Comments\IComment; +use OCP\IURLGenerator; +use OCP\IUserManager; +use OCP\Notification\IManager; +use OCP\Notification\INotification; +use Test\TestCase; + +class ListenerTest extends TestCase { + /** @var IManager|\PHPUnit_Framework_MockObject_MockObject */ + protected $notificationManager; + + /** @var IUserManager|\PHPUnit_Framework_MockObject_MockObject */ + protected $userManager; + + /** @var IURLGenerator|\PHPUnit_Framework_MockObject_MockObject */ + protected $urlGenerator; + + /** @var Listener */ + protected $listener; + + protected function setUp() { + parent::setUp(); + + $this->notificationManager = $this->getMockBuilder('\OCP\Notification\IManager')->getMock(); + $this->userManager = $this->getMockBuilder('\OCP\IUserManager')->getMock(); + $this->urlGenerator = $this->getMockBuilder('OCP\IURLGenerator')->getMock(); + + $this->listener = new Listener( + $this->notificationManager, + $this->userManager, + $this->urlGenerator + ); + } + + public function eventProvider() { + return [ + [CommentsEvent::EVENT_ADD, 'notify'], + [CommentsEvent::EVENT_DELETE, 'markProcessed'] + ]; + } + + /** + * @dataProvider eventProvider + * @param string $eventType + * @param string $notificationMethod + */ + public function testEvaluate($eventType, $notificationMethod) { + $message = '@foobar and @barfoo you should know, @foo@bar.com is valid' . + ' and so is @bar@foo.org@foobar.io I hope that clarifies everything.' . + ' cc @23452-4333-54353-2342 @yolo!'; + + /** @var IComment|\PHPUnit_Framework_MockObject_MockObject $comment */ + $comment = $this->getMockBuilder('\OCP\Comments\IComment')->getMock(); + $comment->expects($this->any()) + ->method('getObjectType') + ->will($this->returnValue('files')); + $comment->expects($this->any()) + ->method('getCreationDateTime') + ->will($this->returnValue(new \DateTime())); + $comment->expects($this->once()) + ->method('getMessage') + ->will($this->returnValue($message)); + + /** @var CommentsEvent|\PHPUnit_Framework_MockObject_MockObject $event */ + $event = $this->getMockBuilder('\OCP\Comments\CommentsEvent') + ->disableOriginalConstructor() + ->getMock(); + $event->expects($this->once()) + ->method('getComment') + ->will($this->returnValue($comment)); + $event->expects(($this->any())) + ->method(('getEvent')) + ->will($this->returnValue($eventType)); + + /** @var INotification|\PHPUnit_Framework_MockObject_MockObject $notification */ + $notification = $this->getMockBuilder('\OCP\Notification\INotification')->getMock(); + $notification->expects($this->any()) + ->method($this->anything()) + ->will($this->returnValue($notification)); + $notification->expects($this->exactly(6)) + ->method('setUser'); + + $this->notificationManager->expects($this->once()) + ->method('createNotification') + ->will($this->returnValue($notification)); + $this->notificationManager->expects($this->exactly(6)) + ->method($notificationMethod) + ->with($this->isInstanceOf('\OCP\Notification\INotification')); + + $this->userManager->expects($this->exactly(6)) + ->method('userExists') + ->withConsecutive( + ['foobar'], + ['barfoo'], + ['foo@bar.com'], + ['bar@foo.org@foobar.io'], + ['23452-4333-54353-2342'], + ['yolo'] + ) + ->will($this->returnValue(true)); + + $this->listener->evaluate($event); + } + + /** + * @dataProvider eventProvider + * @param string $eventType + */ + public function testEvaluateNoMentions($eventType) { + $message = 'a boring comment without mentions'; + + /** @var IComment|\PHPUnit_Framework_MockObject_MockObject $comment */ + $comment = $this->getMockBuilder('\OCP\Comments\IComment')->getMock(); + $comment->expects($this->any()) + ->method('getObjectType') + ->will($this->returnValue('files')); + $comment->expects($this->any()) + ->method('getCreationDateTime') + ->will($this->returnValue(new \DateTime())); + $comment->expects($this->once()) + ->method('getMessage') + ->will($this->returnValue($message)); + + /** @var CommentsEvent|\PHPUnit_Framework_MockObject_MockObject $event */ + $event = $this->getMockBuilder('\OCP\Comments\CommentsEvent') + ->disableOriginalConstructor() + ->getMock(); + $event->expects($this->once()) + ->method('getComment') + ->will($this->returnValue($comment)); + $event->expects(($this->any())) + ->method(('getEvent')) + ->will($this->returnValue($eventType)); + + $this->notificationManager->expects($this->never()) + ->method('createNotification'); + $this->notificationManager->expects($this->never()) + ->method('notify'); + $this->notificationManager->expects($this->never()) + ->method('markProcessed'); + + $this->userManager->expects($this->never()) + ->method('userExists'); + + $this->listener->evaluate($event); + } + + public function testUnsupportedCommentObjectType() { + /** @var IComment|\PHPUnit_Framework_MockObject_MockObject $comment */ + $comment = $this->getMockBuilder('\OCP\Comments\IComment')->getMock(); + $comment->expects($this->once()) + ->method('getObjectType') + ->will($this->returnValue('vcards')); + $comment->expects($this->never()) + ->method('getMessage'); + + /** @var CommentsEvent|\PHPUnit_Framework_MockObject_MockObject $event */ + $event = $this->getMockBuilder('\OCP\Comments\CommentsEvent') + ->disableOriginalConstructor() + ->getMock(); + $event->expects($this->once()) + ->method('getComment') + ->will($this->returnValue($comment)); + $event->expects(($this->any())) + ->method(('getEvent')) + ->will($this->returnValue(CommentsEvent::EVENT_ADD)); + + $this->listener->evaluate($event); + } + + public function testEvaluateUserDoesNotExist() { + $message = '@foobar bla bla bla'; + + /** @var IComment|\PHPUnit_Framework_MockObject_MockObject $comment */ + $comment = $this->getMockBuilder('\OCP\Comments\IComment')->getMock(); + $comment->expects($this->any()) + ->method('getObjectType') + ->will($this->returnValue('files')); + $comment->expects($this->any()) + ->method('getCreationDateTime') + ->will($this->returnValue(new \DateTime())); + $comment->expects($this->once()) + ->method('getMessage') + ->will($this->returnValue($message)); + + /** @var CommentsEvent|\PHPUnit_Framework_MockObject_MockObject $event */ + $event = $this->getMockBuilder('\OCP\Comments\CommentsEvent') + ->disableOriginalConstructor() + ->getMock(); + $event->expects($this->once()) + ->method('getComment') + ->will($this->returnValue($comment)); + $event->expects(($this->any())) + ->method(('getEvent')) + ->will($this->returnValue(CommentsEvent::EVENT_ADD)); + + /** @var INotification|\PHPUnit_Framework_MockObject_MockObject $notification */ + $notification = $this->getMockBuilder('\OCP\Notification\INotification')->getMock(); + $notification->expects($this->any()) + ->method($this->anything()) + ->will($this->returnValue($notification)); + $notification->expects($this->never()) + ->method('setUser'); + + $this->notificationManager->expects($this->once()) + ->method('createNotification') + ->will($this->returnValue($notification)); + $this->notificationManager->expects($this->never()) + ->method('notify'); + + $this->userManager->expects($this->once()) + ->method('userExists') + ->withConsecutive( + ['foobar'] + ) + ->will($this->returnValue(false)); + + $this->listener->evaluate($event); + } + + /** + * @dataProvider eventProvider + * @param string $eventType + * @param string $notificationMethod + */ + public function testEvaluateOneMentionPerUser($eventType, $notificationMethod) { + $message = '@foobar bla bla bla @foobar'; + + /** @var IComment|\PHPUnit_Framework_MockObject_MockObject $comment */ + $comment = $this->getMockBuilder('\OCP\Comments\IComment')->getMock(); + $comment->expects($this->any()) + ->method('getObjectType') + ->will($this->returnValue('files')); + $comment->expects($this->any()) + ->method('getCreationDateTime') + ->will($this->returnValue(new \DateTime())); + $comment->expects($this->once()) + ->method('getMessage') + ->will($this->returnValue($message)); + + /** @var CommentsEvent|\PHPUnit_Framework_MockObject_MockObject $event */ + $event = $this->getMockBuilder('\OCP\Comments\CommentsEvent') + ->disableOriginalConstructor() + ->getMock(); + $event->expects($this->once()) + ->method('getComment') + ->will($this->returnValue($comment)); + $event->expects(($this->any())) + ->method(('getEvent')) + ->will($this->returnValue($eventType)); + + /** @var INotification|\PHPUnit_Framework_MockObject_MockObject $notification */ + $notification = $this->getMockBuilder('\OCP\Notification\INotification')->getMock(); + $notification->expects($this->any()) + ->method($this->anything()) + ->will($this->returnValue($notification)); + $notification->expects($this->once()) + ->method('setUser'); + + $this->notificationManager->expects($this->once()) + ->method('createNotification') + ->will($this->returnValue($notification)); + $this->notificationManager->expects($this->once()) + ->method($notificationMethod) + ->with($this->isInstanceOf('\OCP\Notification\INotification')); + + $this->userManager->expects($this->once()) + ->method('userExists') + ->withConsecutive( + ['foobar'] + ) + ->will($this->returnValue(true)); + + $this->listener->evaluate($event); + } + + /** + * @dataProvider eventProvider + * @param string $eventType + */ + public function testEvaluateNoSelfMention($eventType) { + $message = '@foobar bla bla bla'; + + /** @var IComment|\PHPUnit_Framework_MockObject_MockObject $comment */ + $comment = $this->getMockBuilder('\OCP\Comments\IComment')->getMock(); + $comment->expects($this->any()) + ->method('getObjectType') + ->will($this->returnValue('files')); + $comment->expects($this->any()) + ->method('getActorType') + ->will($this->returnValue('users')); + $comment->expects($this->any()) + ->method('getActorId') + ->will($this->returnValue('foobar')); + $comment->expects($this->any()) + ->method('getCreationDateTime') + ->will($this->returnValue(new \DateTime())); + $comment->expects($this->once()) + ->method('getMessage') + ->will($this->returnValue($message)); + + /** @var CommentsEvent|\PHPUnit_Framework_MockObject_MockObject $event */ + $event = $this->getMockBuilder('\OCP\Comments\CommentsEvent') + ->disableOriginalConstructor() + ->getMock(); + $event->expects($this->once()) + ->method('getComment') + ->will($this->returnValue($comment)); + $event->expects(($this->any())) + ->method(('getEvent')) + ->will($this->returnValue($eventType)); + + /** @var INotification|\PHPUnit_Framework_MockObject_MockObject $notification */ + $notification = $this->getMockBuilder('\OCP\Notification\INotification')->getMock(); + $notification->expects($this->any()) + ->method($this->anything()) + ->will($this->returnValue($notification)); + $notification->expects($this->never()) + ->method('setUser'); + + $this->notificationManager->expects($this->once()) + ->method('createNotification') + ->will($this->returnValue($notification)); + $this->notificationManager->expects($this->never()) + ->method('notify'); + $this->notificationManager->expects($this->never()) + ->method('markProcessed'); + + $this->userManager->expects($this->never()) + ->method('userExists'); + + $this->listener->evaluate($event); + } + +} diff --git a/apps/comments/tests/Unit/Notification/NotifierTest.php b/apps/comments/tests/Unit/Notification/NotifierTest.php new file mode 100644 index 00000000000..f3cc0153ab1 --- /dev/null +++ b/apps/comments/tests/Unit/Notification/NotifierTest.php @@ -0,0 +1,499 @@ + + * + * @author Arthur Schiwon + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +namespace OCA\Comments\Tests\Unit\Notification; + +use OCA\Comments\Notification\Notifier; +use OCP\Comments\IComment; +use OCP\Comments\ICommentsManager; +use OCP\Comments\NotFoundException; +use OCP\Files\Folder; +use OCP\Files\Node; +use OCP\IL10N; +use OCP\IUser; +use OCP\IUserManager; +use OCP\L10N\IFactory; +use OCP\Notification\INotification; +use Test\TestCase; + +class NotifierTest extends TestCase { + + /** @var Notifier */ + protected $notifier; + + /** @var IFactory|\PHPUnit_Framework_MockObject_MockObject */ + protected $l10nFactory; + + /** @var Folder|\PHPUnit_Framework_MockObject_MockObject */ + protected $folder; + + /** @var ICommentsManager|\PHPUnit_Framework_MockObject_MockObject */ + protected $commentsManager; + + /** @var IUserManager|\PHPUnit_Framework_MockObject_MockObject */ + protected $userManager; + + + /** @var string */ + protected $lc = 'tlh_KX'; + + /** @var INotification|\PHPUnit_Framework_MockObject_MockObject */ + protected $notification; + + /** @var IL10N|\PHPUnit_Framework_MockObject_MockObject */ + protected $l; + + /** @var IComment|\PHPUnit_Framework_MockObject_MockObject */ + protected $comment; + + protected function setUp() { + parent::setUp(); + + $this->l10nFactory = $this->getMockBuilder('OCP\L10N\IFactory')->getMock(); + $this->folder = $this->getMockBuilder('OCP\Files\Folder')->getMock(); + $this->commentsManager = $this->getMockBuilder('OCP\Comments\ICommentsManager')->getMock(); + $this->userManager = $this->getMockBuilder('OCP\IUserManager')->getMock(); + + $this->notifier = new Notifier( + $this->l10nFactory, + $this->folder, + $this->commentsManager, + $this->userManager + ); + + $this->l = $this->getMockBuilder('OCP\IL10N')->getMock(); + $this->notification = $this->getMockBuilder('OCP\Notification\INotification')->getMock(); + $this->comment = $this->getMockBuilder('OCP\Comments\IComment')->getMock(); + } + + public function testPrepareSuccess() { + $fileName = 'Gre\'thor.odp'; + $displayName = 'Huraga'; + $message = 'You were mentioned in a comment on "Gre\'thor.odp" by Huraga.'; + + /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ + $user = $this->getMockBuilder('OCP\IUser')->getMock(); + $user->expects($this->once()) + ->method('getDisplayName') + ->willReturn($displayName); + + /** @var Node|\PHPUnit_Framework_MockObject_MockObject */ + $node = $this->getMockBuilder('OCP\Files\Node')->getMock(); + $node + ->expects($this->once()) + ->method('getName') + ->willReturn($fileName); + + $this->folder + ->expects($this->once()) + ->method('getById') + ->with('678') + ->willReturn([$node]); + + $this->notification + ->expects($this->once()) + ->method('getApp') + ->willReturn('comments'); + $this->notification + ->expects($this->once()) + ->method('getSubject') + ->willReturn('mention'); + $this->notification + ->expects($this->once()) + ->method('getSubjectParameters') + ->willReturn(['files', '678']); + $this->notification + ->expects($this->once()) + ->method('setParsedSubject') + ->with($message); + + $this->l + ->expects($this->once()) + ->method('t') + ->with('You were mentioned in a comment on "%s" by %s.', [$fileName, $displayName]) + ->willReturn($message); + + $this->l10nFactory + ->expects($this->once()) + ->method('get') + ->willReturn($this->l); + + $this->comment + ->expects($this->any()) + ->method('getActorId') + ->willReturn('huraga'); + $this->comment + ->expects($this->any()) + ->method('getActorType') + ->willReturn('users'); + + $this->commentsManager + ->expects(($this->once())) + ->method('get') + ->willReturn($this->comment); + + $this->userManager + ->expects($this->once()) + ->method('get') + ->with('huraga') + ->willReturn($user); + + $this->notifier->prepare($this->notification, $this->lc); + } + + public function testPrepareSuccessDeletedUser() { + $fileName = 'Gre\'thor.odp'; + $displayName = 'a now deleted user'; + $message = 'You were mentioned in a comment on "Gre\'thor.odp" by a now deleted user.'; + + /** @var Node|\PHPUnit_Framework_MockObject_MockObject */ + $node = $this->getMockBuilder('OCP\Files\Node')->getMock(); + $node + ->expects($this->once()) + ->method('getName') + ->willReturn($fileName); + + $this->folder + ->expects($this->once()) + ->method('getById') + ->with('678') + ->willReturn([$node]); + + $this->notification + ->expects($this->once()) + ->method('getApp') + ->willReturn('comments'); + $this->notification + ->expects($this->once()) + ->method('getSubject') + ->willReturn('mention'); + $this->notification + ->expects($this->once()) + ->method('getSubjectParameters') + ->willReturn(['files', '678']); + $this->notification + ->expects($this->once()) + ->method('setParsedSubject') + ->with($message); + + $this->l + ->expects($this->once()) + ->method('t') + ->with('You were mentioned in a comment on "%s" by a now deleted user.', [ $fileName ]) + ->willReturn($message); + + $this->l10nFactory + ->expects($this->once()) + ->method('get') + ->willReturn($this->l); + + $this->comment + ->expects($this->any()) + ->method('getActorId') + ->willReturn('huraga'); + $this->comment + ->expects($this->any()) + ->method('getActorType') + ->willReturn(ICommentsManager::DELETED_USER); + + $this->commentsManager + ->expects(($this->once())) + ->method('get') + ->willReturn($this->comment); + + $this->userManager + ->expects($this->never()) + ->method('get'); + + $this->notifier->prepare($this->notification, $this->lc); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testPrepareDifferentApp() { + $this->folder + ->expects($this->never()) + ->method('getById'); + + $this->notification + ->expects($this->once()) + ->method('getApp') + ->willReturn('constructions'); + $this->notification + ->expects($this->never()) + ->method('getSubject'); + $this->notification + ->expects($this->never()) + ->method('getSubjectParameters'); + $this->notification + ->expects($this->never()) + ->method('setParsedSubject'); + + $this->l10nFactory + ->expects($this->never()) + ->method('get'); + + $this->commentsManager + ->expects(($this->never())) + ->method('get'); + + $this->userManager + ->expects($this->never()) + ->method('get'); + + $this->notifier->prepare($this->notification, $this->lc); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testPrepareNotFound() { + $this->folder + ->expects($this->never()) + ->method('getById'); + + $this->notification + ->expects($this->once()) + ->method('getApp') + ->willReturn('comments'); + $this->notification + ->expects($this->never()) + ->method('getSubject'); + $this->notification + ->expects($this->never()) + ->method('getSubjectParameters'); + $this->notification + ->expects($this->never()) + ->method('setParsedSubject'); + + $this->l10nFactory + ->expects($this->never()) + ->method('get'); + + $this->commentsManager + ->expects(($this->once())) + ->method('get') + ->willThrowException(new NotFoundException()); + + $this->userManager + ->expects($this->never()) + ->method('get'); + + $this->notifier->prepare($this->notification, $this->lc); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testPrepareDifferentSubject() { + $displayName = 'Huraga'; + + /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ + $user = $this->getMockBuilder('OCP\IUser')->getMock(); + $user->expects($this->once()) + ->method('getDisplayName') + ->willReturn($displayName); + + $this->folder + ->expects($this->never()) + ->method('getById'); + + $this->notification + ->expects($this->once()) + ->method('getApp') + ->willReturn('comments'); + $this->notification + ->expects($this->once()) + ->method('getSubject') + ->willReturn('unlike'); + $this->notification + ->expects($this->never()) + ->method('getSubjectParameters'); + $this->notification + ->expects($this->never()) + ->method('setParsedSubject'); + + $this->l + ->expects($this->never()) + ->method('t'); + + $this->l10nFactory + ->expects($this->once()) + ->method('get') + ->willReturn($this->l); + + $this->comment + ->expects($this->any()) + ->method('getActorId') + ->willReturn('huraga'); + $this->comment + ->expects($this->any()) + ->method('getActorType') + ->willReturn('users'); + + $this->commentsManager + ->expects(($this->once())) + ->method('get') + ->willReturn($this->comment); + + $this->userManager + ->expects($this->once()) + ->method('get') + ->with('huraga') + ->willReturn($user); + + $this->notifier->prepare($this->notification, $this->lc); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testPrepareNotFiles() { + $displayName = 'Huraga'; + + /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ + $user = $this->getMockBuilder('OCP\IUser')->getMock(); + $user->expects($this->once()) + ->method('getDisplayName') + ->willReturn($displayName); + + $this->folder + ->expects($this->never()) + ->method('getById'); + + $this->notification + ->expects($this->once()) + ->method('getApp') + ->willReturn('comments'); + $this->notification + ->expects($this->once()) + ->method('getSubject') + ->willReturn('mention'); + $this->notification + ->expects($this->once()) + ->method('getSubjectParameters') + ->willReturn(['ships', '678']); + $this->notification + ->expects($this->never()) + ->method('setParsedSubject'); + + $this->l + ->expects($this->never()) + ->method('t'); + + $this->l10nFactory + ->expects($this->once()) + ->method('get') + ->willReturn($this->l); + + $this->comment + ->expects($this->any()) + ->method('getActorId') + ->willReturn('huraga'); + $this->comment + ->expects($this->any()) + ->method('getActorType') + ->willReturn('users'); + + $this->commentsManager + ->expects(($this->once())) + ->method('get') + ->willReturn($this->comment); + + $this->userManager + ->expects($this->once()) + ->method('get') + ->with('huraga') + ->willReturn($user); + + $this->notifier->prepare($this->notification, $this->lc); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testPrepareUnresolvableFileID() { + $displayName = 'Huraga'; + + /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ + $user = $this->getMockBuilder('OCP\IUser')->getMock(); + $user->expects($this->once()) + ->method('getDisplayName') + ->willReturn($displayName); + + $this->folder + ->expects($this->once()) + ->method('getById') + ->with('678') + ->willReturn([]); + + $this->notification + ->expects($this->once()) + ->method('getApp') + ->willReturn('comments'); + $this->notification + ->expects($this->once()) + ->method('getSubject') + ->willReturn('mention'); + $this->notification + ->expects($this->once()) + ->method('getSubjectParameters') + ->willReturn(['files', '678']); + $this->notification + ->expects($this->never()) + ->method('setParsedSubject'); + + $this->l + ->expects($this->never()) + ->method('t'); + + $this->l10nFactory + ->expects($this->once()) + ->method('get') + ->willReturn($this->l); + + $this->comment + ->expects($this->any()) + ->method('getActorId') + ->willReturn('huraga'); + $this->comment + ->expects($this->any()) + ->method('getActorType') + ->willReturn('users'); + + $this->commentsManager + ->expects(($this->once())) + ->method('get') + ->willReturn($this->comment); + + $this->userManager + ->expects($this->once()) + ->method('get') + ->with('huraga') + ->willReturn($user); + + $this->notifier->prepare($this->notification, $this->lc); + } + +} diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php index b6f8d8f458d..3f2212a7f24 100644 --- a/lib/private/AppFramework/DependencyInjection/DIContainer.php +++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php @@ -161,6 +161,10 @@ class DIContainer extends SimpleContainer implements IAppContainer { return $this->getServer()->getRootFolder(); }); + $this->registerService('OCP\\Files\\Folder', function() { + return $this->getServer()->getUserFolder(); + }); + $this->registerService('OCP\\Http\\Client\\IClientService', function($c) { return $this->getServer()->getHTTPClientService(); }); diff --git a/lib/private/Comments/Manager.php b/lib/private/Comments/Manager.php index 59678775d88..f7b23dd5f58 100644 --- a/lib/private/Comments/Manager.php +++ b/lib/private/Comments/Manager.php @@ -26,6 +26,7 @@ namespace OC\Comments; use Doctrine\DBAL\Exception\DriverException; use OCP\Comments\CommentsEvent; use OCP\Comments\IComment; +use OCP\Comments\ICommentsEventHandler; use OCP\Comments\ICommentsManager; use OCP\Comments\NotFoundException; use OCP\DB\QueryBuilder\IQueryBuilder; @@ -33,7 +34,6 @@ use OCP\IDBConnection; use OCP\IConfig; use OCP\ILogger; use OCP\IUser; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; class Manager implements ICommentsManager { @@ -46,30 +46,30 @@ class Manager implements ICommentsManager { /** @var IConfig */ protected $config; - /** @var EventDispatcherInterface */ - protected $dispatcher; - /** @var IComment[] */ protected $commentsCache = []; + /** @var \Closure[] */ + protected $eventHandlerClosures = []; + + /** @var ICommentsEventHandler[] */ + protected $eventHandlers = []; + /** * Manager constructor. * * @param IDBConnection $dbConn * @param ILogger $logger * @param IConfig $config - * @param EventDispatcherInterface $dispatcher */ public function __construct( IDBConnection $dbConn, ILogger $logger, - IConfig $config, - EventDispatcherInterface $dispatcher + IConfig $config ) { $this->dbConn = $dbConn; $this->logger = $logger; $this->config = $config; - $this->dispatcher = $dispatcher; } /** @@ -455,10 +455,7 @@ class Manager implements ICommentsManager { } if ($affectedRows > 0 && $comment instanceof IComment) { - $this->dispatcher->dispatch(CommentsEvent::EVENT_DELETE, new CommentsEvent( - CommentsEvent::EVENT_DELETE, - $comment - )); + $this->sendEvent(CommentsEvent::EVENT_DELETE, $comment); } return ($affectedRows > 0); @@ -525,13 +522,9 @@ class Manager implements ICommentsManager { if ($affectedRows > 0) { $comment->setId(strval($qb->getLastInsertId())); + $this->sendEvent(CommentsEvent::EVENT_ADD, $comment); } - $this->dispatcher->dispatch(CommentsEvent::EVENT_ADD, new CommentsEvent( - CommentsEvent::EVENT_ADD, - $comment - )); - return $affectedRows > 0; } @@ -565,10 +558,7 @@ class Manager implements ICommentsManager { throw new NotFoundException('Comment to update does ceased to exist'); } - $this->dispatcher->dispatch(CommentsEvent::EVENT_UPDATE, new CommentsEvent( - CommentsEvent::EVENT_UPDATE, - $comment - )); + $this->sendEvent(CommentsEvent::EVENT_UPDATE, $comment); return $affectedRows > 0; } @@ -751,4 +741,51 @@ class Manager implements ICommentsManager { } return ($affectedRows > 0); } + + /** + * registers an Entity to the manager, so event notifications can be send + * to consumers of the comments infrastructure + * + * @param \Closure $closure + */ + public function registerEventHandler(\Closure $closure) { + $this->eventHandlerClosures[] = $closure; + $this->eventHandlers = []; + } + + /** + * returns valid, registered entities + * + * @return \OCP\Comments\ICommentsEventHandler[] + */ + private function getEventHandlers() { + if(!empty($this->eventHandlers)) { + return $this->eventHandlers; + } + + $this->eventHandlers = []; + foreach ($this->eventHandlerClosures as $name => $closure) { + $entity = $closure(); + if (!($entity instanceof ICommentsEventHandler)) { + throw new \InvalidArgumentException('The given entity does not implement the ICommentsEntity interface'); + } + $this->eventHandlers[$name] = $entity; + } + + return $this->eventHandlers; + } + + /** + * sends notifications to the registered entities + * + * @param $eventType + * @param IComment $comment + */ + private function sendEvent($eventType, IComment $comment) { + $entities = $this->getEventHandlers(); + $event = new CommentsEvent($eventType, $comment); + foreach ($entities as $entity) { + $entity->handle($event); + } + } } diff --git a/lib/public/Comments/ICommentsEventHandler.php b/lib/public/Comments/ICommentsEventHandler.php new file mode 100644 index 00000000000..33524199012 --- /dev/null +++ b/lib/public/Comments/ICommentsEventHandler.php @@ -0,0 +1,39 @@ + + * + * @author Arthur Schiwon + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +namespace OCP\Comments; + +/** + * Interface ICommentsEventHandler + * + * @package OCP\Comments + * @since 9.2.0 + */ +interface ICommentsEventHandler { + + /** + * @param CommentsEvent $event + * @since 9.2.0 + */ + public function handle(CommentsEvent $event); +} diff --git a/lib/public/Comments/ICommentsManager.php b/lib/public/Comments/ICommentsManager.php index f2edaaa5eaf..98169fb335f 100644 --- a/lib/public/Comments/ICommentsManager.php +++ b/lib/public/Comments/ICommentsManager.php @@ -237,4 +237,13 @@ interface ICommentsManager { */ public function deleteReadMarksOnObject($objectType, $objectId); + /** + * registers an Entity to the manager, so event notifications can be send + * to consumers of the comments infrastructure + * + * @param \Closure $closure + * @since 9.2.0 + */ + public function registerEventHandler(\Closure $closure); + } diff --git a/tests/lib/Comments/FakeManager.php b/tests/lib/Comments/FakeManager.php index 7186529e718..7cd146e7cb2 100644 --- a/tests/lib/Comments/FakeManager.php +++ b/tests/lib/Comments/FakeManager.php @@ -38,4 +38,6 @@ class FakeManager implements \OCP\Comments\ICommentsManager { public function deleteReadMarksFromUser(\OCP\IUser $user) {} public function deleteReadMarksOnObject($objectType, $objectId) {} + + public function registerEventHandler(\Closure $closure) {} } diff --git a/tests/lib/Comments/ManagerTest.php b/tests/lib/Comments/ManagerTest.php index 730d82d9d0d..9c0f791c0ca 100644 --- a/tests/lib/Comments/ManagerTest.php +++ b/tests/lib/Comments/ManagerTest.php @@ -2,6 +2,9 @@ namespace Test\Comments; +use OC\Comments\Comment; +use OCP\Comments\CommentsEvent; +use OCP\Comments\ICommentsEventHandler; use OCP\Comments\ICommentsManager; use OCP\IUser; use Test\TestCase; @@ -355,7 +358,7 @@ class ManagerTest extends TestCase { public function testSaveNew() { $manager = $this->getManager(); - $comment = new \OC\Comments\Comment(); + $comment = new Comment(); $comment ->setActor('users', 'alice') ->setObject('files', 'file64') @@ -375,7 +378,7 @@ class ManagerTest extends TestCase { public function testSaveUpdate() { $manager = $this->getManager(); - $comment = new \OC\Comments\Comment(); + $comment = new Comment(); $comment ->setActor('users', 'alice') ->setObject('files', 'file64') @@ -396,7 +399,7 @@ class ManagerTest extends TestCase { */ public function testSaveUpdateException() { $manager = $this->getManager(); - $comment = new \OC\Comments\Comment(); + $comment = new Comment(); $comment ->setActor('users', 'alice') ->setObject('files', 'file64') @@ -415,7 +418,7 @@ class ManagerTest extends TestCase { */ public function testSaveIncomplete() { $manager = $this->getManager(); - $comment = new \OC\Comments\Comment(); + $comment = new Comment(); $comment->setMessage('from no one to nothing'); $manager->save($comment); } @@ -426,7 +429,7 @@ class ManagerTest extends TestCase { $manager = $this->getManager(); for($i = 0; $i < 3; $i++) { - $comment = new \OC\Comments\Comment(); + $comment = new Comment(); $comment ->setActor('users', 'alice') ->setObject('files', 'file64') @@ -630,4 +633,35 @@ class ManagerTest extends TestCase { $this->assertNull($dateTimeGet); } + public function testSendEvent() { + $handler1 = $this->getMockBuilder(ICommentsEventHandler::class)->getMock(); + $handler1->expects($this->exactly(3)) + ->method('handle'); + + $handler2 = $this->getMockBuilder(ICommentsEventHandler::class)->getMock(); + $handler1->expects($this->exactly(3)) + ->method('handle'); + + $manager = $this->getManager(); + $manager->registerEventHandler(function () use ($handler1) {return $handler1; }); + $manager->registerEventHandler(function () use ($handler2) {return $handler2; }); + + $comment = new Comment(); + $comment + ->setActor('users', 'alice') + ->setObject('files', 'file64') + ->setMessage('very beautiful, I am impressed!') + ->setVerb('comment'); + + // Add event + $manager->save($comment); + + // Update event + $comment->setMessage('Different topic'); + $manager->save($comment); + + // Delete event + $manager->delete($comment->getId()); + } + }