Merge pull request #59 from nextcloud/push-notification

Allow devices to register for push notifications
This commit is contained in:
Joas Schilling 2017-04-24 15:07:34 +02:00 коммит произвёл GitHub
Родитель 6982181dc3 edbb99192e
Коммит af90c15598
16 изменённых файлов: 1805 добавлений и 28 удалений

Просмотреть файл

@ -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>

Просмотреть файл

@ -15,7 +15,7 @@
<licence>AGPL</licence>
<author>Joas Schilling</author>
<version>1.2.0</version>
<version>2.0.0</version>
<types>
<logging/>

Просмотреть файл

@ -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']],
],
];

213
docs/push-v2.md Normal file
Просмотреть файл

@ -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.

Просмотреть файл

@ -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);
}
/**

Просмотреть файл

@ -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() {

Просмотреть файл

@ -45,6 +45,9 @@ class Capabilities implements ICapability {
'icons',
'rich-strings',
],
'push' => [
'devices',
],
],
];
}

Просмотреть файл

@ -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;
}
}

211
lib/Push.php Normal file
Просмотреть файл

@ -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;
}
}

Просмотреть файл

@ -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);

Просмотреть файл

@ -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),
);
}

Просмотреть файл

@ -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']);
}
}

Просмотреть файл

@ -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() {

Просмотреть файл

@ -38,6 +38,9 @@ class CapabilitiesTest extends TestCase {
'icons',
'rich-strings',
],
'push' => [
'devices',
],
],
], $capabilities->getCapabilities());
}

Просмотреть файл

@ -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());
}
}

487
tests/Unit/PushTest.php Normal file
Просмотреть файл

@ -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);
}
}