feat(sms): Switch to AWS SNS for SMS

https://github.com/mozilla/fxa-auth-server/pull/1964
r=philbooth,jbuck
This commit is contained in:
Phil Booth 2017-06-28 13:17:29 -07:00 коммит произвёл GitHub
Родитель a358d7c7b8
Коммит 7ce5c05250
12 изменённых файлов: 806 добавлений и 860 удалений

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

@ -663,33 +663,23 @@ var conf = convict({
format: Boolean, format: Boolean,
env: 'SMS_USE_MOCK' env: 'SMS_USE_MOCK'
}, },
apiKey: {
doc: 'API key for the SMS service',
default: 'YOU MUST CHANGE ME',
format: String,
env: 'SMS_API_KEY'
},
apiSecret: {
doc: 'API secret for the SMS service',
default: 'YOU MUST CHANGE ME',
format: String,
env: 'SMS_API_SECRET'
},
isStatusGeoEnabled: { isStatusGeoEnabled: {
doc: 'Indicates whether the status endpoint should do geo-ip lookup', doc: 'Indicates whether the status endpoint should do geo-ip lookup',
default: true, default: true,
format: Boolean, format: Boolean,
env: 'SMS_STATUS_GEO_ENABLED' env: 'SMS_STATUS_GEO_ENABLED'
}, },
senderIds: { apiRegion: {
doc: 'Sender ids keyed by the ISO 3166-1 alpha-2 country code (region) they apply to', doc: 'AWS region',
default: { default: 'us-east-1',
CA: '16474909977', format: String,
GB: 'Firefox', env: 'SMS_API_REGION'
US: '15036789977'
}, },
format: Object, countryCodes: {
env: 'SMS_SENDER_IDS' doc: 'Allow sending SMS to these ISO 3166-1 alpha-2 country codes',
default: ['CA', 'GB', 'US'],
format: Array,
env: 'SMS_COUNTRY_CODES'
}, },
installFirefoxLink: { installFirefoxLink: {
doc: 'Link for the installFirefox SMS template', doc: 'Link for the installFirefox SMS template',

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

@ -22,8 +22,7 @@ module.exports = (log, db, config, customs, sms) => {
} }
const getGeoData = require('../geodb')(log) const getGeoData = require('../geodb')(log)
const SENDER_IDS = config.sms.senderIds const REGIONS = new Set(config.sms.countryCodes)
const REGIONS = new Set(Object.keys(SENDER_IDS))
const IS_STATUS_GEO_ENABLED = config.sms.isStatusGeoEnabled const IS_STATUS_GEO_ENABLED = config.sms.isStatusGeoEnabled
return [ return [
@ -52,12 +51,12 @@ module.exports = (log, db, config, customs, sms) => {
const templateName = TEMPLATE_NAMES.get(request.payload.messageId) const templateName = TEMPLATE_NAMES.get(request.payload.messageId)
const acceptLanguage = request.app.acceptLanguage const acceptLanguage = request.app.acceptLanguage
let phoneNumberUtil, parsedPhoneNumber, senderId let phoneNumberUtil, parsedPhoneNumber
customs.check(request, sessionToken.email, 'connectDeviceSms') customs.check(request, sessionToken.email, 'connectDeviceSms')
.then(parsePhoneNumber) .then(parsePhoneNumber)
.then(validatePhoneNumber) .then(validatePhoneNumber)
.then(getRegionSpecificSenderId) .then(validateRegion)
.then(createSigninCode) .then(createSigninCode)
.then(sendMessage) .then(sendMessage)
.then(logSuccess) .then(logSuccess)
@ -75,12 +74,11 @@ module.exports = (log, db, config, customs, sms) => {
} }
} }
function getRegionSpecificSenderId () { function validateRegion () {
const region = phoneNumberUtil.getRegionCodeForNumber(parsedPhoneNumber) const region = phoneNumberUtil.getRegionCodeForNumber(parsedPhoneNumber)
request.emitMetricsEvent(`sms.region.${region}`) request.emitMetricsEvent(`sms.region.${region}`)
senderId = SENDER_IDS[region] if (! REGIONS.has(region)) {
if (! senderId) {
throw error.invalidRegion(region) throw error.invalidRegion(region)
} }
} }
@ -92,7 +90,7 @@ module.exports = (log, db, config, customs, sms) => {
} }
function sendMessage (signinCode) { function sendMessage (signinCode) {
return sms.send(phoneNumber, senderId, templateName, acceptLanguage, signinCode) return sms.send(phoneNumber, templateName, acceptLanguage, signinCode)
} }
function logSuccess () { function logSuccess () {

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

@ -4,28 +4,27 @@
'use strict' 'use strict'
var Nexmo = require('nexmo') var AWS = require('aws-sdk')
var MockNexmo = require('../mock-nexmo') var MockSNS = require('../../test/mock-sns')
var P = require('bluebird') var P = require('bluebird')
var error = require('../error') var error = require('../error')
module.exports = function (log, translator, templates, config) { module.exports = function (log, translator, templates, config) {
var smsConfig = config.sms var smsConfig = config.sms
var nexmo = smsConfig.useMock ? new MockNexmo(log, config) : new Nexmo({ var smsOptions = {
apiKey: smsConfig.apiKey, region: smsConfig.apiRegion
apiSecret: smsConfig.apiSecret }
}) var SNS
if (smsConfig.useMock) {
var sendSms = promisify('sendSms', nexmo.message) SNS = new MockSNS(smsOptions, config)
var NEXMO_ERRORS = new Map([ } else {
[ '1', error.tooManyRequests(smsConfig.throttleWaitTime) ] SNS = new AWS.SNS(smsOptions)
]) }
return { return {
send: function (phoneNumber, senderId, templateName, acceptLanguage, signinCode) { send: function (phoneNumber, templateName, acceptLanguage, signinCode) {
log.trace({ log.trace({
op: 'sms.send', op: 'sms.send',
senderId: senderId,
templateName: templateName, templateName: templateName,
acceptLanguage: acceptLanguage acceptLanguage: acceptLanguage
}) })
@ -33,41 +32,49 @@ module.exports = function (log, translator, templates, config) {
return P.resolve() return P.resolve()
.then(function () { .then(function () {
var message = getMessage(templateName, acceptLanguage, signinCode) var message = getMessage(templateName, acceptLanguage, signinCode)
var params = {
return sendSms(senderId, phoneNumber, message.trim()) Message: message.trim(),
}) MessageAttributes: {
.then(function (result) { 'AWS.SNS.SMS.MaxPrice': {
var resultCount = result.messages && result.messages.length // The maximum amount in USD that you are willing to spend to send the SMS message.
if (resultCount !== 1) { DataType: 'String',
// I don't expect this condition to be entered, certainly I haven't StringValue: '1.0'
// seen it in testing. But because I'm making an assumption about },
// the result format, I want to log an error if my assumption proves 'AWS.SNS.SMS.SenderID': {
// to be wrong in production. // Up to 11 alphanumeric characters, including at least one letter and no spaces
log.error({ op: 'sms.send.error', err: new Error('Unexpected result count'), resultCount: resultCount }) DataType: 'String',
StringValue: 'Firefox'
},
'AWS.SNS.SMS.SMSType': {
// 'Promotional' for cheap marketing messages, 'Transactional' for critical transactions
DataType: 'String',
StringValue: 'Promotional'
}
},
PhoneNumber: phoneNumber
} }
result = result.messages[0] return SNS.publish(params).promise()
var status = result.status .then(function (result) {
// https://docs.nexmo.com/messaging/sms-api/api-reference#status-codes
if (status === '0') {
log.info({ log.info({
op: 'sms.send.success', op: 'sms.send.success',
senderId: senderId,
templateName: templateName, templateName: templateName,
acceptLanguage: acceptLanguage acceptLanguage: acceptLanguage,
messageId: result.MessageId
}) })
} else {
var reason = result['error-text']
log.error({ op: 'sms.send.error', reason: reason, status: status })
throw NEXMO_ERRORS.get(status) || error.messageRejected(reason, status)
}
}) })
} .catch(function (sendError) {
} log.error({
op: 'sms.send.error',
message: sendError.message,
code: sendError.code,
statusCode: sendError.statusCode
})
function promisify (methodName, object) { throw error.messageRejected(sendError.message, sendError.code)
return P.promisify(object[methodName], { context: object }) })
})
}
} }
function getMessage (templateName, acceptLanguage, signinCode) { function getMessage (templateName, acceptLanguage, signinCode) {

1142
npm-shrinkwrap.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@ -33,7 +33,7 @@
"readmeFilename": "README.md", "readmeFilename": "README.md",
"dependencies": { "dependencies": {
"ajv": "4.1.7", "ajv": "4.1.7",
"aws-sdk": "2.2.10", "aws-sdk": "2.77.0",
"base64url": "1.0.6", "base64url": "1.0.6",
"binary-split": "0.1.2", "binary-split": "0.1.2",
"bluebird": "3.4.7", "bluebird": "3.4.7",
@ -76,7 +76,7 @@
"acorn": "5.0.3", "acorn": "5.0.3",
"commander": "2.9.0", "commander": "2.9.0",
"eslint-plugin-fxa": "git+https://github.com/mozilla/eslint-plugin-fxa#master", "eslint-plugin-fxa": "git+https://github.com/mozilla/eslint-plugin-fxa#master",
"fxa-auth-db-mysql": "git+https://github.com/mozilla/fxa-auth-db-mysql.git#master", "fxa-auth-db-mysql": "git+https://github.com/mozilla/fxa-auth-db-mysql.git#train-89",
"fxa-conventional-changelog": "1.1.0", "fxa-conventional-changelog": "1.1.0",
"grunt": "1.0.1", "grunt": "1.0.1",
"grunt-bump": "0.8.0", "grunt-bump": "0.8.0",

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

@ -1,36 +0,0 @@
#!/usr/bin/env node
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict'
const config = require('../../config').getProperties()
const NOT_SET = 'YOU MUST CHANGE ME'
if (config.sms.apiKey === NOT_SET || config.sms.apiSecret === NOT_SET) {
fail('Come back and try again when you\'ve set SMS_API_KEY and SMS_API_SECRET.')
}
const log = require('../../lib/log')(config.log.level, 'sms-balance')
require('../../lib/senders/translator')(config.i18n.supportedLanguages, config.i18n.defaultLanguage)
.then(translator => {
return require('../../lib/senders')(log, config, {}, null, translator)
})
.then(senders => {
return senders.sms.balance()
})
.then(result => {
console.log(result)
})
.catch(error => {
fail(error.stack || error.message)
})
function fail (message) {
console.error(message)
process.exit(1)
}

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

@ -7,11 +7,6 @@
'use strict' 'use strict'
const config = require('../../config').getProperties() const config = require('../../config').getProperties()
const NOT_SET = 'YOU MUST CHANGE ME'
if (config.sms.apiKey === NOT_SET || config.sms.apiSecret === NOT_SET) {
fail('Come back and try again when you\'ve set SMS_API_KEY and SMS_API_SECRET.')
}
const args = parseArgs() const args = parseArgs()
const log = require('../../lib/log')(config.log.level, 'send-sms') const log = require('../../lib/log')(config.log.level, 'send-sms')
@ -42,28 +37,25 @@ function fail (message) {
} }
function parseArgs () { function parseArgs () {
let acceptLanguage, messageId, senderId, phoneNumber let acceptLanguage, messageName, phoneNumber
switch (process.argv.length) { switch (process.argv.length) {
/* eslint-disable indent, no-fallthrough */ /* eslint-disable indent, no-fallthrough */
case 6:
acceptLanguage = process.argv[5]
case 5: case 5:
messageId = process.argv[4] acceptLanguage = process.argv[5]
case 4: case 4:
senderId = process.argv[3] messageName = process.argv[4]
case 3: case 3:
phoneNumber = process.argv[2] phoneNumber = process.argv[2]
break break
default: default:
fail(`Usage: ${process.argv[1]} phoneNumber [senderId] [messageId] [acceptLanguage]`) fail(`Usage: ${process.argv[1]} phoneNumber [messageName] [acceptLanguage]`)
/* eslint-enable indent, no-fallthrough */ /* eslint-enable indent, no-fallthrough */
} }
return [ return [
phoneNumber, phoneNumber,
senderId || 'Firefox', messageName || 'installFirefox',
messageId || 1,
acceptLanguage || 'en' acceptLanguage || 'en'
] ]
} }

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

@ -1,81 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict'
const assert = require('insist')
// noPreserveCache is needed to prevent the mock mailer from
// being used for all future tests that include mock-nexmo.
const proxyquire = require('proxyquire').noPreserveCache()
const sinon = require('sinon')
const config = require('../../config').getProperties()
describe('mock-nexmo', () => {
let log
let mailer
let mockNexmo
const MockNexmo = proxyquire('../../lib/mock-nexmo', {
nodemailer: {
createTransport: () => mailer
}
})
before(() => {
mailer = {
sendMail: sinon.spy((config, callback) => callback())
}
log = {
info: sinon.spy()
}
mockNexmo = new MockNexmo(log, config)
})
afterEach(() => {
mailer.sendMail.reset()
log.info.reset()
})
it('constructor creates an instance', () => {
assert.ok(mockNexmo)
})
describe('message.sendSms', () => {
it('returns status: 0 with options, callback', (done) => {
mockNexmo.message.sendSms('senderid', '+019999999999', 'message', {}, (err, resp) => {
assert.strictEqual(err, null)
assert.equal(resp.messages.length, 1)
assert.strictEqual(resp.messages[0].status, '0')
assert.equal(log.info.callCount, 1)
assert.equal(mailer.sendMail.callCount, 1)
const sendConfig = mailer.sendMail.args[0][0]
assert.equal(sendConfig.from, config.smtp.sender)
assert.equal(sendConfig.to, 'sms.+019999999999@restmail.net')
assert.equal(sendConfig.subject, 'MockNexmo.message.sendSms')
assert.equal(sendConfig.text, 'message')
done()
})
})
it('returns status: 0 without options, only callback', (done) => {
mockNexmo.message.sendSms('senderid', '+019999999999', 'message', (err, resp) => {
assert.strictEqual(err, null)
assert.equal(resp.messages.length, 1)
assert.strictEqual(resp.messages[0].status, '0')
assert.equal(log.info.callCount, 1)
assert.equal(mailer.sendMail.callCount, 1)
const sendConfig = mailer.sendMail.args[0][0]
assert.equal(sendConfig.from, config.smtp.sender)
assert.equal(sendConfig.to, 'sms.+019999999999@restmail.net')
assert.equal(sendConfig.subject, 'MockNexmo.message.sendSms')
assert.equal(sendConfig.text, 'message')
done()
})
})
})
})

62
test/local/mock-sns.js Normal file
Просмотреть файл

@ -0,0 +1,62 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict'
const assert = require('insist')
// noPreserveCache is needed to prevent the mock mailer from
// being used for all future tests that include mock-nexmo.
const proxyquire = require('proxyquire').noPreserveCache()
const sinon = require('sinon')
const config = require('../../config').getProperties()
describe('mock-sns', () => {
let mailer
let mockSNS
const MockSNS = proxyquire('../../test/mock-sns', {
nodemailer: {
createTransport: () => mailer
}
})
before(() => {
mailer = {
sendMail: sinon.spy((config, callback) => callback())
}
mockSNS = new MockSNS(null, config)
})
afterEach(() => {
mailer.sendMail.reset()
})
it('constructor creates an instance', () => {
assert.ok(mockSNS)
})
describe('message.sendSms', () => {
let result
beforeEach(() => {
return mockSNS.publish({
PhoneNumber: '+019999999999',
Message: 'message'
}).promise().then(r => result = r)
})
it('returns message id', () => {
assert.deepEqual(result, { MessageId: 'fake message id' })
})
it('calls mailer.sendMail correctly', () => {
assert.equal(mailer.sendMail.callCount, 1)
const sendConfig = mailer.sendMail.args[0][0]
assert.equal(sendConfig.from, config.smtp.sender)
assert.equal(sendConfig.to, 'sms.+019999999999@restmail.net')
assert.equal(sendConfig.subject, 'MockSNS.publish')
assert.equal(sendConfig.text, 'message')
})
})
})

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

@ -45,11 +45,7 @@ describe('/sms with the signinCodes feature included in the payload', () => {
config = { config = {
sms: { sms: {
enabled: true, enabled: true,
senderIds: { countryCodes: [ 'CA', 'GB', 'US' ],
CA: '16474909977',
GB: 'Firefox',
US: '15036789977'
},
isStatusGeoEnabled: true isStatusGeoEnabled: true
} }
} }
@ -107,12 +103,11 @@ describe('/sms with the signinCodes feature included in the payload', () => {
it('called sms.send correctly', () => { it('called sms.send correctly', () => {
assert.equal(sms.send.callCount, 1) assert.equal(sms.send.callCount, 1)
const args = sms.send.args[0] const args = sms.send.args[0]
assert.equal(args.length, 5) assert.equal(args.length, 4)
assert.equal(args[0], '+18885083401') assert.equal(args[0], '+18885083401')
assert.equal(args[1], '15036789977') assert.equal(args[1], 'installFirefox')
assert.equal(args[2], 'installFirefox') assert.equal(args[2], 'en-US')
assert.equal(args[3], 'en-US') assert.equal(args[3], signinCode)
assert.equal(args[4], signinCode)
}) })
it('called log.flowEvent correctly', () => { it('called log.flowEvent correctly', () => {
@ -152,7 +147,6 @@ describe('/sms with the signinCodes feature included in the payload', () => {
assert.equal(sms.send.callCount, 1) assert.equal(sms.send.callCount, 1)
const args = sms.send.args[0] const args = sms.send.args[0]
assert.equal(args[0], '+14168483114') assert.equal(args[0], '+14168483114')
assert.equal(args[1], '16474909977')
}) })
it('called log.flowEvent correctly', () => { it('called log.flowEvent correctly', () => {
@ -183,7 +177,6 @@ describe('/sms with the signinCodes feature included in the payload', () => {
assert.equal(sms.send.callCount, 1) assert.equal(sms.send.callCount, 1)
const args = sms.send.args[0] const args = sms.send.args[0]
assert.equal(args[0], '+442078553000') assert.equal(args[0], '+442078553000')
assert.equal(args[1], 'Firefox')
}) })
it('called log.flowEvent correctly', () => { it('called log.flowEvent correctly', () => {
@ -323,11 +316,7 @@ describe('/sms without the signinCodes feature included in the payload', () => {
config = { config = {
sms: { sms: {
enabled: true, enabled: true,
senderIds: { countryCodes: [ 'CA', 'GB', 'US' ],
CA: '16474909977',
GB: 'Firefox',
US: '15036789977'
},
isStatusGeoEnabled: true isStatusGeoEnabled: true
} }
} }
@ -402,7 +391,7 @@ describe('/sms/status', () => {
config = { config = {
sms: { sms: {
enabled: true, enabled: true,
senderIds: { 'US': '18005551212' }, countryCodes: [ 'US' ],
isStatusGeoEnabled: true isStatusGeoEnabled: true
} }
} }
@ -550,7 +539,7 @@ describe('/sms/status with disabled geo-ip lookup', () => {
config = { config = {
sms: { sms: {
enabled: true, enabled: true,
senderIds: { 'US': '18005551212' }, countryCodes: [ 'US' ],
isStatusGeoEnabled: false isStatusGeoEnabled: false
} }
} }
@ -603,7 +592,7 @@ describe('/sms/status with query param and enabled geo-ip lookup', () => {
config = { config = {
sms: { sms: {
enabled: true, enabled: true,
senderIds: { 'RO': '0215555111' }, countryCodes: [ 'RO' ],
isStatusGeoEnabled: true isStatusGeoEnabled: true
} }
} }
@ -648,7 +637,7 @@ describe('/sms/status with query param and disabled geo-ip lookup', () => {
config = { config = {
sms: { sms: {
enabled: true, enabled: true,
senderIds: { 'GB': '03456000000' }, countryCodes: [ 'GB' ],
isStatusGeoEnabled: false isStatusGeoEnabled: false
} }
} }

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

@ -17,31 +17,20 @@ const log = {
trace: sinon.spy() trace: sinon.spy()
} }
let nexmoStatus = '0' let snsResult = P.resolve({
const sendSms = sinon.spy((from, to, message, callback) => { MessageId: 'foo'
callback(null, {
message_count: '1',
messages: [
{
to: to.substr(1),
'message-id': 'foo',
status: nexmoStatus,
'error-text': 'bar',
'remaining-balance': '42',
'message-price': '1',
'network': 'baz'
}
]
})
}) })
function Nexmo () {} const publish = sinon.spy(params => ({
Nexmo.prototype.message = { sendSms } promise: () => snsResult
}))
function SNS () {}
SNS.prototype.publish = publish
let mockConstructed = false let mockConstructed = false
function MockNexmo () { function MockSNS () {
mockConstructed = true mockConstructed = true
} }
MockNexmo.prototype = Nexmo.prototype MockSNS.prototype = SNS.prototype
describe('lib/senders/sms:', () => { describe('lib/senders/sms:', () => {
let sms let sms
@ -52,7 +41,7 @@ describe('lib/senders/sms:', () => {
require(`${ROOT_DIR}/lib/senders/templates`)() require(`${ROOT_DIR}/lib/senders/templates`)()
]).spread((translator, templates) => { ]).spread((translator, templates) => {
sms = proxyquire(`${ROOT_DIR}/lib/senders/sms`, { sms = proxyquire(`${ROOT_DIR}/lib/senders/sms`, {
nexmo: Nexmo 'aws-sdk': { SNS }
})(log, translator, templates, { })(log, translator, templates, {
sms: { sms: {
apiKey: 'foo', apiKey: 'foo',
@ -66,7 +55,7 @@ describe('lib/senders/sms:', () => {
}) })
afterEach(() => { afterEach(() => {
sendSms.reset() publish.reset()
log.error.reset() log.error.reset()
log.info.reset() log.info.reset()
log.trace.reset() log.trace.reset()
@ -75,27 +64,39 @@ describe('lib/senders/sms:', () => {
it('interface is correct', () => { it('interface is correct', () => {
assert.equal(typeof sms.send, 'function', 'sms.send is function') assert.equal(typeof sms.send, 'function', 'sms.send is function')
assert.equal(sms.send.length, 5, 'sms.send expects 5 arguments') assert.equal(sms.send.length, 4, 'sms.send expects 4 arguments')
assert.equal(Object.keys(sms).length, 1, 'sms has no other methods') assert.equal(Object.keys(sms).length, 1, 'sms has no other methods')
}) })
it('sends a valid sms without a signinCode', () => { it('sends a valid sms without a signinCode', () => {
return sms.send('+442078553000', 'Firefox', 'installFirefox', 'en') return sms.send('+442078553000', 'installFirefox', 'en')
.then(() => { .then(() => {
assert.equal(sendSms.callCount, 1, 'nexmo.message.sendSms was called once') assert.equal(publish.callCount, 1, 'AWS.SNS.publish was called once')
const args = sendSms.args[0] assert.equal(publish.args[0].length, 1, 'AWS.SNS.publish was passed one argument')
assert.equal(args.length, 4, 'nexmo.message.sendSms was passed four arguments') assert.deepEqual(publish.args[0][0], {
assert.equal(args[0], 'Firefox', 'nexmo.message.sendSms was passed the correct sender id') Message: 'As requested, here is a link to install Firefox on your mobile device: https://baz/qux',
assert.equal(args[1], '+442078553000', 'nexmo.message.sendSms was passed the correct phone number') MessageAttributes: {
assert.equal(args[2], 'As requested, here is a link to install Firefox on your mobile device: https://baz/qux', 'nexmo.message.sendSms was passed the correct message') 'AWS.SNS.SMS.MaxPrice': {
assert.equal(typeof args[3], 'function', 'nexmo.message.sendSms was passed a callback function') DataType: 'String',
StringValue: '1.0'
},
'AWS.SNS.SMS.SenderID': {
DataType: 'String',
StringValue: 'Firefox'
},
'AWS.SNS.SMS.SMSType': {
DataType: 'String',
StringValue: 'Promotional'
}
},
PhoneNumber: '+442078553000'
}, 'AWS.SNS.publish was passed the correct argument')
assert.equal(log.trace.callCount, 1, 'log.trace was called once') assert.equal(log.trace.callCount, 1, 'log.trace was called once')
assert.equal(log.trace.args[0].length, 1, 'log.trace was passed one argument') assert.equal(log.trace.args[0].length, 1, 'log.trace was passed one argument')
assert.deepEqual(log.trace.args[0][0], { assert.deepEqual(log.trace.args[0][0], {
op: 'sms.send', op: 'sms.send',
senderId: 'Firefox',
templateName: 'installFirefox', templateName: 'installFirefox',
acceptLanguage: 'en' acceptLanguage: 'en'
}, 'log.trace was passed the correct data') }, 'log.trace was passed the correct data')
@ -104,9 +105,9 @@ describe('lib/senders/sms:', () => {
assert.equal(log.info.args[0].length, 1, 'log.info was passed one argument') assert.equal(log.info.args[0].length, 1, 'log.info was passed one argument')
assert.deepEqual(log.info.args[0][0], { assert.deepEqual(log.info.args[0][0], {
op: 'sms.send.success', op: 'sms.send.success',
senderId: 'Firefox',
templateName: 'installFirefox', templateName: 'installFirefox',
acceptLanguage: 'en' acceptLanguage: 'en',
messageId: 'foo'
}, 'log.info was passed the correct data') }, 'log.info was passed the correct data')
assert.equal(log.error.callCount, 0, 'log.error was not called') assert.equal(log.error.callCount, 0, 'log.error was not called')
@ -114,10 +115,10 @@ describe('lib/senders/sms:', () => {
}) })
it('sends a valid sms with a signinCode', () => { it('sends a valid sms with a signinCode', () => {
return sms.send('+442078553000', 'Firefox', 'installFirefox', 'en', Buffer.from('++//ff0=', 'base64')) return sms.send('+442078553000', 'installFirefox', 'en', Buffer.from('++//ff0=', 'base64'))
.then(() => { .then(() => {
assert.equal(sendSms.callCount, 1, 'nexmo.message.sendSms was called once') assert.equal(publish.callCount, 1, 'AWS.SNS.publish was called once')
assert.equal(sendSms.args[0][2], 'As requested, here is a link to install Firefox on your mobile device: https://wibble/--__ff0', 'nexmo.message.sendSms was passed the correct message') assert.equal(publish.args[0][0].Message, 'As requested, here is a link to install Firefox on your mobile device: https://wibble/--__ff0', 'AWS.SNS.publish was passed the correct message')
assert.equal(log.trace.callCount, 1, 'log.trace was called once') assert.equal(log.trace.callCount, 1, 'log.trace was called once')
assert.equal(log.info.callCount, 1, 'log.info was called once') assert.equal(log.info.callCount, 1, 'log.info was called once')
@ -127,7 +128,7 @@ describe('lib/senders/sms:', () => {
}) })
it('fails to send an sms with an invalid template name', () => { it('fails to send an sms with an invalid template name', () => {
return sms.send('+442078553000', 'Firefox', 'wibble', 'en', Buffer.from('++//ff0=', 'base64')) return sms.send('+442078553000', 'wibble', 'en', Buffer.from('++//ff0=', 'base64'))
.then(() => assert.fail('sms.send should have rejected')) .then(() => assert.fail('sms.send should have rejected'))
.catch(error => { .catch(error => {
assert.equal(error.errno, 131, 'error.errno was set correctly') assert.equal(error.errno, 131, 'error.errno was set correctly')
@ -142,55 +143,44 @@ describe('lib/senders/sms:', () => {
templateName: 'wibble' templateName: 'wibble'
}, 'log.error was passed the correct data') }, 'log.error was passed the correct data')
assert.equal(sendSms.callCount, 0, 'nexmo.message.sendSms was not called') assert.equal(publish.callCount, 0, 'AWS.SNS.publish was not called')
})
})
it('fails to send an sms that is throttled by the network provider', () => {
nexmoStatus = '1'
return sms.send('+442078553000', 'Firefox', 'installFirefox', 'en', Buffer.from('++//ff0=', 'base64'))
.then(() => assert.fail('sms.send should have rejected'))
.catch(error => {
assert.equal(error.errno, 114, 'error.errno was set correctly')
assert.equal(error.message, 'Client has sent too many requests', 'error.message was set correctly')
assert.equal(log.trace.callCount, 1, 'log.trace was called once')
assert.equal(log.info.callCount, 0, 'log.info was not called')
assert.equal(sendSms.callCount, 1, 'nexmo.message.sendSms was called once')
}) })
}) })
it('fails to send an sms that is rejected by the network provider', () => { it('fails to send an sms that is rejected by the network provider', () => {
nexmoStatus = '2' snsResult = P.reject({
return sms.send('+442078553000', 'Firefox', 'installFirefox', 'en', Buffer.from('++//ff0=', 'base64')) statusCode: 400,
code: 42,
message: 'this is an error'
})
return sms.send('+442078553000', 'installFirefox', 'en', Buffer.from('++//ff0=', 'base64'))
.then(() => assert.fail('sms.send should have rejected')) .then(() => assert.fail('sms.send should have rejected'))
.catch(error => { .catch(error => {
assert.equal(error.errno, 132, 'error.errno was set correctly') assert.equal(error.errno, 132, 'error.errno was set correctly')
assert.equal(error.message, 'Message rejected', 'error.message was set correctly') assert.equal(error.message, 'Message rejected', 'error.message was set correctly')
assert.equal(error.output.payload.reason, 'bar', 'error.reason was set correctly') assert.equal(error.output.payload.reason, 'this is an error', 'error.reason was set correctly')
assert.equal(error.output.payload.reasonCode, '2', 'error.reasonCode was set correctly') assert.equal(error.output.payload.reasonCode, 42, 'error.reasonCode was set correctly')
assert.equal(log.trace.callCount, 1, 'log.trace was called once') assert.equal(log.trace.callCount, 1, 'log.trace was called once')
assert.equal(log.info.callCount, 0, 'log.info was not called') assert.equal(log.info.callCount, 0, 'log.info was not called')
assert.equal(sendSms.callCount, 1, 'nexmo.message.sendSms was called once') assert.equal(publish.callCount, 1, 'AWS.SNS.publish was called once')
}) })
}) })
it('uses the Nexmo constructor if `useMock: false`', () => { it('uses the SNS constructor if `useMock: false`', () => {
assert.equal(mockConstructed, false) assert.equal(mockConstructed, false)
}) })
it('uses the NexmoMock constructor if `useMock: true`', () => { it('uses the MockSNS constructor if `useMock: true`', () => {
return P.all([ return P.all([
require(`${ROOT_DIR}/lib/senders/translator`)(['en'], 'en'), require(`${ROOT_DIR}/lib/senders/translator`)(['en'], 'en'),
require(`${ROOT_DIR}/lib/senders/templates`)() require(`${ROOT_DIR}/lib/senders/templates`)()
]).spread((translator, templates) => { ]).spread((translator, templates) => {
sms = proxyquire(`${ROOT_DIR}/lib/senders/sms`, { sms = proxyquire(`${ROOT_DIR}/lib/senders/sms`, {
nexmo: Nexmo, 'aws-sdk': { SNS },
'../mock-nexmo': MockNexmo '../../test/mock-sns': MockSNS
})(log, translator, templates, { })(log, translator, templates, {
sms: { sms: {
apiKey: 'foo', apiKey: 'foo',

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

@ -4,11 +4,11 @@
'use strict' 'use strict'
/** const P = require('../lib/promise')
* Mock out Nexmo for functional tests. `sendSms` always succeeds.
*/
function MockNexmo(log, config) { module.exports = MockSNS
function MockSNS (options, config) {
const mailerOptions = { const mailerOptions = {
host: config.smtp.host, host: config.smtp.host,
secure: config.smtp.secure, secure: config.smtp.secure,
@ -24,33 +24,24 @@ function MockNexmo(log, config) {
const mailer = require('nodemailer').createTransport(mailerOptions) const mailer = require('nodemailer').createTransport(mailerOptions)
return { return {
message: { publish (params) {
/** const promise = new P(resolve => {
* Drop message on the ground, call callback with `0` (send-OK) status.
*/
sendSms: function sendSms (senderId, phoneNumber, message, options, callback) {
// this is the same as how the Nexmo version works.
if (! callback) {
callback = options
options = {}
}
log.info({ op: 'sms.send.mock' })
// HACK: Enable remote tests to see what was sent // HACK: Enable remote tests to see what was sent
mailer.sendMail({ mailer.sendMail({
from: config.smtp.sender, from: config.smtp.sender,
to: `sms.${phoneNumber}@restmail.net`, to: `sms.${params.PhoneNumber}@restmail.net`,
subject: 'MockNexmo.message.sendSms', subject: 'MockSNS.publish',
text: message text: params.Message
}, () => { }, () => {
callback(null, { resolve({
messages: [{ status: '0' }] MessageId: 'fake message id'
}) })
}) })
})
return {
promise: () => promise
} }
} }
} }
} }
module.exports = MockNexmo