Skip to content

Commit 85fce4b

Browse files
committed
Adding SNS
Fixups Get basic tests for SNS going. Fix to nest createPlatformEndpoint before sending payload Don't need my own classifyInstallations Use refactored GCM code Fix this JSON too. Refactor this code. Modify SNSPushAdapter Kill these tests for now. More fixes Make SNSPushAdapter more testable More tests
1 parent bbcaa34 commit 85fce4b

File tree

2 files changed

+370
-0
lines changed

2 files changed

+370
-0
lines changed

spec/SNSPushAdapter.spec.js

+169
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
var SNSPushAdapter = require('../src/Adapters/Push/SNSPushAdapter');
2+
describe('SNSPushAdapter', () => {
3+
4+
var pushConfig = {
5+
pushTypes: {
6+
ios: "APNS_ID",
7+
android: "GCM_ID"
8+
},
9+
accessKey: "accessKey",
10+
secretKey: "secretKey",
11+
region: "region"
12+
};
13+
14+
it('can be initialized', (done) => {
15+
// Make mock config
16+
17+
var snsPushAdapter = new SNSPushAdapter(pushConfig);
18+
var arnMap = snsPushAdapter.arnMap;
19+
20+
expect(arnMap.ios).toEqual("APNS_ID");
21+
expect(arnMap.android).toEqual("GCM_ID");
22+
23+
done();
24+
});
25+
26+
it('can get valid push types', (done) => {
27+
var snsPushAdapter = new SNSPushAdapter(pushConfig);
28+
expect(snsPushAdapter.getValidPushTypes()).toEqual(['ios', 'android']);
29+
done();
30+
});
31+
32+
it('can classify installation', (done) => {
33+
// Mock installations
34+
var validPushTypes = ['ios', 'android'];
35+
var installations = [
36+
{
37+
deviceType: 'android',
38+
deviceToken: 'androidToken'
39+
},
40+
{
41+
deviceType: 'ios',
42+
deviceToken: 'iosToken'
43+
},
44+
{
45+
deviceType: 'win',
46+
deviceToken: 'winToken'
47+
},
48+
{
49+
deviceType: 'android',
50+
deviceToken: undefined
51+
}
52+
];
53+
var snsPushAdapter = new SNSPushAdapter(pushConfig);
54+
var deviceMap = SNSPushAdapter.classifyInstallations(installations, validPushTypes);
55+
expect(deviceMap['android']).toEqual([makeDevice('androidToken')]);
56+
expect(deviceMap['ios']).toEqual([makeDevice('iosToken')]);
57+
expect(deviceMap['win']).toBe(undefined);
58+
done();
59+
});
60+
61+
it('can send push notifications', (done) => {
62+
var snsPushAdapter = new SNSPushAdapter(pushConfig);
63+
64+
// Mock SNS sender
65+
var snsSender = jasmine.createSpyObj('sns', ['createPlatformEndpoint', 'publish']);
66+
snsPushAdapter.sns = snsSender;
67+
68+
// Mock android ios senders
69+
var androidSender = jasmine.createSpy('send')
70+
var iosSender = jasmine.createSpy('send')
71+
72+
var senderMap = {
73+
ios: iosSender,
74+
android: androidSender
75+
};
76+
snsPushAdapter.senderMap = senderMap;
77+
78+
// Mock installations
79+
var installations = [
80+
{
81+
deviceType: 'android',
82+
deviceToken: 'androidToken'
83+
},
84+
{
85+
deviceType: 'ios',
86+
deviceToken: 'iosToken'
87+
},
88+
{
89+
deviceType: 'win',
90+
deviceToken: 'winToken'
91+
},
92+
{
93+
deviceType: 'android',
94+
deviceToken: undefined
95+
}
96+
];
97+
var data = {};
98+
99+
snsPushAdapter.send(data, installations);
100+
// Check SNS sender
101+
expect(androidSender).toHaveBeenCalled();
102+
var args = androidSender.calls.first().args;
103+
expect(args[0]).toEqual(data);
104+
expect(args[1]).toEqual([
105+
makeDevice('androidToken')
106+
]);
107+
// Check ios sender
108+
expect(iosSender).toHaveBeenCalled();
109+
args = iosSender.calls.first().args;
110+
expect(args[0]).toEqual(data);
111+
expect(args[1]).toEqual([
112+
makeDevice('iosToken')
113+
]);
114+
done();
115+
});
116+
117+
it('can generate the right Android payload', (done) => {
118+
var data = {"action": "com.example.UPDATE_STATUS"};
119+
var pushId = '123';
120+
var timeStamp = 1456728000;
121+
122+
var returnedData = SNSPushAdapter.generateAndroidPayload(data, pushId, timeStamp);
123+
var expectedData = { GCM: '{"priority":"normal","data":{"time":"1970-01-17T20:38:48.000Z","push_id":"123"}}'};
124+
expect(returnedData).toEqual(expectedData)
125+
done();
126+
});
127+
128+
it('can generate the right iOS payload', (done) => {
129+
var data = {"aps": {"alert" : "Check out these awesome deals!"}};
130+
var pushId = '123';
131+
var timeStamp = 1456728000;
132+
133+
var returnedData = SNSPushAdapter.generateiOSPayload(data);
134+
var expectedData = { APNS: '{"aps":{"alert":"Check out these awesome deals!"}}' };
135+
expect(returnedData).toEqual(expectedData)
136+
done();
137+
});
138+
139+
it('can exchange device tokens for an Amazon Resource Number (ARN)', (done) => {
140+
var snsPushAdapter = new SNSPushAdapter(pushConfig);
141+
142+
// Mock out Amazon SNS token exchange
143+
var snsSender = jasmine.createSpyObj('sns', ['createPlatformEndpoint']);
144+
snsPushAdapter.sns = snsSender;
145+
146+
// Mock installations
147+
var installations = [
148+
{
149+
deviceType: 'android',
150+
deviceToken: 'androidToken'
151+
}
152+
];
153+
154+
snsPushAdapter.getPlatformArn(makeDevice("androidToken"), "android");
155+
expect(snsSender.createPlatformEndpoint).toHaveBeenCalled();
156+
args = snsSender.createPlatformEndpoint.calls.first().args;
157+
expect(args[0].PlatformApplicationArn).toEqual("GCM_ID");
158+
expect(args[0].Token).toEqual("androidToken");
159+
done();
160+
});
161+
162+
function makeDevice(deviceToken, appIdentifier) {
163+
return {
164+
deviceToken: deviceToken,
165+
appIdentifier: appIdentifier
166+
};
167+
}
168+
169+
});

