Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/notifications.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoas Schilling <coding@schilljs.com>2017-04-24 16:07:34 +0300
committerGitHub <noreply@github.com>2017-04-24 16:07:34 +0300
commitaf90c155980a4b51ce16c3ffadc48dced85ca115 (patch)
tree0421bc0fd0df3dfaddb4078d1a9439732a0d6a35
parent6982181dc3583ef8843fd1fa8ddfc35a03978358 (diff)
parentedbb99192eba9fe8d4630214b7aff1d35a3066a0 (diff)
Merge pull request #59 from nextcloud/push-notification
Allow devices to register for push notifications
-rwxr-xr-xappinfo/database.xml61
-rw-r--r--appinfo/info.xml2
-rw-r--r--appinfo/routes.php2
-rw-r--r--docs/push-v2.md213
-rw-r--r--lib/App.php10
-rw-r--r--lib/AppInfo/Application.php8
-rw-r--r--lib/Capabilities.php3
-rw-r--r--lib/Controller/PushController.php242
-rw-r--r--lib/Push.php211
-rw-r--r--tests/Unit/AppInfo/AppTest.php12
-rw-r--r--tests/Unit/AppInfo/ApplicationTest.php7
-rw-r--r--tests/Unit/AppInfo/RoutesTest.php2
-rw-r--r--tests/Unit/AppTest.php62
-rw-r--r--tests/Unit/CapabilitiesTest.php3
-rw-r--r--tests/Unit/Controller/PushControllerTest.php508
-rw-r--r--tests/Unit/PushTest.php487
16 files changed, 1805 insertions, 28 deletions
diff --git a/appinfo/database.xml b/appinfo/database.xml
index 66ecdcb..07febf9 100755
--- a/appinfo/database.xml
+++ b/appinfo/database.xml
@@ -4,6 +4,7 @@
<create>true</create>
<overwrite>false</overwrite>
<charset>utf8</charset>
+
<table>
<name>*dbprefix*notifications</name>
<declaration>
@@ -118,4 +119,64 @@
</index>
</declaration>
</table>
+
+ <table>
+ <name>*dbprefix*notifications_pushtokens</name>
+ <declaration>
+ <field>
+ <name>uid</name>
+ <type>text</type>
+ <notnull>true</notnull>
+ <length>64</length>
+ </field>
+ <field>
+ <name>token</name>
+ <type>integer</type>
+ <default>0</default>
+ <notnull>true</notnull>
+ <length>4</length>
+ </field>
+ <field>
+ <name>deviceidentifier</name>
+ <type>text</type>
+ <notnull>true</notnull>
+ <length>128</length>
+ </field>
+ <field>
+ <name>devicepublickey</name>
+ <type>text</type>
+ <notnull>true</notnull>
+ <length>512</length>
+ </field>
+ <field>
+ <name>devicepublickeyhash</name>
+ <type>text</type>
+ <notnull>true</notnull>
+ <length>128</length>
+ </field>
+ <field>
+ <name>pushtokenhash</name>
+ <type>text</type>
+ <notnull>true</notnull>
+ <length>128</length>
+ </field>
+ <field>
+ <name>proxyserver</name>
+ <type>text</type>
+ <notnull>true</notnull>
+ <length>256</length>
+ </field>
+
+ <index>
+ <name>oc_notifpushtoken</name>
+ <unique>true</unique>
+ <field>
+ <name>uid</name>
+ </field>
+ <field>
+ <name>token</name>
+ </field>
+ </index>
+ </declaration>
+ </table>
</database>
diff --git a/appinfo/info.xml b/appinfo/info.xml
index 47223b5..3873d18 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -15,7 +15,7 @@
<licence>AGPL</licence>
<author>Joas Schilling</author>
- <version>1.2.0</version>
+ <version>2.0.0</version>
<types>
<logging/>
diff --git a/appinfo/routes.php b/appinfo/routes.php
index 6c11dc3..205c353 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -24,5 +24,7 @@ return [
['name' => 'Endpoint#listNotifications', 'url' => '/api/{apiVersion}/notifications', 'verb' => 'GET', 'requirements' => ['apiVersion' => 'v(1|2)']],
['name' => 'Endpoint#getNotification', 'url' => '/api/{apiVersion}/notifications/{id}', 'verb' => 'GET', 'requirements' => ['apiVersion' => 'v(1|2)', 'id' => '\d+']],
['name' => 'Endpoint#deleteNotification', 'url' => '/api/{apiVersion}/notifications/{id}', 'verb' => 'DELETE', 'requirements' => ['apiVersion' => 'v(1|2)', 'id' => '\d+']],
+ ['name' => 'Push#registerDevice', 'url' => '/api/{apiVersion}/push', 'verb' => 'POST', 'requirements' => ['apiVersion' => 'v2']],
+ ['name' => 'Push#removeDevice', 'url' => '/api/{apiVersion}/push', 'verb' => 'DELETE', 'requirements' => ['apiVersion' => 'v2']],
],
];
diff --git a/docs/push-v2.md b/docs/push-v2.md
new file mode 100644
index 0000000..84aed3e
--- /dev/null
+++ b/docs/push-v2.md
@@ -0,0 +1,213 @@
+# Push notifications as a Nextcloud client device
+
+
+
+## Checking the capabilities of the Nextcloud server
+
+In order to find out if notifications support push on the server you can run a request against the capabilities endpoint: `/ocs/v2.php/cloud/capabilities`
+
+```
+{
+ "ocs": {
+ ...
+ "data": {
+ ...
+ "capabilities": {
+ ...
+ "notifications": {
+ "push": [
+ ...
+ "devices"
+ ]
+ }
+ }
+ }
+ }
+}
+```
+
+
+
+## Subscribing at the Nextcloud server
+
+1. **Only on first registration on the server** The device generates a `rsa2048` key pair (`devicePrivateKey` and `devicePublicKey`).
+
+2. The device generates the `PushToken` for *Apple Push Notification Service* (iOS) or *Firebase Cloud Messaging* (Android)
+
+3. The device generates a `sha512` hash of the `PushToken` (`PushTokenHash`)
+
+4. The device then sends the `devicePublicKey`, `PushTokenHash` and `proxyServerUrl` to the Nextcloud server:
+
+ ```
+ POST /ocs/v2.php/apps/notifications/api/v2/push
+
+ {
+ "pushTokenHash": "{{PushTokenHash}}",
+ "devicePublicKey": "{{devicePublicKey}}",
+ "proxyServer": "{{proxyServerUrl}}"
+ }
+ ```
+
+ ​
+
+### Response
+
+The server replies with the following status codes:
+
+| Status code | Meaning |
+| ----------- | ---------------------------------------- |
+| 200 | No further action by the device required |
+| 201 | Push token was created/updated and **needs to be sent to the `Proxy`** |
+| 400 | Invalid device public key; device does not use a token to authenticate; the push token hash is invalid formatted; the proxy server URL is invalid; |
+| 401 | Device is not logged in |
+
+
+
+#### Body in case of success
+
+In case of `200` and `201` the reply has more information in the body:
+
+| Key | Type | |
+| ---------------- | ------------ | ---------------------------------------- |
+| publicKey | string (512) | rsa2048 public key of the user account on the instance |
+| deviceIdentifier | string (128) | unique identifier encrypted with the users private key |
+| signature | string (512) | base64 encoded signature of the deviceIdentifier |
+
+
+
+#### Body in case of an error
+
+In case of `400` the following `message` can appear in the body:
+
+| Error | Description |
+| ------------------------ | ---------------------------------------- |
+| `INVALID_PUSHTOKEN_HASH` | The hash of the push token was not a valid `sha512` hash. |
+| `INVALID_SESSION_TOKEN` | The authentication token of the request could not be identified. Check whether a password was used to login. |
+| `INVALID_DEVICE_KEY` | The device key does not match the one registered to the provided session token. |
+| `INVALID_PROXY_SERVER` | The proxy server was not a valid https URL. |
+
+
+
+## Unsubcribing at the Nextcloud server
+
+When an account is removed from a device, the device should unregister on the server. Otherwise the server sends unnecessary push notifications and might be blocked because of spam.
+
+
+
+The device should then send a `DELETE` request to the Nextcloud server:
+
+```
+DELETE /ocs/v2.php/apps/notifications/api/v2/push
+```
+
+
+
+### Response
+
+The server replies with the following status codes:
+
+| Status code | Meaning |
+| ----------- | ---------------------------------------- |
+| 200 | Push token was not registered on the server |
+| 202 | Push token was deleted and **needs to be deleted from the `Proxy`** |
+| 400 | Device does not use a token to authenticate |
+| 401 | Device is not logged in |
+
+
+
+#### Body in case of an error
+
+In case of `400` the following `message` can appear in the body:
+
+| Error | Description |
+| ----------------------- | ---------------------------------------- |
+| `INVALID_SESSION_TOKEN` | The authentication token of the request could not be identified. |
+
+
+
+## Subscribing at the Push Proxy
+
+The device sends the`PushToken` as well as the `deviceIdentifier`, `signature` and the user´s `publicKey` (from the server´s response) to the Push Proxy:
+
+```
+POST /devices
+
+{
+ "pushToken": "{{PushToken}}",
+ "deviceIdentifier": "{{deviceIdentifier}}",
+ "deviceIdentifierSignature": "{{signature}}",
+ "userPublicKey": "{{userPublicKey}}"
+}
+```
+
+
+
+### Response
+
+The server replies with the following status codes:
+
+| Status code | Meaning |
+| ----------- | ---------------------------------------- |
+| 200 | Push token was written to the databse |
+| 400 | Push token, public key or device identifier is malformed, the signature does not match |
+| 403 | Device is not allowed to write the push token of the device identifier |
+| 409 | In case of a conflict the device can retry with the additional field `cloudId` with the value `{{userid}}@{{serverurl}}` which allows the proxy to verify the public key and device identifier belongs to the given user on the instance |
+
+
+
+## Unsubscribing at the Push Proxy
+
+The device sends the `deviceIdentifier`, `deviceIdentifierSignature` and the user´s `publicKey` (from the server´s response) to the Push Proxy:
+
+```
+DELETE /devices
+
+{
+ "deviceIdentifier": "{{deviceIdentifier}}",
+ "deviceIdentifierSignature": "{{signature}}",
+ "userPublicKey": "{{userPublicKey}}"
+}
+```
+
+
+
+### Response
+
+The server replies with the following status codes:
+
+| Status code | Meaning |
+| ----------- | ---------------------------------------- |
+| 200 | Push token was deleted from the database |
+| 400 | Public key or device identifier is malformed |
+| 403 | Device identifier and device public key didn't match or could not be found |
+
+
+
+## Pushed notifications
+
+The pushed notifications is defined by the [Firebase Cloud Messaging HTTP Protocol](https://firebase.google.com/docs/cloud-messaging/http-server-ref#send-downstream). The sample content of a Nextcloud push notification looks like the following:
+
+```json
+{
+ "to" : "APA91bHun4MxP5egoKMwt2KZFBaFUH-1RYqx...",
+ "notification" : {
+ "body" : "NEW_NOTIFICATION",
+ "body_loc_key" : "NEW_NOTIFICATION",
+ "title" : "NEW_NOTIFICATION",
+ "title_loc_key" : "NEW_NOTIFICATION"
+ },
+ "data" : {
+ "subject" : "*Encrypted subject*",
+ "signature" : "*Signature*"
+ }
+}
+```
+
+| Attribute | Meaning |
+| ----------- | ---------------------------------------- |
+| `subject` | The subject is encrypted with the device´s *public key*. |
+| `signature` | The signature is a sha512 signature over the encrypted subject using the user´s private key. |
+
+### Verification
+So a device should verify the signature using the user´s public key.
+If the signature is okay, the subject can be decrypted using the device´s private key.
diff --git a/lib/App.php b/lib/App.php
index 6cb0a8a..ccc2e4f 100644
--- a/lib/App.php
+++ b/lib/App.php
@@ -28,9 +28,12 @@ use OCP\Notification\INotification;
class App implements IApp {
/** @var Handler */
protected $handler;
+ /** @var Push */
+ protected $push;
- public function __construct(Handler $handler) {
+ public function __construct(Handler $handler, Push $push) {
$this->handler = $handler;
+ $this->push = $push;
}
/**
@@ -39,7 +42,10 @@ class App implements IApp {
* @since 8.2.0
*/
public function notify(INotification $notification) {
- $this->handler->add($notification);
+ $notificationId = $this->handler->add($notification);
+
+ $notificationToPush = $this->handler->getById($notificationId, $notification->getUser());
+ $this->push->pushToDevice($notificationToPush);
}
/**
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index c1577a0..af7e625 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -21,9 +21,11 @@
namespace OCA\Notifications\AppInfo;
+use OC\Authentication\Token\IProvider;
use OCA\Notifications\App;
use OCA\Notifications\Capabilities;
use OCA\Notifications\Controller\EndpointController;
+use OCP\AppFramework\IAppContainer;
use OCP\Util;
class Application extends \OCP\AppFramework\App {
@@ -31,8 +33,12 @@ class Application extends \OCP\AppFramework\App {
parent::__construct('notifications');
$container = $this->getContainer();
- $container->registerAlias('EndpointController', EndpointController::class);
$container->registerCapability(Capabilities::class);
+
+ // FIXME this is for automatic DI because it is not in DIContainer
+ $container->registerService(IProvider::class, function(IAppContainer $c) {
+ return $c->getServer()->query(IProvider::class);
+ });
}
public function register() {
diff --git a/lib/Capabilities.php b/lib/Capabilities.php
index a6ca12c..6b832f6 100644
--- a/lib/Capabilities.php
+++ b/lib/Capabilities.php
@@ -45,6 +45,9 @@ class Capabilities implements ICapability {
'icons',
'rich-strings',
],
+ 'push' => [
+ 'devices',
+ ],
],
];
}
diff --git a/lib/Controller/PushController.php b/lib/Controller/PushController.php
new file mode 100644
index 0000000..aef6eb3
--- /dev/null
+++ b/lib/Controller/PushController.php
@@ -0,0 +1,242 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\Notifications\Controller;
+
+use OC\Authentication\Exceptions\InvalidTokenException;
+use OC\Authentication\Token\IProvider;
+use OC\Authentication\Token\IToken;
+use OC\Security\IdentityProof\Manager;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\AppFramework\OCSController;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+use OCP\IRequest;
+use OCP\ISession;
+use OCP\IUser;
+use OCP\IUserSession;
+
+class PushController extends OCSController {
+
+ /** @var IDBConnection */
+ private $db;
+
+ /** @var ISession */
+ private $session;
+
+ /** @var IUserSession */
+ private $userSession;
+
+ /** @var IProvider */
+ private $tokenProvider;
+
+ /** @var Manager */
+ private $identityProof;
+
+ /**
+ * @param string $appName
+ * @param IRequest $request
+ * @param IDBConnection $db
+ * @param ISession $session
+ * @param IUserSession $userSession
+ * @param IProvider $tokenProvider
+ * @param Manager $identityProof
+ */
+ public function __construct($appName, IRequest $request, IDBConnection $db, ISession $session, IUserSession $userSession, IProvider $tokenProvider, Manager $identityProof) {
+ parent::__construct($appName, $request);
+
+ $this->db = $db;
+ $this->session = $session;
+ $this->userSession = $userSession;
+ $this->tokenProvider = $tokenProvider;
+ $this->identityProof = $identityProof;
+ }
+
+ /**
+ * @NoAdminRequired
+ *
+ * @param string $pushTokenHash
+ * @param string $devicePublicKey
+ * @param string $proxyServer
+ * @return DataResponse
+ */
+ public function registerDevice($pushTokenHash, $devicePublicKey, $proxyServer) {
+ $user = $this->userSession->getUser();
+ if (!$user instanceof IUser) {
+ return new DataResponse([], Http::STATUS_UNAUTHORIZED);
+ }
+
+ if (!preg_match('/^([a-f0-9]{128})$/', $pushTokenHash)) {
+ return new DataResponse(['message' => 'INVALID_PUSHTOKEN_HASH'], Http::STATUS_BAD_REQUEST);
+ }
+
+ if (
+ ((strlen($devicePublicKey) !== 450 || strpos($devicePublicKey, "\n" . '-----END PUBLIC KEY-----') !== 425) &&
+ (strlen($devicePublicKey) !== 451 || strpos($devicePublicKey, "\n" . '-----END PUBLIC KEY-----' . "\n") !== 425)) ||
+ strpos($devicePublicKey, '-----BEGIN PUBLIC KEY-----' . "\n") !== 0) {
+ return new DataResponse(['message' => 'INVALID_DEVICE_KEY'], Http::STATUS_BAD_REQUEST);
+ }
+
+ if (
+ !filter_var($proxyServer, FILTER_VALIDATE_URL) ||
+ strlen($proxyServer) > 256 ||
+ !preg_match('/^(https\:\/\/|http\:\/\/localhost(\:[0-9]{0,5})?\/)/', $proxyServer)
+ ) {
+ return new DataResponse(['message' => 'INVALID_PROXY_SERVER'], Http::STATUS_BAD_REQUEST);
+ }
+
+ $tokenId = $this->session->get('token-id');
+ try {
+ $token = $this->tokenProvider->getTokenById($tokenId);
+ } catch (InvalidTokenException $e) {
+ return new DataResponse(['message' => 'INVALID_SESSION_TOKEN'], Http::STATUS_BAD_REQUEST);
+ }
+
+ $key = $this->identityProof->getKey($user);
+
+ $deviceIdentifier = json_encode([$user->getCloudId(), $token->getId()]);
+ openssl_sign($deviceIdentifier, $signature, $key->getPrivate(), OPENSSL_ALGO_SHA512);
+ $deviceIdentifier = base64_encode(hash('sha512', $deviceIdentifier, true));
+
+ $created = $this->savePushToken($user, $token, $deviceIdentifier, $devicePublicKey, $pushTokenHash, $proxyServer);
+
+ return new DataResponse([
+ 'publicKey' => $key->getPublic(),
+ 'deviceIdentifier' => $deviceIdentifier,
+ 'signature' => base64_encode($signature),
+ ], $created ? Http::STATUS_CREATED : Http::STATUS_OK);
+ }
+
+ /**
+ * @NoAdminRequired
+ *
+ * @return DataResponse
+ */
+ public function removeDevice() {
+ $user = $this->userSession->getUser();
+ if (!$user instanceof IUser) {
+ return new DataResponse([], Http::STATUS_UNAUTHORIZED);
+ }
+
+ $tokenId = $this->session->get('token-id');
+ try {
+ $token = $this->tokenProvider->getTokenById($tokenId);
+ } catch (InvalidTokenException $e) {
+ return new DataResponse(['message' => 'INVALID_SESSION_TOKEN'], Http::STATUS_BAD_REQUEST);
+ }
+
+ if ($this->deletePushToken($user, $token)) {
+ return new DataResponse([], Http::STATUS_ACCEPTED);
+ }
+
+ return new DataResponse([], Http::STATUS_OK);
+ }
+
+ /**
+ * @param IUser $user
+ * @param IToken $token
+ * @param string $deviceIdentifier
+ * @param string $devicePublicKey
+ * @param string $pushTokenHash
+ * @param string $proxyServer
+ * @return bool If the hash was new to the database
+ */
+ protected function savePushToken(IUser $user, IToken $token, $deviceIdentifier, $devicePublicKey, $pushTokenHash, $proxyServer) {
+ $query = $this->db->getQueryBuilder();
+ $query->select('*')
+ ->from('notifications_pushtokens')
+ ->where($query->expr()->eq('uid', $query->createNamedParameter($user->getUID())))
+ ->andWhere($query->expr()->eq('token', $query->createNamedParameter($token->getId())));
+ $result = $query->execute();
+ $row = $result->fetch();
+ $result->closeCursor();
+
+ if (!$row) {
+ return $this->insertPushToken($user, $token, $deviceIdentifier, $devicePublicKey, $pushTokenHash, $proxyServer);
+ }
+
+ return $this->updatePushToken($user, $token, $devicePublicKey, $pushTokenHash, $proxyServer);
+ }
+
+ /**
+ * @param IUser $user
+ * @param IToken $token
+ * @param string $deviceIdentifier
+ * @param string $devicePublicKey
+ * @param string $pushTokenHash
+ * @param string $proxyServer
+ * @return bool If the entry was created
+ */
+ protected function insertPushToken(IUser $user, IToken $token, $deviceIdentifier, $devicePublicKey, $pushTokenHash, $proxyServer) {
+ $devicePublicKeyHash = hash('sha512', $devicePublicKey);
+
+ $query = $this->db->getQueryBuilder();
+ $query->insert('notifications_pushtokens')
+ ->values([
+ 'uid' => $query->createNamedParameter($user->getUID()),
+ 'token' => $query->createNamedParameter($token->getId(), IQueryBuilder::PARAM_INT),
+ 'deviceidentifier' => $query->createNamedParameter($deviceIdentifier),
+ 'devicepublickey' => $query->createNamedParameter($devicePublicKey),
+ 'devicepublickeyhash' => $query->createNamedParameter($devicePublicKeyHash),
+ 'pushtokenhash' => $query->createNamedParameter($pushTokenHash),
+ 'proxyserver' => $query->createNamedParameter($proxyServer),
+ ]);
+ return $query->execute() > 0;
+ }
+
+ /**
+ * @param IUser $user
+ * @param IToken $token
+ * @param string $devicePublicKey
+ * @param string $pushTokenHash
+ * @param string $proxyServer
+ * @return bool If the entry was updated
+ */
+ protected function updatePushToken(IUser $user, IToken $token, $devicePublicKey, $pushTokenHash, $proxyServer) {
+ $devicePublicKeyHash = hash('sha512', $devicePublicKey);
+
+ $query = $this->db->getQueryBuilder();
+ $query->update('notifications_pushtokens')
+ ->set('devicepublickey', $query->createNamedParameter($devicePublicKey))
+ ->set('devicepublickeyhash', $query->createNamedParameter($devicePublicKeyHash))
+ ->set('pushtokenhash', $query->createNamedParameter($pushTokenHash))
+ ->set('proxyserver', $query->createNamedParameter($proxyServer))
+ ->where($query->expr()->eq('uid', $query->createNamedParameter($user->getUID())))
+ ->andWhere($query->expr()->eq('token', $query->createNamedParameter($token->getId(), IQueryBuilder::PARAM_INT)));
+
+ return $query->execute() !== 0;
+ }
+
+ /**
+ * @param IUser $user
+ * @param IToken $token
+ * @return bool If the entry was deleted
+ */
+ protected function deletePushToken(IUser $user, IToken $token) {
+ $query = $this->db->getQueryBuilder();
+ $query->delete('notifications_pushtokens')
+ ->where($query->expr()->eq('uid', $query->createNamedParameter($user->getUID())))
+ ->andWhere($query->expr()->eq('token', $query->createNamedParameter($token->getId(), IQueryBuilder::PARAM_INT)));
+
+ return $query->execute() !== 0;
+ }
+}
diff --git a/lib/Push.php b/lib/Push.php
new file mode 100644
index 0000000..5859e25
--- /dev/null
+++ b/lib/Push.php
@@ -0,0 +1,211 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\Notifications;
+
+
+use OC\Authentication\Exceptions\InvalidTokenException;
+use OC\Authentication\Token\IProvider;
+use OC\Security\IdentityProof\Key;
+use OC\Security\IdentityProof\Manager;
+use OCP\AppFramework\Http;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\Http\Client\IClientService;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\ILogger;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\Notification\IManager as INotificationManager;
+use OCP\Notification\INotification;
+
+class Push {
+ /** @var IDBConnection */
+ protected $db;
+ /** @var INotificationManager */
+ protected $notificationManager;
+ /** @var IConfig */
+ protected $config;
+ /** @var IProvider */
+ protected $tokenProvider;
+ /** @var Manager */
+ private $keyManager;
+ /** @var IUserManager */
+ private $userManager;
+ /** @var IClientService */
+ protected $clientService;
+ /** @var ILogger */
+ protected $log;
+
+ public function __construct(IDBConnection $connection, INotificationManager $notificationManager, IConfig $config, IProvider $tokenProvider, Manager $keyManager, IUserManager $userManager, IClientService $clientService, ILogger $log) {
+ $this->db = $connection;
+ $this->notificationManager = $notificationManager;
+ $this->config = $config;
+ $this->tokenProvider = $tokenProvider;
+ $this->keyManager = $keyManager;
+ $this->userManager = $userManager;
+ $this->clientService = $clientService;
+ $this->log = $log;
+ }
+
+ /**
+ * @param INotification $notification
+ */
+ public function pushToDevice(INotification $notification) {
+ $user = $this->userManager->get($notification->getUser());
+ if (!($user instanceof IUser)) {
+ return;
+ }
+
+ $devices = $this->getDevicesForUser($notification->getUser());
+ if (empty($devices)) {
+ return;
+ }
+
+ $language = $this->config->getUserValue($notification->getUser(), 'core', 'lang', 'en');
+ try {
+ $notification = $this->notificationManager->prepare($notification, $language);
+ } catch (\InvalidArgumentException $e) {
+ return;
+ }
+
+ $userKey = $this->keyManager->getKey($user);
+
+ $pushNotifications = [];
+ foreach ($devices as $device) {
+ try {
+ $payload = json_encode($this->encryptAndSign($userKey, $device, $notification));
+
+ $proxyServer = rtrim($device['proxyserver'], '/');
+ if (!isset($pushNotifications[$proxyServer])) {
+ $pushNotifications[$proxyServer] = [];
+ }
+ $pushNotifications[$proxyServer][] = $payload;
+ } catch (InvalidTokenException $e) {
+ // Token does not exist anymore, should drop the push device entry
+ $this->deletePushToken($device['token']);
+ } catch (\InvalidArgumentException $e) {
+ // Failed to encrypt message for device: public key is invalid
+ $this->deletePushToken($device['token']);
+ }
+ }
+
+ if (empty($pushNotifications)) {
+ return;
+ }
+
+ $client = $this->clientService->newClient();
+ foreach ($pushNotifications as $proxyServer => $notifications) {
+ try {
+ $response = $client->post($proxyServer . '/notifications', [
+ 'body' => [
+ 'notifications' => $notifications,
+ ],
+ ]);
+ } catch (\Exception $e) {
+ $this->log->logException($e, [
+ 'app' => 'notifications',
+ ]);
+ continue;
+ }
+
+ $status = $response->getStatusCode();
+ if ($status !== Http::STATUS_OK && $status !== Http::STATUS_SERVICE_UNAVAILABLE) {
+ $body = $response->getBody();
+ $this->log->error('Could not send notification to push server [{url}]: {error}',[
+ 'error' => is_string($body) ? $body : 'no reason given',
+ 'url' => $proxyServer,
+ 'app' => 'notifications',
+ ]);
+ } else if ($status === Http::STATUS_SERVICE_UNAVAILABLE && $this->config->getSystemValue('debug', false)) {
+ $body = $response->getBody();
+ $this->log->debug('Could not send notification to push server [{url}]: {error}',[
+ 'error' => is_string($body) ? $body : 'no reason given',
+ 'url' => $proxyServer,
+ 'app' => 'notifications',
+ ]);
+ }
+ }
+ }
+
+ /**
+ * @param Key $userKey
+ * @param array $device
+ * @param INotification $notification
+ * @return array
+ * @throws InvalidTokenException
+ * @throws \InvalidArgumentException
+ */
+ protected function encryptAndSign(Key $userKey, array $device, INotification $notification) {
+ // Check if the token is still valid...
+ $this->tokenProvider->getTokenById($device['token']);
+
+ $data = [
+ 'app' => $notification->getApp(),
+ 'subject' => $notification->getParsedSubject(),
+ ];
+
+ if (!openssl_public_encrypt(json_encode($data), $encryptedSubject, $device['devicepublickey'], OPENSSL_PKCS1_PADDING)) {
+ $this->log->error(openssl_error_string(), ['app' => 'notifications']);
+ throw new \InvalidArgumentException('Failed to encrypt message for device');
+ }
+
+ openssl_sign($encryptedSubject, $signature, $userKey->getPrivate(), OPENSSL_ALGO_SHA512);
+ $base64EncryptedSubject = base64_encode(hash('sha512', $encryptedSubject, true));
+ $base64Signature = base64_encode($signature);
+
+ return [
+ 'deviceIdentifier' => $device['deviceidentifier'],
+ 'pushTokenHash' => $device['pushtokenhash'],
+ 'subject' => $base64EncryptedSubject,
+ 'signature' => $base64Signature,
+ ];
+ }
+
+ /**
+ * @param string $uid
+ * @return array[]
+ */
+ protected function getDevicesForUser($uid) {
+ $query = $this->db->getQueryBuilder();
+ $query->select('*')
+ ->from('notifications_pushtokens')
+ ->where($query->expr()->eq('uid', $query->createNamedParameter($uid)));
+
+ $result = $query->execute();
+ $devices = $result->fetchAll();
+ $result->closeCursor();
+
+ return $devices;
+ }
+
+ /**
+ * @param int $tokenId
+ * @return bool
+ */
+ protected function deletePushToken($tokenId) {
+ $query = $this->db->getQueryBuilder();
+ $query->delete('notifications_pushtokens')
+ ->where($query->expr()->eq('token', $query->createNamedParameter($tokenId, IQueryBuilder::PARAM_INT)));
+
+ return $query->execute() !== 0;
+ }
+}
diff --git a/tests/Unit/AppInfo/AppTest.php b/tests/Unit/AppInfo/AppTest.php
index 0b1ccea..9e4ec8f 100644
--- a/tests/Unit/AppInfo/AppTest.php
+++ b/tests/Unit/AppInfo/AppTest.php
@@ -22,6 +22,7 @@
namespace OCA\Notifications\Tests\Unit\AppInfo;
+use OC\User\Session;
use OCA\Notifications\App;
use OCA\Notifications\Tests\Unit\TestCase;
use OCP\IRequest;
@@ -46,14 +47,9 @@ class AppTest extends TestCase {
protected function setUp() {
parent::setUp();
- $this->manager = $this->getMockBuilder(IManager::class)
- ->getMock();
-
- $this->request = $this->getMockBuilder(IRequest::class)
- ->getMock();
-
- $this->session = $this->getMockBuilder(IUserSession::class)
- ->getMock();
+ $this->manager = $this->createMock(IManager::class);
+ $this->request = $this->createMock(IRequest::class);
+ $this->session = $this->createMock(Session::class);
$this->overwriteService('NotificationManager', $this->manager);
$this->overwriteService('Request', $this->request);
diff --git a/tests/Unit/AppInfo/ApplicationTest.php b/tests/Unit/AppInfo/ApplicationTest.php
index 3dd6595..652de26 100644
--- a/tests/Unit/AppInfo/ApplicationTest.php
+++ b/tests/Unit/AppInfo/ApplicationTest.php
@@ -26,7 +26,9 @@ use OCA\Notifications\App;
use OCA\Notifications\AppInfo\Application;
use OCA\Notifications\Capabilities;
use OCA\Notifications\Controller\EndpointController;
+use OCA\Notifications\Controller\PushController;
use OCA\Notifications\Handler;
+use OCA\Notifications\Push;
use OCA\Notifications\Tests\Unit\TestCase;
use OCP\AppFramework\IAppContainer;
use OCP\AppFramework\OCSController;
@@ -63,12 +65,13 @@ class ApplicationTest extends TestCase {
array(App::class, IApp::class),
array(Capabilities::class),
array(Handler::class),
+ array(Push::class),
// Controller/
- array('EndpointController', EndpointController::class),
- array('EndpointController', OCSController::class),
array(EndpointController::class),
array(EndpointController::class, OCSController::class),
+ array(PushController::class),
+ array(PushController::class, OCSController::class),
);
}
diff --git a/tests/Unit/AppInfo/RoutesTest.php b/tests/Unit/AppInfo/RoutesTest.php
index ccac673..4e58432 100644
--- a/tests/Unit/AppInfo/RoutesTest.php
+++ b/tests/Unit/AppInfo/RoutesTest.php
@@ -37,6 +37,6 @@ class RoutesTest extends TestCase {
$this->assertCount(1, $routes);
$this->assertArrayHasKey('ocs', $routes);
$this->assertInternalType('array', $routes['ocs']);
- $this->assertGreaterThanOrEqual(3, sizeof($routes['ocs']));
+ $this->assertCount(5, $routes['ocs']);
}
}
diff --git a/tests/Unit/AppTest.php b/tests/Unit/AppTest.php
index 691a562..64e395f 100644
--- a/tests/Unit/AppTest.php
+++ b/tests/Unit/AppTest.php
@@ -25,12 +25,14 @@ namespace OCA\Notifications\Tests\Unit;
use OCA\Notifications\App;
use OCA\Notifications\Handler;
+use OCA\Notifications\Push;
use OCP\Notification\INotification;
class AppTest extends TestCase {
/** @var Handler|\PHPUnit_Framework_MockObject_MockObject */
protected $handler;
-
+ /** @var Push|\PHPUnit_Framework_MockObject_MockObject */
+ protected $push;
/** @var INotification|\PHPUnit_Framework_MockObject_MockObject */
protected $notification;
@@ -40,34 +42,68 @@ class AppTest extends TestCase {
protected function setUp() {
parent::setUp();
- $this->handler = $this->getMockBuilder(Handler::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->notification = $this->getMockBuilder(INotification::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $this->handler = $this->createMock(Handler::class);
+ $this->push = $this->createMock(Push::class);
+ $this->notification = $this->createMock(INotification::class);
$this->app = new App(
- $this->handler
+ $this->handler,
+ $this->push
);
}
- public function testNotify() {
+ public function dataNotify() {
+ return [
+ [23, 'user1'],
+ [42, 'user2'],
+ ];
+ }
+
+ /**
+ * @dataProvider dataNotify
+ *
+ * @param int $id
+ * @param string $user
+ */
+ public function testNotify($id, $user) {
+ $this->notification->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+
$this->handler->expects($this->once())
->method('add')
+ ->with($this->notification)
+ ->willReturn($id);
+ $this->handler->expects($this->once())
+ ->method('getById')
+ ->with($id, $user)
+ ->willReturn($this->notification);
+ $this->push->expects($this->once())
+ ->method('pushToDevice')
->with($this->notification);
$this->app->notify($this->notification);
}
- public function testGetCount() {
+ public function dataGetCount() {
+ return [
+ [23],
+ [42],
+ ];
+ }
+
+ /**
+ * @dataProvider dataGetCount
+ *
+ * @param int $count
+ */
+ public function testGetCount($count) {
$this->handler->expects($this->once())
->method('count')
->with($this->notification)
- ->willReturn(42);
+ ->willReturn($count);
- $this->assertSame(42, $this->app->getCount($this->notification));
+ $this->assertSame($count, $this->app->getCount($this->notification));
}
public function testMarkProcessed() {
diff --git a/tests/Unit/CapabilitiesTest.php b/tests/Unit/CapabilitiesTest.php
index 764e8de..802d2b9 100644
--- a/tests/Unit/CapabilitiesTest.php
+++ b/tests/Unit/CapabilitiesTest.php
@@ -38,6 +38,9 @@ class CapabilitiesTest extends TestCase {
'icons',
'rich-strings',
],
+ 'push' => [
+ 'devices',
+ ],
],
], $capabilities->getCapabilities());
}
diff --git a/tests/Unit/Controller/PushControllerTest.php b/tests/Unit/Controller/PushControllerTest.php
new file mode 100644
index 0000000..2d2401d
--- /dev/null
+++ b/tests/Unit/Controller/PushControllerTest.php
@@ -0,0 +1,508 @@
+<?php
+/**
+ * @author Joas Schilling <coding@schilljs.com>
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ *
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\Notifications\Tests\Unit\Controller;
+
+use OC\Authentication\Exceptions\InvalidTokenException;
+use OC\Authentication\Token\IProvider;
+use OC\Authentication\Token\IToken;
+use OC\Security\IdentityProof\Key;
+use OC\Security\IdentityProof\Manager;
+use OCA\Notifications\Controller\PushController;
+use OCA\Notifications\Tests\Unit\TestCase;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\IDBConnection;
+use OCP\IRequest;
+use OCP\ISession;
+use OCP\IUser;
+use OCP\IUserSession;
+
+class PushControllerTest extends TestCase {
+ /** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */
+ protected $request;
+ /** @var IDBConnection|\PHPUnit_Framework_MockObject_MockObject */
+ protected $db;
+ /** @var ISession|\PHPUnit_Framework_MockObject_MockObject */
+ protected $session;
+ /** @var IUserSession|\PHPUnit_Framework_MockObject_MockObject */
+ protected $userSession;
+ /** @var IProvider|\PHPUnit_Framework_MockObject_MockObject */
+ protected $tokenProvider;
+ /** @var Manager|\PHPUnit_Framework_MockObject_MockObject */
+ protected $identityProof;
+
+ /** @var IUser|\PHPUnit_Framework_MockObject_MockObject */
+ protected $user;
+ /** @var PushController */
+ protected $controller;
+
+ protected $devicePublicKey = '-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Or1KumSDfk8dT0MuCW9
+WS5wkVOpNsbz2OIJFBYrBvu6joC2iQo9StONMaXoTQj5Ucak9UBtC60PHyTkIDFb
+HOpCST5onmIAtZdqHN/3ABOBeHVU/notdRIl/menGM64jiqGWvE06F1+yZ8GGcGQ
+8RKzabqMd2K1iUohXP625uzTABVaiwz3u8nGEwui5R6Pf5Fy6DccuqdUMtJIfW21
+Z4Tj48Tw+pR+fUrGpa1Wg+wiwlg7ISK8Symml1Rd6hSRXK2t8Opm/kjH9ZX8oVwn
+RSO1ehjzRpTY+gdw/5gvwMZI0XmrIanZmZHwePRR4HC6FLPrL2OQG3gWikDIPyTS
+hQIDAQAB
+-----END PUBLIC KEY-----';
+
+ protected $userPrivateKey = '-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDPR0uV6e1cNSoy
+vsITBvGyYpOIn9vI7zpEhk7FGGwdOTd2dxxJ2ikegRJ6Fr2Ojce15K3zfiasXPen
+TAQuFEXecGoP9WY+DS5X1LfCpj9EeAOBfVGKeQDst5z/GoXeU+YqWbayJTp6vFRj
+7o5X6QDCCXy25Kt4snNDWTHPlMc44BLjZ6w+Wj0D2ySlz1dGpunc0vwYN/uEyjr9
+ztmiN82TZtZHgzN43DJSv7tLufsZgGsWnVlytXmsi4QuCAKcm92X2ZtIXkn5niMW
+DxJJepqFx7pC3ILXMZKYolAtt91VvLiGQjzURhq7HA4QdqvFyKXp0uLN2rKZjqQ0
+2nUzC34XAgMBAAECggEAFrL/Ew7IIKXt1hrP1BeZlmh3MaoX/pw8LE7tB2aSSG0A
+pueKYIgUorON23LsFVVvfnrpldXF1HBl6ptHhehQcnirFM5SAQ+eeJ3h9d4Q5aWi
+9KZNrLVtpX7CIam86UkU1qR2fnHXQqOnNj5ktjndDGLPlpPaN2CLgN+etdXcL10g
+G5fltrFnTzYgkYap/eNkY+ivA+0xqc1l3jP2i5PHihv1adcoiOuam36GARM9C51X
+fyWvMtxMvkRAZsdTATtRcQsEoJuQ3Rvseei38forkQdRn9p61UW8VT6Wa/+DWebO
+Ll4OAv1RH4H2V6nrYY2ILJNnPzP8V4hjP9OGEAUQ8QKBgQDssSBUmb8Ztt6SsHNr
+fgnbJBGAYizB1oAr6W1kLTQCq+BYirSYWMcJ/rakx+VCPmZ1fbbGYjPX5yVUsskx
+jQ/GUT7D8lMIQNZiI9CqWR0+fJpVJ/zxwrPT2jqu8lEJxq2i/WB0nRHCgosGBTmw
+UqhRGLkE5Ds14Q0zePZbdpAAyQKBgQDgL+yftcJEam8c3ipkrv02aT7vghoB0pAg
+JNSSwhXED1CTboccY4daOfTYdt/PnkVmndENrUGMRyEbAY0DDK6hclG6/gE3fwn4
+mL33IIzQ9BCoXxr3tcS0r4iQjbGKorUNJW1OwmkqyMZ4POF9BSkLXpTTcJaM5WxU
+8JU9PmLX3wKBgFNpuLMX27j8MUQQ2xwuttp7w48zCgLlzRWsldiP9ZxbZhzOBQcL
+glmLYmJ/79OAmisduqP/R7X2x7kpqK3FwKFrUGtNouVttB+x73+ZGC1FTD5mcUXi
+D+3BIp002EpRsi+Wi7+M+w1JZCUjAkmZV6f8xndq11MNlNFm96sUBXvBAoGAJ9hc
+tgYYARDprrfN0RdI6eLKzMbS2IAUHaJuJadZNv+B0rJSUTlfVSn32oFGRiBbNWHX
+RhcFD2mU+LfN2DzozMkEvbdnf/WUUBrVqJagcILwcvx0TpJ/451PKGIGrB0/EJcW
+Vmk3R+NnYvdvHElOgjbNPMdF+sTL/EzGOZxc9QECgYBNY4LAAKqrw47p+lcRi31O
+X4fhdGWAIFyiUliPDkxzEl8857FbT5c6qhdes3Gyc9tSF1wh0X7lpCDquWXYLP1V
+9WNvdon+YMRi9BKpO0SlE07lwFANBpz+wJkhONVJBMzvKbxEnMRPRJ4lWa0VAAGE
+j2ZL3j2Nwefj3HrR/AkeFA==
+-----END PRIVATE KEY-----
+';
+
+ protected $userPublicKey = '-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz0dLlentXDUqMr7CEwbx
+smKTiJ/byO86RIZOxRhsHTk3dnccSdopHoESeha9jo3HteSt834mrFz3p0wELhRF
+3nBqD/VmPg0uV9S3wqY/RHgDgX1RinkA7Lec/xqF3lPmKlm2siU6erxUY+6OV+kA
+wgl8tuSreLJzQ1kxz5THOOAS42esPlo9A9skpc9XRqbp3NL8GDf7hMo6/c7ZojfN
+k2bWR4MzeNwyUr+7S7n7GYBrFp1ZcrV5rIuELggCnJvdl9mbSF5J+Z4jFg8SSXqa
+hce6QtyC1zGSmKJQLbfdVby4hkI81EYauxwOEHarxcil6dLizdqymY6kNNp1Mwt+
+FwIDAQAB
+-----END PUBLIC KEY-----
+';
+
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->request = $this->createMock(IRequest::class);
+ $this->db = $this->createMock(IDBConnection::class);
+ $this->session = $this->createMock(ISession::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->tokenProvider = $this->createMock(IProvider::class);
+ $this->identityProof = $this->createMock(Manager::class);
+ }
+
+ protected function getController(array $methods = []) {
+ if (empty($methods)) {
+ return new PushController(
+ 'notifications',
+ $this->request,
+ $this->db,
+ $this->session,
+ $this->userSession,
+ $this->tokenProvider,
+ $this->identityProof
+ );
+ }
+
+ return $this->getMockBuilder(PushController::class)
+ ->setConstructorArgs([
+ 'notifications',
+ $this->request,
+ $this->db,
+ $this->session,
+ $this->userSession,
+ $this->tokenProvider,
+ $this->identityProof,
+ ])
+ ->setMethods($methods)
+ ->getMock();
+ }
+
+ public function dataRegisterDevice() {
+ return [
+ 'not authenticated' => [
+ '',
+ '',
+ '',
+ false,
+ 0,
+ false,
+ null,
+ [],
+ Http::STATUS_UNAUTHORIZED
+ ],
+ 'too short token hash' => [
+ 'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e47',
+ '',
+ '',
+ true,
+ 0,
+ false,
+ null,
+ ['message' => 'INVALID_PUSHTOKEN_HASH'],
+ Http::STATUS_BAD_REQUEST,
+ ],
+ 'too long token hash' => [
+ 'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e4722',
+ '',
+ '',
+ true,
+ 0,
+ false,
+ null,
+ ['message' => 'INVALID_PUSHTOKEN_HASH'],
+ Http::STATUS_BAD_REQUEST,
+ ],
+ 'invalid char in token hash' => [
+ 'rb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472',
+ '',
+ '',
+ true,
+ 0,
+ false,
+ null,
+ ['message' => 'INVALID_PUSHTOKEN_HASH'],
+ Http::STATUS_BAD_REQUEST,
+ ],
+ 'device key invalid start' => [
+ 'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472',
+ substr($this->devicePublicKey, 1),
+ '',
+ true,
+ 0,
+ false,
+ null,
+ ['message' => 'INVALID_DEVICE_KEY'],
+ Http::STATUS_BAD_REQUEST,
+ ],
+ 'device key invalid end' => [
+ 'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472',
+ substr($this->devicePublicKey, 0, -1),
+ '',
+ true,
+ 0,
+ false,
+ null,
+ ['message' => 'INVALID_DEVICE_KEY'],
+ Http::STATUS_BAD_REQUEST,
+ ],
+ 'device key too much end' => [
+ 'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472',
+ $this->devicePublicKey . "\n\n",
+ '',
+ true,
+ 0,
+ false,
+ null,
+ ['message' => 'INVALID_DEVICE_KEY'],
+ Http::STATUS_BAD_REQUEST,
+ ],
+ 'device key without trailing new line' => [
+ 'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472',
+ $this->devicePublicKey,
+ '',
+ true,
+ 0,
+ false,
+ null,
+ ['message' => 'INVALID_PROXY_SERVER'],
+ Http::STATUS_BAD_REQUEST,
+ ],
+ 'device key with trailing new line' => [
+ 'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472',
+ $this->devicePublicKey . "\n",
+ '',
+ true,
+ 0,
+ false,
+ null,
+ ['message' => 'INVALID_PROXY_SERVER'],
+ Http::STATUS_BAD_REQUEST,
+ ],
+ 'invalid push proxy' => [
+ 'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472',
+ $this->devicePublicKey,
+ 'localhost',
+ true,
+ 0,
+ false,
+ null,
+ ['message' => 'INVALID_PROXY_SERVER'],
+ Http::STATUS_BAD_REQUEST,
+ ],
+ 'using localhost' => [
+ 'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472',
+ $this->devicePublicKey,
+ 'http://localhost/',
+ true,
+ 23,
+ false,
+ null,
+ ['message' => 'INVALID_SESSION_TOKEN'],
+ Http::STATUS_BAD_REQUEST,
+ ],
+ 'using localhost with port' => [
+ 'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472',
+ $this->devicePublicKey,
+ 'http://localhost:8088/',
+ true,
+ 23,
+ false,
+ null,
+ ['message' => 'INVALID_SESSION_TOKEN'],
+ Http::STATUS_BAD_REQUEST,
+ ],
+ 'using production' => [
+ 'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472',
+ $this->devicePublicKey,
+ 'https://push-notifications.nextcloud.com/',
+ true,
+ 23,
+ false,
+ null,
+ ['message' => 'INVALID_SESSION_TOKEN'],
+ Http::STATUS_BAD_REQUEST,
+ ],
+ 'created or updated' => [
+ 'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472',
+ $this->devicePublicKey,
+ 'https://push-notifications.nextcloud.com/',
+ true,
+ 23,
+ true,
+ true,
+ [
+ 'publicKey' => $this->userPublicKey,
+ 'deviceIdentifier' => 'XUCEZ1EHvTUcVhIvrQQQ1XcP0ZD2BFdFqw4EYbOhBfiEgXgirurR4x/ve4GSSyfivvbQOdOkZUM+g4m+tSb0Ew==',
+ 'signature' => 'LRhbXO71WYX9qqDbQX7C+87YaaFfWoT/vG0DlaXdBz6+lhyOA0dw/1Ggz3fd7RerCQ0MfgnnTyxO+cSeRpUaPdA2yPjfoiPpfYA5SOJQGF3comS/HYna3fHiFDbOoM3BJOnjvqiSZdxA/ICdyl2mEEC5wO7AZ4OZKBTa5XfL7eSCXZLEv1YldqcLOStbXrI7voDQocTMJxoQZI/j8BVcf2i3D6F454aXIFDrYYzC2PQY+CKJoXZW0m0RMWaTM2B8tBmFFwrmaGLDqcjjpd33TsTtsV5DB7WimffLBPpOuGV4Z1Kiagp/mxpPLz2NImNV79mDX9gY3ZppCZTwChP5qQ==',
+ ],
+ Http::STATUS_CREATED,
+ ],
+ 'not updated' => [
+ 'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472',
+ $this->devicePublicKey,
+ 'https://push-notifications.nextcloud.com/',
+ true,
+ 42,
+ true,
+ false,
+ [
+ 'publicKey' => $this->userPublicKey,
+ 'deviceIdentifier' => 'x9vSImcGjhzR9BfZ/XbbUqqCCNC4bHKsX7vkQWNZRd1/MiY+OuF02fx8K08My0RpkNnwj/rQ/gVSU1oEdFwkww==',
+ 'signature' => 'J9AcdJt5youJmMnBhS+Cc9ytArynIKtCRoNf/m0oOFO/e0hWHqs1NRdQBe81qzYIjf0+bj0Q97X9Xv1rnVJesPkQUbGaa4nAPt+viGSfvzTptjX4LKgqm8B3UkduBA262IcaWgM5P84gUqelkQIC1nIqq/MJTuC6oQ5lUwIV1a92ZurDjhwH4b3f7/ZLTTOTRD0DWN9W/yOyF1qECivgePR3eu+mkcBzXVU/TDZDJic9G7xhqcTnWV6qk+aKyzdNo1tu5W7mF+v5vF6rrGZrq55vPLWAHApTD7P+NFV01BnaCuN7/qGJNVs7m7EH03jpOw7y3jqNMmcmonYrJSMVqg==',
+ ],
+ Http::STATUS_OK,
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider dataRegisterDevice
+ *
+ * @param string $pushTokenHash
+ * @param string $devicePublicKey
+ * @param string $proxyServer
+ * @param bool $userIsValid
+ * @param int $tokenId
+ * @param bool $tokenIsValid
+ * @param bool $deviceCreated
+ * @param array $payload
+ * @param int $status
+ */
+ public function testRegisterDevice($pushTokenHash, $devicePublicKey, $proxyServer, $userIsValid, $tokenId, $tokenIsValid, $deviceCreated, $payload, $status) {
+ $controller = $this->getController([
+ 'savePushToken',
+ ]);
+
+ $user = $this->createMock(IUser::class);
+ if ($userIsValid) {
+ $this->userSession->expects($this->any())
+ ->method('getUser')
+ ->willReturn($user);
+ } else {
+ $this->userSession->expects($this->any())
+ ->method('getUser')
+ ->willReturn(null);
+ }
+
+ $this->session->expects($tokenId > 0 ? $this->once() : $this->never())
+ ->method('get')
+ ->with('token-id')
+ ->willReturn($tokenId);
+
+ if ($tokenIsValid) {
+ $token = $this->createMock(IToken::class);
+ $token->expects($this->once())
+ ->method('getId')
+ ->willReturn($tokenId);
+ $this->tokenProvider->expects($this->any())
+ ->method('getTokenById')
+ ->with($tokenId)
+ ->willReturn($token);
+
+ $key = $this->createMock(Key::class);
+ $key->expects($this->once())
+ ->method('getPrivate')
+ ->willReturn($this->userPrivateKey);
+ $key->expects($this->once())
+ ->method('getPublic')
+ ->willReturn($this->userPublicKey);
+
+ $this->identityProof->expects($this->once())
+ ->method('getKey')
+ ->with($user)
+ ->willReturn($key);
+
+ $controller->expects($this->once())
+ ->method('savePushToken')
+ ->with($user, $token, $this->anything(), $devicePublicKey, $pushTokenHash, $proxyServer)
+ ->willReturn($deviceCreated);
+ } else {
+ $controller->expects($this->never())
+ ->method('savePushToken');
+
+ $this->tokenProvider->expects($this->any())
+ ->method('getTokenById')
+ ->with($tokenId)
+ ->willThrowException(new InvalidTokenException());
+ }
+
+ $response = $controller->registerDevice($pushTokenHash, $devicePublicKey, $proxyServer);
+ $this->assertInstanceOf(DataResponse::class, $response);
+ $this->assertSame($status, $response->getStatus());
+ $this->assertSame($payload, $response->getData());
+ }
+
+ public function dataRemoveDevice() {
+ return [
+ 'not authenticated' => [
+ false,
+ 0,
+ false,
+ null,
+ [],
+ Http::STATUS_UNAUTHORIZED
+ ],
+ 'invalid token' => [
+ true,
+ 23,
+ false,
+ null,
+ ['message' => 'INVALID_SESSION_TOKEN'],
+ Http::STATUS_BAD_REQUEST,
+ ],
+ 'using production' => [
+ true,
+ 23,
+ false,
+ null,
+ ['message' => 'INVALID_SESSION_TOKEN'],
+ Http::STATUS_BAD_REQUEST,
+ ],
+ 'created or updated' => [
+ true,
+ 23,
+ true,
+ true,
+ [],
+ Http::STATUS_ACCEPTED,
+ ],
+ 'not updated' => [
+ true,
+ 42,
+ true,
+ false,
+ [],
+ Http::STATUS_OK,
+ ],
+ ];
+ }
+
+
+ /**
+ * @dataProvider dataRemoveDevice
+ *
+ * @param bool $userIsValid
+ * @param int $tokenId
+ * @param bool $tokenIsValid
+ * @param bool $deviceDeleted
+ * @param array $payload
+ * @param int $status
+ */
+ public function testRemoveDevice($userIsValid, $tokenId, $tokenIsValid, $deviceDeleted, $payload, $status) {
+ $controller = $this->getController([
+ 'deletePushToken',
+ ]);
+
+ $user = $this->createMock(IUser::class);
+ if ($userIsValid) {
+ $this->userSession->expects($this->any())
+ ->method('getUser')
+ ->willReturn($user);
+ } else {
+ $this->userSession->expects($this->any())
+ ->method('getUser')
+ ->willReturn(null);
+ }
+
+ $this->session->expects($tokenId > 0 ? $this->once() : $this->never())
+ ->method('get')
+ ->with('token-id')
+ ->willReturn($tokenId);
+
+ if ($tokenIsValid) {
+ $token = $this->createMock(IToken::class);
+ $this->tokenProvider->expects($this->any())
+ ->method('getTokenById')
+ ->with($tokenId)
+ ->willReturn($token);
+
+ $controller->expects($this->once())
+ ->method('deletePushToken')
+ ->with($user, $token)
+ ->willReturn($deviceDeleted);
+ } else {
+ $controller->expects($this->never())
+ ->method('deletePushToken');
+
+ $this->tokenProvider->expects($this->any())
+ ->method('getTokenById')
+ ->with($tokenId)
+ ->willThrowException(new InvalidTokenException());
+ }
+
+ $response = $controller->removeDevice();
+ $this->assertInstanceOf(DataResponse::class, $response);
+ $this->assertSame($status, $response->getStatus());
+ $this->assertSame($payload, $response->getData());
+ }
+
+}
diff --git a/tests/Unit/PushTest.php b/tests/Unit/PushTest.php
new file mode 100644
index 0000000..6399b37
--- /dev/null
+++ b/tests/Unit/PushTest.php
@@ -0,0 +1,487 @@
+<?php
+/**
+ * @author Joas Schilling <coding@schilljs.com>
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ *
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\Notifications\Tests\Unit;
+
+
+use OC\Authentication\Exceptions\InvalidTokenException;
+use OC\Authentication\Token\IProvider;
+use OC\Security\IdentityProof\Key;
+use OC\Security\IdentityProof\Manager;
+use OCA\Notifications\Push;
+use OCP\AppFramework\Http;
+use OCP\Http\Client\IClient;
+use OCP\Http\Client\IResponse;
+use OCP\IConfig;
+use OCP\Http\Client\IClientService;
+use OCP\IDBConnection;
+use OCP\ILogger;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\Notification\IManager as INotificationManager;
+use OCP\Notification\INotification;
+
+/**
+ * Class PushTest
+ *
+ * @package OCA\Notifications\Tests\Unit
+ * @group DB
+ */
+class PushTest extends TestCase {
+ /** @var IDBConnection */
+ protected $db;
+ /** @var INotificationManager|\PHPUnit_Framework_MockObject_MockObject */
+ protected $notificationManager;
+ /** @var IConfig|\PHPUnit_Framework_MockObject_MockObject */
+ protected $config;
+ /** @var IProvider|\PHPUnit_Framework_MockObject_MockObject */
+ protected $tokenProvider;
+ /** @var Manager|\PHPUnit_Framework_MockObject_MockObject */
+ protected $keyManager;
+ /** @var IUserManager|\PHPUnit_Framework_MockObject_MockObject */
+ protected $userManager;
+ /** @var IClientService|\PHPUnit_Framework_MockObject_MockObject */
+ protected $clientService;
+ /** @var ILogger|\PHPUnit_Framework_MockObject_MockObject */
+ protected $logger;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->db = \OC::$server->getDatabaseConnection();
+ $this->notificationManager = $this->createMock(INotificationManager::class);
+ $this->config = $this->createMock(IConfig::class);
+ $this->tokenProvider = $this->createMock(IProvider::class);
+ $this->keyManager = $this->createMock(Manager::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->clientService = $this->createMock(IClientService::class);
+ $this->logger = $this->createMock(ILogger::class);
+ }
+
+ /**
+ * @param string[] $methods
+ * @return Push|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected function getPush(array $methods = []) {
+ if (!empty($methods)) {
+ return $this->getMockBuilder(Push::class)
+ ->setConstructorArgs([
+ $this->db,
+ $this->notificationManager,
+ $this->config,
+ $this->tokenProvider,
+ $this->keyManager,
+ $this->userManager,
+ $this->clientService,
+ $this->logger,
+ ])
+ ->setMethods($methods)
+ ->getMock();
+ }
+
+ return new Push(
+ $this->db,
+ $this->notificationManager,
+ $this->config,
+ $this->tokenProvider,
+ $this->keyManager,
+ $this->userManager,
+ $this->clientService,
+ $this->logger
+ );
+ }
+
+ public function testPushToDeviceInvalidUser() {
+ $push = $this->getPush();
+ $this->keyManager->expects($this->never())
+ ->method('getKey');
+ $this->clientService->expects($this->never())
+ ->method('newClient');
+
+ /** @var INotification|\PHPUnit_Framework_MockObject_MockObject $notification */
+ $notification = $this->createMock(INotification::class);
+ $notification->expects($this->once())
+ ->method('getUser')
+ ->willReturn('invalid');
+
+ $this->userManager->expects($this->once())
+ ->method('get')
+ ->with('invalid')
+ ->willReturn(null);
+
+ $push->pushToDevice($notification);
+ }
+
+ public function testPushToDeviceNoDevices() {
+ $push = $this->getPush(['getDevicesForUser']);
+ $this->keyManager->expects($this->never())
+ ->method('getKey');
+ $this->clientService->expects($this->never())
+ ->method('newClient');
+
+ /** @var INotification|\PHPUnit_Framework_MockObject_MockObject $notification */
+ $notification = $this->createMock(INotification::class);
+ $notification->expects($this->exactly(2))
+ ->method('getUser')
+ ->willReturn('valid');
+
+ /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */
+ $user = $this->createMock(IUser::class);
+
+ $this->userManager->expects($this->once())
+ ->method('get')
+ ->with('valid')
+ ->willReturn($user);
+
+ $push->expects($this->once())
+ ->method('getDevicesForUser')
+ ->willReturn([]);
+
+ $push->pushToDevice($notification);
+ }
+
+ public function testPushToDeviceNotPrepared() {
+ $push = $this->getPush(['getDevicesForUser']);
+ $this->keyManager->expects($this->never())
+ ->method('getKey');
+ $this->clientService->expects($this->never())
+ ->method('newClient');
+
+ /** @var INotification|\PHPUnit_Framework_MockObject_MockObject $notification */
+ $notification = $this->createMock(INotification::class);
+ $notification->expects($this->exactly(3))
+ ->method('getUser')
+ ->willReturn('valid');
+
+ /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */
+ $user = $this->createMock(IUser::class);
+
+ $this->userManager->expects($this->once())
+ ->method('get')
+ ->with('valid')
+ ->willReturn($user);
+
+ $push->expects($this->once())
+ ->method('getDevicesForUser')
+ ->willReturn([[
+ 'proxyserver' => 'proxyserver1',
+ 'token' => 'token1',
+ ]]);
+
+ $this->config->expects($this->once())
+ ->method('getUserValue')
+ ->with('valid', 'core', 'lang', 'en')
+ ->willReturn('de');
+
+ $this->notificationManager->expects($this->once())
+ ->method('prepare')
+ ->with($notification, 'de')
+ ->willThrowException(new \InvalidArgumentException());
+
+ $push->pushToDevice($notification);
+ }
+
+ public function testPushToDeviceInvalidToken() {
+ $push = $this->getPush(['getDevicesForUser', 'encryptAndSign', 'deletePushToken']);
+ $this->clientService->expects($this->never())
+ ->method('newClient');
+
+ /** @var INotification|\PHPUnit_Framework_MockObject_MockObject $notification */
+ $notification = $this->createMock(INotification::class);
+ $notification->expects($this->exactly(3))
+ ->method('getUser')
+ ->willReturn('valid');
+
+ /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */
+ $user = $this->createMock(IUser::class);
+
+ $this->userManager->expects($this->once())
+ ->method('get')
+ ->with('valid')
+ ->willReturn($user);
+
+ $push->expects($this->once())
+ ->method('getDevicesForUser')
+ ->willReturn([[
+ 'proxyserver' => 'proxyserver1',
+ 'token' => 23,
+ ]]);
+
+ $this->config->expects($this->once())
+ ->method('getUserValue')
+ ->with('valid', 'core', 'lang', 'en')
+ ->willReturn('ru');
+
+ $this->notificationManager->expects($this->once())
+ ->method('prepare')
+ ->with($notification, 'ru')
+ ->willReturnArgument(0);
+
+
+ /** @var Key|\PHPUnit_Framework_MockObject_MockObject $key */
+ $key = $this->createMock(Key::class);
+
+ $this->keyManager->expects($this->once())
+ ->method('getKey')
+ ->with($user)
+ ->willReturn($key);
+
+ $push->expects($this->once())
+ ->method('encryptAndSign')
+ ->willThrowException(new InvalidTokenException());
+
+ $push->expects($this->once())
+ ->method('deletePushToken')
+ ->with(23);
+
+ $push->pushToDevice($notification);
+ }
+
+ public function testPushToDeviceEncryptionError() {
+ $push = $this->getPush(['getDevicesForUser', 'encryptAndSign', 'deletePushToken']);
+ $this->clientService->expects($this->never())
+ ->method('newClient');
+
+ /** @var INotification|\PHPUnit_Framework_MockObject_MockObject $notification */
+ $notification = $this->createMock(INotification::class);
+ $notification->expects($this->exactly(3))
+ ->method('getUser')
+ ->willReturn('valid');
+
+ /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */
+ $user = $this->createMock(IUser::class);
+
+ $this->userManager->expects($this->once())
+ ->method('get')
+ ->with('valid')
+ ->willReturn($user);
+
+ $push->expects($this->once())
+ ->method('getDevicesForUser')
+ ->willReturn([[
+ 'proxyserver' => 'proxyserver1',
+ 'token' => 23,
+ ]]);
+
+ $this->config->expects($this->once())
+ ->method('getUserValue')
+ ->with('valid', 'core', 'lang', 'en')
+ ->willReturn('ru');
+
+ $this->notificationManager->expects($this->once())
+ ->method('prepare')
+ ->with($notification, 'ru')
+ ->willReturnArgument(0);
+
+
+ /** @var Key|\PHPUnit_Framework_MockObject_MockObject $key */
+ $key = $this->createMock(Key::class);
+
+ $this->keyManager->expects($this->once())
+ ->method('getKey')
+ ->with($user)
+ ->willReturn($key);
+
+ $push->expects($this->once())
+ ->method('encryptAndSign')
+ ->willThrowException(new \InvalidArgumentException());
+
+ $push->expects($this->once())
+ ->method('deletePushToken')
+ ->with(23);
+
+ $push->pushToDevice($notification);
+ }
+
+ public function dataPushToDeviceSending() {
+ return [
+ [true],
+ [false],
+ ];
+ }
+
+ /**
+ * @dataProvider dataPushToDeviceSending
+ * @param bool $isDebug
+ */
+ public function testPushToDeviceSending($isDebug) {
+ $push = $this->getPush(['getDevicesForUser', 'encryptAndSign', 'deletePushToken']);
+
+ /** @var INotification|\PHPUnit_Framework_MockObject_MockObject $notification */
+ $notification = $this->createMock(INotification::class);
+ $notification->expects($this->exactly(3))
+ ->method('getUser')
+ ->willReturn('valid');
+
+ /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */
+ $user = $this->createMock(IUser::class);
+
+ $this->userManager->expects($this->once())
+ ->method('get')
+ ->with('valid')
+ ->willReturn($user);
+
+ $push->expects($this->once())
+ ->method('getDevicesForUser')
+ ->willReturn([
+ [
+ 'proxyserver' => 'proxyserver1',
+ 'token' => 16,
+ ],
+ [
+ 'proxyserver' => 'proxyserver1/',
+ 'token' => 23,
+ ],
+ [
+ 'proxyserver' => 'badrequest',
+ 'token' => 42,
+ ],
+ [
+ 'proxyserver' => 'unavailable',
+ 'token' => 48,
+ ],
+ [
+ 'proxyserver' => 'ok',
+ 'token' => 64,
+ ],
+ ]);
+
+ $this->config->expects($this->once())
+ ->method('getUserValue')
+ ->with('valid', 'core', 'lang', 'en')
+ ->willReturn('ru');
+
+ $this->notificationManager->expects($this->once())
+ ->method('prepare')
+ ->with($notification, 'ru')
+ ->willReturnArgument(0);
+
+ /** @var Key|\PHPUnit_Framework_MockObject_MockObject $key */
+ $key = $this->createMock(Key::class);
+
+ $this->keyManager->expects($this->once())
+ ->method('getKey')
+ ->with($user)
+ ->willReturn($key);
+
+ $push->expects($this->exactly(5))
+ ->method('encryptAndSign')
+ ->willReturn(['Payload']);
+
+ $push->expects($this->never())
+ ->method('deletePushToken');
+
+ /** @var IClient|\PHPUnit_Framework_MockObject_MockObject $client */
+ $client = $this->createMock(IClient::class);
+
+ $this->clientService->expects($this->once())
+ ->method('newClient')
+ ->willReturn($client);
+
+ $e = new \Exception();
+ $client->expects($this->at(0))
+ ->method('post')
+ ->with('proxyserver1/notifications', [
+ 'body' => [
+ 'notifications' => ['["Payload"]', '["Payload"]'],
+ ],
+ ])
+ ->willThrowException($e);
+
+ $this->logger->expects($this->at(0))
+ ->method('logException')
+ ->with($e, [
+ 'app' => 'notifications',
+ ]);
+
+ /** @var IResponse|\PHPUnit_Framework_MockObject_MockObject $response1 */
+ $response1 = $this->createMock(IResponse::class);
+ $response1->expects($this->once())
+ ->method('getStatusCode')
+ ->willReturn(Http::STATUS_BAD_REQUEST);
+ $response1->expects($this->once())
+ ->method('getBody')
+ ->willReturn(null);
+ $client->expects($this->at(1))
+ ->method('post')
+ ->with('badrequest/notifications', [
+ 'body' => [
+ 'notifications' => ['["Payload"]'],
+ ],
+ ])
+ ->willReturn($response1);
+
+ $this->logger->expects($this->at(1))
+ ->method('error')
+ ->with('Could not send notification to push server [{url}]: {error}', [
+ 'error' => 'no reason given',
+ 'url' => 'badrequest',
+ 'app' => 'notifications',
+ ]);
+
+ /** @var IResponse|\PHPUnit_Framework_MockObject_MockObject $response1 */
+ $response2 = $this->createMock(IResponse::class);
+ $response2->expects($this->once())
+ ->method('getStatusCode')
+ ->willReturn(Http::STATUS_SERVICE_UNAVAILABLE);
+ $response2->expects($isDebug ? $this->once() : $this->never())
+ ->method('getBody')
+ ->willReturn('Maintenance');
+ $client->expects($this->at(2))
+ ->method('post')
+ ->with('unavailable/notifications', [
+ 'body' => [
+ 'notifications' => ['["Payload"]'],
+ ],
+ ])
+ ->willReturn($response2);
+
+ $this->config->expects($this->once())
+ ->method('getSystemValue')
+ ->with('debug', false)
+ ->willReturn($isDebug);
+
+ $this->logger->expects($isDebug ? $this->at(2) : $this->never())
+ ->method('debug')
+ ->with('Could not send notification to push server [{url}]: {error}', [
+ 'error' => 'Maintenance',
+ 'url' => 'unavailable',
+ 'app' => 'notifications',
+ ]);
+
+ /** @var IResponse|\PHPUnit_Framework_MockObject_MockObject $response1 */
+ $response3 = $this->createMock(IResponse::class);
+ $response3->expects($this->once())
+ ->method('getStatusCode')
+ ->willReturn(Http::STATUS_OK);
+ $client->expects($this->at(3))
+ ->method('post')
+ ->with('ok/notifications', [
+ 'body' => [
+ 'notifications' => ['["Payload"]'],
+ ],
+ ])
+ ->willReturn($response3);
+
+ $push->pushToDevice($notification);
+ }
+}