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:
Родитель
a358d7c7b8
Коммит
7ce5c05250
|
@ -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) {
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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
|
Загрузка…
Ссылка в новой задаче