src/Adapters/Push/SNSPushAdapter.js

+201
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
"use strict";
2+
// SNSAdapter
3+
//
4+
// Uses SNS for push notification
5+
import PushAdapter from './PushAdapter';
6+
7+
const Parse = require('parse/node').Parse;
8+
const GCM = require('../../GCM');
9+
const cryptoUtils = require('../../cryptoUtils');
10+
11+
const AWS = require('aws-sdk');
12+
const path = require('path');
13+
const rest = require('../../rest');
14+
15+
var DEFAULT_REGION = "us-east-1";
16+
import { classifyInstallations } from './PushAdapterUtils';
17+
18+
export class SNSPushAdapter extends PushAdapter {
19+
20+
// Publish to an SNS endpoint
21+
// Providing AWS access and secret keys is mandatory
22+
// Region will use sane defaults if omitted
23+
constructor(pushConfig = {}) {
24+
super(pushConfig);
25+
this.validPushTypes = ['ios', 'android'];
26+
this.availablePushTypes = [];
27+
this.arnMap = {};
28+
this.senderMap = {};
29+
30+
if (!pushConfig.accessKey || !pushConfig.secretKey) {
31+
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
32+
'Need to provide AWS keys');
33+
}
34+
35+
if (pushConfig.pushTypes) {
36+
let pushTypes = Object.keys(pushConfig.pushTypes);
37+
for (let pushType of pushTypes) {
38+
if (this.validPushTypes.indexOf(pushType) < 0) {
39+
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
40+
'Push to ' + pushTypes + ' is not supported');
41+
}
42+
this.availablePushTypes.push(pushType);
43+
switch (pushType) {
44+
case 'ios':
45+
this.senderMap[pushType] = this.sendToAPNS.bind(this);
46+
this.arnMap[pushType] = pushConfig.pushTypes[pushType];
47+
break;
48+
case 'android':
49+
this.senderMap[pushType] = this.sendToGCM.bind(this);
50+
this.arnMap[pushType] = pushConfig.pushTypes[pushType];
51+
break;
52+
}
53+
}
54+
}
55+
56+
AWS.config.update({
57+
accessKeyId: pushConfig.accessKey,
58+
secretAccessKey: pushConfig.secretKey,
59+
region: pushConfig.region || DEFAULT_REGION
60+
});
61+
62+
// Instantiate after config is setup.
63+
this.sns = new AWS.SNS();
64+
}
65+
66+
getValidPushTypes() {
67+
return this.availablePushTypes;
68+
}
69+
70+
static classifyInstallations(installations, validTypes) {
71+
return classifyInstallations(installations, validTypes)
72+
}
73+
74+
//Generate proper json for APNS message
75+
static generateiOSPayload(data) {
76+
return {
77+
'APNS': JSON.stringify(data)
78+
};
79+
}
80+
81+
// Generate proper json for GCM message
82+
static generateAndroidPayload(data, pushId, timeStamp) {
83+
if (!pushId || !timeStamp) {
84+
let gcmData = GCM.prepareGCMPayload();
85+
pushId = gcmData.pushId;
86+
timeStamp = gcmData.timeStamp;
87+
}
88+
var payload = GCM.generateGCMPayload(data.data, pushId, timeStamp, data.expirationTime);
89+
90+
// SNS is verify sensitive to the body being JSON stringified but not GCM key.
91+
return {
92+
'GCM': JSON.stringify(payload)
93+
};
94+
}
95+
96+
sendToAPNS(data, devices) {
97+
var payload = SNSPushAdapter.generateiOSPayload(data);
98+
99+
return this.sendToSNS(payload, devices, 'ios');
100+
}
101+
102+
sendToGCM(data, devices) {
103+
var payload = SNSPushAdapter.generateAndroidPayload(data);
104+
return this.sendToSNS(payload, devices, 'android');
105+
}
106+
107+
sendToSNS(payload, devices, pushType) {
108+
var promises = [];
109+
var exchangePromises = [];
110+
var self = this;
111+
112+
// Exchange the device token for the Amazon resource ID
113+
for (let device of devices) {
114+
exchangePromises.push(this.exchangeTokenPromise(device, pushType));
115+
}
116+
117+
// Publish off to SNS!
118+
Parse.Promise.when(exchangePromises).then(function(arns) {
119+
for (let arn of arns) {
120+
promises.push(self.sendSNSPayload(arn, payload));
121+
}
122+
});
123+
124+
return promises;
125+
}
126+
127+
128+
/**
129+
* Request a Amazon Resource Identifier if one is not set.
130+
*/
131+
getPlatformArn(device, pushType, callback) {
132+
var params = {
133+
PlatformApplicationArn: this.arnMap[pushType],
134+
Token: device.deviceToken
135+
};
136+
137+
this.sns.createPlatformEndpoint(params, callback);
138+
}
139+
140+
/**
141+
* Exchange the device token for an ARN
142+
*/
143+
exchangeTokenPromise(device, pushType) {
144+
return new Parse.Promise((resolve, reject) => {
145+
this.getPlatformArn(device, pushType, function (err, data) {
146+
if (data.EndpointArn) {
147+
console.log("Generating ARN " + data.EndpointArn);
148+
resolve(data.EndpointArn);
149+
} else {
150+
console.log(err);
151+
reject(err);
152+
}
153+
});
154+
});
155+
}
156+
157+
// Bulk publishing is not yet supported on Amazon SNS.
158+
sendSNSPayload(arn, payload) {
159+
160+
var object = {
161+
Message: JSON.stringify(payload),
162+
MessageStructure: 'json',
163+
TargetArn: arn
164+
};
165+
166+
return new Parse.Promise((resolve, reject) => {
167+
this.sns.publish(object, function (err, data) {
168+
if (err != null) {
169+
console.log("Error sending push " + err);
170+
return reject(err);
171+
}
172+
console.log("Sent push to " + object.TargetArn);
173+
resolve(object);
174+
});
175+
});
176+
}
177+
178+
// For a given config object, endpoint and payload, publish via SNS
179+
// Returns a promise containing the SNS object publish response
180+
send(data, installations) {
181+
let sendPromises = [];
182+
let deviceMap = classifyInstallations(installations, this.availablePushTypes);
183+
184+
for (let pushType in deviceMap) {
185+
186+
let sender = this.senderMap[pushType];
187+
188+
if (!sender) {
189+
console.log('Can not find sender for push type %s, %j', pushType, data);
190+
continue;
191+
}
192+
193+
let devices = deviceMap[pushType];
194+
sendPromises.push(sender(data, devices));
195+
}
196+
return Parse.Promise.when(sendPromises);
197+
}
198+
}
199+
200+
export default SNSPushAdapter;
201+
module.exports = SNSPushAdapter;

0 commit comments

Comments
 (0)