Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

### Changed

- `POST /ocs/v2.php/apps/app_api/api/v1/notification` now answers `404` when the `notifications` app is not enabled, instead of `200` for a notification that nobody delivers.

## [34.0.0]

### Deprecated
Expand Down
13 changes: 13 additions & 0 deletions lib/Controller/NotificationsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@
use OCA\AppAPI\Attribute\AppAPIAuth;
use OCA\AppAPI\Notifications\ExNotificationsManager;
use OCA\AppAPI\ResponseDefinitions;
use OCP\App\IAppManager;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\OCS\OCSNotFoundException;
use OCP\AppFramework\OCSController;
use OCP\IRequest;
use OCP\Notification\INotification;
use Psr\Log\LoggerInterface;

/**
* @psalm-import-type AppAPINotification from ResponseDefinitions
Expand All @@ -31,6 +34,8 @@ class NotificationsController extends OCSController {
public function __construct(
IRequest $request,
private ExNotificationsManager $exNotificationsManager,
private IAppManager $appManager,
private LoggerInterface $logger,
) {
parent::__construct(Application::APP_ID, $request);

Expand All @@ -43,14 +48,22 @@ public function __construct(
* @param array<string, mixed> $params Notification parameters
*
* @return DataResponse<Http::STATUS_OK, AppAPINotification, array{}>
* @throws OCSNotFoundException The notifications app is not enabled, so nothing could deliver the notification
*
* 200: Notification sent
* 404: The notifications app is not enabled on this instance
*/
#[AppAPIAuth]
#[PublicPage]
#[NoCSRFRequired]
public function sendNotification(array $params): Response {
$appId = $this->request->getHeader('ex-app-id');
// The core notification manager delivers to registered notifier apps only; with the notifications app
// absent it delivers to nobody and reports nothing, so the ExApp would see a 200 for a lost notification.
if (!$this->appManager->isEnabledForAnyone('notifications')) {
$this->logger->warning('ExApp "{appId}" sent a notification, but the notifications app is not enabled', ['appId' => $appId]);
throw new OCSNotFoundException('The notifications app is not enabled, the notification cannot be delivered');
}
$userId = explode(':', base64_decode($this->request->getHeader('authorization-app-api')), 2)[0];
$notification = $this->exNotificationsManager->sendNotification($appId, $userId, $params);
return new DataResponse($this->notificationToArray($notification), Http::STATUS_OK);
Expand Down
28 changes: 28 additions & 0 deletions openapi-full.json
Original file line number Diff line number Diff line change
Expand Up @@ -5766,6 +5766,34 @@
}
}
}
},
"404": {
"description": "The notifications app is not enabled, so nothing could deliver the notification",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {}
}
}
}
}
}
}
}
}
}
Expand Down
28 changes: 28 additions & 0 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1917,6 +1917,34 @@
}
}
}
},
"404": {
"description": "The notifications app is not enabled, so nothing could deliver the notification",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {}
}
}
}
}
}
}
}
}
}
Expand Down
82 changes: 82 additions & 0 deletions tests/php/Controller/NotificationsControllerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\AppAPI\Tests\php\Controller;

use DateTime;
use OCA\AppAPI\Controller\NotificationsController;
use OCA\AppAPI\Notifications\ExNotificationsManager;
use OCP\App\IAppManager;
use OCP\AppFramework\Http;
use OCP\AppFramework\OCS\OCSNotFoundException;
use OCP\IRequest;
use OCP\Notification\INotification;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;

class NotificationsControllerTest extends TestCase {

private const APP_ID = 'test_exapp';
private const PARAMS = ['object' => 'file', 'object_id' => '42', 'subject_type' => 'done', 'subject_params' => []];

private NotificationsController $controller;
private ExNotificationsManager&MockObject $exNotificationsManager;
private IAppManager&MockObject $appManager;
private LoggerInterface&MockObject $logger;

protected function setUp(): void {
parent::setUp();
$request = $this->createMock(IRequest::class);
$request->method('getHeader')->willReturnMap([
['ex-app-id', self::APP_ID],
['authorization-app-api', base64_encode('alice:secret')],
]);
$this->exNotificationsManager = $this->createMock(ExNotificationsManager::class);
$this->appManager = $this->createMock(IAppManager::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->controller = new NotificationsController(
$request, $this->exNotificationsManager, $this->appManager, $this->logger,
);
}

public function testSendNotificationFailsLoudlyWhenNotificationsAppIsNotEnabled(): void {
$this->appManager->method('isEnabledForAnyone')->with('notifications')->willReturn(false);
$this->exNotificationsManager->expects($this->never())->method('sendNotification');
$this->logger->expects($this->once())->method('warning');

$this->expectException(OCSNotFoundException::class);
$this->controller->sendNotification(self::PARAMS);
Comment thread
oleksandr-nc marked this conversation as resolved.
}

public function testSendNotificationDeliversWhenNotificationsAppIsEnabled(): void {
$this->appManager->method('isEnabledForAnyone')->with('notifications')->willReturn(true);
$notification = $this->createMock(INotification::class);
$notification->method('getApp')->willReturn(self::APP_ID);
$notification->method('getUser')->willReturn('alice');
$notification->method('getDateTime')->willReturn(new DateTime('2026-01-01T00:00:00+00:00'));
$notification->method('getObjectType')->willReturn('file');
$notification->method('getObjectId')->willReturn('42');
$notification->method('getParsedSubject')->willReturn('');
$notification->method('getParsedMessage')->willReturn('');
$notification->method('getLink')->willReturn('');
$notification->method('getIcon')->willReturn('');
$this->exNotificationsManager->expects($this->once())
->method('sendNotification')
->with(self::APP_ID, 'alice', self::PARAMS)
->willReturn($notification);
$this->logger->expects($this->never())->method('warning');

$response = $this->controller->sendNotification(self::PARAMS);

$this->assertSame(Http::STATUS_OK, $response->getStatus());
$this->assertSame(self::APP_ID, $response->getData()['app']);
$this->assertSame('alice', $response->getData()['user']);
}
}
Loading