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