From 912b1bde44be485bb8ef0b3f3ea4b13b30c4d7e0 Mon Sep 17 00:00:00 2001 From: Robin Windey Date: Sun, 30 Jul 2023 22:26:15 +0200 Subject: [PATCH] Implement Notifications (#213) (#214) * Implement Notifications * Notification adjustments * Add documentation * Remove code smells * Update psalm-baseline to ignore IRootFolder dependency --- README.md | 10 + doc/img/notifications.png | Bin 0 -> 25576 bytes lib/AppInfo/Application.php | 5 + lib/BackgroundJobs/ProcessFileJob.php | 54 ++-- lib/Notification/Notifier.php | 127 +++++++++ lib/Service/EventService.php | 3 + lib/Service/INotificationService.php | 38 +++ lib/Service/NotificationService.php | 67 +++++ tests/Integration/AppTest.php | 17 +- tests/Integration/Notification/AppFake.php | 49 ++++ .../Notification/NotificationTest.php | 203 +++++++++++++ .../BackgroundJobs/ProcessFileJobTest.php | 53 +++- tests/Unit/Notification/NotifierTest.php | 266 ++++++++++++++++++ .../Unit/Service/NotificationServiceTest.php | 129 +++++++++ tests/psalm-baseline.xml | 9 +- 15 files changed, 995 insertions(+), 35 deletions(-) create mode 100644 doc/img/notifications.png create mode 100644 lib/Notification/Notifier.php create mode 100644 lib/Service/INotificationService.php create mode 100644 lib/Service/NotificationService.php create mode 100644 tests/Integration/Notification/AppFake.php create mode 100644 tests/Integration/Notification/NotificationTest.php create mode 100644 tests/Unit/Notification/NotifierTest.php create mode 100644 tests/Unit/Service/NotificationServiceTest.php diff --git a/README.md b/README.md index b71a2ee..66d5e65 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ - [Per workflow settings](#per-workflow-settings) - [Global settings](#global-settings) - [Testing your configuration](#testing-your-configuration) + - [Get feedback via Notifications](#get-feedback-via-notifications) - [How it works](#how-it-works) - [General](#general) - [PDF](#pdf) @@ -165,6 +166,15 @@ To **test** if your file gets processed properly you can do the following steps: File versions

+### Get feedback via Notifications + +The Workflow OCR app supports sending notifications to the user in case anything went wrong during the [asynchronous OCR processing](#how-it-works). To enable this feature, you have to install and enable the [`Notifications`](https://github.com/nextcloud/notifications) app in your Nextcloud instance. + +

+ Notifications +

+ + ## How it works ### General

diff --git a/doc/img/notifications.png b/doc/img/notifications.png new file mode 100644 index 0000000000000000000000000000000000000000..44867bfcfe63ae8478060641ba41087ba21344e1 GIT binary patch literal 25576 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::class, GlobalSettingsService::class); $context->registerServiceAlias(IEventService::class, EventService::class); $context->registerServiceAlias(IOcrBackendInfoService::class, OcrBackendInfoService::class); + $context->registerServiceAlias(INotificationService::class, NotificationService::class); // BUG #43 $context->registerService(ICommand::class, function () { @@ -94,6 +98,7 @@ public function register(IRegistrationContext $context): void { OcrProcessorFactory::registerOcrProcessors($context); $context->registerEventListener(RegisterOperationsEvent::class, RegisterFlowOperationsListener::class); + $context->registerNotifierService(Notifier::class); } /** diff --git a/lib/BackgroundJobs/ProcessFileJob.php b/lib/BackgroundJobs/ProcessFileJob.php index a07b98a..1a4e06a 100644 --- a/lib/BackgroundJobs/ProcessFileJob.php +++ b/lib/BackgroundJobs/ProcessFileJob.php @@ -33,6 +33,7 @@ use OCA\WorkflowOcr\Helper\IProcessingFileAccessor; use OCA\WorkflowOcr\Model\WorkflowSettings; use OCA\WorkflowOcr\Service\IEventService; +use OCA\WorkflowOcr\Service\INotificationService; use OCA\WorkflowOcr\Service\IOcrService; use OCA\WorkflowOcr\Wrapper\IFilesystem; use OCA\WorkflowOcr\Wrapper\IViewFactory; @@ -69,6 +70,8 @@ class ProcessFileJob extends \OCP\BackgroundJob\QueuedJob { private $userSession; /** @var IProcessingFileAccessor */ private $processingFileAccessor; + /** @var INotificationService */ + private $notificationService; public function __construct( LoggerInterface $logger, @@ -80,6 +83,7 @@ public function __construct( IUserManager $userManager, IUserSession $userSession, IProcessingFileAccessor $processingFileAccessor, + INotificationService $notificationService, ITimeFactory $timeFactory) { parent::__construct($timeFactory); $this->logger = $logger; @@ -91,6 +95,7 @@ public function __construct( $this->userManager = $userManager; $this->userSession = $userSession; $this->processingFileAccessor = $processingFileAccessor; + $this->notificationService = $notificationService; } /** @@ -101,14 +106,16 @@ protected function run($argument) : void { [$success, $filePath, $uid, $settings] = $this->tryParseArguments($argument); if (!$success) { + $this->notificationService->createErrorNotification($uid, 'Failed to parse arguments inside the OCR process. Please have a look at your servers logfile for more details.'); return; } try { $this->initUserEnvironment($uid); - $this->processFile($filePath, $settings); + $this->processFile($filePath, $settings, $uid); } catch (\Throwable $ex) { $this->logger->error($ex->getMessage(), ['exception' => $ex]); + $this->notificationService->createErrorNotification($uid, 'An error occured while executing the OCR process. Please have a look at your servers logfile for more details.'); } finally { $this->shutdownUserEnvironment(); } @@ -165,26 +172,35 @@ private function tryParseArguments($argument) : array { /** * @param string $filePath The file to be processed * @param WorkflowSettings $settings The settings to be used for processing + * @param string $userId The user who triggered the processing */ - private function processFile(string $filePath, WorkflowSettings $settings) : void { - $node = $this->getNode($filePath); + private function processFile(string $filePath, WorkflowSettings $settings, string $userId) : void { + $node = $this->getNode($filePath, $userId); if ($node === null) { return; } + $nodeId = $node->getId(); + try { $ocrFile = $this->ocrService->ocrFile($node, $settings); - } catch (OcrNotPossibleException $ocrNpEx) { - $this->logger->error('OCR for file ' . $node->getPath() . ' not possible. Message: ' . $ocrNpEx->getMessage()); - return; - } catch (OcrProcessorNotFoundException $ocrNfEx) { - $this->logger->error('OCR processor not found for mimetype ' . $node->getMimeType()); + } catch(\Throwable $throwable) { + if ($throwable instanceof(OcrNotPossibleException::class)) { + $msg = 'OCR for file ' . $node->getPath() . ' not possible. Message: ' . $throwable->getMessage(); + } elseif ($throwable instanceof(OcrProcessorNotFoundException::class)) { + $msg = 'OCR processor not found for mimetype ' . $node->getMimeType(); + } else { + throw $throwable; + } + + $this->logger->error($msg); + $this->notificationService->createErrorNotification($userId, $msg, $nodeId); + return; } $fileContent = $ocrFile->getFileContent(); - $nodeId = $node->getId(); $originalFileExtension = $node->getExtension(); $newFileExtension = $ocrFile->getFileExtension(); @@ -200,17 +216,21 @@ private function processFile(string $filePath, WorkflowSettings $settings) : voi $this->eventService->textRecognized($ocrFile, $node); } - private function getNode(string $filePath) : ?Node { + private function getNode(string $filePath, string $userId) : ?Node { try { /** @var File */ $node = $this->rootFolder->get($filePath); } catch (NotFoundException $nfEx) { - $this->logger->warning('Could not process file \'' . $filePath . '\'. File was not found'); + $msg = 'Could not process file \'' . $filePath . '\'. File was not found'; + $this->logger->warning($msg); + $this->notificationService->createErrorNotification($userId, $msg); return null; } if (!$node instanceof Node || $node->getType() !== FileInfo::TYPE_FILE) { - $this->logger->warning('Skipping process for \'' . $filePath . '\'. It is not a file'); + $msg = 'Skipping process for \'' . $filePath . '\'. It is not a file'; + $this->logger->warning($msg); + $this->notificationService->createErrorNotification($userId, $msg); return null; } @@ -218,17 +238,17 @@ private function getNode(string $filePath) : ?Node { } /** - * * @param string $uid The owners userId of the file to be processed + * * @param string $userId The owners userId of the file to be processed */ - private function initUserEnvironment(string $uid) : void { + private function initUserEnvironment(string $userId) : void { /** @var IUser */ - $user = $this->userManager->get($uid); + $user = $this->userManager->get($userId); if (!$user) { - throw new NoUserException("User with uid '$uid' was not found"); + throw new NoUserException("User with uid '$userId' was not found"); } $this->userSession->setUser($user); - $this->filesystem->init($uid, '/' . $uid . '/files'); + $this->filesystem->init($userId, '/' . $userId . '/files'); } private function shutdownUserEnvironment() : void { diff --git a/lib/Notification/Notifier.php b/lib/Notification/Notifier.php new file mode 100644 index 0000000..46035e7 --- /dev/null +++ b/lib/Notification/Notifier.php @@ -0,0 +1,127 @@ + + * + * @author Robin Windey + * + * @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\WorkflowOcr\Notification; + +use OCA\WorkflowOcr\AppInfo\Application; +use OCP\Files\File; +use OCP\Files\IRootFolder; +use OCP\IURLGenerator; +use OCP\L10N\IFactory; +use OCP\Notification\AlreadyProcessedException; +use OCP\Notification\INotification; +use OCP\Notification\INotifier; +use Psr\Log\LoggerInterface; + +class Notifier implements INotifier { + /** @var IFactory*/ + private $l10nFactory; + /** @var IURLGenerator */ + private $urlGenerator; + /** @var IRootFolder */ + private $rootFolder; + /** @var LoggerInterface */ + private $logger; + + public function __construct(IFactory $factory, + IURLGenerator $urlGenerator, + IRootFolder $rootFolder, + LoggerInterface $logger) { + $this->l10nFactory = $factory; + $this->urlGenerator = $urlGenerator; + $this->rootFolder = $rootFolder; + $this->logger = $logger; + } + + /** + * Identifier of the notifier, only use [a-z0-9_] + * @return string + */ + public function getID(): string { + return Application::APP_NAME; + } + + /** + * Human readable name describing the notifier + * @return string + */ + public function getName(): string { + return $this->l10nFactory->get(Application::APP_NAME)->t('Workflow OCR'); + } + + /** + * @param INotification $notification + * @param string $languageCode The code of the language that should be used to prepare the notification + */ + public function prepare(INotification $notification, string $languageCode): INotification { + if ($notification->getApp() !== Application::APP_NAME) { + throw new \InvalidArgumentException(); + } + + $notification->setIcon($this->urlGenerator->imagePath(Application::APP_NAME, 'app-dark.svg')); + $l = $this->l10nFactory->get(Application::APP_NAME, $languageCode); + + // Currently we only support sending notifications for ocr_error + if ($notification->getSubject() !== 'ocr_error') { + throw new \InvalidArgumentException(); + } + + $message = $notification->getSubjectParameters()['message']; + $notification + ->setParsedSubject($l->t('Workflow OCR error')) + ->setParsedMessage($message); + // Only add file info if we have one ... + if ($notification->getObjectType() === 'file' && $notification->getObjectId()) { + $richParams = $this->getRichParamForFile($notification); + $notification->setRichSubject($l->t('Workflow OCR error for file {file}'), $richParams); + } + return $notification; + } + + private function getRichParamForFile(INotification $notification) : array { + try { + $userFolder = $this->rootFolder->getUserFolder($notification->getUser()); + /** @var File[] */ + $files = $userFolder->getById($notification->getObjectId()); + /** @var File $file */ + $file = array_shift($files); + $relativePath = $userFolder->getRelativePath($file->getPath()); + } catch (\Throwable $th) { + $this->logger->error($th->getMessage(), ['exception' => $th]); + throw new AlreadyProcessedException(); + } + + return [ + 'file' => [ + 'type' => 'file', + 'id' => $file->getId(), + 'name' => $file->getName(), + 'path' => $relativePath, + 'link' => $this->urlGenerator->linkToRouteAbsolute('files.viewcontroller.showFile', ['fileid' => $notification->getObjectId()]) + ] + ]; + } +} diff --git a/lib/Service/EventService.php b/lib/Service/EventService.php index e94a21d..92f57e0 100644 --- a/lib/Service/EventService.php +++ b/lib/Service/EventService.php @@ -38,6 +38,9 @@ public function __construct(IEventDispatcher $eventDispatcher) { $this->eventDispatcher = $eventDispatcher; } + /** + * @return void + */ public function textRecognized(OcrProcessorResult $result, File $node) { $event = new TextRecognizedEvent($result->getRecognizedText(), $node); $this->eventDispatcher->dispatchTyped($event); diff --git a/lib/Service/INotificationService.php b/lib/Service/INotificationService.php new file mode 100644 index 0000000..5d0f159 --- /dev/null +++ b/lib/Service/INotificationService.php @@ -0,0 +1,38 @@ + + * + * @author Robin Windey + * + * @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\WorkflowOcr\Service; + +interface INotificationService { + + /** + * Create a new notification for the given user if the OCR process of the given file failed. + * @param string $userId The user ID of the user that should receive the notification. + * @param string $message The error message that should be displayed in the notification. + * @param int $fileId Optional file ID of the file that failed to OCR. If given, user can jump to the file via link. + */ + public function createErrorNotification(?string $userId, string $message, int $fileId = null); +} diff --git a/lib/Service/NotificationService.php b/lib/Service/NotificationService.php new file mode 100644 index 0000000..0fdc880 --- /dev/null +++ b/lib/Service/NotificationService.php @@ -0,0 +1,67 @@ + + * + * @author Robin Windey + * + * @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\WorkflowOcr\Service; + +use OCA\WorkflowOcr\AppInfo\Application; +use OCP\Notification\IManager; + +/* +* This class is used to create new NC notifications. +* They will be displayed later with the help of Nofification\Notifier. +*/ +class NotificationService implements INotificationService { + + private IManager $notificationManager; + + public function __construct(IManager $notificationManager) { + $this->notificationManager = $notificationManager; + } + + /** + * @return void + */ + public function createErrorNotification(?string $userId, string $message, int $fileId = null) { + // We don't create unbound notifications + if (!$userId) { + return; + } + + $notification = $this->notificationManager->createNotification(); + $notification->setApp(Application::APP_NAME) + ->setUser($userId) + ->setDateTime(new \DateTime()) + ->setSubject('ocr_error', ['message' => $message]); + + if ($fileId) { + $notification->setObject('file', strval($fileId)); + } else { + $notification->setObject('ocr', 'ocr'); + } + + $this->notificationManager->notify($notification); + } +} diff --git a/tests/Integration/AppTest.php b/tests/Integration/AppTest.php index c3d12cd..67f4171 100644 --- a/tests/Integration/AppTest.php +++ b/tests/Integration/AppTest.php @@ -1,14 +1,21 @@ * + * @license GNU AGPL version 3 or any later version * - * This file is licensed under the Affero General Public License version 3 or - * later. See the COPYING file. + * 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. * - * @author Robin Windey + * 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. * - * @copyright Robin Windey 2020 + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . */ namespace OCA\WorkflowOcr\Tests\Integration; diff --git a/tests/Integration/Notification/AppFake.php b/tests/Integration/Notification/AppFake.php new file mode 100644 index 0000000..14e79b6 --- /dev/null +++ b/tests/Integration/Notification/AppFake.php @@ -0,0 +1,49 @@ + + * + * @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\WorkflowOcr\Tests\Integration\Notification; + +use OCP\Notification\IApp; +use OCP\Notification\INotification; + +class AppFake implements IApp { + private $notifications = []; + private $processed = []; + + public function notify(INotification $notification): void { + $this->notifications[] = $notification; + } + + public function markProcessed(INotification $notification): void { + $this->processed[] = $notification; + } + + public function getCount(INotification $notification): int { + return 0; + } + + public function getNotifications() { + return $this->notifications; + } + + public function getProcessed() { + return $this->processed; + } +} diff --git a/tests/Integration/Notification/NotificationTest.php b/tests/Integration/Notification/NotificationTest.php new file mode 100644 index 0000000..226d097 --- /dev/null +++ b/tests/Integration/Notification/NotificationTest.php @@ -0,0 +1,203 @@ + + * + * @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\WorkflowOcr\Tests\Integration\Notification; + +use OC\BackgroundJob\JobList; +use OCA\WorkflowOcr\BackgroundJobs\ProcessFileJob; +use OCA\WorkflowOcr\Exception\OcrNotPossibleException; +use OCA\WorkflowOcr\Helper\IProcessingFileAccessor; +use OCA\WorkflowOcr\Service\IEventService; +use OCA\WorkflowOcr\Service\INotificationService; +use OCA\WorkflowOcr\Service\IOcrService; +use OCA\WorkflowOcr\Service\NotificationService; +use OCA\WorkflowOcr\Wrapper\IFilesystem; +use OCA\WorkflowOcr\Wrapper\IViewFactory; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\DB\QueryBuilder\IExpressionBuilder; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\Files\File; +use OCP\Files\FileInfo; +use OCP\Files\IRootFolder; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IUser; +use OCP\IUserManager; +use OCP\IUserSession; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +class NotificationTest extends TestCase { + /** @var AppFake */ + private $appFake; + /** @var LoggerInterface|MockObject */ + private $logger; + /** @var IRootFolder|MockObject */ + private $rootFolder; + /** @var IOcrService|MockObject */ + private $ocrService; + /** @var IEventService|MockObject */ + private $eventService; + /** @var IViewFactory|MockObject */ + private $viewFactory; + /** @var IFilesystem|MockObject */ + private $filesystem; + /** @var IUserSession|MockObject */ + private $userSession; + /** @var IUserManager|MockObject */ + private $userManager; + /** @var IUser|MockObject */ + private $user; + /** @var IProcessingFileAccessor|MockObject */ + private $processingFileAccessor; + /** @var INotificationService|MockObject */ + private $notificationService; + /** @var JobList */ + private $jobList; + /** @var ProcessFileJob */ + private $processFileJob; + + protected function setUp() : void { + parent::setUp(); + // We use a faked notification receiver app to keep track of any notifications created + \OC::$server->get(\OCP\Notification\IManager::class)->registerApp(AppFake::class); + + // Use real Notification service to be able to check if notifications get created + $this->notificationService = new NotificationService(\OC::$server->get(\OCP\Notification\IManager::class)); + + $this->logger = $this->createMock(LoggerInterface::class); + $this->rootFolder = $this->createMock(IRootFolder::class); + $this->ocrService = $this->createMock(IOcrService::class); + $this->eventService = $this->createMock(IEventService::class); + $this->viewFactory = $this->createMock(IViewFactory::class); + $this->filesystem = $this->createMock(IFilesystem::class); + $this->userSession = $this->createMock(IUserSession::class); + $this->processingFileAccessor = $this->createMock(IProcessingFileAccessor::class); + + /** @var MockObject|IUserManager */ + $userManager = $this->createMock(IUserManager::class); + $user = $this->createMock(IUser::class); + $userManager->method('get') + ->withAnyParameters() + ->willReturn($user); + + $this->userManager = $userManager; + $this->user = $user; + + $this->processFileJob = new ProcessFileJob( + $this->logger, + $this->rootFolder, + $this->ocrService, + $this->eventService, + $this->viewFactory, + $this->filesystem, + $this->userManager, + $this->userSession, + $this->processingFileAccessor, + $this->notificationService, + $this->createMock(ITimeFactory::class) + ); + + /** @var IConfig */ + $configMock = $this->createMock(IConfig::class); + /** @var ITimeFactory */ + $timeFactoryMock = $this->createMock(ITimeFactory::class); + /** @var MockObject|IDbConnection */ + $connectionMock = $this->createMock(IDBConnection::class); + /** @var MockObject|IQueryBuilder */ + $queryBuilderMock = $this->createMock(IQueryBuilder::class); + $expressionBuilderMock = $this->createMock(IExpressionBuilder::class); + + $queryBuilderMock->method('delete') + ->withAnyParameters() + ->willReturn($queryBuilderMock); + $queryBuilderMock->method('set') + ->withAnyParameters() + ->willReturn($queryBuilderMock); + $queryBuilderMock->method('update') + ->withAnyParameters() + ->willReturn($queryBuilderMock); + $queryBuilderMock->method('expr') + ->withAnyParameters() + ->willReturn($expressionBuilderMock); + $connectionMock->method('getQueryBuilder') + ->withAnyParameters() + ->willReturn($queryBuilderMock); + + $this->jobList = new JobList( + $connectionMock, + $configMock, + $timeFactoryMock, + $this->logger + ); + + $this->processFileJob->setId(111); + $this->processFileJob->setArgument([ + 'filePath' => '/admin/files/somefile.pdf', + 'uid' => 'someuser', + 'settings' => '{}' + ]); + } + + public function testBackgroundJobCreatesErrorNotificationIfOcrFailed() { + $fileMock = $this->createValidFileMock(); + $this->rootFolder->method('get') + ->with('/admin/files/somefile.pdf') + ->willReturn($fileMock); + + $this->ocrService->expects($this->once()) + ->method('ocrFile') + ->withAnyParameters() + ->willThrowException(new OcrNotPossibleException('Some error')); + $appFake = \OC::$server->get(AppFake::class); + + $this->processFileJob->start($this->jobList); + + $notifications = $appFake->getNotifications(); + $this->assertCount(1, $notifications); + + $notification = $notifications[0]; + $this->assertEquals('workflow_ocr', $notification->getApp()); + $this->assertEquals('ocr_error', $notification->getSubject()); + $this->assertEquals('OCR for file /admin/files/somefile.pdf not possible. Message: Some error', $notification->getSubjectParameters()['message']); + } + + /** + * @return File|MockObject + */ + private function createValidFileMock(string $mimeType = 'application/pdf', string $content = 'someFileContent', string $fileExtension = "pdf", string $path = "/admin/files/somefile.pdf") { + /** @var MockObject|File */ + $fileMock = $this->createMock(File::class); + $fileMock->method('getType') + ->willReturn(FileInfo::TYPE_FILE); + $fileMock->method('getMimeType') + ->willReturn($mimeType); + $fileMock->method('getContent') + ->willReturn($content); + $fileMock->method('getId') + ->willReturn(42); + $fileMock->method('getExtension') + ->willReturn($fileExtension); + $fileMock->method('getPath') + ->willReturn($path); + return $fileMock; + } +} diff --git a/tests/Unit/BackgroundJobs/ProcessFileJobTest.php b/tests/Unit/BackgroundJobs/ProcessFileJobTest.php index 653139f..07f78f9 100644 --- a/tests/Unit/BackgroundJobs/ProcessFileJobTest.php +++ b/tests/Unit/BackgroundJobs/ProcessFileJobTest.php @@ -32,6 +32,7 @@ use OCA\WorkflowOcr\Helper\IProcessingFileAccessor; use OCA\WorkflowOcr\OcrProcessors\OcrProcessorResult; use OCA\WorkflowOcr\Service\IEventService; +use OCA\WorkflowOcr\Service\INotificationService; use OCA\WorkflowOcr\Service\IOcrService; use OCA\WorkflowOcr\Wrapper\IFilesystem; use OCA\WorkflowOcr\Wrapper\IView; @@ -74,6 +75,8 @@ class ProcessFileJobTest extends TestCase { private $user; /** @var IProcessingFileAccessor|MockObject */ private $processingFileAccessor; + /** @var INotificationService|MockObject */ + private $notificationService; /** @var JobList */ private $jobList; /** @var ProcessFileJob */ @@ -90,6 +93,7 @@ public function setUp() : void { $this->filesystem = $this->createMock(IFilesystem::class); $this->userSession = $this->createMock(IUserSession::class); $this->processingFileAccessor = $this->createMock(IProcessingFileAccessor::class); + $this->notificationService = $this->createMock(INotificationService::class); /** @var MockObject|IUserManager */ $userManager = $this->createMock(IUserManager::class); @@ -111,6 +115,7 @@ public function setUp() : void { $this->userManager, $this->userSession, $this->processingFileAccessor, + $this->notificationService, $this->createMock(ITimeFactory::class) ); $this->processFileJob->setId(111); @@ -171,7 +176,7 @@ public function testCatchesExceptionAndResetsUserEnvironment() { ->method('setUser') ->withConsecutive([$this->user], [null]); - $this->processFileJob->execute($this->jobList); + $this->processFileJob->start($this->jobList); } /** @@ -191,7 +196,7 @@ public function testDoesNothingOnInvalidArguments($argument, $invalidCount) { $this->logger->expects($this->exactly($invalidCount)) ->method('warning'); - $this->processFileJob->execute($this->jobList); + $this->processFileJob->start($this->jobList); } /** @@ -203,7 +208,7 @@ public function testCallsInitFilesystem(array $arguments, string $user, string $ ->method('init') ->with($user, $rootFolderPath); - $this->processFileJob->execute($this->jobList); + $this->processFileJob->start($this->jobList); } /** @@ -215,7 +220,7 @@ public function testCallsGetOnRootFolder(array $arguments, string $user, string ->method('get') ->with($arguments['filePath']); - $this->processFileJob->execute($this->jobList); + $this->processFileJob->start($this->jobList); } /** @@ -235,7 +240,7 @@ public function testCallsOcr_IfIsFile(array $arguments, string $user, string $ro ->method('ocrFile') ->with($fileMock); - $this->processFileJob->execute($this->jobList); + $this->processFileJob->start($this->jobList); } /** @@ -273,7 +278,7 @@ public function testCreatesNewFileVersionAndEmitsTextRecognizedEvent(array $argu ->method('textRecognized') ->with($ocrResult, $fileMock); - $this->processFileJob->execute($this->jobList); + $this->processFileJob->start($this->jobList); } public function testNotFoundLogsWarning_AndDoesNothingAfterwards() { @@ -286,7 +291,7 @@ public function testNotFoundLogsWarning_AndDoesNothingAfterwards() { $this->ocrService->expects($this->never()) ->method('ocrFile'); - $this->processFileJob->execute($this->jobList); + $this->processFileJob->start($this->jobList); } /** @@ -300,7 +305,7 @@ public function testDoesNotCallOcr_OnNonFile($invalidNode) { $this->ocrService->expects($this->never()) ->method('ocrFile'); - $this->processFileJob->execute($this->jobList); + $this->processFileJob->start($this->jobList); } /** @@ -321,7 +326,7 @@ public function testLogsError_OnOcrException(Exception $exception) { $this->viewFactory->expects($this->never()) ->method('create'); - $this->processFileJob->execute($this->jobList); + $this->processFileJob->start($this->jobList); } public function testThrowsNoUserException_OnNonExistingUser() { @@ -349,13 +354,14 @@ public function testThrowsNoUserException_OnNonExistingUser() { $userManager, $this->userSession, $this->processingFileAccessor, + $this->notificationService, $this->createMock(ITimeFactory::class) ); $processFileJob->setId(111); $arguments = ['filePath' => '/nonexistinguser/files/someInvalidStuff', 'settings' => '{}']; $processFileJob->setArgument($arguments); - $processFileJob->execute($this->jobList); + $processFileJob->start($this->jobList); } /** @@ -397,7 +403,7 @@ public function testCallsProcessingFileAccessor(array $arguments, string $user, return true; })); - $this->processFileJob->execute($this->jobList); + $this->processFileJob->start($this->jobList); $this->assertEquals(1, $calledWith42); $this->assertEquals(1, $calledWithNull); @@ -432,7 +438,30 @@ public function testDoesNotCreateNewFileVersionIfOcrContentWasEmpty(array $argum $this->eventService->expects($this->once()) ->method('textRecognized'); - $this->processFileJob->execute($this->jobList); + $this->processFileJob->start($this->jobList); + } + + public function testLogsNonOcrExceptionsFromOcrService() { + $this->processFileJob->setArgument(['filePath' => '/admin/files/somefile.pdf', 'settings' => '{}']); + $mimeType = 'application/pdf'; + $content = 'someFileContent'; + $exception = new \Exception('someException'); + + $fileMock = $this->createValidFileMock($mimeType, $content); + $this->rootFolder->method('get') + ->willReturn($fileMock); + + $this->ocrService->expects($this->once()) + ->method('ocrFile') + ->willThrowException($exception); + + $this->logger->expects($this->once()) + ->method('error'); + + $this->viewFactory->expects($this->never()) + ->method('create'); + + $this->processFileJob->start($this->jobList); } public function dataProvider_InvalidArguments() { diff --git a/tests/Unit/Notification/NotifierTest.php b/tests/Unit/Notification/NotifierTest.php new file mode 100644 index 0000000..b5df259 --- /dev/null +++ b/tests/Unit/Notification/NotifierTest.php @@ -0,0 +1,266 @@ + + * + * @author Robin Windey + * + * @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\WorkflowOcr\Tests\Unit\Notification; + +use OC\Notification\Notification; +use OCA\WorkflowOcr\Notification\Notifier; +use OCP\Files\File; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\L10N\IFactory; +use OCP\Notification\AlreadyProcessedException; +use OCP\Notification\INotification; +use OCP\RichObjectStrings\IValidator; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +class NotifierTest extends TestCase { + /** @var IFactory|MockObject */ + private $l10nFactory; + + /** @var IRootFolder|MockObject */ + private $rootFolder; + + /** @var LoggerInterface|MockObject */ + private $logger; + + /** @var IURLGenerator|MockObject */ + private $urlGenerator; + + /** @var Notifier */ + private $notifier; + + public function setUp() : void { + $this->l10nFactory = $this->createMock(IFactory::class); + $this->rootFolder = $this->createMock(IRootFolder::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + + $this->notifier = new Notifier( + $this->l10nFactory, + $this->urlGenerator, + $this->rootFolder, + $this->logger + ); + } + + public function testGetIdReturnsWorkflowOcrName() { + $this->assertEquals('workflow_ocr', $this->notifier->getId()); + } + + public function testGetDisplayNameReturnsWorkflowOcrName() { + /** @var IL10N|MockObject */ + $l10n = $this->createMock(IL10N::class); + $l10n->expects($this->once()) + ->method('t') + ->with('Workflow OCR') + ->willReturn('Workflow OCR'); + $this->l10nFactory->expects($this->once()) + ->method('get') + ->with('workflow_ocr') + ->willReturn($l10n); + $this->assertEquals('Workflow OCR', $this->notifier->getName()); + } + + public function testPrepareThrowsInvalidArgumentExceptionOnAppNotWorkflowOcr() { + /** @var INotification|MockObject */ + $notification = $this->createMock(INotification::class); + $notification->expects($this->once()) + ->method('getApp') + ->willReturn('not_workflow_ocr'); + $this->expectException(\InvalidArgumentException::class); + $this->notifier->prepare($notification, 'en'); + } + + public function testPrepareThrowsInvalidArgumentExceptionIfNotificationSubjectNotOcrError() { + /** @var INotification|MockObject */ + $notification = $this->createMock(INotification::class); + $notification->expects($this->once()) + ->method('getApp') + ->willReturn('workflow_ocr'); + $notification->expects($this->once()) + ->method('getSubject') + ->willReturn('not_ocr_error'); + $this->expectException(\InvalidArgumentException::class); + $this->notifier->prepare($notification, 'en'); + } + + public function testPrepareConstructsOcrErrorCorrectlyWithFileId() { + /** @var IValidator|MockObject */ + $validator = $this->createMock(IValidator::class); + /** @var IL10N|MockObject */ + $l10n = $this->createMock(IL10N::class); + $l10n->expects($this->exactly(2)) + ->method('t') + ->withConsecutive( + ['Workflow OCR error'], + ['Workflow OCR error for file {file}'] + ) + ->willReturnOnConsecutiveCalls( + 'Workflow OCR', + 'Workflow OCR error for file {file}' + ); + $this->l10nFactory->expects($this->once()) + ->method('get') + ->with('workflow_ocr') + ->willReturn($l10n); + + $notification = new Notification($validator); + $notification->setUser('user'); + $notification->setApp('workflow_ocr'); + $notification->setSubject('ocr_error', ['message' => 'mymessage']); + $notification->setObject('file', '123'); + + /** @var File|MockObject */ + $file = $this->createMock(File::class); + $file->expects($this->once()) + ->method('getPath') + ->willReturn('admin/files/file.txt'); + $file->expects($this->once()) + ->method('getName') + ->willReturn('file.txt'); + $file->expects($this->once()) + ->method('getId') + ->willReturn('123'); + /** @var Folder|MockObject */ + $userFolder = $this->createMock(Folder::class); + $userFolder->expects($this->once()) + ->method('getById') + ->with('123') + ->willReturn(['file' => $file]); + $userFolder->expects($this->once()) + ->method('getRelativePath') + ->with('admin/files/file.txt') + ->willReturn('files/file.txt'); + $this->rootFolder->expects($this->once()) + ->method('getUserFolder') + ->with('user') + ->willReturn($userFolder); + $this->urlGenerator->expects($this->once()) + ->method('imagePath') + ->with('workflow_ocr', 'app-dark.svg') + ->willReturn('http://localhost/index.php/apps/workflow_ocr/app-dark.svg'); + $this->urlGenerator->expects($this->once()) + ->method('linkToRouteAbsolute') + ->with('files.viewcontroller.showFile', ['fileid' => '123']) + ->willReturn('http://localhost/index.php/apps/files/?file=123'); + + $preparedNotification = $this->notifier->prepare($notification, 'en'); + + $richSubject = $preparedNotification->getRichSubject(); + $richSubjectParams = $preparedNotification->getRichSubjectParameters(); + + $this->assertEquals('Workflow OCR error for file {file}', $richSubject); + $this->assertEquals(['file' => [ + 'type' => 'file', + 'id' => '123', + 'name' => 'file.txt', + 'path' => 'files/file.txt', + 'link' => 'http://localhost/index.php/apps/files/?file=123' + ]], $richSubjectParams); + } + + public function testPrepareConstructsOcrErrorCorrectlyWithoutFile() { + /** @var IValidator|MockObject */ + $validator = $this->createMock(IValidator::class); + /** @var IL10N|MockObject */ + $l10n = $this->createMock(IL10N::class); + $l10n->expects($this->once()) + ->method('t') + ->with('Workflow OCR error') + ->willReturn('Workflow OCR error'); + $this->l10nFactory->expects($this->once()) + ->method('get') + ->with('workflow_ocr') + ->willReturn($l10n); + + $notification = new Notification($validator); + $notification->setUser('user'); + $notification->setApp('workflow_ocr'); + $notification->setSubject('ocr_error', ['message' => 'mymessage']); + $notification->setObject('ocr', 'ocr'); + + $this->urlGenerator->expects($this->once()) + ->method('imagePath') + ->with('workflow_ocr', 'app-dark.svg') + ->willReturn('http://localhost/index.php/apps/workflow_ocr/app-dark.svg'); + $this->urlGenerator->expects($this->never()) + ->method('linkToRouteAbsolute'); + + $preparedNotification = $this->notifier->prepare($notification, 'en'); + + $richSubject = $preparedNotification->getRichSubject(); + $richSubjectParams = $preparedNotification->getRichSubjectParameters(); + + $this->assertEquals('', $richSubject); + $this->assertEmpty($richSubjectParams); + } + + public function testThrowsAlreadyProcessedExceptionIfFileCannotBeRead() { + /** @var IValidator|MockObject */ + $validator = $this->createMock(IValidator::class); + /** @var IL10N|MockObject */ + $l10n = $this->createMock(IL10N::class); + $l10n->expects($this->once()) + ->method('t') + ->with('Workflow OCR error') + ->willReturn('Workflow OCR'); + $this->l10nFactory->expects($this->once()) + ->method('get') + ->with('workflow_ocr') + ->willReturn($l10n); + + $notification = new Notification($validator); + $notification->setUser('user'); + $notification->setApp('workflow_ocr'); + $notification->setSubject('ocr_error', ['message' => 'mymessage']); + $notification->setObject('file', '123'); + + /** @var Folder|MockObject */ + $userFolder = $this->createMock(Folder::class); + $userFolder->expects($this->once()) + ->method('getById') + ->with('123') + ->willThrowException(new \OCP\Files\NotFoundException()); + $userFolder->expects($this->never()) + ->method('getRelativePath'); + $this->rootFolder->expects($this->once()) + ->method('getUserFolder') + ->with('user') + ->willReturn($userFolder); + $this->urlGenerator->expects($this->once()) + ->method('imagePath') + ->with('workflow_ocr', 'app-dark.svg') + ->willReturn('http://localhost/index.php/apps/workflow_ocr/app-dark.svg'); + + $this->expectException(AlreadyProcessedException::class); + $this->notifier->prepare($notification, 'en'); + } +} diff --git a/tests/Unit/Service/NotificationServiceTest.php b/tests/Unit/Service/NotificationServiceTest.php new file mode 100644 index 0000000..1f5f839 --- /dev/null +++ b/tests/Unit/Service/NotificationServiceTest.php @@ -0,0 +1,129 @@ + + * + * @author Robin Windey + * + * @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\WorkflowOcr\Tests\Unit\Service; + +use OC\Notification\Notification; +use OCA\WorkflowOcr\Service\NotificationService; +use OCP\Notification\IManager; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class NotificationServiceTest extends TestCase { + /** @var IManager|MockObject */ + private $notificationManager; + + /** @var Notification|MockObject */ + private $notification; + + /** @var NotificationService */ + private $service; + + public function setUp() : void { + $this->notificationManager = $this->createMock(IManager::class); + $this->notification = $this->createMock(Notification::class); + $this->service = new NotificationService($this->notificationManager); + parent::setUp(); + } + + public function testCreateErrorNotificationWithFileId() { + $this->notificationManager->expects($this->once()) + ->method('createNotification') + ->willReturn($this->notification); + $this->notification->expects($this->once()) + ->method('setApp') + ->with('workflow_ocr') + ->willReturn($this->notification); + $this->notification->expects($this->once()) + ->method('setUser') + ->with('user1') + ->willReturn($this->notification); + $this->notification->expects($this->once()) + ->method('setDateTime') + ->willReturn($this->notification); + $this->notification->expects($this->once()) + ->method('setSubject') + ->with('ocr_error', ['message' => 'testnotification']) + ->willReturn($this->notification); + $this->notification->expects($this->once()) + ->method('setObject') + ->with('file', '123') + ->willReturn($this->notification); + $this->notificationManager->expects($this->once()) + ->method('notify') + ->with($this->notification); + + $this->service->createErrorNotification("user1", "testnotification", 123); + } + + public function testCreateErrorNotificationWithoutFileId() { + $this->notificationManager->expects($this->once()) + ->method('createNotification') + ->willReturn($this->notification); + $this->notification->expects($this->once()) + ->method('setApp') + ->with('workflow_ocr') + ->willReturn($this->notification); + $this->notification->expects($this->once()) + ->method('setUser') + ->with('user1') + ->willReturn($this->notification); + $this->notification->expects($this->once()) + ->method('setDateTime') + ->willReturn($this->notification); + $this->notification->expects($this->once()) + ->method('setSubject') + ->with('ocr_error', ['message' => 'testnotification']) + ->willReturn($this->notification); + $this->notification->expects($this->once()) + ->method('setObject') + ->with('ocr', 'ocr'); + $this->notificationManager->expects($this->once()) + ->method('notify') + ->with($this->notification); + + $this->service->createErrorNotification("user1", "testnotification"); + } + + public function testCreateErrorNotificationDoesNothingIfUserIdIsNotSet() { + $this->notificationManager->expects($this->never()) + ->method('createNotification'); + $this->notificationManager->expects($this->never()) + ->method('notify'); + $this->notification->expects($this->never()) + ->method('setApp'); + $this->notification->expects($this->never()) + ->method('setUser'); + $this->notification->expects($this->never()) + ->method('setDateTime'); + $this->notification->expects($this->never()) + ->method('setSubject'); + $this->notification->expects($this->never()) + ->method('setObject'); + + $this->service->createErrorNotification(null, "testnotification", 123); + } +} diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml index 92aff0b..8b9d31b 100644 --- a/tests/psalm-baseline.xml +++ b/tests/psalm-baseline.xml @@ -1,5 +1,5 @@ - + $this->rootFolder @@ -10,6 +10,13 @@ NoUserException + + + $this->rootFolder + IRootFolder + IRootFolder + + $this->rootFolder