diff --git a/Whisper-Push-Notifications.md b/Whisper-Push-Notifications.md index 6206083..830cb9f 100644 --- a/Whisper-Push-Notifications.md +++ b/Whisper-Push-Notifications.md @@ -457,16 +457,315 @@ var removeClientSession = function () { # Complete Sample -Full sample page used for testing can be found here: https://gist.github.com/farazdagi/5ac082ddf006d00de422385f07d41ad3 +We rely on Mocha.js test, to make sure that all aspects of notification sending is covered in reproducible and easy-to-follow manner. -In order to use that page you need to start the following: +In order to use the test you need: +- to clone `status-go` repo and `npm install` there (so that Mocha.js and other node modules are installed) +- to run `make` (to build the `statusd` daemon) +- to start the following: ```bash statusd --datadir app1 --http --httpport 8645 wnode # as Device A statusd --datadir app2 --http --httpport 8745 wnode # as Device B statusd --datadir wnode1 wnode --notify --identity ./wnodekey --injectaccounts=false --firebaseauth ./firebaseauthkey + ``` -If you run that page in gist, you should see sth like this: +At this point, if you run `npm test`, you should see something like this: +![image](https://cloud.githubusercontent.com/assets/188194/25640962/6a09b66e-2f9a-11e7-9a33-e86f2b9a11d1.png) + +Here is full source-code of the test (the most up to date version of test is available as [status-go/static/tests/whisper.js](https://github.com/status-im/status-go/blob/develop/static/tests/whisper.js)) + +```js +context('push notifications', function () { + this.timeout(5000); + var discoveryPubKey = '0x040edb0d71a3dbe928e154fcb696ffbda359b153a90efc2b46f0043ce9f5dbe55b77b9328fd841a1db5273758624afadd5b39638d4c35b36b3a96e1a586c1b4c2a'; + var discoverServerTopic = '0x268302f3'; // DISCOVER_NOTIFICATION_SERVER + var proposeServerTopic = '0x08e3d8c0'; // PROPOSE_NOTIFICATION_SERVER + var acceptServerTopic = '0x04f7dea6'; // ACCEPT_NOTIFICATION_SERVER + var ackClientSubscriptionTopic = '0x93dafe28'; // ACK_NOTIFICATION_SERVER_SUBSCRIPTION + var sendNotificationTopic = '0x69915296'; // SEND_NOTIFICATION + var newChatSessionTopic = '0x509579a2'; // NEW_CHAT_SESSION + var ackNewChatSessionTopic = '0xd012aae8'; // ACK_NEW_CHAT_SESSION + var newDeviceRegistrationTopic = '0x14621a51'; // NEW_DEVICE_REGISTRATION + var ackDeviceRegistrationTopic = '0x424358d6'; // ACK_DEVICE_REGISTRATION + var checkClientSessionTopic = '0x8745d931'; // CHECK_CLIENT_SESSION + var confirmClientSessionTopic = '0xd3202c5f'; // CONFIRM_CLIENT_SESSION + var dropClientSessionTopic = '0x3a6656bb'; // DROP_CLIENT_SESSION + + // ensures that message had payload (which is HEX-encoded JSON) + var extractPayload = function (message) { + expect(message).to.have.property('payload'); + return JSON.parse(web3.toAscii(message.payload)); + }; + + var identity1 = ''; // pub key of device 1 + var identity2 = ''; // pub key of device 2 + var chatKeySharingTopic = makeTopic(); // topic used by device1 to send chat key to device 2 + + context('prepare devices', function () { + it('create key pair to be used as main identity on device1', function () { + var keyId = node1.shh.newKeyPair(); + assert.lengthOf(keyId, 64); + + identity1 = node1.shh.getPublicKey(keyId); + assert.lengthOf(identity1, 132); + + expect(node1.shh.hasKeyPair(identity1)).to.equal(true); + expect(node1.shh.hasKeyPair(identity2)).to.equal(false); + }); + + it('create key pair to be used as main identity on device2', function () { + var keyId = node2.shh.newKeyPair(); + assert.lengthOf(keyId, 64); + + identity2 = node2.shh.getPublicKey(keyId); + assert.lengthOf(identity1, 132); + + expect(node2.shh.hasKeyPair(identity1)).to.equal(false); + expect(node2.shh.hasKeyPair(identity2)).to.equal(true); + }); + }); + + context('run device1', function () { + var serverId = ''; // accepted/selected server id + var subscriptionKeyId = ''; // symkey provided by server, and used to configure client-server subscription + var chatKeyId = ''; // symkey provided by server, and shared among clients so that they can trigger notifications + var appChatId = ''; // chat id that identifies device1-device2 interaction session on RN app level + + + it('start discovery by sending discovery request', function () { + var message = { + type: "asym", + sig: identity1, + key: discoveryPubKey, + topic: discoverServerTopic, + ttl: 20 + }; + expect(node1.shh.post(message)).to.equal(null); + }); + + it('watch for server proposals', function (done) { + watchFilter(node1.shh.filter({ + type: "asym", + sig: discoveryPubKey, + key: identity1, + topics: [proposeServerTopic] + }), function (err, message) { + if (err) return done(err); + + // process payload + var payload = extractPayload(message); + expect(payload).to.have.property('server'); + serverId = payload.server; + + done(); + }); + }); + + it('client accepts server', function () { + var message = { + type: "asym", + sig: identity1, + key: discoveryPubKey, + topic: acceptServerTopic, + payload: '{"server": "' + serverId + '"}', + ttl: 20 + }; + expect(node1.shh.post(message)).to.equal(null); + }); + + it('watch for server ACK response and save provided subscription key', function (done) { + watchFilter(node1.shh.filter({ + type: "asym", + key: identity1, + topics: [ackClientSubscriptionTopic] + }), function (err, message) { + if (err) return done(err); + + // process payload + var payload = extractPayload(message); + expect(payload).to.have.property('server'); + expect(payload).to.have.property('key'); + + // save subscription key + subscriptionKeyId = node1.shh.addSymmetricKeyDirect(payload.key); + assert.lengthOf(subscriptionKeyId, 64); + expect(node1.shh.hasSymmetricKey(subscriptionKeyId)).to.equal(true); + + done(); + }); + }); + + it('create chat session', function () { + appChatId = makeTopic(); // globally unique chat id + var message = { + type: "sym", + sig: identity1, + key: subscriptionKeyId, + topic: newChatSessionTopic, + payload: '{"chat": "' + appChatId + '"}', + ttl: 20 + }; + expect(node1.shh.post(message)).to.equal(null); + }); + + it('watch for server to respond with chat key', function (done) { + watchFilter(node1.shh.filter({ + type: "asym", + key: identity1, + topics: [ackNewChatSessionTopic] + }), function (err, message) { + if (err) return done(err); + + // process payload + var payload = extractPayload(message); + expect(payload).to.have.property('server'); + expect(payload).to.have.property('key'); + + // save subscription key + chatKeyId = node1.shh.addSymmetricKeyDirect(payload.key); + assert.lengthOf(chatKeyId, 64); + expect(node1.shh.hasSymmetricKey(chatKeyId)).to.equal(true); + + done(); + }); + }); + + it('register device with a given chat', function (done) { + // this obtained from https://status-sandbox-c1b34.firebaseapp.com/ + var deviceId = 'ca5pRJc6L8s:APA91bHpYFtpxvXx6uOayGmnNVnktA4PEEZdquCCt3fWR5ldLzSy1A37Tsbzk5Gavlmk1d_fvHRVnK7xPAhFFl-erF7O87DnIEstW6DEyhyiKZYA4dXFh6uy323f9A3uw5hEtT_kQVhT'; + var message = { + type: "sym", + sig: identity1, + key: chatKeyId, + topic: newDeviceRegistrationTopic, + payload: '{"device": "' + deviceId + '"}', + ttl: 20 + }; + expect(node1.shh.post(message)).to.equal(null); + + // watch for server server ACK + watchFilter(node1.shh.filter({ + type: "asym", + key: identity1, + topics: [ackDeviceRegistrationTopic] + }), function (err, message) { + if (err) return done(err); + + // process payload + var payload = extractPayload(message); + expect(payload).to.have.property('server'); + + done(); + }); + }); + + it('share chat key, so that another device can send us notifications', function () { + var chatKey = node1.shh.getSymmetricKey(chatKeyId); + assert.lengthOf(chatKey, 66); + var message = { + type: "asym", + sig: identity1, + key: identity2, + topic: chatKeySharingTopic, + payload: '{"chat": "' + appChatId + '", "key": "' + chatKey + '"}', + ttl: 20 + }; + expect(node1.shh.post(message)).to.equal(null); + }); + }); + + context('run device2', function () { + var chatKeyId = ''; + + it('watch for device1 to send us chat key', function (done) { + watchFilter(node2.shh.filter({ + type: "asym", + key: identity2, + topics: [chatKeySharingTopic] + }), function (err, message) { + if (err) return done(err); + + // process payload + var payload = extractPayload(message); + expect(payload).to.have.property('chat'); + expect(payload).to.have.property('key'); + + // persist chat key + chatKeyId = node2.shh.addSymmetricKeyDirect(payload.key); + assert.lengthOf(chatKeyId, 64); + expect(node2.shh.hasSymmetricKey(chatKeyId)).to.equal(true); + + done(); + }); + }); + + it('trigger notification (from device2, on device1)', function () { + var message = { + type: "sym", + sig: identity2, + key: chatKeyId, + topic: sendNotificationTopic, + payload: '{' // see https://firebase.google.com/docs/cloud-messaging/http-server-ref + + '"notification": {' + + '"title": "status.im notification",' + + '"body": "Hello this is test notification!",' + + '"icon": "https://status.im/img/logo.png",' + + '"click_action": "https://status.im"' + + '},' + + '"to": "{{ ID }}"' // this get replaced by device id your've registered + + '}', + ttl: 20 + }; + expect(node2.shh.post(message)).to.equal(null); + }); + }); + + context('misc methods and cleanup', function () { + + it('check client session', function (done) { + // request status + var message = { + type: "asym", + sig: identity1, + key: discoveryPubKey, + topic: checkClientSessionTopic, + ttl: 20 + }; + expect(node1.shh.post(message)).to.equal(null); + + // process server's response + watchFilter(node1.shh.filter({ + type: "asym", + key: identity1, + topics: [confirmClientSessionTopic] + }), function (err, message) { + if (err) return done(err); + + // process payload + var payload = extractPayload(message); + expect(payload).to.have.property('server'); + expect(payload).to.have.property('key'); + + done(); + }); + }); + + it('remove client session', function () { + var message = { + type: "asym", + sig: identity1, + key: discoveryPubKey, + topic: dropClientSessionTopic, + ttl: 20 + }; + expect(node1.shh.post(message)).to.equal(null); + }); + }); +}); +``` + +Provided you've updated deviceId (to point to your device), once you run the test, you should get sth like this: ![image](https://cloud.githubusercontent.com/assets/188194/24966636/34a226ea-1fb0-11e7-9b18-b3595ed108e6.png)