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,
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: {
doc: 'Indicates whether the status endpoint should do geo-ip lookup',
default: true,
format: Boolean,
env: 'SMS_STATUS_GEO_ENABLED'
},
senderIds: {
doc: 'Sender ids keyed by the ISO 3166-1 alpha-2 country code (region) they apply to',
default: {
CA: '16474909977',
GB: 'Firefox',
US: '15036789977'
apiRegion: {
doc: 'AWS region',
default: 'us-east-1',
format: String,
env: 'SMS_API_REGION'
},
format: Object,
env: 'SMS_SENDER_IDS'
countryCodes: {
doc: 'Allow sending SMS to these ISO 3166-1 alpha-2 country codes',
default: ['CA', 'GB', 'US'],
format: Array,
env: 'SMS_COUNTRY_CODES'
},
installFirefoxLink: {
doc: 'Link for the installFirefox SMS template',

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

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

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

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

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

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

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

@ -33,7 +33,7 @@
"readmeFilename": "README.md",
"dependencies": {
"ajv": "4.1.7",
"aws-sdk": "2.2.10",
"aws-sdk": "2.77.0",
"base64url": "1.0.6",
"binary-split": "0.1.2",
"bluebird": "3.4.7",
@ -76,7 +76,7 @@
"acorn": "5.0.3",
"commander": "2.9.0",
"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",
"grunt": "1.0.1",
"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'
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 log = require('../../lib/log')(config.log.level, 'send-sms')
@ -42,28 +37,25 @@ function fail (message) {
}
function parseArgs () {
let acceptLanguage, messageId, senderId, phoneNumber
let acceptLanguage, messageName, phoneNumber
switch (process.argv.length) {
/* eslint-disable indent, no-fallthrough */
case 6:
acceptLanguage = process.argv[5]
case 5:
messageId = process.argv[4]
acceptLanguage = process.argv[5]
case 4:
senderId = process.argv[3]
messageName = process.argv[4]
case 3:
phoneNumber = process.argv[2]
break
default:
fail(`Usage: ${process.argv[1]} phoneNumber [senderId] [messageId] [acceptLanguage]`)
fail(`Usage: ${process.argv[1]} phoneNumber [messageName] [acceptLanguage]`)
/* eslint-enable indent, no-fallthrough */
}
return [
phoneNumber,
senderId || 'Firefox',
messageId || 1,
messageName || 'installFirefox',
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 = {
sms: {
enabled: true,
senderIds: {
CA: '16474909977',
GB: 'Firefox',
US: '15036789977'
},
countryCodes: [ 'CA', 'GB', 'US' ],
isStatusGeoEnabled: true
}
}
@ -107,12 +103,11 @@ describe('/sms with the signinCodes feature included in the payload', () => {
it('called sms.send correctly', () => {
assert.equal(sms.send.callCount, 1)
const args = sms.send.args[0]
assert.equal(args.length, 5)
assert.equal(args.length, 4)
assert.equal(args[0], '+18885083401')
assert.equal(args[1], '15036789977')
assert.equal(args[2], 'installFirefox')
assert.equal(args[3], 'en-US')
assert.equal(args[4], signinCode)
assert.equal(args[1], 'installFirefox')
assert.equal(args[2], 'en-US')
assert.equal(args[3], signinCode)
})
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)
const args = sms.send.args[0]
assert.equal(args[0], '+14168483114')
assert.equal(args[1], '16474909977')
})
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)
const args = sms.send.args[0]
assert.equal(args[0], '+442078553000')
assert.equal(args[1], 'Firefox')
})
it('called log.flowEvent correctly', () => {
@ -323,11 +316,7 @@ describe('/sms without the signinCodes feature included in the payload', () => {
config = {
sms: {
enabled: true,
senderIds: {
CA: '16474909977',
GB: 'Firefox',
US: '15036789977'
},
countryCodes: [ 'CA', 'GB', 'US' ],
isStatusGeoEnabled: true
}
}
@ -402,7 +391,7 @@ describe('/sms/status', () => {
config = {
sms: {
enabled: true,
senderIds: { 'US': '18005551212' },
countryCodes: [ 'US' ],
isStatusGeoEnabled: true
}
}
@ -550,7 +539,7 @@ describe('/sms/status with disabled geo-ip lookup', () => {
config = {
sms: {
enabled: true,
senderIds: { 'US': '18005551212' },
countryCodes: [ 'US' ],
isStatusGeoEnabled: false
}
}
@ -603,7 +592,7 @@ describe('/sms/status with query param and enabled geo-ip lookup', () => {
config = {
sms: {
enabled: true,
senderIds: { 'RO': '0215555111' },
countryCodes: [ 'RO' ],
isStatusGeoEnabled: true
}
}
@ -648,7 +637,7 @@ describe('/sms/status with query param and disabled geo-ip lookup', () => {
config = {
sms: {
enabled: true,
senderIds: { 'GB': '03456000000' },
countryCodes: [ 'GB' ],
isStatusGeoEnabled: false
}
}

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

@ -17,31 +17,20 @@ const log = {
trace: sinon.spy()
}
let nexmoStatus = '0'
const sendSms = sinon.spy((from, to, message, callback) => {
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'
}
]
let snsResult = P.resolve({
MessageId: 'foo'
})
})
function Nexmo () {}
Nexmo.prototype.message = { sendSms }
const publish = sinon.spy(params => ({
promise: () => snsResult
}))
function SNS () {}
SNS.prototype.publish = publish
let mockConstructed = false
function MockNexmo () {
function MockSNS () {
mockConstructed = true
}
MockNexmo.prototype = Nexmo.prototype
MockSNS.prototype = SNS.prototype
describe('lib/senders/sms:', () => {
let sms
@ -52,7 +41,7 @@ describe('lib/senders/sms:', () => {
require(`${ROOT_DIR}/lib/senders/templates`)()
]).spread((translator, templates) => {
sms = proxyquire(`${ROOT_DIR}/lib/senders/sms`, {
nexmo: Nexmo
'aws-sdk': { SNS }
})(log, translator, templates, {
sms: {
apiKey: 'foo',
@ -66,7 +55,7 @@ describe('lib/senders/sms:', () => {
})
afterEach(() => {
sendSms.reset()
publish.reset()
log.error.reset()
log.info.reset()
log.trace.reset()
@ -75,27 +64,39 @@ describe('lib/senders/sms:', () => {
it('interface is correct', () => {
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')
})
it('sends a valid sms without a signinCode', () => {
return sms.send('+442078553000', 'Firefox', 'installFirefox', 'en')
return sms.send('+442078553000', 'installFirefox', 'en')
.then(() => {
assert.equal(sendSms.callCount, 1, 'nexmo.message.sendSms was called once')
const args = sendSms.args[0]
assert.equal(args.length, 4, 'nexmo.message.sendSms was passed four arguments')
assert.equal(args[0], 'Firefox', 'nexmo.message.sendSms was passed the correct sender id')
assert.equal(args[1], '+442078553000', 'nexmo.message.sendSms was passed the correct phone number')
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')
assert.equal(typeof args[3], 'function', 'nexmo.message.sendSms was passed a callback function')
assert.equal(publish.callCount, 1, 'AWS.SNS.publish was called once')
assert.equal(publish.args[0].length, 1, 'AWS.SNS.publish was passed one argument')
assert.deepEqual(publish.args[0][0], {
Message: 'As requested, here is a link to install Firefox on your mobile device: https://baz/qux',
MessageAttributes: {
'AWS.SNS.SMS.MaxPrice': {
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.args[0].length, 1, 'log.trace was passed one argument')
assert.deepEqual(log.trace.args[0][0], {
op: 'sms.send',
senderId: 'Firefox',
templateName: 'installFirefox',
acceptLanguage: 'en'
}, '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.deepEqual(log.info.args[0][0], {
op: 'sms.send.success',
senderId: 'Firefox',
templateName: 'installFirefox',
acceptLanguage: 'en'
acceptLanguage: 'en',
messageId: 'foo'
}, 'log.info was passed the correct data')
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', () => {
return sms.send('+442078553000', 'Firefox', 'installFirefox', 'en', Buffer.from('++//ff0=', 'base64'))
return sms.send('+442078553000', 'installFirefox', 'en', Buffer.from('++//ff0=', 'base64'))
.then(() => {
assert.equal(sendSms.callCount, 1, 'nexmo.message.sendSms 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.callCount, 1, 'AWS.SNS.publish was called once')
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.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', () => {
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'))
.catch(error => {
assert.equal(error.errno, 131, 'error.errno was set correctly')
@ -142,55 +143,44 @@ describe('lib/senders/sms:', () => {
templateName: 'wibble'
}, 'log.error was passed the correct data')
assert.equal(sendSms.callCount, 0, 'nexmo.message.sendSms 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')
assert.equal(publish.callCount, 0, 'AWS.SNS.publish was not called')
})
})
it('fails to send an sms that is rejected by the network provider', () => {
nexmoStatus = '2'
return sms.send('+442078553000', 'Firefox', 'installFirefox', 'en', Buffer.from('++//ff0=', 'base64'))
snsResult = P.reject({
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'))
.catch(error => {
assert.equal(error.errno, 132, 'error.errno 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.reasonCode, '2', 'error.reasonCode was set correctly')
assert.equal(error.output.payload.reason, 'this is an error', 'error.reason 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.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)
})
it('uses the NexmoMock constructor if `useMock: true`', () => {
it('uses the MockSNS constructor if `useMock: true`', () => {
return P.all([
require(`${ROOT_DIR}/lib/senders/translator`)(['en'], 'en'),
require(`${ROOT_DIR}/lib/senders/templates`)()
]).spread((translator, templates) => {
sms = proxyquire(`${ROOT_DIR}/lib/senders/sms`, {
nexmo: Nexmo,
'../mock-nexmo': MockNexmo
'aws-sdk': { SNS },
'../../test/mock-sns': MockSNS
})(log, translator, templates, {
sms: {
apiKey: 'foo',

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

@ -4,11 +4,11 @@
'use strict'
/**
* Mock out Nexmo for functional tests. `sendSms` always succeeds.
*/
const P = require('../lib/promise')
function MockNexmo(log, config) {
module.exports = MockSNS
function MockSNS (options, config) {
const mailerOptions = {
host: config.smtp.host,
secure: config.smtp.secure,
@ -24,33 +24,24 @@ function MockNexmo(log, config) {
const mailer = require('nodemailer').createTransport(mailerOptions)
return {
message: {
/**
* 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' })
publish (params) {
const promise = new P(resolve => {
// HACK: Enable remote tests to see what was sent
mailer.sendMail({
from: config.smtp.sender,
to: `sms.${phoneNumber}@restmail.net`,
subject: 'MockNexmo.message.sendSms',
text: message
to: `sms.${params.PhoneNumber}@restmail.net`,
subject: 'MockSNS.publish',
text: params.Message
}, () => {
callback(null, {
messages: [{ status: '0' }]
resolve({
MessageId: 'fake message id'
})
})
})
return {
promise: () => promise
}
}
}
}
module.exports = MockNexmo