diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ece707ed0..5e372bdbc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,6 +71,7 @@ jobs: git clone --depth 1 https://github.com/nextcloud/server.git -b ${{ matrix.nextcloudVersion }} cd server && git submodule update --init ./occ maintenance:install --admin-pass=admin + git clone --depth 1 https://github.com/nextcloud/notifications.git -b ${{ matrix.nextcloudVersion }} apps/notifications - name: PHP stan run: make phpstan diff --git a/bootstrap.php b/bootstrap.php index 275626f83..cda8f2cc4 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -17,6 +17,7 @@ $classLoader->addPsr4("OCA\\OpenProject\\Settings\\", __DIR__ . '/lib/Settings', true); $classLoader->addPsr4("OCP\\", $serverPath . '/lib/public', true); $classLoader->addPsr4("OC\\", $serverPath . '/lib/private', true); +$classLoader->addPsr4("OCA\\Notifications\\", $serverPath . '/apps/notifications/lib/', true); $classLoader->addPsr4("OCA\\Files\\Event\\", $serverPath . '/apps/files/lib/Event', true); $classLoader->addPsr4("OCA\\OpenProject\\AppInfo\\", __DIR__ . '/lib/AppInfo', true); $classLoader->addPsr4("OCA\\OpenProject\\Controller\\", __DIR__ . '/lib/Controller', true); diff --git a/lib/Controller/ConfigController.php b/lib/Controller/ConfigController.php index a24bbde10..036fffa13 100755 --- a/lib/Controller/ConfigController.php +++ b/lib/Controller/ConfigController.php @@ -98,7 +98,6 @@ public function clearUserInfo(string $userId = null) { $this->config->deleteUserValue($userId, Application::APP_ID, 'user_id'); $this->config->deleteUserValue($userId, Application::APP_ID, 'user_name'); $this->config->deleteUserValue($userId, Application::APP_ID, 'refresh_token'); - $this->config->deleteUserValue($userId, Application::APP_ID, 'last_notification_check'); } /** diff --git a/lib/Notification/Notifier.php b/lib/Notification/Notifier.php index de93057ed..a633887c6 100644 --- a/lib/Notification/Notifier.php +++ b/lib/Notification/Notifier.php @@ -12,15 +12,18 @@ namespace OCA\OpenProject\Notification; use InvalidArgumentException; +use OCA\OpenProject\Service\OpenProjectAPIService; +use OCP\IConfig; use OCP\IURLGenerator; use OCP\IUserManager; use OCP\L10N\IFactory; +use OCP\Notification\IDismissableNotifier; use OCP\Notification\IManager as INotificationManager; use OCP\Notification\INotification; use OCP\Notification\INotifier; use OCA\OpenProject\AppInfo\Application; -class Notifier implements INotifier { +class Notifier implements INotifier, IDismissableNotifier { /** @var IFactory */ protected $factory; @@ -34,6 +37,15 @@ class Notifier implements INotifier { /** @var IURLGenerator */ protected $url; + /** + * @var OpenProjectAPIService + */ + private $openprojectAPIService; + + /** + * @var IConfig + */ + private $config; /** * @param IFactory $factory * @param IUserManager $userManager @@ -43,11 +55,15 @@ class Notifier implements INotifier { public function __construct(IFactory $factory, IUserManager $userManager, INotificationManager $notificationManager, - IURLGenerator $urlGenerator) { + IURLGenerator $urlGenerator, + OpenProjectAPIService $openprojectAPIService, + IConfig $config) { $this->factory = $factory; $this->userManager = $userManager; $this->notificationManager = $notificationManager; $this->url = $urlGenerator; + $this->openprojectAPIService = $openprojectAPIService; + $this->config = $config; } /** @@ -85,23 +101,39 @@ public function prepare(INotification $notification, string $languageCode): INot $l = $this->factory->get('integration_openproject', $languageCode); switch ($notification->getSubject()) { - case 'new_open_tickets': + case 'op_notification': $p = $notification->getSubjectParameters(); - $nbNotifications = (int) ($p['nbNotifications'] ?? 0); - $content = $l->t('OpenProject activity'); + $openprojectUrl = $this->config->getAppValue( + Application::APP_ID, 'oauth_instance_url' + ); + $link = OpenProjectAPIService::sanitizeUrl( + $openprojectUrl . '/notifications/details/' . $p['wpId'] . '/activity/' + ); + // see https://github.com/nextcloud/server/issues/1706 for docs $richSubjectInstance = [ 'type' => 'file', 'id' => 0, - 'name' => $p['link'], + 'name' => $link, 'path' => '', - 'link' => $p['link'], + 'link' => $link, ]; + $message = $p['projectTitle'] . ' - '; + foreach ($p['reasons'] as $reason) { + $message .= $reason . ','; + } + $message = rtrim($message, ','); + $message .= ' ' . $l->t('by') . ' '; + + foreach ($p['actors'] as $actor) { + $message .= $actor . ','; + } + $message = rtrim($message, ','); - $notification->setParsedSubject($content) + $notification->setParsedSubject('(' . $p['count']. ') ' . $p['resourceTitle']) ->setParsedMessage('--') - ->setLink($p['link'] ?? '') + ->setLink($link) ->setRichMessage( - $l->n('You have %s new notification in {instance}', 'You have %s new notifications in {instance}', $nbNotifications, [$nbNotifications]), + $message, [ 'instance' => $richSubjectInstance, ] @@ -114,4 +146,27 @@ public function prepare(INotification $notification, string $languageCode): INot throw new InvalidArgumentException(); } } + + + /** + * @inheritDoc + */ + public function dismissNotification(INotification $notification): void { + if ($notification->getApp() !== Application::APP_ID) { + throw new \InvalidArgumentException('Unhandled app'); + } + $refreshNotificationsInProgress = $this->config->getUserValue( + $notification->getUser(), + Application::APP_ID, + 'refresh-notifications-in-progress', + 'false' + ); + if ($refreshNotificationsInProgress === 'false') { + $parameters = $notification->getSubjectParameters(); + $this->openprojectAPIService->markAllNotificationsOfWorkPackageAsRead( + $parameters['wpId'], + $notification->getUser() + ); + } + } } diff --git a/lib/Service/OpenProjectAPIService.php b/lib/Service/OpenProjectAPIService.php index a0ec8879d..e8cdee03b 100644 --- a/lib/Service/OpenProjectAPIService.php +++ b/lib/Service/OpenProjectAPIService.php @@ -14,6 +14,7 @@ use DateTime; use DateTimeZone; use Exception; +use OCA\Notifications\Handler; use OCP\Files\Node; use OCA\OpenProject\Exception\OpenprojectErrorException; use OCA\OpenProject\Exception\OpenprojectResponseException; @@ -89,6 +90,12 @@ class OpenProjectAPIService { * @var ICache */ private $cache = null; + + /** + * @var Handler + */ + private $handler; + /** * Service to make requests to OpenProject v3 (JSON) API */ @@ -103,7 +110,8 @@ public function __construct( IClientService $clientService, IRootFolder $storage, IURLGenerator $urlGenerator, - ICacheFactory $cacheFactory) { + ICacheFactory $cacheFactory, + Handler $handler = null) { $this->appName = $appName; $this->userManager = $userManager; $this->avatarManager = $avatarManager; @@ -114,7 +122,7 @@ public function __construct( $this->client = $clientService->newClient(); $this->storage = $storage; $this->urlGenerator = $urlGenerator; - + $this->handler = $handler; $this->cache = $cacheFactory->createDistributed(); } @@ -135,6 +143,10 @@ public function checkNotifications(): void { * @return void */ private function checkNotificationsForUser(string $userId): void { + if ($this->handler === null) { + // notifications app seems not to exist + return; + } $accessToken = $this->config->getUserValue($userId, Application::APP_ID, 'token'); $notificationEnabled = ($this->config->getUserValue( $userId, @@ -142,31 +154,73 @@ private function checkNotificationsForUser(string $userId): void { 'notification_enabled', $this->config->getAppValue(Application::APP_ID, 'default_enable_notifications', '0')) === '1'); if ($accessToken && $notificationEnabled) { - $lastNotificationCheck = $this->config->getUserValue($userId, Application::APP_ID, 'last_notification_check'); - $lastNotificationCheck = $lastNotificationCheck === '' ? 0 : (int)$lastNotificationCheck; - $newLastNotificationCheck = time(); - $openprojectUrl = $this->config->getAppValue(Application::APP_ID, 'oauth_instance_url'); $notifications = $this->getNotifications($userId); - if (!isset($notifications['error']) && count($notifications) > 0) { + $aggregatedNotifications = []; + if (!isset($notifications['error'])) { + foreach ($notifications as $n) { + $wpId = preg_replace('/.*\//', '', $n['_links']['resource']['href']); + if (!array_key_exists($wpId, $aggregatedNotifications)) { + $aggregatedNotifications[$wpId] = [ + 'wpId' => $wpId, + 'resourceTitle' => $n['_links']['resource']['title'], + 'projectTitle' => $n['_links']['project']['title'], + 'count' => 1, + 'updatedAt' => $n['updatedAt'] + ]; + } else { + $storedUpdatedAt = \Safe\strtotime($aggregatedNotifications[$wpId]['updatedAt']); + if (\Safe\strtotime($n['updatedAt']) > $storedUpdatedAt) { + // currently the code never comes here because the notifications are ordered + // by 'updatedAt' but as backup I would keep it + $aggregatedNotifications[$wpId]['updatedAt'] = $n['updatedAt']; + } + $aggregatedNotifications[$wpId]['count']++; + } + $aggregatedNotifications[$wpId]['reasons'][] = $n['reason']; + $aggregatedNotifications[$wpId]['actors'][] = $n['_links']['actor']['title']; + } + $manager = $this->notificationManager; + $notificationsFilter = $manager->createNotification(); + $notificationsFilter->setApp(Application::APP_ID) + ->setUser($userId); $this->config->setUserValue( $userId, Application::APP_ID, - 'last_notification_check', - "$newLastNotificationCheck" + 'refresh-notifications-in-progress', + 'true' ); - $nbRelevantNotifications = 0; - foreach ($notifications as $n) { - $createdAt = new DateTime($n['createdAt']); - if ($createdAt->getTimestamp() > $lastNotificationCheck) { - $nbRelevantNotifications++; + $currentNotifications = $this->handler->get($notificationsFilter); + foreach ($currentNotifications as $notificationId => $currentNotification) { + $parametersCurrentNotifications = $currentNotification->getSubjectParameters(); + $wpId = $parametersCurrentNotifications['wpId']; + if (isset($aggregatedNotifications[$wpId])) { + $currentNotificationUpdateTime = \Safe\strtotime($parametersCurrentNotifications['updatedAt']); + $newNotificationUpdateTime = \Safe\strtotime($aggregatedNotifications[$wpId]['updatedAt']); + + if ($newNotificationUpdateTime <= $currentNotificationUpdateTime) { + // nothing changed with any notification associated with that WP + // so get rid of it + unset($aggregatedNotifications[$wpId]); + } else { + $manager->markProcessed($currentNotification); + } + } else { // there are no notifications in OP associated with that WP + $manager->markProcessed($currentNotification); } } - if ($nbRelevantNotifications > 0) { - $this->sendNCNotification($userId, 'new_open_tickets', [ - 'nbNotifications' => $nbRelevantNotifications, - 'link' => self::sanitizeUrl($openprojectUrl . '/notifications') - ]); + + foreach ($aggregatedNotifications as $n) { + $n['reasons'] = array_unique($n['reasons']); + $n['actors'] = array_unique($n['actors']); + // TODO can we use https://github.com/nextcloud/notifications/blob/master/docs/notification-workflow.md#defer-and-flush ? + $this->sendNCNotification($userId, 'op_notification', $n); } + $this->config->setUserValue( + $userId, + Application::APP_ID, + 'refresh-notifications-in-progress', + 'false' + ); } } } @@ -232,7 +286,13 @@ public function getNotifications(string $userId): array { $result = $this->request($userId, 'notifications', $params); if (isset($result['error'])) { return $result; - } elseif (!isset($result['_embedded']['elements'])) { + } elseif ( + !isset($result['_embedded']['elements']) || + ( // if there is an element, it also has to contain '_links' + isset($result['_embedded']['elements'][0]) && + !isset($result['_embedded']['elements'][0]['_links']) + ) + ) { return ['error' => 'Malformed response']; } diff --git a/tests/lib/Controller/ConfigControllerTest.php b/tests/lib/Controller/ConfigControllerTest.php index 71d195ba3..91658acaa 100644 --- a/tests/lib/Controller/ConfigControllerTest.php +++ b/tests/lib/Controller/ConfigControllerTest.php @@ -702,7 +702,7 @@ public function testSetAdminConfigClearUserDataChangeNCOauthClient( if ($deleteUserValues === true) { $configMock - ->expects($this->exactly(12)) // 6 times for each user + ->expects($this->exactly(10)) // 5 times for each user ->method('deleteUserValue') ->withConsecutive( ['admin', 'integration_openproject', 'token'], @@ -710,13 +710,11 @@ public function testSetAdminConfigClearUserDataChangeNCOauthClient( ['admin', 'integration_openproject', 'user_id'], ['admin', 'integration_openproject', 'user_name'], ['admin', 'integration_openproject', 'refresh_token'], - ['admin', 'integration_openproject', 'last_notification_check'], [$this->user1->getUID(), 'integration_openproject', 'token'], [$this->user1->getUID(), 'integration_openproject', 'login'], [$this->user1->getUID(), 'integration_openproject', 'user_id'], [$this->user1->getUID(), 'integration_openproject', 'user_name'], [$this->user1->getUID(), 'integration_openproject', 'refresh_token'], - [$this->user1->getUID(), 'integration_openproject', 'last_notification_check'], ); } else { $configMock diff --git a/tests/lib/Service/OpenProjectAPIServiceCheckNotificationsTest.php b/tests/lib/Service/OpenProjectAPIServiceCheckNotificationsTest.php index 58997dbf4..031386e18 100644 --- a/tests/lib/Service/OpenProjectAPIServiceCheckNotificationsTest.php +++ b/tests/lib/Service/OpenProjectAPIServiceCheckNotificationsTest.php @@ -9,6 +9,8 @@ namespace OCA\OpenProject\Service; +use OCA\Notifications\Handler; +use OCP\Http\Client\IClientService; use OCP\Http\Client\IResponse; use OCP\IConfig; use OCP\IUserManager; @@ -17,159 +19,321 @@ use PHPUnit\Framework\TestCase; class OpenProjectAPIServiceCheckNotificationsTest extends TestCase { - /** - * @var string - */ - private $oPNotificationAPIResponse = ' - { - "_type": "Collection", - "_embedded": { - "elements": [ - { - "_type": "Notification", - "id": 21, - "reason": "commented", - "createdAt": "2022-05-11T10:10:10Z" - }, - { - "_type": "Notification", - "id": 22, - "reason": "commented", - "createdAt": "2022-05-12T10:10:10Z" - }, - { - "_type": "Notification", - "id": 23, - "reason": "commented", - "createdAt": "2022-05-13T10:10:10Z" - }, - { - "_type": "Notification", - "id": 25, - "reason": "commented", - "createdAt": "2022-05-14T10:10:10Z" - } - ] - } - }'; + /** @var IConfig $configMock */ + private $configMock; /** - * @return array - */ - public function checkNotificationDataProvider(): array { - return [ - [ '', 4, true ], // last_notification_check was not set yet - [ '1652132430', 4, true ], // all notifications are were created after the last_notification_check - [ '1652350210', 2, true ], // some notifications were created after and some befor the last_notification_check - [ '1652605230', 0, false] // all notifications are older that last_notification_check - ]; - } - /** - * @dataProvider checkNotificationDataProvider + * @return void + * @before */ - public function testCheckNotifications( - string $lastNotificationCheck, - int $countOfReportedOPNotifications, - bool $nextCloudNotificationFired - ): void { - $configMock = $this->getMockBuilder(IConfig::class)->getMock(); - $configMock + public function setUpMocks(): void { + $this->configMock = $this->getMockBuilder(IConfig::class)->getMock(); + $this->configMock ->method('getUserValue') ->withConsecutive( [$this->anything(), 'integration_openproject', 'token'], [$this->anything(), 'integration_openproject', 'notification_enabled'], - [$this->anything(), 'integration_openproject', 'last_notification_check'], [$this->anything(), 'integration_openproject', 'token'], [$this->anything(), 'integration_openproject', 'refresh_token'], ) ->willReturnOnConsecutiveCalls( '123456', '1', - $lastNotificationCheck, '123456', 'refresh-token', ); - $configMock - ->expects($this->once()) - ->method('setUserValue') - ->with( - $this->anything(), - 'integration_openproject', - 'last_notification_check', - $this->anything() - ); - $configMock + $this->configMock ->method('getAppValue') ->withConsecutive( ['integration_openproject', 'default_enable_notifications','0'], - ['integration_openproject', 'oauth_instance_url'], ['integration_openproject', 'client_id'], ['integration_openproject', 'client_secret'], ['integration_openproject', 'oauth_instance_url'], - ['integration_openproject', 'client_id'], - ['integration_openproject', 'oauth_instance_url'], )->willReturnOnConsecutiveCalls( '0', - 'https://openproject', 'clientID', 'SECRET', 'https://openproject', - 'clientID', - 'https://openproject' ); + } + /** + * @param string|null $oPNotificationAPIResponse + * @return IClientService + */ + private function getClientServiceMock($oPNotificationAPIResponse = null): IClientService { $response = $this->getMockBuilder(IResponse::class)->getMock(); - $response->method('getBody')->willReturn($this->oPNotificationAPIResponse); + if ($oPNotificationAPIResponse === null) { + $oPNotificationAPIResponse = '{ + "_type": "Collection", + "_embedded": { + "elements": + '; + $oPNotificationAPIResponse .= file_get_contents( + __DIR__ . '/../../jest/fixtures/notificationsResponse.json' + ); + $oPNotificationAPIResponse .= '}}'; + } + $response->method('getBody')->willReturn($oPNotificationAPIResponse); $ocClient = $this->getMockBuilder('\OCP\Http\Client\IClient')->getMock(); $ocClient->method('get')->willReturn($response); $clientService = $this->getMockBuilder('\OCP\Http\Client\IClientService')->getMock(); $clientService->method('newClient')->willReturn($ocClient); + return $clientService; + } - $notificationManagerMock = $this->getMockBuilder(IManager::class)->getMock(); - - if ($nextCloudNotificationFired) { - $notificationMock = $this->getMockBuilder(INotification::class) - ->getMock(); - - $notificationMock - ->expects($this->once()) - ->method('setSubject') - ->with( - 'new_open_tickets', - [ - 'nbNotifications' => $countOfReportedOPNotifications, - 'link' => 'https://openproject/notifications' - ] - ); - - $notificationManagerMock - ->expects($this->once()) - ->method('createNotification') - ->willReturn($notificationMock); - - $notificationManagerMock - ->expects($this->once()) - ->method('notify'); - } else { - $notificationManagerMock - ->expects($this->never()) - ->method('notify'); - } - $service = new OpenProjectAPIService( + /** + * @param IManager $notificationManagerMock + * @param IClientService $clientServiceMock + * @param Handler $handlerMock + * @return OpenProjectAPIService + */ + private function getService($notificationManagerMock, $clientServiceMock, $handlerMock): OpenProjectAPIService { + return new OpenProjectAPIService( 'integration_openproject', \OC::$server->get(IUserManager::class), $this->createMock(\OCP\IAvatarManager::class), $this->createMock(\Psr\Log\LoggerInterface::class), $this->createMock(\OCP\IL10N::class), - $configMock, + $this->configMock, $notificationManagerMock, - $clientService, + $clientServiceMock, $this->createMock(\OCP\Files\IRootFolder::class), $this->createMock(\OCP\IURLGenerator::class), $this->createMock(\OCP\ICacheFactory::class), + $handlerMock, + ); + } + + public function testCheckNotifications(): void { + $notificationManagerMock = $this->getMockBuilder(IManager::class)->getMock(); + $notificationMock = $this->getMockBuilder(INotification::class) + ->getMock(); + + $notificationMock + ->expects($this->exactly(3)) + ->method('setSubject') + ->withConsecutive( + [ + 'op_notification', + [ + 'wpId' => '36', + 'resourceTitle' => 'write a software', + 'projectTitle' => 'Dev-large', + 'count' => 2, + 'reasons' => ['assigned'], + 'actors' => ['Admin de DEV user'], + 'updatedAt' => '2022-08-17T10:28:12Z' + ] + ], + [ + 'op_notification', + [ + 'wpId' => '17', + 'resourceTitle' => 'Create wireframes for new landing page', + 'projectTitle' => 'Scrum project', + 'count' => 5, + 'reasons' => [0 => 'assigned', 3 => 'mentioned'], + 'actors' => [0 => 'Admin de DEV user', 2 => 'Artur Neumann'], + 'updatedAt' => '2022-08-17T10:27:41Z' + ] + ], + [ + 'op_notification', + [ + 'wpId' => '18', + 'resourceTitle' => 'Contact form', + 'projectTitle' => 'Scrum project', + 'count' => 1, + 'reasons' => ['mentioned'], + 'actors' => ['Artur Neumann'], + 'updatedAt' => '2022-08-09T08:00:08Z' + ] + ] + ); + + $notificationManagerMock + ->expects($this->exactly(4)) //once for marking as read and once for every notification + ->method('createNotification') + ->willReturn($notificationMock); + + $notificationManagerMock + ->expects($this->exactly(3)) + ->method('notify'); + + $service = $this->getService( + $notificationManagerMock, + $this->getClientServiceMock(), + $this->createMock(Handler::class) ); + $service->checkNotifications(); + } + public function testCheckNotificationsAfterAllNotificationsAreMarkedAsRead(): void { + $oPNotificationAPIResponse = '{ + "_type": "Collection", + "_embedded": { + "elements": [] + } + } + '; + $notificationManagerMock = $this->getMockBuilder(IManager::class)->getMock(); + $notificationMock = $this->getMockBuilder(INotification::class) + ->getMock(); + + $notificationMock + ->expects($this->never()) + ->method('setSubject'); + + $notificationManagerMock + ->expects($this->exactly(1)) //for marking as read + ->method('createNotification') + ->willReturn($notificationMock); + + $notificationManagerMock + ->expects($this->exactly(2)) + ->method('markProcessed'); + + $currentNotificationMock0 = $this->getMockBuilder(INotification::class)->getMock(); + $currentNotificationMock0 + ->method('getSubjectParameters') + ->willReturn(['wpId' => 34, 'updatedAt' => '2022-11-08T06:34:40Z']); + $currentNotificationMock1 = $this->getMockBuilder(INotification::class)->getMock(); + $currentNotificationMock1 + ->method('getSubjectParameters') + ->willReturn(['wpId' => 16, 'updatedAt' => '2022-11-07T06:34:40Z']); + + $handlerMock = $this->getMockBuilder(Handler::class)->disableOriginalConstructor()->getMock(); + $handlerMock->method('get') + ->willReturn( + [12 => $currentNotificationMock0, 13 => $currentNotificationMock1] + ); + $service = $this->getService( + $notificationManagerMock, + $this->getClientServiceMock($oPNotificationAPIResponse), + $handlerMock + ); + $service->checkNotifications(); + } + public function testCheckNotificationsAfterAllNotificationsOfOneWPAreMarkedAsRead(): void { + $notificationManagerMock = $this->getMockBuilder(IManager::class)->getMock(); + $notificationMock = $this->getMockBuilder(INotification::class) + ->getMock(); + + $notificationMock + ->expects($this->exactly(3)) // new notifications should be set + ->method('setSubject'); + + $notificationManagerMock + ->expects($this->exactly(4)) //once for marking as read and once for every notification + ->method('createNotification') + ->willReturn($notificationMock); + + $notificationManagerMock + ->expects($this->exactly(3)) // for new notifications + ->method('notify'); + + $notificationManagerMock + ->expects($this->exactly(2)) + ->method('markProcessed'); // for current ones that do not exist in the new response + + // current notifications of WP that do not exist in the new response + $currentNotificationMock0 = $this->getMockBuilder(INotification::class)->getMock(); + $currentNotificationMock0 + ->method('getSubjectParameters') + ->willReturn(['wpId' => 34, 'updatedAt' => '2022-11-08T06:34:40Z']); + $currentNotificationMock1 = $this->getMockBuilder(INotification::class)->getMock(); + $currentNotificationMock1 + ->method('getSubjectParameters') + ->willReturn(['wpId' => 16, 'updatedAt' => '2022-11-07T06:34:40Z']); + + $handlerMock = $this->getMockBuilder(Handler::class)->disableOriginalConstructor()->getMock(); + $handlerMock->method('get') + ->willReturn( + [12 => $currentNotificationMock0, 13 => $currentNotificationMock1] + ); + $service = $this->getService( + $notificationManagerMock, + $this->getClientServiceMock(), + $handlerMock + ); + $service->checkNotifications(); + } + public function testCheckNotificationsAfterOneWPReceivedANewNotification(): void { + $notificationManagerMock = $this->getMockBuilder(IManager::class)->getMock(); + $notificationMock = $this->getMockBuilder(INotification::class) + ->getMock(); + $notificationMock + ->expects($this->exactly(3)) + ->method('setSubject') + ->withConsecutive( + [ + 'op_notification', + [ + 'wpId' => '36', + 'resourceTitle' => 'write a software', + 'projectTitle' => 'Dev-large', + 'count' => 2, + 'reasons' => ['assigned'], + 'actors' => ['Admin de DEV user'], + 'updatedAt' => '2022-08-17T10:28:12Z' + ] + ], + [ + 'op_notification', + [ + 'wpId' => '17', + 'resourceTitle' => 'Create wireframes for new landing page', + 'projectTitle' => 'Scrum project', + 'count' => 5, + 'reasons' => [0 => 'assigned', 3 => 'mentioned'], + 'actors' => [0 => 'Admin de DEV user', 2 => 'Artur Neumann'], + 'updatedAt' => '2022-08-17T10:27:41Z' + ] + ], + [ + 'op_notification', + [ + 'wpId' => '18', + 'resourceTitle' => 'Contact form', + 'projectTitle' => 'Scrum project', + 'count' => 1, + 'reasons' => ['mentioned'], + 'actors' => ['Artur Neumann'], + 'updatedAt' => '2022-08-09T08:00:08Z' + ] + ] + ); + + $notificationManagerMock + ->expects($this->exactly(4)) //once for marking as read and once for every notification + ->method('createNotification') + ->willReturn($notificationMock); + + $notificationManagerMock + ->expects($this->exactly(3)) // for new notifications + ->method('notify'); + + $notificationManagerMock + ->expects($this->exactly(1)) + ->method('markProcessed'); // for the notification that needs to be upldated + + // this notification is also part of the response, but the response also + // contains an other newer notification of that WP + $currentNotificationMock0 = $this->getMockBuilder(INotification::class)->getMock(); + $currentNotificationMock0 + ->method('getSubjectParameters') + ->willReturn(['wpId' => 36, 'updatedAt' => '2022-08-17T10:13:25Z']); + + $handlerMock = $this->getMockBuilder(Handler::class)->disableOriginalConstructor()->getMock(); + $handlerMock->method('get') + ->willReturn([12 => $currentNotificationMock0]); + $service = $this->getService( + $notificationManagerMock, + $this->getClientServiceMock(), + $handlerMock + ); $service->checkNotifications(); } } diff --git a/tests/lib/Service/OpenProjectAPIServiceTest.php b/tests/lib/Service/OpenProjectAPIServiceTest.php index 2252797ea..fbb7be0b0 100644 --- a/tests/lib/Service/OpenProjectAPIServiceTest.php +++ b/tests/lib/Service/OpenProjectAPIServiceTest.php @@ -17,6 +17,7 @@ use OC\Avatar\GuestAvatar; use OC\Http\Client\Client; use OC_Util; +use OCA\Notifications\Handler; use OCA\OpenProject\Exception\OpenprojectErrorException; use OCA\OpenProject\Exception\OpenprojectResponseException; use OCP\Files\IRootFolder; @@ -313,6 +314,7 @@ private function getOpenProjectAPIService( $storageMock, $urlGeneratorMock, $this->createMock(ICacheFactory::class), + $this->createMock(Handler::class), ); } @@ -341,7 +343,8 @@ private function getServiceMock( $this->createMock(IClientService::class), $this->createMock(IRootFolder::class), $this->createMock(IURLGenerator::class), - $cacheFactoryMock + $cacheFactoryMock, + $this->createMock(Handler::class), ]) ->onlyMethods($onlyMethods) ->getMock(); @@ -508,7 +511,7 @@ public function testGetNotificationsRequest() { $providerResponse ->setStatus(Http::STATUS_OK) ->addHeader('Content-Type', 'application/json') - ->setBody(["_embedded" => ["elements" => [['some' => 'data']]]]); + ->setBody(["_embedded" => ["elements" => [['_links' => 'data']]]]); $this->builder ->uponReceiving('a GET request to /notifications') @@ -518,7 +521,7 @@ public function testGetNotificationsRequest() { $result = $this->service->getNotifications( 'testUser' ); - $this->assertSame([['some' => 'data']], $result); + $this->assertSame([['_links' => 'data']], $result); } /** @@ -1216,6 +1219,7 @@ public function testRequestException( $this->createMock(IRootFolder::class), $this->createMock(IURLGenerator::class), $this->createMock(ICacheFactory::class), + $this->createMock(Handler::class), ); $response = $service->request('', '', []);