diff --git a/.eslintrc b/.eslintrc index 7810f22..9c53569 100644 --- a/.eslintrc +++ b/.eslintrc @@ -27,9 +27,10 @@ "rules": { "quotes": [2, "single"], "max-len": [2, {"code": 120, "tabWidth": 2}], - "no-console": 2, + "no-console": 1, "handle-callback-err": 2, "no-trailing-spaces": 0, + "no-use-before-define": 0, "arrow-body-style": 0, "padded-blocks": 0, "no-shadow": 0, @@ -39,4 +40,4 @@ "no-param-reassign": 0, "func-names": 0 } -} \ No newline at end of file +} diff --git a/config/default.json b/config/default.json index f16a10a..d2891cb 100644 --- a/config/default.json +++ b/config/default.json @@ -17,8 +17,11 @@ "secret": "VAIwf/lXL6vlPkn2DyPwjrWT2JaTm6YJ3Dc1p6Pi", "region": "us-east-1", "title": "Outfit", - "gsmAppArn": "arn:aws:sns:us-east-1:093525834944:app/GCM/outfit-development", - "apnsAppArn": "arn:aws:sns:us-east-1:093525834944:app/APNS_SANDBOX/outfit-development" + "appsArns": { + "android": "arn:aws:sns:us-east-1:093525834944:app/GCM/outfit-development", + "ios": "arn:aws:sns:us-east-1:093525834944:app/APNS_SANDBOX/outfit-development" + }, + "defaultApp": "android" }, "amqp": { "url": "amqp://localhost" diff --git a/config/production.json b/config/production.json index d5c2340..48c1c6d 100644 --- a/config/production.json +++ b/config/production.json @@ -17,8 +17,8 @@ "secret": "PUSH_SECRET", "region": "PUSH_REGION", "title": "PUSH_TITLE", - "gsmAppArn": "GSM_APP_ARN", - "apnsAppArn": "APNS_APP_ARN" + "appsArns": "APPS_ARNS", + "defaultApp": "DEFAULT_APP" }, "amqp": { "url": "AMQP_URL" diff --git a/package.json b/package.json index 3e58579..25c777f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pingeon", "description": "", - "version": "1.0.1", + "version": "1.1.0", "homepage": "", "main": "src/", "keywords": [ @@ -61,7 +61,7 @@ "promdash": "1.1.0", "raven": "0.12.1", "serve-favicon": "2.3.0", - "smart-config": "0.7.1", + "smart-config": "0.7.2", "source-map-support": "0.4.2", "worque": "0.9.3" }, diff --git a/src/helpers/aws-utils.js b/src/helpers/aws-utils.js index e7f3453..b2872ff 100644 --- a/src/helpers/aws-utils.js +++ b/src/helpers/aws-utils.js @@ -1,6 +1,6 @@ const _ = require('lodash'); const config = require('smart-config'); -const { title, gsmAppArn, apnsAppArn } = config.get('push'); +const { title, appsArns, defaultApp } = config.get('push'); const debug = require('debug')('app:helpers:aws-utils'); const RecipientProfile = require('../services/recipient-profile/model'); @@ -20,8 +20,12 @@ function getPushMessage({ platform, message, payload }) { return JSON.stringify(pushMessage); } -function getPlatformApplicationArn(platform) { - return platform === 'android' ? gsmAppArn : apnsAppArn; +function getPlatformApplicationArn(app) { + app = _.get(app, 'name') || app || defaultApp; + + const appArn = appsArns[app]; + if (!appArn) throw new Error('No ARN for the app ' + app); + return appArn; } function getLogGroup(platformApplicationArn) { diff --git a/src/helpers/push-receive-status.js b/src/helpers/push-receive-status.js index 7ad641a..d207fba 100644 --- a/src/helpers/push-receive-status.js +++ b/src/helpers/push-receive-status.js @@ -1,17 +1,27 @@ const debug = require('debug')('app:helpers:push-receive-status'); const Notification = require('../services/notification/model'); -function saveSuccessful({ platformApplicationArn, providerMessageId, platform, token, message, payload }) { - const result = { sendDate: new Date(), platformApplicationArn, providerMessageId, platform, token, message, payload }; +function saveSuccessful({ app, platformApplicationArn, providerMessageId, platform, token, message, payload }) { + const result = { + sendDate: new Date(), + app, + platformApplicationArn, + providerMessageId, + platform, + token, + message, + payload + }; Notification.create(result); debug('push sent', result); return result; } -function saveFailed({ platform, token, message, payload, error }) { - const failedPush = { platform, token, message, payload, error }; - Notification.create(failedPush); +function saveFailed({ error, ...failedPush }) { + error = { message: error.message, stack: error.stack }; + + Notification.create({ error, ...failedPush }); debug('push sent', failedPush); } diff --git a/src/helpers/push-send.js b/src/helpers/push-send.js index 7cd7f6a..16c70f3 100644 --- a/src/helpers/push-send.js +++ b/src/helpers/push-send.js @@ -10,10 +10,9 @@ const AWS = require('aws-sdk'); AWS.config.update({ accessKeyId: key, secretAccessKey: secret, region }); const sns = promisifyAll(new AWS.SNS()); -async function send({ platform, token, message, payload }) { +async function send({ app, platform, token, message, payload }) { try { - - const platformApplicationArn = awsUtils.getPlatformApplicationArn(platform); + const platformApplicationArn = awsUtils.getPlatformApplicationArn(app); const pushMessage = awsUtils.getPushMessage({ platform, message, payload }); const { EndpointArn } = await sns.createPlatformEndpointAsync({ @@ -26,11 +25,11 @@ async function send({ platform, token, message, payload }) { Message: pushMessage, MessageStructure: 'json', TargetArn: EndpointArn }); - + return pushReceiveStatus.saveSuccessful({ platformApplicationArn, providerMessageId: MessageId, - platform, token, message, payload + platform, token, message, payload, app }); } catch (error) { if (awsUtils.isOldToken(error)) return awsUtils.deleteOldToken(token); diff --git a/src/services/notification/model.js b/src/services/notification/model.js index e1d2245..c90c223 100644 --- a/src/services/notification/model.js +++ b/src/services/notification/model.js @@ -14,6 +14,7 @@ const schema = new Schema({ token: String, payload: Object, message: String, + app: Object, providerMessageId: String, sendDate: Date, received: Boolean, diff --git a/src/services/provider/index.js b/src/services/provider/index.js index 432a0f3..a6dd7fb 100644 --- a/src/services/provider/index.js +++ b/src/services/provider/index.js @@ -45,8 +45,8 @@ module.exports = function () { app.service('/provider/push/token', { create(data) { - const { message, payload, token } = data; - return pushNotifyToken({ message, payload, token }); + const { message, payload, token, app } = data; + return pushNotifyToken({ message, payload, token, app }); } }); diff --git a/src/services/provider/routes/push-notify-recipient.js b/src/services/provider/routes/push-notify-recipient.js index 16f4431..fa46c4a 100644 --- a/src/services/provider/routes/push-notify-recipient.js +++ b/src/services/provider/routes/push-notify-recipient.js @@ -1,27 +1,24 @@ const RecipientProvider = require('../../recipient-profile/model'); -const Notification = require('../../notification/model'); const _ = require('lodash'); const Promise = require('bluebird'); const pushHelper = require('../../../helpers/push-send'); module.exports = async({ message, payload, recipientId }) => { + const credentials = await getRecipientCredentials({ recipientId, message, payload }); + return await send(credentials); +}; - async function createNotifications({ recipientId, message, payload }) { - return await Promise.all( - _(await RecipientProvider.find({ recipientId, providerType: 'push' })) - .uniq('token') - .map(({ recipientId, token, platform, deviceId }) => { - return Notification.create({ recipientId, token, platform, deviceId, message, payload }); - }) - .value() - ); - } - - function send(toSend) { - return Promise.map(toSend, push => pushHelper.send(push)); - } - - const pushes = await createNotifications({ recipientId, message, payload }); +async function getRecipientCredentials({ recipientId, message, payload }) { + return await Promise.all( + _(await RecipientProvider.find({ recipientId, providerType: 'push' })) + .uniq('token') + .map((pushProfile) => { + return { recipientId, message, payload, ...pushProfile.toJSON() }; + }) + .value() + ); +} - return await send(pushes); -}; +function send(toSend) { + return Promise.map(toSend, push => pushHelper.send(push)); +} diff --git a/src/services/provider/routes/push-notify-token.js b/src/services/provider/routes/push-notify-token.js index 95889ef..9dc8090 100644 --- a/src/services/provider/routes/push-notify-token.js +++ b/src/services/provider/routes/push-notify-token.js @@ -1,8 +1,8 @@ const Notification = require('../../notification/model'); const pushHelper = require('../../../helpers/push-send'); -module.exports = async({ token, message, payload, platform, deviceId }) => { - const push = { token, message, payload, platform, deviceId }; +module.exports = async({ token, message, payload, platform, deviceId, app }) => { + const push = { token, message, payload, platform, deviceId, app }; await Notification.create(push); return await pushHelper.send(push); diff --git a/src/services/recipient-profile/model.js b/src/services/recipient-profile/model.js index 05b1d76..59558f5 100644 --- a/src/services/recipient-profile/model.js +++ b/src/services/recipient-profile/model.js @@ -23,6 +23,7 @@ const schema = new Schema({ default: Date.now }, deviceId: String, + app: Object, token: String }); schema.plugin(renameId({ newIdName: 'id', mongoose })); diff --git a/src/services/recipient-profile/routes/push-register.js b/src/services/recipient-profile/routes/push-register.js index 3b84f1b..0559314 100644 --- a/src/services/recipient-profile/routes/push-register.js +++ b/src/services/recipient-profile/routes/push-register.js @@ -1,8 +1,8 @@ const RecipientProvider = require('../model'); -module.exports = async({ platform, deviceId, token, recipientId }) => { +module.exports = async({ platform, deviceId, token, recipientId, app }) => { const newRecipientProvider = { - recipientId, deviceId, token, platform, + recipientId, deviceId, token, platform, app, providerType: 'push', registeredDate: new Date() }; diff --git a/test/batch-notify-push.test.js b/test/batch-notify-push.test.js new file mode 100644 index 0000000..2c7b2ae --- /dev/null +++ b/test/batch-notify-push.test.js @@ -0,0 +1,76 @@ +require('./test-env'); + +const config = require('smart-config'); +const { toObject } = require('node-helpers'); +const Notification = require('../src/services/notification/model'); + +const platform = 'android'; +const app = { name: 'android' }; +const token = String(new Date()); + +describe('Push send', () => { + + describe('success', () => { + let recipientId; + const message = String(new Date()); + + before(async() => { + const recipient = await helpers.createRandomRecipient(); + recipientId = String(recipient.id); + ctx.recipientProfile = await helpers.createRecipientProfile({ + recipientId, providerType: 'push', platform, app, token + }); + }); + + it('should be sent', async() => { + await request + .post('/notification/batch') + .send({ + recipients: [recipientId], + providers: { push: { message } } + }); + + await helpers.timeout(1000); + const notification = toObject(await Notification.findOne({ message })); + + assert.equal(notification.token, token); + assert.equal(notification.platform, platform); + assert.equal(notification.app.name, app.name); + assert.equal(notification.message, message); + assert(notification.sendDate); + }); + }); + + describe('default app', () => { + + let recipientWithoutApp; + const message = String(new Date() + 'no app'); + + before(async() => { + const recipient = await helpers.createRandomRecipient(); + recipientWithoutApp = String(recipient.id); + ctx.recipientProfile = await helpers.createRecipientProfile({ + recipientId: recipientWithoutApp, providerType: 'push', platform, token + }); + }); + + it('should use default app', async() => { + await request + .post('/notification/batch') + .send({ + recipients: [recipientWithoutApp], + providers: { push: { message } } + }); + + await helpers.timeout(1000); + const notification = toObject(await Notification.findOne({ message })); + + assert.equal(notification.token, token); + assert.equal(notification.platform, platform); + assert.equal(notification.message, message); + assert(notification.sendDate); + assert(!notification.error); + }); + }); + +}); diff --git a/test/helpers/index.js b/test/helpers/index.js index 3e890f9..0e44d84 100644 --- a/test/helpers/index.js +++ b/test/helpers/index.js @@ -14,6 +14,12 @@ function createRandomRecipient(recipient) { }, recipient)); } +function timeout(ms) { + return new Promise(res => { + setTimeout(res, ms); + }); +} + async function createRecipientProfile({ recipientId, address = 'some', ...other }) { recipientId = recipientId || (await createRandomRecipient({ id: recipientId })).id; @@ -24,5 +30,6 @@ module.exports = { createRandomRecipient, createRecipientProfile, db, + timeout, randomId }; diff --git a/test/push-send-helper.test.js b/test/push-send-helper.test.js index 2415c70..1f3cc96 100644 --- a/test/push-send-helper.test.js +++ b/test/push-send-helper.test.js @@ -8,6 +8,7 @@ const Notification = require('../src/services/notification/model'); const RecipientProfile = require('../src/services/recipient-profile/model'); const platform = 'android'; +const app = { name: 'android' }; const token = String(new Date()); const message = String(new Date()); const error = { message: String(new Date()) }; @@ -30,9 +31,10 @@ describe('Push', () => { it('should be sent', () => { return pushProvider - .send({ platform, token, message, payload }) + .send({ platform, token, message, payload, app }) .then(async res => { assert.equal(res.platform, platform); + assert.equal(res.app, app); assert.equal(res.message, message); assert.equal(res.token, token); assert.deepEqual(res.payload, payload); @@ -55,7 +57,7 @@ describe('Push', () => { }); before(() => { - pushProvider.send({ platform, token, message, payload }); + pushProvider.send({ platform, token, message, payload, app }); }); it('should be fail', async() => { @@ -71,22 +73,22 @@ describe('Push', () => { const oldTokenError = { message: 'Invalid parameter: This endpoint is already registered with a different token.' }; const oldToken = String(new Date()); - before(async () => { - await pushRegister({ token: oldToken, deviceId: 'some', recipientId: 'some' }); + before(async() => { + await pushRegister({ token: oldToken, deviceId: 'some', recipientId: 'some', app }); }); before(async() => { const res = await RecipientProfile.findOne({ token: oldToken }); assert.ok(res); }); - + before(() => { createEndpointStub.returns({ EndpointArn }); publishStub.throws(oldTokenError); }); before(() => { - pushProvider.send({ platform, token: oldToken, message, payload }); + pushProvider.send({ platform, token: oldToken, message, payload, app }); }); it('should be no old device token', async() => { diff --git a/test/push-send-real.test.js b/test/push-send-real.test.js index 52bbee9..391eb3a 100644 --- a/test/push-send-real.test.js +++ b/test/push-send-real.test.js @@ -1,8 +1,10 @@ const assert = require('assert'); const pushProvider = require('../src/helpers/push-send'); -const token = '5c35a598c08f0e8e3d973714fd00df295db7c555f74ddfb4e60c1fdd7662d602'; -const platform = 'ios'; +const token = 'd387VFC_RBs:APA91bFTfo0-GYuOpRHmaPqyERHKJGpOtHhUib9unDTzwr9DRqUO5scoh2ffftzI4DIEnNom4szjVImQWljVy7B1l' + + 'nMuQfd-WD6IS727ewCcx3Yp227qsJiv03fHsHfTtfOANhpz2vEX'; +const platform = 'android'; +const app = 'android'; const message = 'Hello!'; const payload = { badge: 5 }; @@ -10,7 +12,7 @@ describe('Push', () => { it('should be sent', () => { return pushProvider - .send({ platform, token, message, payload }) + .send({ platform, token, message, payload, app }) .then(() => { setTimeout(() => assert(ctx.pushSendSpy.called), 100000); });