feat(server): lazily get devices array on the request object (#2107) r=vladikoff,vbudhram

Fixes #2106.

Prevents us from accidentally calling db.devices more than once per request. I saw one definite case of this in /recovery_email/verify_code and it's possible there were others. I'll also be making use of this property heavily for the amplitude events, so it will get further usage imminently.

Making the change necessitated pulling calls to db.devices out of lib/push, which triggered some refactoring that almost got away from me. I'll add inline commentary to call out why things have changed the way they have, but most push methods now take an extra devices argument and a few other methods became redundant so I deleted them. I don't think I've broken anything.
This commit is contained in:
Phil Booth 2017-09-12 20:17:08 +01:00 коммит произвёл Vlad Filippov
Родитель dae0e58340
Коммит f084830bcf
19 изменённых файлов: 440 добавлений и 539 удалений

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

@ -28,6 +28,6 @@ var profileUpdatesQueue = new SQSReceiver(config.profileServerMessaging.region,
DB.connect(config[config.db.backend])
.then(
function (db) {
profileUpdates(profileUpdatesQueue, push(log, db, config))
profileUpdates(profileUpdatesQueue, push(log, db, config), db)
}
)

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

@ -57,7 +57,9 @@ module.exports = (log, db, push) => {
deviceName = synthesizeName(deviceInfo)
}
if (sessionToken.tokenVerified) {
push.notifyDeviceConnected(sessionToken.uid, deviceName, result.id)
request.app.devices.then(devices =>
push.notifyDeviceConnected(sessionToken.uid, devices, deviceName, result.id)
)
}
if (isPlaceholderDevice) {
log.info({

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

@ -2,27 +2,25 @@
* 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/. */
const P = require('./../promise')
'use strict'
module.exports = function (log) {
return function start(messageQueue, push) {
return function start(messageQueue, push, db) {
function handleProfileUpdated(message) {
const uid = message && message.uid
return new P(resolve => {
log.info({ op: 'handleProfileUpdated', uid: uid, action: 'notify' })
resolve(push.notifyProfileUpdated(message.uid))
})
.catch(function(err) {
log.error({ op: 'handleProfileUpdated', uid: uid, action: 'error', err: err, stack: err && err.stack })
})
.then(function () {
log.info({ op: 'handleProfileUpdated', uid: uid, action: 'delete' })
// We always delete the message, we are not really mission critical
message.del()
})
log.info({ op: 'handleProfileUpdated', uid, action: 'notify' })
return db.devices(uid)
.then(devices => push.notifyProfileUpdated(uid, devices))
.catch(err => log.error({ op: 'handleProfileUpdated', uid, action: 'error', err, stack: err && err.stack }))
.then(() => {
log.info({ op: 'handleProfileUpdated', uid, action: 'delete' })
// We always delete the message, we are not really mission critical
message.del()
})
}
messageQueue.on('data', handleProfileUpdated)

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

@ -11,7 +11,7 @@ var ERR_NO_PUSH_CALLBACK = 'No Push Callback'
var ERR_DATA_BUT_NO_KEYS = 'Data payload present but missing key(s)'
var ERR_TOO_MANY_DEVICES = 'Too many devices connected to account'
var LOG_OP_PUSH_TO_DEVICES = 'push.pushToDevices'
var LOG_OP_PUSH_TO_DEVICES = 'push.sendPush'
var PUSH_PAYLOAD_SCHEMA_VERSION = 1
var PUSH_COMMANDS = {
@ -253,190 +253,148 @@ module.exports = function (log, db, config) {
return {
isValidPublicKey: isValidPublicKey,
isValidPublicKey,
/**
* Notifies all devices that there was an update to the account
* Notify devices that there was an update to the account
*
* @param {String} uid
* @param {Device[]} devices
* @param {String} reason
* @param {Object} [options]
* @param {String[]} [options.includedDeviceIds]
* @param {String[]} [options.excludedDeviceIds]
* @param {String} [options.data]
* @param {String} [options.TTL] (in seconds)
* @promise
*/
notifyUpdate: function notifyUpdate(uid, reason) {
reason = reason || 'accountVerify'
return this.pushToAllDevices(uid, reason)
notifyUpdate (uid, devices, reason, options = {}) {
if (options.includedDeviceIds) {
const include = new Set(options.includedDeviceIds)
devices = devices.filter(device => include.has(device.id))
if (devices.length === 0) {
return P.reject('devices empty')
}
} else if (options.excludedDeviceIds) {
const exclude = new Set(options.excludedDeviceIds)
devices = devices.filter(device => ! exclude.has(device.id))
}
return this.sendPush(uid, devices, reason, filterOptions(options))
},
/**
* Notifies all devices (except the one who joined) that a new device joined the account
* Notify devices (except currentDeviceId) that a new device was connected
*
* @param {String} uid
* @param {Device[]} devices
* @param {String} deviceName
* @param {String} currentDeviceId
* @promise
*/
notifyDeviceConnected: function notifyDeviceConnected(uid, deviceName, currentDeviceId) {
var data = Buffer.from(JSON.stringify({
version: PUSH_PAYLOAD_SCHEMA_VERSION,
command: PUSH_COMMANDS.DEVICE_CONNECTED,
data: {
deviceName: deviceName
}
}))
var options = { data: data, excludedDeviceIds: [currentDeviceId] }
return this.pushToAllDevices(uid, 'deviceConnected', options)
notifyDeviceConnected (uid, devices, deviceName, currentDeviceId) {
return this.notifyUpdate(uid, devices, 'deviceConnected', {
data: encodePayload({
version: PUSH_PAYLOAD_SCHEMA_VERSION,
command: PUSH_COMMANDS.DEVICE_CONNECTED,
data: {
deviceName
}
}),
excludedDeviceIds: [ currentDeviceId ]
})
},
/**
* Notifies a device that it is now disconnected from the account
* Notify devices that a device was disconnected from the account
*
* @param {String} uid
* @param {Device[]} devices
* @param {String} idToDisconnect
* @promise
*/
notifyDeviceDisconnected: function notifyDeviceDisconnected(uid, idToDisconnect) {
var data = Buffer.from(JSON.stringify({
version: PUSH_PAYLOAD_SCHEMA_VERSION,
command: PUSH_COMMANDS.DEVICE_DISCONNECTED,
data: {
id: idToDisconnect
}
}))
var options = { data: data, TTL: TTL_DEVICE_DISCONNECTED }
return this.pushToAllDevices(uid, 'deviceDisconnected', options)
},
/**
* Notifies all devices that a the profile attached to the account was updated
*
* @param {String} uid
* @promise
*/
notifyProfileUpdated: function notifyProfileUpdated(uid) {
var data = Buffer.from(JSON.stringify({
version: PUSH_PAYLOAD_SCHEMA_VERSION,
command: PUSH_COMMANDS.PROFILE_UPDATED
}))
var options = { data: data }
return this.pushToAllDevices(uid, 'profileUpdated', options)
},
/**
* Notifies a set of devices that the password was changed
*
* @param {String} uid
* @param {Device[]} devices
* @promise
*/
notifyPasswordChanged: function notifyPasswordChanged(uid, devices) {
var data = Buffer.from(JSON.stringify({
version: PUSH_PAYLOAD_SCHEMA_VERSION,
command: PUSH_COMMANDS.PASSWORD_CHANGED
}))
var options = { data: data, TTL: TTL_PASSWORD_CHANGED }
return this.sendPush(uid, devices, 'passwordChange', options)
},
/**
* Notifies a set of devices that the password was reset
*
* @param {String} uid
* @param {Device[]} devices
* @promise
*/
notifyPasswordReset: function notifyPasswordReset(uid, devices) {
var data = Buffer.from(JSON.stringify({
version: PUSH_PAYLOAD_SCHEMA_VERSION,
command: PUSH_COMMANDS.PASSWORD_RESET
}))
var options = { data: data, TTL: TTL_PASSWORD_RESET }
return this.sendPush(uid, devices, 'passwordReset', options)
},
/**
* Notifies a set of devices that the account no longer exists
*
* @param {String} uid
* @param {Device[]} devices
* @promise
*/
notifyAccountDestroyed: function notifyAccountDestroyed(uid, devices) {
var data = Buffer.from(JSON.stringify({
version: PUSH_PAYLOAD_SCHEMA_VERSION,
command: PUSH_COMMANDS.ACCOUNT_DESTROYED,
data: {
uid: uid
}
}))
var options = { data: data, TTL: TTL_ACCOUNT_DESTROYED }
return this.sendPush(uid, devices, 'accountDestroyed', options)
},
/**
* Send a push notification with or without data to all the devices in the account (except the ones in the excludedDeviceIds)
*
* @param {String} uid
* @param {String} reason
* @param {Object} options
* @param {String} options.excludedDeviceIds
* @param {String} options.data
* @param {String} options.TTL (in seconds)
* @promise
*/
pushToAllDevices: function pushToAllDevices(uid, reason, options) {
options = options || {}
var self = this
return db.devices(uid).then(
function (devices) {
if (options.excludedDeviceIds) {
devices = devices.filter(function(device) {
return options.excludedDeviceIds.indexOf(device.id) === -1
})
notifyDeviceDisconnected (uid, devices, idToDisconnect) {
return this.sendPush(uid, devices, 'deviceDisconnected', {
data: encodePayload({
version: PUSH_PAYLOAD_SCHEMA_VERSION,
command: PUSH_COMMANDS.DEVICE_DISCONNECTED,
data: {
id: idToDisconnect
}
var pushOptions = filterOptions(options)
return self.sendPush(uid, devices, reason, pushOptions)
})
}),
TTL: TTL_DEVICE_DISCONNECTED
})
},
/**
* Send a push notification with or without data to a set of devices in the account
* Notify devices that the profile attached to the account was updated
*
* @param {String} uid
* @param {String[]} ids
* @param {String} reason
* @param {Object} options
* @param {String} options.data
* @param {String} options.TTL (in seconds)
* @param {Device[]} devices
* @promise
*/
pushToDevices: function pushToDevices(uid, ids, reason, options) {
var self = this
return db.devices(uid).then(
function (devices) {
devices = devices.filter(function(device) {
return ids.indexOf(device.id) !== -1
})
if (devices.length === 0) {
return P.reject('Devices ids not found in devices')
notifyProfileUpdated (uid, devices) {
return this.sendPush(uid, devices, 'profileUpdated', {
data: encodePayload({
version: PUSH_PAYLOAD_SCHEMA_VERSION,
command: PUSH_COMMANDS.PROFILE_UPDATED
})
})
},
/**
* Notify devices that the password was changed
*
* @param {String} uid
* @param {Device[]} devices
* @promise
*/
notifyPasswordChanged (uid, devices) {
return this.sendPush(uid, devices, 'passwordChange', {
data: encodePayload({
version: PUSH_PAYLOAD_SCHEMA_VERSION,
command: PUSH_COMMANDS.PASSWORD_CHANGED
}),
TTL: TTL_PASSWORD_CHANGED
})
},
/**
* Notify devices that the password was reset
*
* @param {String} uid
* @param {Device[]} devices
* @promise
*/
notifyPasswordReset (uid, devices) {
return this.sendPush(uid, devices, 'passwordReset', {
data: encodePayload({
version: PUSH_PAYLOAD_SCHEMA_VERSION,
command: PUSH_COMMANDS.PASSWORD_RESET
}),
TTL: TTL_PASSWORD_RESET
})
},
/**
* Notify devices that the account no longer exists
*
* @param {String} uid
* @param {Device[]} devices
* @promise
*/
notifyAccountDestroyed (uid, devices) {
return this.sendPush(uid, devices, 'accountDestroyed', {
data: encodePayload({
version: PUSH_PAYLOAD_SCHEMA_VERSION,
command: PUSH_COMMANDS.ACCOUNT_DESTROYED,
data: {
uid
}
var pushOptions = filterOptions(options || {})
return self.sendPush(uid, devices, reason, pushOptions)
})
},
/**
* Send a push notification with or without data to one device in the account
*
* @param {String} uid
* @param {String} id
* @param {String} reason
* @param {Object} options
* @param {String} options.data
* @param {String} options.TTL (in seconds)
* @promise
*/
pushToDevice: function pushToDevice(uid, id, reason, options) {
return this.pushToDevices(uid, [id], reason, options)
}),
TTL: TTL_ACCOUNT_DESTROYED
})
},
/**
@ -541,3 +499,8 @@ module.exports = function (log, db, config) {
}
}
}
function encodePayload (data) {
return Buffer.from(JSON.stringify(data))
}

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

@ -1343,10 +1343,10 @@ module.exports = (log, db, mailer, Password, config, customs, checkPassword, pus
},
handler: function accountReset(request, reply) {
log.begin('Account.reset', request)
var accountResetToken = request.auth.credentials
var authPW = request.payload.authPW
var account, sessionToken, keyFetchToken, verifyHash, wrapKb, devicesToNotify
var hasSessionToken = request.payload.sessionToken
const accountResetToken = request.auth.credentials
const authPW = request.payload.authPW
const hasSessionToken = request.payload.sessionToken
let account, sessionToken, keyFetchToken, verifyHash, wrapKb
request.validateMetricsContext()
@ -1358,25 +1358,13 @@ module.exports = (log, db, mailer, Password, config, customs, checkPassword, pus
}
request.setMetricsFlowCompleteSignal(flowCompleteSignal)
return fetchDevicesToNotify()
.then(resetAccountData)
return resetAccountData()
.then(createSessionToken)
.then(createKeyFetchToken)
.then(recordSecurityEvent)
.then(createResponse)
.then(reply, reply)
function fetchDevicesToNotify() {
// We fetch the devices to notify before resetAccountData() because
// db.resetAccount() deletes all the devices saved in the account.
return db.devices(accountResetToken.uid)
.then(
function(devices) {
devicesToNotify = devices
}
)
}
function resetAccountData () {
let authSalt, password, wrapWrapKb
return random.hex(32, 32)
@ -1411,7 +1399,9 @@ module.exports = (log, db, mailer, Password, config, customs, checkPassword, pus
.then(
function () {
// Notify the devices that the account has changed.
push.notifyPasswordReset(accountResetToken.uid, devicesToNotify)
request.app.devices.then(devices =>
push.notifyPasswordReset(accountResetToken.uid, devices)
)
return db.account(accountResetToken.uid)
.then((accountData) => {
@ -1578,7 +1568,8 @@ module.exports = (log, db, mailer, Password, config, customs, checkPassword, pus
)
.then(
function () {
push.notifyAccountDestroyed(uid, devicesToNotify).catch(function () {})
push.notifyAccountDestroyed(uid, devicesToNotify)
.catch(() => {})
return P.all([
log.notifyAttachedServices('delete', request, {
uid: uid,

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

@ -189,6 +189,10 @@ module.exports = (log, db, config, customs, push, devices) => {
data: Buffer.from(JSON.stringify(payload))
}
if (body.to !== 'all') {
pushOptions.includedDeviceIds = body.to
}
if (body.excluded) {
pushOptions.excludedDeviceIds = body.excluded
}
@ -200,15 +204,11 @@ module.exports = (log, db, config, customs, push, devices) => {
const endpointAction = 'devicesNotify'
return customs.checkAuthenticated(endpointAction, ip, uid)
.then(() => {
if (body.to === 'all') {
push.pushToAllDevices(uid, endpointAction, pushOptions)
.catch(catchPushError)
} else {
push.pushToDevices(uid, body.to, endpointAction, pushOptions)
.catch(catchPushError)
}
})
.then(() => request.app.devices)
.then(devices =>
push.notifyUpdate(uid, devices, endpointAction, pushOptions)
.catch(catchPushError)
)
.then(() => {
// Emit a metrics event for when a user sends tabs between devices.
// In the future we will aim to get this event directly from sync telemetry,
@ -420,8 +420,10 @@ module.exports = (log, db, config, customs, push, devices) => {
.then(res => {
result = res
})
.then(() => push.notifyDeviceDisconnected(uid, id)
.catch(() => {})
.then(() => request.app.devices)
.then(devices =>
push.notifyDeviceDisconnected(uid, devices, id)
.catch(() => {})
)
.then(() => {
return P.all([

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

@ -413,7 +413,9 @@ module.exports = (log, db, mailer, config, customs, push) => {
request.emitMetricsEvent('account.confirmed', {
uid: uid
})
push.notifyUpdate(uid, 'accountConfirm')
request.app.devices.then(devices =>
push.notifyUpdate(uid, devices, 'accountConfirm')
)
}
})
.catch(err => {
@ -432,7 +434,9 @@ module.exports = (log, db, mailer, config, customs, push) => {
})
.then(() => {
if (device) {
push.notifyDeviceConnected(uid, device.name, device.id)
request.app.devices.then(devices =>
push.notifyDeviceConnected(uid, devices, device.name, device.id)
)
}
})
.then(() => {
@ -471,7 +475,9 @@ module.exports = (log, db, mailer, config, customs, push) => {
})
.then(() => {
// send a push notification to all devices that the account changed
push.notifyUpdate(uid, 'accountVerify')
request.app.devices.then(devices =>
push.notifyUpdate(uid, devices, 'accountVerify')
)
// remove verification reminders
verificationReminder.delete({
@ -791,7 +797,7 @@ module.exports = (log, db, mailer, config, customs, push) => {
return db.setPrimaryEmail(uid, email.normalizedEmail)
})
.then(() => {
push.notifyProfileUpdated(uid)
request.app.devices.then(devices => push.notifyProfileUpdated(uid, devices))
log.notifyAttachedServices('primaryEmailChanged', request, {
uid: account.uid,
email: email

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

@ -184,18 +184,15 @@ module.exports = function (
function fetchDevicesToNotify() {
// We fetch the devices to notify before changePassword() because
// db.resetAccount() deletes all the devices saved in the account.
return db.devices(passwordChangeToken.uid)
.then(
function(devices) {
devicesToNotify = devices
// If the originating sessionToken belongs to a device,
// do not send the notification to that device. It will
// get informed about the change via WebChannel message.
if (originatingDeviceId) {
devicesToNotify = devicesToNotify.filter(d => (d.id !== originatingDeviceId))
}
}
)
return request.app.devices.then(devices => {
devicesToNotify = devices
// If the originating sessionToken belongs to a device,
// do not send the notification to that device. It will
// get informed about the change via WebChannel message.
if (originatingDeviceId) {
devicesToNotify = devicesToNotify.filter(d => (d.id !== originatingDeviceId))
}
})
}
function changePassword() {

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

@ -3,12 +3,12 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const fs = require('fs')
const Hapi = require('hapi')
const path = require('path')
const url = require('url')
const Hapi = require('hapi')
const userAgent = require('./userAgent')
const HEX_STRING = require('./routes/validators').HEX_STRING
const { HEX_STRING } = require('./routes/validators')
function trimLocale(header) {
if (! header) {
@ -275,6 +275,17 @@ function create(log, error, config, routes, db, translator) {
defineLazyGetter(request.app, 'ua', () => userAgent(request.headers['user-agent']))
defineLazyGetter(request.app, 'geo', () => getGeoData(request.app.clientAddress))
defineLazyGetter(request.app, 'devices', () => {
let uid
if (request.auth && request.auth.credentials) {
uid = request.auth.credentials.uid
} else if (request.payload && request.payload.uid) {
uid = request.payload.uid
}
return db.devices(uid)
})
if (request.headers.authorization) {
// Log some helpful details for debugging authentication problems.

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

@ -5,18 +5,16 @@
'use strict'
const assert = require('insist')
var proxyquire = require('proxyquire')
const config = require('../../config').getProperties()
const proxyquire = require('proxyquire')
const { mockDB, spyLog } = require('../mocks')
const { PushManager } = require('../push_helper')
var P = require('../../lib/promise')
var config = require('../../config').getProperties()
var spyLog = require('../mocks').spyLog
var mockUid = Buffer.from('foo')
var PushManager = require('../push_helper').PushManager
const mockUid = 'foo'
describe('e2e/push', () => {
let pushManager
before(() => {
pushManager = new PushManager({
server: 'wss://push.services.mozilla.com/',
@ -24,31 +22,9 @@ describe('e2e/push', () => {
})
})
it(
'pushToAllDevices sends notifications using a real push server',
() => {
return pushManager.getSubscription().then(function (subscription) { // eslint-disable-line no-unreachable
var mockDbResult = {
devices: function (/* uid */) {
return P.resolve([
{
'id': '0f7aa00356e5416e82b3bef7bc409eef',
'isCurrentDevice': true,
'lastAccessTime': 1449235471335,
'name': 'My Phone',
'type': 'mobile',
'pushCallback': subscription.endpoint,
'pushPublicKey': 'BBXOKjUb84pzws1wionFpfCBjDuCh4-s_1b52WA46K5wYL2gCWEOmFKWn_NkS5nmJwTBuO8qxxdjAIDtNeklvQc',
'pushAuthKey': 'GSsIiaD2Mr83iPqwFNK4rw',
'pushEndpointExpired': false
}
])
},
updateDevice: function () {
return P.resolve()
}
}
it('notifyUpdate sends notifications using a real push server', () => {
return pushManager.getSubscription()
.then(subscription => {
let count = 0
var thisSpyLog = spyLog({
info: function (log) {
@ -58,15 +34,27 @@ describe('e2e/push', () => {
}
})
var push = proxyquire('../../lib/push', {})(thisSpyLog, mockDbResult, config)
var push = proxyquire('../../lib/push', {})(thisSpyLog, mockDB(), config)
var options = {
data: Buffer.from('foodata')
}
return push.pushToAllDevices(mockUid, 'accountVerify', options).then(function() {
assert.equal(thisSpyLog.error.callCount, 0, 'No errors should have been logged')
assert.equal(count, 1)
})
return push.notifyUpdate(mockUid, [
{
id: '0f7aa00356e5416e82b3bef7bc409eef',
isCurrentDevice: true,
lastAccessTime: 1449235471335,
name: 'My Phone',
type: 'mobile',
pushCallback: subscription.endpoint,
pushPublicKey: 'BBXOKjUb84pzws1wionFpfCBjDuCh4-s_1b52WA46K5wYL2gCWEOmFKWn_NkS5nmJwTBuO8qxxdjAIDtNeklvQc',
pushAuthKey: 'GSsIiaD2Mr83iPqwFNK4rw',
pushEndpointExpired: false
}
], 'accountVerify', options)
.then(() => {
assert.equal(thisSpyLog.error.callCount, 0, 'No errors should have been logged')
assert.equal(count, 1, 'log.info::push.account_verify.success was called once')
})
})
}
)
})
})

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

@ -116,10 +116,11 @@ describe('devices', () => {
assert.equal(push.notifyDeviceConnected.callCount, 1, 'push.notifyDeviceConnected was called once')
args = push.notifyDeviceConnected.args[0]
assert.equal(args.length, 3, 'push.notifyDeviceConnected was passed three arguments')
assert.equal(args.length, 4, 'push.notifyDeviceConnected was passed four arguments')
assert.equal(args[0], sessionToken.uid, 'first argument was uid')
assert.equal(args[1], device.name, 'second arguent was device name')
assert.equal(args[2], deviceId, 'third argument was device id')
assert.ok(Array.isArray(args[1]), 'second argument was devices array')
assert.equal(args[2], device.name, 'third arguent was device name')
assert.equal(args[3], deviceId, 'fourth argument was device id')
})
})
@ -156,7 +157,7 @@ describe('devices', () => {
assert.equal(push.notifyDeviceConnected.callCount, 1, 'push.notifyDeviceConnected was called once')
assert.equal(push.notifyDeviceConnected.args[0][0], sessionToken.uid, 'uid was correct')
assert.equal(push.notifyDeviceConnected.args[0][1], 'Firefox', 'device name was included')
assert.equal(push.notifyDeviceConnected.args[0][2], 'Firefox', 'device name was included')
})
})

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

@ -6,7 +6,7 @@ const assert = require('insist')
const EventEmitter = require('events').EventEmitter
const sinon = require('sinon')
const spyLog = require('../../mocks').spyLog
const { mockDB, spyLog } = require('../../mocks')
const profileUpdates = require('../../../lib/profile/updates')
const P = require('../../../lib/promise')
@ -30,7 +30,7 @@ const mockPush = {
}
function mockProfileUpdates(log) {
return profileUpdates(log)(mockDeliveryQueue, mockPush)
return profileUpdates(log)(mockDeliveryQueue, mockPush, mockDB())
}
describe('profile updates', () => {

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

@ -7,128 +7,124 @@
const ROOT_DIR = '../..'
const assert = require('insist')
var proxyquire = require('proxyquire')
var sinon = require('sinon')
var ajv = require('ajv')()
var fs = require('fs')
var path = require('path')
const proxyquire = require('proxyquire')
const sinon = require('sinon')
const ajv = require('ajv')()
const fs = require('fs')
const path = require('path')
const P = require(`${ROOT_DIR}/lib/promise`)
const mocks = require('../mocks')
const mockLog = mocks.mockLog
var mockUid = 'deadbeef'
var mockConfig = {}
const mockUid = 'deadbeef'
const mockConfig = {}
const PUSH_PAYLOADS_SCHEMA_PATH = `${ROOT_DIR}/docs/pushpayloads.schema.json`
var TTL = '42'
const TTL = '42'
const pushModulePath = `${ROOT_DIR}/lib/push`
var mockDbEmpty = {
devices: function () {
return P.resolve([])
}
}
var mockDevices = [
{
'id': '0f7aa00356e5416e82b3bef7bc409eef',
'isCurrentDevice': true,
'lastAccessTime': 1449235471335,
'name': 'My Phone',
'type': 'mobile',
'pushCallback': 'https://updates.push.services.mozilla.com/update/abcdef01234567890abcdefabcdef01234567890abcdef',
'pushPublicKey': mocks.MOCK_PUSH_KEY,
'pushAuthKey': 'w3b14Zjc-Afj2SDOLOyong==',
'pushEndpointExpired': false
},
{
'id': '3a45e6d0dae543qqdKyqjuvAiEupsnOd',
'isCurrentDevice': false,
'lastAccessTime': 1417699471335,
'name': 'My Desktop',
'type': null,
'pushCallback': 'https://updates.push.services.mozilla.com/update/d4c5b1e3f5791ef83896c27519979b93a45e6d0da34c75',
'pushPublicKey': mocks.MOCK_PUSH_KEY,
'pushAuthKey': 'w3b14Zjc-Afj2SDOLOyong==',
'pushEndpointExpired': false
},
{
'id': '50973923bc3e4507a0aa4e285513194a',
'isCurrentDevice': false,
'lastAccessTime': 1402149471335,
'name': 'My Ipad',
'type': null,
'uaOS': 'iOS',
'pushCallback': 'https://updates.push.services.mozilla.com/update/50973923bc3e4507a0aa4e285513194a',
'pushPublicKey': mocks.MOCK_PUSH_KEY,
'pushAuthKey': 'w3b14Zjc-Afj2SDOLOyong==',
'pushEndpointExpired': false
}
]
var mockDbResult = {
devices: function (/* uid */) {
return P.resolve(mockDevices)
}
}
describe('push', () => {
let mockDb, mockDevices
beforeEach(() => {
mockDb = mocks.mockDB()
mockDevices = [
{
'id': '0f7aa00356e5416e82b3bef7bc409eef',
'isCurrentDevice': true,
'lastAccessTime': 1449235471335,
'name': 'My Phone',
'type': 'mobile',
'pushCallback': 'https://updates.push.services.mozilla.com/update/abcdef01234567890abcdefabcdef01234567890abcdef',
'pushPublicKey': mocks.MOCK_PUSH_KEY,
'pushAuthKey': 'w3b14Zjc-Afj2SDOLOyong==',
'pushEndpointExpired': false
},
{
'id': '3a45e6d0dae543qqdKyqjuvAiEupsnOd',
'isCurrentDevice': false,
'lastAccessTime': 1417699471335,
'name': 'My Desktop',
'type': null,
'pushCallback': 'https://updates.push.services.mozilla.com/update/d4c5b1e3f5791ef83896c27519979b93a45e6d0da34c75',
'pushPublicKey': mocks.MOCK_PUSH_KEY,
'pushAuthKey': 'w3b14Zjc-Afj2SDOLOyong==',
'pushEndpointExpired': false
},
{
'id': '50973923bc3e4507a0aa4e285513194a',
'isCurrentDevice': false,
'lastAccessTime': 1402149471335,
'name': 'My Ipad',
'type': null,
'uaOS': 'iOS',
'pushCallback': 'https://updates.push.services.mozilla.com/update/50973923bc3e4507a0aa4e285513194a',
'pushPublicKey': mocks.MOCK_PUSH_KEY,
'pushAuthKey': 'w3b14Zjc-Afj2SDOLOyong==',
'pushEndpointExpired': false
}
]
})
it(
'pushToDevices throws on device not found',
'notifyUpdate rejects on device not found',
() => {
var push = require(pushModulePath)(mockLog(), mockDbEmpty, mockConfig)
const push = require(pushModulePath)(mockLog(), mockDb, mockConfig)
sinon.spy(push, 'sendPush')
return push.pushToDevices([mockUid], 'bogusid').then(function () {
assert(false, 'must throw')
}, function(err) {
assert(! push.sendPush.called)
})
return push.notifyUpdate(mockUid, mockDevices, 'wibble', {
includedDeviceIds: [ 'bogusid' ]
}).then(
() => assert(false, 'must throw'),
err => assert(! push.sendPush.called)
)
}
)
it(
'pushToAllDevices does not throw on empty device result',
'notifyUpdate does not reject on empty device array',
() => {
var thisMockLog = mockLog({
const thisMockLog = mockLog({
info: function (log) {
if (log.name === 'push.account_verify.success') {
assert.fail('must not call push.success')
}
}
})
const push = require(pushModulePath)(thisMockLog, mockDb, mockConfig)
var push = require(pushModulePath)(thisMockLog, mockDbEmpty, mockConfig)
return push.pushToAllDevices(mockUid, 'accountVerify')
return push.notifyUpdate(mockUid, [], 'accountVerify')
}
)
it(
'pushToAllDevices does not send notification to an excluded device',
'notifyUpdate does not send notification to an excluded device',
() => {
var mocks = {
const mocks = {
'web-push': {
sendNotification: function (sub, payload, options) {
return P.resolve()
}
}
}
const push = proxyquire(pushModulePath, mocks)(mockLog(), mockDb, mockConfig)
const options = { excludedDeviceIds: [ mockDevices[0].id ] }
var push = proxyquire(pushModulePath, mocks)(mockLog(), mockDbResult, mockConfig)
var options = { excludedDeviceIds: [mockDevices[0].id] }
return push.pushToAllDevices(mockUid, 'accountVerify', options)
return push.notifyUpdate(mockUid, mockDevices, 'accountVerify', options)
}
)
it(
'pushToAllDevices calls sendPush',
'notifyUpdate calls sendPush',
() => {
var push = require(pushModulePath)(mockLog(), mockDbResult, mockConfig)
sinon.stub(push, 'sendPush')
var excluded = [mockDevices[0].id, mockDevices[2].id]
var data = Buffer.from('foobar')
var options = { data: data, excludedDeviceIds: excluded, TTL: TTL }
return push.pushToAllDevices(mockUid, 'deviceConnected', options)
const push = require(pushModulePath)(mockLog(), mockDb, mockConfig)
sinon.stub(push, 'sendPush', () => P.resolve())
const excluded = [ mockDevices[0].id, mockDevices[2].id ]
const data = Buffer.from('foobar')
const options = { data: data, excludedDeviceIds: excluded, TTL: TTL }
return push.notifyUpdate(mockUid, mockDevices, 'deviceConnected', options)
.then(function() {
assert.ok(push.sendPush.calledOnce, 'push was called')
assert.equal(push.sendPush.getCall(0).args[0], mockUid)
@ -140,43 +136,6 @@ describe('push', () => {
}
)
it(
'pushToDevices calls sendPush',
() => {
var push = require(pushModulePath)(mockLog(), mockDbResult, mockConfig)
sinon.stub(push, 'sendPush')
var data = Buffer.from('foobar')
var options = { data: data, TTL: TTL }
return push.pushToDevices(mockUid, [mockDevices[0].id], 'deviceConnected', options)
.then(function() {
assert.ok(push.sendPush.calledOnce, 'push was called')
assert.equal(push.sendPush.getCall(0).args[0], mockUid)
assert.deepEqual(push.sendPush.getCall(0).args[1], [mockDevices[0]])
assert.equal(push.sendPush.getCall(0).args[2], 'deviceConnected')
assert.deepEqual(push.sendPush.getCall(0).args[3], { data: data, TTL: TTL })
push.sendPush.restore()
})
}
)
it(
'pushToDevice calls pushToDevices',
() => {
var push = require(pushModulePath)(mockLog(), mockDbResult, mockConfig)
sinon.stub(push, 'pushToDevices')
var data = Buffer.from('foobar')
var options = { data: data, TTL: TTL }
push.pushToDevice(mockUid, mockDevices[0].id, 'deviceConnected', options)
assert.ok(push.pushToDevices.calledOnce, 'pushToDevices was called')
assert.equal(push.pushToDevices.getCall(0).args[0], mockUid)
assert.deepEqual(push.pushToDevices.getCall(0).args[1], [mockDevices[0].id])
assert.equal(push.pushToDevices.getCall(0).args[2], 'deviceConnected')
assert.deepEqual(push.pushToDevices.getCall(0).args[3], { data: data, TTL: TTL })
push.pushToDevices.restore()
}
)
it(
'sendPush sends notifications with a TTL of 0',
() => {
@ -199,7 +158,7 @@ describe('push', () => {
}
}
var push = proxyquire(pushModulePath, mocks)(thisMockLog, mockDbResult, mockConfig)
const push = proxyquire(pushModulePath, mocks)(thisMockLog, mockDb, mockConfig)
return push.sendPush(mockUid, mockDevices, 'accountVerify')
.then(() => {
assert.equal(successCalled, 2)
@ -229,7 +188,7 @@ describe('push', () => {
}
}
var push = proxyquire(pushModulePath, mocks)(thisMockLog, mockDbResult, mockConfig)
const push = proxyquire(pushModulePath, mocks)(thisMockLog, mockDb, mockConfig)
var options = { TTL: TTL }
return push.sendPush(mockUid, mockDevices, 'accountVerify', options)
.then(() => {
@ -255,7 +214,7 @@ describe('push', () => {
}
}
var push = proxyquire(pushModulePath, mocks)(mockLog(), mockDbResult, mockConfig)
const push = proxyquire(pushModulePath, mocks)(mockLog(), mockDb, mockConfig)
var options = { data: data }
return push.sendPush(mockUid, mockDevices, 'accountVerify', options)
.then(() => {
@ -278,7 +237,7 @@ describe('push', () => {
}
}
const push = proxyquire(pushModulePath, mocks)(mockLog(), mockDbResult, mockConfig)
const push = proxyquire(pushModulePath, mocks)(mockLog(), mockDb, mockConfig)
const options = { data: data }
return push.sendPush(mockUid, mockDevices, 'devicesNotify', options)
.then(() => {
@ -303,7 +262,7 @@ describe('push', () => {
}
}
const push = proxyquire(pushModulePath, mocks)(mockLog(), mockDbResult, mockConfig)
const push = proxyquire(pushModulePath, mocks)(mockLog(), mockDb, mockConfig)
const options = { data: data }
return push.sendPush(mockUid, mockDevices, 'devicesNotify', options)
.then(() => {
@ -329,7 +288,7 @@ describe('push', () => {
}
}
const push = proxyquire(pushModulePath, mocks)(mockLog(), mockDbResult, mockConfig)
const push = proxyquire(pushModulePath, mocks)(mockLog(), mockDb, mockConfig)
const options = { data: data }
return push.sendPush(mockUid, mockDevices, 'devicesNotify', options)
.then(() => {
@ -400,7 +359,7 @@ describe('push', () => {
'pushEndpointExpired': false
}]
var push = require(pushModulePath)(thisMockLog, mockDbResult, mockConfig)
const push = require(pushModulePath)(thisMockLog, mockDb, mockConfig)
var options = { data: Buffer.from('foobar') }
return push.sendPush(mockUid, devices, 'accountVerify', options)
.then(() => {
@ -427,7 +386,7 @@ describe('push', () => {
'name': 'My Phone'
}]
var push = require(pushModulePath)(thisMockLog, mockDbResult, mockConfig)
const push = require(pushModulePath)(thisMockLog, mockDb, mockConfig)
return push.sendPush(mockUid, devices, 'accountVerify')
.then(() => {
assert.equal(count, 1)
@ -456,7 +415,7 @@ describe('push', () => {
}
}
var push = proxyquire(pushModulePath, mocks)(thisMockLog, mockDbResult, mockConfig)
const push = proxyquire(pushModulePath, mocks)(thisMockLog, mockDb, mockConfig)
return push.sendPush(mockUid, [mockDevices[0]], 'accountVerify')
.then(() => {
assert.equal(count, 1)
@ -484,7 +443,7 @@ describe('push', () => {
}
}
}
var push = proxyquire(pushModulePath, mocks)(thisMockLog, mockDbResult, mockConfig)
const push = proxyquire(pushModulePath, mocks)(thisMockLog, mockDb, mockConfig)
return push.sendPush(mockUid, devices, 'accountVerify').then(function () {
assert.equal(thisMockLog.error.callCount, 0, 'log.error was not called')
@ -493,7 +452,7 @@ describe('push', () => {
}).then(function () {
assert.equal(thisMockLog.error.callCount, 1, 'log.error was called')
var arg = thisMockLog.error.getCall(0).args[0]
assert.equal(arg.op, 'push.pushToDevices')
assert.equal(arg.op, 'push.sendPush')
assert.equal(arg.err.message, 'Too many devices connected to account')
})
}
@ -502,12 +461,6 @@ describe('push', () => {
it(
'push resets device push data when push server responds with a 400 level error',
() => {
var mockDb = {
updateDevice: sinon.spy(function () {
return P.resolve()
})
}
let count = 0
var thisMockLog = mockLog({
info: function (log) {
@ -532,7 +485,7 @@ describe('push', () => {
}
}
var push = proxyquire(pushModulePath, mocks)(thisMockLog, mockDb, mockConfig)
const push = proxyquire(pushModulePath, mocks)(thisMockLog, mockDb, mockConfig)
// Careful, the argument gets modified in-place.
var device = JSON.parse(JSON.stringify(mockDevices[0]))
return push.sendPush(mockUid, [device], 'accountVerify')
@ -545,12 +498,6 @@ describe('push', () => {
it(
'push resets device push data when a failure is caused by bad encryption keys',
() => {
var mockDb = {
updateDevice: sinon.spy(function () {
return P.resolve()
})
}
let count = 0
var thisMockLog = mockLog({
info: function (log) {
@ -574,7 +521,7 @@ describe('push', () => {
}
}
var push = proxyquire(pushModulePath, mocks)(thisMockLog, mockDb, mockConfig)
const push = proxyquire(pushModulePath, mocks)(thisMockLog, mockDb, mockConfig)
// Careful, the argument gets modified in-place.
var device = JSON.parse(JSON.stringify(mockDevices[0]))
device.pushPublicKey = 'E' + device.pushPublicKey.substring(1) // make the key invalid
@ -588,12 +535,6 @@ describe('push', () => {
it(
'push does not reset device push data after an unexpected failure',
() => {
var mockDb = {
updateDevice: sinon.spy(function () {
return P.resolve()
})
}
let count = 0
var thisMockLog = mockLog({
info: function (log) {
@ -614,7 +555,7 @@ describe('push', () => {
}
}
var push = proxyquire(pushModulePath, mocks)(thisMockLog, mockDb, mockConfig)
const push = proxyquire(pushModulePath, mocks)(thisMockLog, mockDb, mockConfig)
return push.sendPush(mockUid, [mockDevices[0]], 'accountVerify')
.then(() => {
assert.equal(count, 1)
@ -623,40 +564,10 @@ describe('push', () => {
)
it(
'notifyUpdate calls pushToAllDevices',
'notifyDeviceConnected calls notifyUpdate',
() => {
var push = require(pushModulePath)(mockLog(), mockDbEmpty, mockConfig)
sinon.spy(push, 'pushToAllDevices')
return push.notifyUpdate(mockUid, 'passwordReset')
.then(function() {
assert.ok(push.pushToAllDevices.calledOnce, 'pushToAllDevices was called')
assert.equal(push.pushToAllDevices.getCall(0).args[0], mockUid)
assert.equal(push.pushToAllDevices.getCall(0).args[1], 'passwordReset')
push.pushToAllDevices.restore()
})
}
)
it(
'notifyUpdate without a 2nd arg calls pushToAllDevices with a accountVerify reason',
() => {
var push = require(pushModulePath)(mockLog(), mockDbEmpty, mockConfig)
sinon.spy(push, 'pushToAllDevices')
return push.notifyUpdate(mockUid)
.then(function() {
assert.ok(push.pushToAllDevices.calledOnce, 'pushToAllDevices was called')
assert.equal(push.pushToAllDevices.getCall(0).args[0], mockUid)
assert.equal(push.pushToAllDevices.getCall(0).args[1], 'accountVerify')
push.pushToAllDevices.restore()
})
}
)
it(
'notifyDeviceConnected calls pushToAllDevices',
() => {
var push = require(pushModulePath)(mockLog(), mockDbEmpty, mockConfig)
sinon.spy(push, 'pushToAllDevices')
const push = require(pushModulePath)(mockLog(), mockDb, mockConfig)
sinon.spy(push, 'notifyUpdate')
var deviceId = 'gjfkd5434jk5h5fd'
var deviceName = 'My phone'
var expectedData = {
@ -666,63 +577,67 @@ describe('push', () => {
deviceName: deviceName
}
}
return push.notifyDeviceConnected(mockUid, deviceName, deviceId).catch(function (err) {
assert.fail('must not throw')
throw err
})
.then(function() {
assert.ok(push.pushToAllDevices.calledOnce, 'pushToAllDevices was called')
assert.equal(push.pushToAllDevices.getCall(0).args[0], mockUid)
assert.equal(push.pushToAllDevices.getCall(0).args[1], 'deviceConnected')
var options = push.pushToAllDevices.getCall(0).args[2]
var payload = JSON.parse(options.data.toString('utf8'))
assert.deepEqual(payload, expectedData)
var schemaPath = path.resolve(__dirname, PUSH_PAYLOADS_SCHEMA_PATH)
var schema = JSON.parse(fs.readFileSync(schemaPath))
assert.ok(ajv.validate(schema, payload))
assert.deepEqual(options.excludedDeviceIds, [deviceId])
push.pushToAllDevices.restore()
})
return push.notifyDeviceConnected(mockUid, mockDevices, deviceName, deviceId)
.catch(err => {
assert.fail('must not throw')
throw err
})
.then(() => {
assert.ok(push.notifyUpdate.calledOnce, 'notifyUpdate was called')
assert.equal(push.notifyUpdate.getCall(0).args[0], mockUid)
assert.equal(push.notifyUpdate.getCall(0).args[1], mockDevices)
assert.equal(push.notifyUpdate.getCall(0).args[2], 'deviceConnected')
const options = push.notifyUpdate.getCall(0).args[3]
const payload = JSON.parse(options.data.toString('utf8'))
assert.deepEqual(payload, expectedData)
const schemaPath = path.resolve(__dirname, PUSH_PAYLOADS_SCHEMA_PATH)
const schema = JSON.parse(fs.readFileSync(schemaPath))
assert.ok(ajv.validate(schema, payload))
assert.deepEqual(options.excludedDeviceIds, [deviceId])
push.notifyUpdate.restore()
})
}
)
it(
'notifyDeviceDisconnected calls pushToAllDevices',
'notifyDeviceDisconnected calls sendPush',
() => {
var mocks = {
const mocks = {
'web-push': {
sendNotification: function (sub, payload, options) {
return P.resolve()
}
}
}
var push = proxyquire(pushModulePath, mocks)(mockLog(), mockDbResult, mockConfig)
sinon.spy(push, 'pushToAllDevices')
var idToDisconnect = mockDevices[0].id
var expectedData = {
const push = proxyquire(pushModulePath, mocks)(mockLog(), mockDb, mockConfig)
sinon.spy(push, 'sendPush')
const idToDisconnect = mockDevices[0].id
const expectedData = {
version: 1,
command: 'fxaccounts:device_disconnected',
data: {
id: idToDisconnect
}
}
return push.notifyDeviceDisconnected(mockUid, idToDisconnect).catch(function (err) {
assert.fail('must not throw')
throw err
})
.then(function() {
assert.ok(push.pushToAllDevices.calledOnce, 'pushToAllDevices was called')
assert.equal(push.pushToAllDevices.getCall(0).args[0], mockUid)
assert.equal(push.pushToAllDevices.getCall(0).args[1], 'deviceDisconnected')
var options = push.pushToAllDevices.getCall(0).args[2]
var payload = JSON.parse(options.data.toString('utf8'))
assert.deepEqual(payload, expectedData)
var schemaPath = path.resolve(__dirname, PUSH_PAYLOADS_SCHEMA_PATH)
var schema = JSON.parse(fs.readFileSync(schemaPath))
assert.ok(ajv.validate(schema, payload))
assert.ok(options.TTL, 'TTL should be set')
push.pushToAllDevices.restore()
})
return push.notifyDeviceDisconnected(mockUid, mockDevices, idToDisconnect)
.catch(err => {
assert.fail('must not throw')
throw err
})
.then(() => {
assert.ok(push.sendPush.calledOnce, 'sendPush was called')
assert.equal(push.sendPush.getCall(0).args[0], mockUid)
assert.equal(push.sendPush.getCall(0).args[1], mockDevices)
assert.equal(push.sendPush.getCall(0).args[2], 'deviceDisconnected')
const options = push.sendPush.getCall(0).args[3]
const payload = JSON.parse(options.data.toString('utf8'))
assert.deepEqual(payload, expectedData)
const schemaPath = path.resolve(__dirname, PUSH_PAYLOADS_SCHEMA_PATH)
const schema = JSON.parse(fs.readFileSync(schemaPath))
assert.ok(ajv.validate(schema, payload))
assert.ok(options.TTL, 'TTL should be set')
push.sendPush.restore()
})
}
)
@ -736,7 +651,7 @@ describe('push', () => {
}
}
}
var push = proxyquire(pushModulePath, mocks)(mockLog(), mockDbResult, mockConfig)
const push = proxyquire(pushModulePath, mocks)(mockLog(), mockDb, mockConfig)
sinon.spy(push, 'sendPush')
var expectedData = {
version: 1,
@ -772,7 +687,7 @@ describe('push', () => {
}
}
}
var push = proxyquire(pushModulePath, mocks)(mockLog(), mockDbEmpty, mockConfig)
const push = proxyquire(pushModulePath, mocks)(mockLog(), mockDb, mockConfig)
sinon.spy(push, 'sendPush')
var expectedData = {
version: 1,
@ -808,7 +723,7 @@ describe('push', () => {
}
}
}
var push = proxyquire(pushModulePath, mocks)(mockLog(), mockDbEmpty, mockConfig)
const push = proxyquire(pushModulePath, mocks)(mockLog(), mockDb, mockConfig)
sinon.spy(push, 'sendPush')
var expectedData = {
version: 1,
@ -866,7 +781,7 @@ describe('push', () => {
}
}
var push = proxyquire(pushModulePath, mocks)(thisMockLog, mockDbResult, mockConfig)
const push = proxyquire(pushModulePath, mocks)(thisMockLog, mockDb, mockConfig)
return push.sendPush(mockUid, mockDevices, 'accountVerify')
.then(() => {
assert.equal(count, 2)
@ -888,7 +803,7 @@ describe('push', () => {
}
}
var push = proxyquire(pushModulePath, mocks)(thisMockLog, mockDbResult, mockConfig)
const push = proxyquire(pushModulePath, mocks)(thisMockLog, mockDb, mockConfig)
return push.sendPush(mockUid, mockDevices, 'anUnknownReasonString').then(
function () {
assert(false, 'calling sendPush should have failed')

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

@ -244,7 +244,7 @@ describe('/account/devices/notify', function () {
assert(false, 'should have thrown')
})
.then(() => assert(false), function (err) {
assert.equal(mockPush.pushToDevices.callCount, 0, 'mockPush.pushToDevices was not called')
assert.equal(mockPush.notifyUpdate.callCount, 0, 'mockPush.notifyUpdate was not called')
assert.equal(err.errno, 107, 'Correct errno for invalid push payload')
})
})
@ -256,22 +256,23 @@ describe('/account/devices/notify', function () {
TTL: 60,
payload: pushPayload
}
// We don't wait on pushToAllDevices in the request handler, that's why
// We don't wait on notifyUpdate in the request handler, that's why
// we have to wait on it manually by spying.
var pushToAllDevicesPromise = P.defer()
mockPush.pushToAllDevices = sinon.spy(function () {
pushToAllDevicesPromise.resolve()
var notifyUpdatePromise = P.defer()
mockPush.notifyUpdate = sinon.spy(function () {
notifyUpdatePromise.resolve()
return P.resolve()
})
return runTest(route, mockRequest, function (response) {
return pushToAllDevicesPromise.promise.then(function () {
return notifyUpdatePromise.promise.then(function () {
assert.equal(mockCustoms.checkAuthenticated.callCount, 1, 'mockCustoms.checkAuthenticated was called once')
assert.equal(mockPush.pushToAllDevices.callCount, 1, 'mockPush.pushToAllDevices was called once')
var args = mockPush.pushToAllDevices.args[0]
assert.equal(args.length, 3, 'mockPush.pushToAllDevices was passed three arguments')
assert.equal(mockPush.notifyUpdate.callCount, 1, 'mockPush.notifyUpdate was called once')
var args = mockPush.notifyUpdate.args[0]
assert.equal(args.length, 4, 'mockPush.notifyUpdate was passed four arguments')
assert.equal(args[0], uid, 'first argument was the device uid')
assert.equal(args[1], 'devicesNotify', 'second argument was the devicesNotify reason')
assert.deepEqual(args[2], {
assert.ok(Array.isArray(args[1]), 'second argument was devices array')
assert.equal(args[2], 'devicesNotify', 'second argument was the devicesNotify reason')
assert.deepEqual(args[3], {
data: Buffer.from(JSON.stringify(pushPayload)),
excludedDeviceIds: ['bogusid'],
TTL: 60
@ -290,11 +291,11 @@ describe('/account/devices/notify', function () {
TTL: 60,
payload: extraPropsPayload
}
// We don't wait on pushToAllDevices in the request handler, that's why
// We don't wait on notifyUpdate in the request handler, that's why
// we have to wait on it manually by spying.
var pushToAllDevicesPromise = P.defer()
mockPush.pushToAllDevices = sinon.spy(function () {
pushToAllDevicesPromise.resolve()
var notifyUpdatePromise = P.defer()
mockPush.notifyUpdate = sinon.spy(function () {
notifyUpdatePromise.resolve()
return Promise.resolve()
})
return runTest(route, mockRequest, function () {
@ -315,25 +316,26 @@ describe('/account/devices/notify', function () {
TTL: 60,
payload: pushPayload
}
// We don't wait on pushToDevices in the request handler, that's why
// We don't wait on notifyUpdate in the request handler, that's why
// we have to wait on it manually by spying.
var pushToDevicesPromise = P.defer()
mockPush.pushToDevices = sinon.spy(function () {
pushToDevicesPromise.resolve()
var notifyUpdatePromise = P.defer()
mockPush.notifyUpdate = sinon.spy(function () {
notifyUpdatePromise.resolve()
return P.resolve()
})
return runTest(route, mockRequest, function (response) {
return pushToDevicesPromise.promise.then(function () {
return notifyUpdatePromise.promise.then(function () {
assert.equal(mockCustoms.checkAuthenticated.callCount, 1, 'mockCustoms.checkAuthenticated was called once')
assert.equal(mockPush.pushToDevices.callCount, 1, 'mockPush.pushToDevices was called once')
var args = mockPush.pushToDevices.args[0]
assert.equal(args.length, 4, 'mockPush.pushToDevices was passed four arguments')
assert.equal(mockPush.notifyUpdate.callCount, 1, 'mockPush.notifyUpdate was called once')
var args = mockPush.notifyUpdate.args[0]
assert.equal(args.length, 4, 'mockPush.notifyUpdate was passed four arguments')
assert.equal(args[0], uid, 'first argument was the device uid')
assert.deepEqual(args[1], ['bogusid1', 'bogusid2'], 'second argument was the list of device ids')
assert.ok(Array.isArray(args[1]), 'second argument was devices array')
assert.equal(args[2], 'devicesNotify', 'third argument was the devicesNotify reason')
assert.deepEqual(args[3], {
data: Buffer.from(JSON.stringify(pushPayload)),
TTL: 60
TTL: 60,
includedDeviceIds: [ 'bogusid1', 'bogusid2' ]
}, 'fourth argument was the push options')
assert.equal(mockLog.activityEvent.callCount, 1, 'log.activityEvent was called once')
args = mockLog.activityEvent.args[0]
@ -351,7 +353,7 @@ describe('/account/devices/notify', function () {
})
it('does not log activity event for non-send-tab-related messages', function () {
mockPush.pushToDevices.reset()
mockPush.notifyUpdate.reset()
mockLog.activityEvent.reset()
mockLog.error.reset()
mockRequest.payload = {
@ -363,7 +365,7 @@ describe('/account/devices/notify', function () {
}
}
return runTest(route, mockRequest, function (response) {
assert.equal(mockPush.pushToDevices.callCount, 1, 'mockPush.pushToDevices was called once')
assert.equal(mockPush.notifyUpdate.callCount, 1, 'mockPush.notifyUpdate was called once')
assert.equal(mockLog.activityEvent.callCount, 0, 'log.activityEvent was not called')
assert.equal(mockLog.error.callCount, 0, 'log.error was not called')
})
@ -419,7 +421,7 @@ describe('/account/devices/notify', function () {
var mockLog = mocks.spyLog()
var mockPush = mocks.mockPush({
pushToDevices: () => P.reject('Devices ids not found in devices')
notifyUpdate: () => P.reject('devices empty')
})
var mockCustoms = {
checkAuthenticated: () => P.resolve()
@ -465,7 +467,7 @@ describe('/account/device/destroy', function () {
assert.ok(mockDB.deleteDevice.calledBefore(mockPush.notifyDeviceDisconnected))
assert.equal(mockPush.notifyDeviceDisconnected.callCount, 1)
assert.equal(mockPush.notifyDeviceDisconnected.firstCall.args[0], mockRequest.auth.credentials.uid)
assert.equal(mockPush.notifyDeviceDisconnected.firstCall.args[1], deviceId)
assert.equal(mockPush.notifyDeviceDisconnected.firstCall.args[2], deviceId)
assert.equal(mockLog.activityEvent.callCount, 1, 'log.activityEvent was called once')
var args = mockLog.activityEvent.args[0]

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

@ -509,9 +509,10 @@ describe('/recovery_email/verify_code', function () {
assert.equal(mockPush.notifyUpdate.callCount, 1, 'mockPush.notifyUpdate should have been called once')
args = mockPush.notifyUpdate.args[0]
assert.equal(args.length, 2, 'mockPush.notifyUpdate should have been passed two arguments')
assert.equal(args.length, 3, 'mockPush.notifyUpdate should have been passed three arguments')
assert.equal(args[0].toString('hex'), uid, 'first argument should have been uid')
assert.equal(args[1], 'accountVerify', 'second argument should have been reason')
assert.ok(Array.isArray(args[1]), 'second argument should have been devices array')
assert.equal(args[2], 'accountVerify', 'third argument should have been reason')
assert.equal(JSON.stringify(response), '{}')
})
@ -639,9 +640,10 @@ describe('/recovery_email/verify_code', function () {
assert.equal(mockPush.notifyUpdate.callCount, 1, 'mockPush.notifyUpdate should have been called once')
args = mockPush.notifyUpdate.args[0]
assert.equal(args.length, 2, 'mockPush.notifyUpdate should have been passed two arguments')
assert.equal(args.length, 3, 'mockPush.notifyUpdate should have been passed three arguments')
assert.equal(args[0].toString('hex'), uid, 'first argument should have been uid')
assert.equal(args[1], 'accountConfirm', 'second argument should have been reason')
assert.ok(Array.isArray(args[1]), 'second argument should have been devices array')
assert.equal(args[2], 'accountConfirm', 'third argument should have been reason')
})
.then(function () {
mockDB.verifyTokens.reset()

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

@ -276,8 +276,8 @@ describe('/password', () => {
]
var mockDB = mocks.mockDB({
email: TEST_EMAIL,
uid: uid,
devices: devices
uid,
devices
})
var mockPush = mocks.mockPush()
var mockMailer = mocks.mockMailer()
@ -286,6 +286,7 @@ describe('/password', () => {
credentials: {
uid: uid
},
devices,
payload: {
authPW: crypto.randomBytes(32).toString('hex'),
wrapKb: crypto.randomBytes(32).toString('hex'),

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

@ -69,7 +69,9 @@ describe('lib/server', () => {
log = mocks.spyLog()
config = getConfig()
routes = getRoutes()
db = mocks.mockDB()
db = mocks.mockDB({
devices: [ { id: 'fake device id' } ]
})
translator = {
getTranslator: sinon.spy(() => ({ en: { format: () => {}, language: 'en' } })),
getLocale: sinon.spy(() => 'en')
@ -93,12 +95,15 @@ describe('lib/server', () => {
assert.equal(log.summary.callCount, 0)
})
describe('successful request, acceptable locale, signinCodes feature enabled:', () => {
describe('successful request, authenticated, acceptable locale, signinCodes feature enabled:', () => {
let request
beforeEach(() => {
response = 'ok'
return instance.inject({
credentials: {
uid: 'fake uid'
},
headers: {
'accept-language': 'fr-CH, fr;q=0.9, en-GB, en;q=0.5',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:57.0) Gecko/20100101 Firefox/57.0',
@ -170,7 +175,18 @@ describe('lib/server', () => {
})
})
describe('another request:', () => {
it('fetched devices correctly', () => {
assert.ok(request.app.devices)
assert.equal(typeof request.app.devices.then, 'function')
assert.equal(db.devices.callCount, 1)
assert.equal(db.devices.args[0].length, 1)
assert.equal(db.devices.args[0][0], 'fake uid')
return request.app.devices.then(devices => {
assert.deepEqual(devices, [ { id: 'fake device id' } ])
})
})
describe('successful request, unauthenticated, uid in payload:', () => {
let secondRequest
beforeEach(() => {
@ -184,7 +200,8 @@ describe('lib/server', () => {
method: 'POST',
url: '/account/create',
payload: {
features: [ 'signinCodes' ]
features: [ 'signinCodes' ],
uid: 'another fake uid'
},
remoteAddress: '194.12.187.0'
}).then(response => secondRequest = response.request)
@ -202,7 +219,6 @@ describe('lib/server', () => {
})
it('second request has its own location info', () => {
assert.notEqual(request, secondRequest)
assert.notEqual(request.app.geo, secondRequest.app.geo)
return secondRequest.app.geo.then(geo => {
assert.equal(geo.location.city, 'Geneva')
@ -213,6 +229,16 @@ describe('lib/server', () => {
assert.equal(geo.timeZone, 'Europe/Zurich')
})
})
it('second request fetched devices correctly', () => {
assert.notEqual(request.app.devices, secondRequest.app.devices)
assert.equal(db.devices.callCount, 2)
assert.equal(db.devices.args[1].length, 1)
assert.equal(db.devices.args[1][0], 'another fake uid')
return request.app.devices.then(devices => {
assert.deepEqual(devices, [ { id: 'fake device id' } ])
})
})
})
})

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

@ -112,9 +112,7 @@ const PUSH_METHOD_NAMES = [
'notifyPasswordReset',
'notifyAccountDestroyed',
'notifyProfileUpdated',
'notifyUpdate',
'pushToAllDevices',
'pushToDevices'
'notifyUpdate'
]
module.exports = {
@ -487,6 +485,7 @@ function mockRequest (data, errors) {
app: {
acceptLanguage: 'en-US',
clientAddress: data.clientAddress || '63.245.221.32',
devices: P.resolve(data.devices || []),
features: new Set(data.features),
geo,
locale: data.locale || 'en-US',

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

@ -119,10 +119,9 @@ describe('remote push db', function() {
.then(function () {
return db.devices(ACCOUNT.uid)
})
.then(function () {
var pushWithUnknown400 = proxyquire('../../lib/push', mocksUnknown400)(mockLog, db, {})
return pushWithUnknown400.pushToAllDevices(ACCOUNT.uid, 'accountVerify')
.then(devices => {
const pushWithUnknown400 = proxyquire('../../lib/push', mocksUnknown400)(mockLog, db, {})
return pushWithUnknown400.notifyUpdate(ACCOUNT.uid, devices, 'accountVerify')
})
.then(function () {
return db.devices(ACCOUNT.uid)
@ -134,11 +133,9 @@ describe('remote push db', function() {
assert.equal(device.pushPublicKey, deviceInfo.pushPublicKey, 'device.pushPublicKey is correct')
assert.equal(device.pushAuthKey, deviceInfo.pushAuthKey, 'device.pushAuthKey is correct')
assert.equal(device.pushEndpointExpired, deviceInfo.pushEndpointExpired, 'device.pushEndpointExpired is correct')
})
.then(function () {
var pushWithKnown400 = proxyquire('../../lib/push', mocksKnown400)(mockLog, db, {})
return pushWithKnown400.pushToAllDevices(ACCOUNT.uid, 'accountVerify')
const pushWithKnown400 = proxyquire('../../lib/push', mocksKnown400)(mockLog, db, {})
return pushWithKnown400.notifyUpdate(ACCOUNT.uid, devices, 'accountVerify')
})
.then(function () {
return db.devices(ACCOUNT.uid)