From e1073cf442613ac92878c8ded30a33db35b30e14 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Mon, 9 May 2016 10:02:07 +0200 Subject: [PATCH 1/7] 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 df41bdfa32..771b35d9c6 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 0000000000..ab751ddb2a --- /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 0000000000..f168779cd0 --- /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 0000000000..f76f160534 --- /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 0000000000..a32bb8ffa7 --- /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 0000000000..5e979fd9bf --- /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 0000000000..df05f29730 --- /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 0000000000..c11d785749 --- /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 0000000000..5ab3978821 --- /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 0000000000..7a63032997 --- /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 0000000000..98a0375e49 --- /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 0000000000..f3cc0153ab --- /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 b6f8d8f458..3f2212a7f2 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 59678775d8..f7b23dd5f5 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 0000000000..3352419901 --- /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 f2edaaa5ea..98169fb335 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 7186529e71..7cd146e7cb 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 730d82d9d0..9c0f791c0c 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()); + } + } From 522b053466aac53a0d3e329f15a29bb1ddc359a3 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Tue, 27 Sep 2016 14:39:35 +0200 Subject: [PATCH 2/7] update autoloading map Signed-off-by: Arthur Schiwon --- lib/composer/composer/autoload_classmap.php | 1 + lib/composer/composer/autoload_static.php | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 0ff46de07d..5cedef83c6 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -61,6 +61,7 @@ return array( 'OCP\\Comments\\CommentsEntityEvent' => $baseDir . '/lib/public/Comments/CommentsEntityEvent.php', 'OCP\\Comments\\CommentsEvent' => $baseDir . '/lib/public/Comments/CommentsEvent.php', 'OCP\\Comments\\IComment' => $baseDir . '/lib/public/Comments/IComment.php', + 'OCP\\Comments\\ICommentsEventHandler' => $baseDir . '/lib/public/Comments/ICommentsEventHandler.php', 'OCP\\Comments\\ICommentsManager' => $baseDir . '/lib/public/Comments/ICommentsManager.php', 'OCP\\Comments\\ICommentsManagerFactory' => $baseDir . '/lib/public/Comments/ICommentsManagerFactory.php', 'OCP\\Comments\\IllegalIDChangeException' => $baseDir . '/lib/public/Comments/IllegalIDChangeException.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 24058a22ed..bce3916c8a 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -91,6 +91,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\Comments\\CommentsEntityEvent' => __DIR__ . '/../../..' . '/lib/public/Comments/CommentsEntityEvent.php', 'OCP\\Comments\\CommentsEvent' => __DIR__ . '/../../..' . '/lib/public/Comments/CommentsEvent.php', 'OCP\\Comments\\IComment' => __DIR__ . '/../../..' . '/lib/public/Comments/IComment.php', + 'OCP\\Comments\\ICommentsEventHandler' => __DIR__ . '/../../..' . '/lib/public/Comments/ICommentsEventHandler.php', 'OCP\\Comments\\ICommentsManager' => __DIR__ . '/../../..' . '/lib/public/Comments/ICommentsManager.php', 'OCP\\Comments\\ICommentsManagerFactory' => __DIR__ . '/../../..' . '/lib/public/Comments/ICommentsManagerFactory.php', 'OCP\\Comments\\IllegalIDChangeException' => __DIR__ . '/../../..' . '/lib/public/Comments/IllegalIDChangeException.php', From 006da9afd725060d48fd81893094256ddced0099 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Fri, 7 Oct 2016 17:27:33 +0200 Subject: [PATCH 3/7] use different files route/controller Signed-off-by: Arthur Schiwon --- apps/comments/lib/Controller/Notifications.php | 8 ++------ apps/comments/tests/Unit/Controller/NotificationsTest.php | 7 ------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/apps/comments/lib/Controller/Notifications.php b/apps/comments/lib/Controller/Notifications.php index f76f160534..c2a8175d17 100644 --- a/apps/comments/lib/Controller/Notifications.php +++ b/apps/comments/lib/Controller/Notifications.php @@ -102,13 +102,9 @@ class Notifications extends Controller { return new NotFoundResponse(); } - $dir = $this->folder->getRelativePath($files[0]->getParent()->getPath()); $url = $this->urlGenerator->linkToRouteAbsolute( - 'files.view.index', - [ - 'dir' => $dir, - 'scrollto' => $files[0]->getName() - ] + 'files.viewcontroller.showFile', + [ 'fileid' => $comment->getObjectId() ] ); $this->markProcessed($comment); diff --git a/apps/comments/tests/Unit/Controller/NotificationsTest.php b/apps/comments/tests/Unit/Controller/NotificationsTest.php index 5ab3978821..e887900a61 100644 --- a/apps/comments/tests/Unit/Controller/NotificationsTest.php +++ b/apps/comments/tests/Unit/Controller/NotificationsTest.php @@ -72,9 +72,6 @@ class NotificationsTest extends TestCase { ->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') @@ -106,10 +103,6 @@ class NotificationsTest extends TestCase { ->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'); From a35d4e7b44764c2eebec77c8b4e8de2636c60cad Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Sat, 8 Oct 2016 01:10:49 +0200 Subject: [PATCH 4/7] remove notifications of deleted comments from the DB Signed-off-by: Arthur Schiwon --- apps/comments/lib/EventHandler.php | 21 +++++++++++++++++- apps/comments/tests/Unit/EventHandlerTest.php | 22 ++++++++++++++----- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/apps/comments/lib/EventHandler.php b/apps/comments/lib/EventHandler.php index a32bb8ffa7..6692ccc520 100644 --- a/apps/comments/lib/EventHandler.php +++ b/apps/comments/lib/EventHandler.php @@ -53,12 +53,20 @@ class EventHandler implements ICommentsEventHandler { return; } - if( $event->getEvent() === CommentsEvent::EVENT_ADD + $eventType = $event->getEvent(); + if( $eventType === CommentsEvent::EVENT_ADD && $event instanceof CommentsEvent ) { $this->onAdd($event); return; } + + if( $eventType === CommentsEvent::EVENT_DELETE + && $event instanceof CommentsEvent + ) { + $this->onDelete($event); + return; + } } /** @@ -75,4 +83,15 @@ class EventHandler implements ICommentsEventHandler { $activityListener = $c->query(ActivityListener::class); $activityListener->commentEvent($event); } + + /** + * @param CommentsEvent $event + */ + private function onDelete(CommentsEvent $event) { + $c = $this->app->getContainer(); + + /** @var NotificationListener $notificationListener */ + $notificationListener = $c->query(NotificationListener::class); + $notificationListener->evaluate($event); + } } diff --git a/apps/comments/tests/Unit/EventHandlerTest.php b/apps/comments/tests/Unit/EventHandlerTest.php index 7a63032997..21b701ea8c 100644 --- a/apps/comments/tests/Unit/EventHandlerTest.php +++ b/apps/comments/tests/Unit/EventHandlerTest.php @@ -71,14 +71,13 @@ class EventHandlerTest extends TestCase { public function notHandledProvider() { return [ - [CommentsEvent::EVENT_DELETE], [CommentsEvent::EVENT_UPDATE] ]; } /** * @dataProvider notHandledProvider - * @param $eventType + * @param string $eventType */ public function testNotHandled($eventType) { /** @var IComment|\PHPUnit_Framework_MockObject_MockObject $comment */ @@ -103,7 +102,18 @@ class EventHandlerTest extends TestCase { $this->eventHandler->handle($event); } - public function testHandled() { + public function handledProvider() { + return [ + [CommentsEvent::EVENT_DELETE], + [CommentsEvent::EVENT_ADD] + ]; + } + + /** + * @dataProvider handledProvider + * @param string $eventType + */ + public function testHandled($eventType) { /** @var IComment|\PHPUnit_Framework_MockObject_MockObject $comment */ $comment = $this->getMockBuilder(IComment::class)->getMock(); $comment->expects($this->once()) @@ -119,7 +129,7 @@ class EventHandlerTest extends TestCase { ->willReturn($comment); $event->expects($this->atLeastOnce()) ->method('getEvent') - ->willReturn(CommentsEvent::EVENT_ADD); + ->willReturn($eventType); $notificationListener = $this->getMockBuilder(NotificationListener::class) ->disableOriginalConstructor() @@ -131,13 +141,13 @@ class EventHandlerTest extends TestCase { $activityListener = $this->getMockBuilder(ActivityListener::class) ->disableOriginalConstructor() ->getMock(); - $activityListener->expects($this->once()) + $activityListener->expects($this->any()) ->method('commentEvent') ->with($event); /** @var IContainer|\PHPUnit_Framework_MockObject_MockObject $c */ $c = $this->getMockBuilder(IContainer::class)->getMock(); - $c->expects($this->exactly(2)) + $c->expects($this->atLeastOnce()) ->method('query') ->withConsecutive([NotificationListener::class], [ActivityListener::class]) ->willReturnOnConsecutiveCalls($notificationListener, $activityListener); From a9671a4dc2cc904ebb852c7804204ac9f245017e Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Sat, 8 Oct 2016 01:17:51 +0200 Subject: [PATCH 5/7] adjust wording of the notification Signed-off-by: Arthur Schiwon --- apps/comments/lib/Notification/Notifier.php | 6 +++--- apps/comments/tests/Unit/Notification/NotifierTest.php | 9 ++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/comments/lib/Notification/Notifier.php b/apps/comments/lib/Notification/Notifier.php index df05f29730..3838a18f9f 100644 --- a/apps/comments/lib/Notification/Notifier.php +++ b/apps/comments/lib/Notification/Notifier.php @@ -93,13 +93,13 @@ class Notifier implements INotifier { $fileName = $nodes[0]->getName(); if($isDeletedActor) { $subject = (string) $l->t( - 'You were mentioned in a comment on "%s" by a now deleted user.', + 'A (now) deleted user mentioned you in a comment on "%s".', [ $fileName ] ); } else { $subject = (string) $l->t( - 'You were mentioned in a comment on "%s" by %s.', - [ $fileName, $displayName ] + '%s mentioned you in a comment on "%s".', + [ $displayName, $fileName ] ); } $notification->setParsedSubject($subject); diff --git a/apps/comments/tests/Unit/Notification/NotifierTest.php b/apps/comments/tests/Unit/Notification/NotifierTest.php index f3cc0153ab..b297611308 100644 --- a/apps/comments/tests/Unit/Notification/NotifierTest.php +++ b/apps/comments/tests/Unit/Notification/NotifierTest.php @@ -89,7 +89,7 @@ class NotifierTest extends TestCase { public function testPrepareSuccess() { $fileName = 'Gre\'thor.odp'; $displayName = 'Huraga'; - $message = 'You were mentioned in a comment on "Gre\'thor.odp" by Huraga.'; + $message = 'Hurage mentioned you in a comment on "Gre\'thor.odp".'; /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ $user = $this->getMockBuilder('OCP\IUser')->getMock(); @@ -130,7 +130,7 @@ class NotifierTest extends TestCase { $this->l ->expects($this->once()) ->method('t') - ->with('You were mentioned in a comment on "%s" by %s.', [$fileName, $displayName]) + ->with('%s mentioned you in a comment on "%s".', [$displayName, $fileName]) ->willReturn($message); $this->l10nFactory @@ -163,8 +163,7 @@ class NotifierTest extends TestCase { 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.'; + $message = 'A (now) deleted user mentioned you in a comment on "Gre\'thor.odp".'; /** @var Node|\PHPUnit_Framework_MockObject_MockObject */ $node = $this->getMockBuilder('OCP\Files\Node')->getMock(); @@ -199,7 +198,7 @@ class NotifierTest extends TestCase { $this->l ->expects($this->once()) ->method('t') - ->with('You were mentioned in a comment on "%s" by a now deleted user.', [ $fileName ]) + ->with('A (now) deleted user mentioned you in a comment on "%s".', [ $fileName ]) ->willReturn($message); $this->l10nFactory From 1bcd2ca8e35dca6e68e5f06506ade0a78a2beae8 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Wed, 12 Oct 2016 18:06:22 +0200 Subject: [PATCH 6/7] emit pre-update event for comments * notifications can be cleaned up, no polluted DB * updating comments will re-notify users or remove notifications, depending on the message Signed-off-by: Arthur Schiwon --- apps/comments/lib/EventHandler.php | 27 ++++++++++---- apps/comments/lib/Notification/Listener.php | 4 +- apps/comments/tests/Unit/EventHandlerTest.php | 37 ++----------------- .../tests/Unit/Notification/ListenerTest.php | 2 + apps/dav/lib/Comments/CommentNode.php | 2 +- lib/private/Comments/Manager.php | 6 +++ lib/public/Comments/CommentsEvent.php | 7 ++-- tests/lib/Comments/ManagerTest.php | 4 +- 8 files changed, 40 insertions(+), 49 deletions(-) diff --git a/apps/comments/lib/EventHandler.php b/apps/comments/lib/EventHandler.php index 6692ccc520..a5f312617e 100644 --- a/apps/comments/lib/EventHandler.php +++ b/apps/comments/lib/EventHandler.php @@ -57,14 +57,29 @@ class EventHandler implements ICommentsEventHandler { if( $eventType === CommentsEvent::EVENT_ADD && $event instanceof CommentsEvent ) { - $this->onAdd($event); + $this->notificationHandler($event); + $this->activityHandler($event); + return; + } + + if( $eventType === CommentsEvent::EVENT_PRE_UPDATE + && $event instanceof CommentsEvent + ) { + $this->notificationHandler($event); + return; + } + + if( $eventType === CommentsEvent::EVENT_UPDATE + && $event instanceof CommentsEvent + ) { + $this->notificationHandler($event); return; } if( $eventType === CommentsEvent::EVENT_DELETE && $event instanceof CommentsEvent ) { - $this->onDelete($event); + $this->notificationHandler($event); return; } } @@ -72,13 +87,9 @@ class EventHandler implements ICommentsEventHandler { /** * @param CommentsEvent $event */ - private function onAdd(CommentsEvent $event) { + private function activityHandler(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); @@ -87,7 +98,7 @@ class EventHandler implements ICommentsEventHandler { /** * @param CommentsEvent $event */ - private function onDelete(CommentsEvent $event) { + private function notificationHandler(CommentsEvent $event) { $c = $this->app->getContainer(); /** @var NotificationListener $notificationListener */ diff --git a/apps/comments/lib/Notification/Listener.php b/apps/comments/lib/Notification/Listener.php index 5e979fd9bf..6870508502 100644 --- a/apps/comments/lib/Notification/Listener.php +++ b/apps/comments/lib/Notification/Listener.php @@ -85,7 +85,9 @@ class Listener { } $notification->setUser($user); - if($event->getEvent() === CommentsEvent::EVENT_DELETE) { + if( $event->getEvent() === CommentsEvent::EVENT_DELETE + || $event->getEvent() === CommentsEvent::EVENT_PRE_UPDATE) + { $this->notificationManager->markProcessed($notification); } else { $this->notificationManager->notify($notification); diff --git a/apps/comments/tests/Unit/EventHandlerTest.php b/apps/comments/tests/Unit/EventHandlerTest.php index 21b701ea8c..f377c01b3c 100644 --- a/apps/comments/tests/Unit/EventHandlerTest.php +++ b/apps/comments/tests/Unit/EventHandlerTest.php @@ -69,42 +69,11 @@ class EventHandlerTest extends TestCase { $this->eventHandler->handle($event); } - public function notHandledProvider() { - return [ - [CommentsEvent::EVENT_UPDATE] - ]; - } - - /** - * @dataProvider notHandledProvider - * @param string $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 handledProvider() { return [ [CommentsEvent::EVENT_DELETE], + [CommentsEvent::EVENT_UPDATE], + [CommentsEvent::EVENT_PRE_UPDATE], [CommentsEvent::EVENT_ADD] ]; } @@ -152,7 +121,7 @@ class EventHandlerTest extends TestCase { ->withConsecutive([NotificationListener::class], [ActivityListener::class]) ->willReturnOnConsecutiveCalls($notificationListener, $activityListener); - $this->app->expects($this->once()) + $this->app->expects($this->atLeastOnce()) ->method('getContainer') ->willReturn($c); diff --git a/apps/comments/tests/Unit/Notification/ListenerTest.php b/apps/comments/tests/Unit/Notification/ListenerTest.php index 98a0375e49..5926264fa0 100644 --- a/apps/comments/tests/Unit/Notification/ListenerTest.php +++ b/apps/comments/tests/Unit/Notification/ListenerTest.php @@ -60,6 +60,8 @@ class ListenerTest extends TestCase { public function eventProvider() { return [ [CommentsEvent::EVENT_ADD, 'notify'], + [CommentsEvent::EVENT_UPDATE, 'notify'], + [CommentsEvent::EVENT_PRE_UPDATE, 'markProcessed'], [CommentsEvent::EVENT_DELETE, 'markProcessed'] ]; } diff --git a/apps/dav/lib/Comments/CommentNode.php b/apps/dav/lib/Comments/CommentNode.php index 101e3d8772..f247921be7 100644 --- a/apps/dav/lib/Comments/CommentNode.php +++ b/apps/dav/lib/Comments/CommentNode.php @@ -173,7 +173,7 @@ class CommentNode implements \Sabre\DAV\INode, \Sabre\DAV\IProperties { * @param $propertyValue * @return bool * @throws BadRequest - * @throws Forbidden + * @throws \Exception */ public function updateComment($propertyValue) { $this->checkWriteAccessOnComment(); diff --git a/lib/private/Comments/Manager.php b/lib/private/Comments/Manager.php index f7b23dd5f5..b3ecab731e 100644 --- a/lib/private/Comments/Manager.php +++ b/lib/private/Comments/Manager.php @@ -536,6 +536,12 @@ class Manager implements ICommentsManager { * @throws NotFoundException */ protected function update(IComment $comment) { + // for properly working preUpdate Events we need the old comments as is + // in the DB and overcome caching. Also avoid that outdated information stays. + $this->uncache($comment->getId()); + $this->sendEvent(CommentsEvent::EVENT_PRE_UPDATE, $this->get($comment->getId())); + $this->uncache($comment->getId()); + $qb = $this->dbConn->getQueryBuilder(); $affectedRows = $qb ->update('comments') diff --git a/lib/public/Comments/CommentsEvent.php b/lib/public/Comments/CommentsEvent.php index a0bff349fb..0d8a783c10 100644 --- a/lib/public/Comments/CommentsEvent.php +++ b/lib/public/Comments/CommentsEvent.php @@ -32,9 +32,10 @@ use Symfony\Component\EventDispatcher\Event; */ class CommentsEvent extends Event { - const EVENT_ADD = 'OCP\Comments\ICommentsManager::addComment'; - const EVENT_UPDATE = 'OCP\Comments\ICommentsManager::updateComment'; - const EVENT_DELETE = 'OCP\Comments\ICommentsManager::deleteComment'; + const EVENT_ADD = 'OCP\Comments\ICommentsManager::addComment'; + const EVENT_PRE_UPDATE = 'OCP\Comments\ICommentsManager::preUpdateComment'; + const EVENT_UPDATE = 'OCP\Comments\ICommentsManager::updateComment'; + const EVENT_DELETE = 'OCP\Comments\ICommentsManager::deleteComment'; /** @var string */ protected $event; diff --git a/tests/lib/Comments/ManagerTest.php b/tests/lib/Comments/ManagerTest.php index 9c0f791c0c..71c918af6c 100644 --- a/tests/lib/Comments/ManagerTest.php +++ b/tests/lib/Comments/ManagerTest.php @@ -635,11 +635,11 @@ class ManagerTest extends TestCase { public function testSendEvent() { $handler1 = $this->getMockBuilder(ICommentsEventHandler::class)->getMock(); - $handler1->expects($this->exactly(3)) + $handler1->expects($this->exactly(4)) ->method('handle'); $handler2 = $this->getMockBuilder(ICommentsEventHandler::class)->getMock(); - $handler1->expects($this->exactly(3)) + $handler1->expects($this->exactly(4)) ->method('handle'); $manager = $this->getManager(); From 70c7781aa8a1737b4c7ca8e935796b1ebc3d9f34 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Fri, 14 Oct 2016 15:36:05 +0200 Subject: [PATCH 7/7] apply some polish Signed-off-by: Arthur Schiwon --- apps/comments/lib/EventHandler.php | 47 ++++++------------- apps/comments/lib/Notification/Listener.php | 6 --- apps/comments/tests/Unit/EventHandlerTest.php | 38 +++++---------- .../tests/Unit/Notification/ListenerTest.php | 23 --------- 4 files changed, 28 insertions(+), 86 deletions(-) diff --git a/apps/comments/lib/EventHandler.php b/apps/comments/lib/EventHandler.php index a5f312617e..8c20c69837 100644 --- a/apps/comments/lib/EventHandler.php +++ b/apps/comments/lib/EventHandler.php @@ -36,12 +36,15 @@ use OCP\Comments\ICommentsEventHandler; * @package OCA\Comments */ class EventHandler implements ICommentsEventHandler { + /** @var ActivityListener */ + private $activityListener; - /** @var Application */ - protected $app; + /** @var NotificationListener */ + private $notificationListener; - public function __construct(Application $app) { - $this->app = $app; + public function __construct(ActivityListener $activityListener, NotificationListener $notificationListener) { + $this->activityListener = $activityListener; + $this->notificationListener = $notificationListener; } /** @@ -55,30 +58,18 @@ class EventHandler implements ICommentsEventHandler { $eventType = $event->getEvent(); if( $eventType === CommentsEvent::EVENT_ADD - && $event instanceof CommentsEvent ) { $this->notificationHandler($event); $this->activityHandler($event); return; } - if( $eventType === CommentsEvent::EVENT_PRE_UPDATE - && $event instanceof CommentsEvent - ) { - $this->notificationHandler($event); - return; - } - - if( $eventType === CommentsEvent::EVENT_UPDATE - && $event instanceof CommentsEvent - ) { - $this->notificationHandler($event); - return; - } - - if( $eventType === CommentsEvent::EVENT_DELETE - && $event instanceof CommentsEvent - ) { + $applicableEvents = [ + CommentsEvent::EVENT_PRE_UPDATE, + CommentsEvent::EVENT_UPDATE, + CommentsEvent::EVENT_DELETE, + ]; + if(in_array($eventType, $applicableEvents)) { $this->notificationHandler($event); return; } @@ -88,21 +79,13 @@ class EventHandler implements ICommentsEventHandler { * @param CommentsEvent $event */ private function activityHandler(CommentsEvent $event) { - $c = $this->app->getContainer(); - - /** @var ActivityListener $listener */ - $activityListener = $c->query(ActivityListener::class); - $activityListener->commentEvent($event); + $this->activityListener->commentEvent($event); } /** * @param CommentsEvent $event */ private function notificationHandler(CommentsEvent $event) { - $c = $this->app->getContainer(); - - /** @var NotificationListener $notificationListener */ - $notificationListener = $c->query(NotificationListener::class); - $notificationListener->evaluate($event); + $this->notificationListener->evaluate($event); } } diff --git a/apps/comments/lib/Notification/Listener.php b/apps/comments/lib/Notification/Listener.php index 6870508502..426e85cac8 100644 --- a/apps/comments/lib/Notification/Listener.php +++ b/apps/comments/lib/Notification/Listener.php @@ -61,12 +61,6 @@ class Listener { 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 diff --git a/apps/comments/tests/Unit/EventHandlerTest.php b/apps/comments/tests/Unit/EventHandlerTest.php index f377c01b3c..bb714993f7 100644 --- a/apps/comments/tests/Unit/EventHandlerTest.php +++ b/apps/comments/tests/Unit/EventHandlerTest.php @@ -23,30 +23,35 @@ 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; + /** @var ActivityListener|\PHPUnit_Framework_MockObject_MockObject */ + protected $activityListener; + + /** @var NotificationListener|\PHPUnit_Framework_MockObject_MockObject */ + protected $notificationListener; protected function setUp() { parent::setUp(); - $this->app = $this->getMockBuilder(Application::class) + $this->activityListener = $this->getMockBuilder(ActivityListener::class) ->disableOriginalConstructor() ->getMock(); - $this->eventHandler = new EventHandler($this->app); + $this->notificationListener = $this->getMockBuilder(NotificationListener::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->eventHandler = new EventHandler($this->activityListener, $this->notificationListener); } public function testNotFiles() { @@ -100,31 +105,14 @@ class EventHandlerTest extends TestCase { ->method('getEvent') ->willReturn($eventType); - $notificationListener = $this->getMockBuilder(NotificationListener::class) - ->disableOriginalConstructor() - ->getMock(); - $notificationListener->expects($this->once()) + $this->notificationListener->expects($this->once()) ->method('evaluate') ->with($event); - $activityListener = $this->getMockBuilder(ActivityListener::class) - ->disableOriginalConstructor() - ->getMock(); - $activityListener->expects($this->any()) + $this->activityListener->expects($this->any()) ->method('commentEvent') ->with($event); - /** @var IContainer|\PHPUnit_Framework_MockObject_MockObject $c */ - $c = $this->getMockBuilder(IContainer::class)->getMock(); - $c->expects($this->atLeastOnce()) - ->method('query') - ->withConsecutive([NotificationListener::class], [ActivityListener::class]) - ->willReturnOnConsecutiveCalls($notificationListener, $activityListener); - - $this->app->expects($this->atLeastOnce()) - ->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 index 5926264fa0..12f388fcff 100644 --- a/apps/comments/tests/Unit/Notification/ListenerTest.php +++ b/apps/comments/tests/Unit/Notification/ListenerTest.php @@ -172,29 +172,6 @@ class ListenerTest extends TestCase { $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';