fix(metrics): stash metrics context during the account reset flow
We are planning to deprecate metrics context payloads on endpoints where it is not strictly required. This means only endpoints that start a flow or endpoints that occur outside of any flow will accept metrics context payloads in the future. The motivation for making this change is to reduce the likelihood of metrics weirdness arising from conflicting metrics context data being set mid-flow. The account reset flow includes three different token types. By stashing metrics context as each new token is created, we can stop sending it to the subsequent endpoints in some future content server change without interrupting metrics.
This commit is contained in:
Родитель
f408af456a
Коммит
804907a32b
|
@ -35,6 +35,7 @@ module.exports = function (log, config) {
|
|||
return {
|
||||
stash,
|
||||
gather,
|
||||
propagate,
|
||||
clear,
|
||||
validate,
|
||||
setFlowCompleteSignal
|
||||
|
@ -148,6 +149,36 @@ module.exports = function (log, config) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Propagates metrics context metadata from one token-derived key to
|
||||
* another. Asynchronous, returns a promise.
|
||||
*
|
||||
* @name propagateMetricsContext
|
||||
* @this request
|
||||
* @param oldToken token to gather the metadata from
|
||||
* @param newToken token to stash the metadata against
|
||||
*/
|
||||
function propagate (oldToken, newToken) {
|
||||
const oldKey = getKey(oldToken)
|
||||
return cache.get(oldKey)
|
||||
.then(metadata => {
|
||||
if (metadata) {
|
||||
return cache.add(getKey(newToken), metadata)
|
||||
.catch(err => log.warn({ op: 'metricsContext.propagate.add', err }))
|
||||
}
|
||||
})
|
||||
.catch(err => log.error({
|
||||
op: 'metricsContext.propagate',
|
||||
err,
|
||||
hasOldToken: !! oldToken,
|
||||
hasOldTokenId: !! (oldToken && oldToken.id),
|
||||
hasOldTokenUid: !! (oldToken && oldToken.uid),
|
||||
hasNewToken: !! newToken,
|
||||
hasNewTokenId: !! (newToken && newToken.id),
|
||||
hasNewTokenUid: !! (newToken && newToken.uid),
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to clear previously-stashed metrics context metadata.
|
||||
*
|
||||
|
|
|
@ -433,6 +433,14 @@ module.exports = function (
|
|||
|
||||
request.validateMetricsContext()
|
||||
|
||||
let flowCompleteSignal
|
||||
if (requestHelper.wantsKeys(request)) {
|
||||
flowCompleteSignal = 'account.signed'
|
||||
} else {
|
||||
flowCompleteSignal = 'account.reset'
|
||||
}
|
||||
request.setMetricsFlowCompleteSignal(flowCompleteSignal)
|
||||
|
||||
// Store flowId and flowBeginTime to send in email
|
||||
let flowId, flowBeginTime
|
||||
if (request.payload.metricsContext) {
|
||||
|
@ -440,78 +448,66 @@ module.exports = function (
|
|||
flowBeginTime = request.payload.metricsContext.flowBeginTime
|
||||
}
|
||||
|
||||
let passwordForgotToken
|
||||
|
||||
return P.all([
|
||||
request.emitMetricsEvent('password.forgot.send_code.start'),
|
||||
customs.check(request, email, 'passwordForgotSendCode')
|
||||
])
|
||||
.then(db.accountRecord.bind(db, email))
|
||||
.then(
|
||||
function (accountRecord) {
|
||||
if (accountRecord.primaryEmail.normalizedEmail !== email.toLowerCase()) {
|
||||
throw error.cannotResetPasswordWithSecondaryEmail()
|
||||
}
|
||||
// The token constructor sets createdAt from its argument.
|
||||
// Clobber the timestamp to prevent prematurely expired tokens.
|
||||
accountRecord.createdAt = undefined
|
||||
return db.createPasswordForgotToken(accountRecord)
|
||||
.then(accountRecord => {
|
||||
if (accountRecord.primaryEmail.normalizedEmail !== email.toLowerCase()) {
|
||||
throw error.cannotResetPasswordWithSecondaryEmail()
|
||||
}
|
||||
)
|
||||
.then(
|
||||
function (passwordForgotToken) {
|
||||
return db.accountEmails(passwordForgotToken.uid)
|
||||
.then(emails => {
|
||||
const geoData = request.app.geo
|
||||
const {
|
||||
browser: uaBrowser,
|
||||
browserVersion: uaBrowserVersion,
|
||||
os: uaOS,
|
||||
osVersion: uaOSVersion,
|
||||
deviceType: uaDeviceType
|
||||
} = request.app.ua
|
||||
// The token constructor sets createdAt from its argument.
|
||||
// Clobber the timestamp to prevent prematurely expired tokens.
|
||||
accountRecord.createdAt = undefined
|
||||
return db.createPasswordForgotToken(accountRecord)
|
||||
})
|
||||
.then(result => {
|
||||
passwordForgotToken = result
|
||||
return P.all([
|
||||
request.stashMetricsContext(passwordForgotToken),
|
||||
db.accountEmails(passwordForgotToken.uid)
|
||||
])
|
||||
})
|
||||
.then(([_, emails]) => {
|
||||
const geoData = request.app.geo
|
||||
const {
|
||||
browser: uaBrowser,
|
||||
browserVersion: uaBrowserVersion,
|
||||
os: uaOS,
|
||||
osVersion: uaOSVersion,
|
||||
deviceType: uaDeviceType
|
||||
} = request.app.ua
|
||||
|
||||
return mailer.sendRecoveryCode(emails, passwordForgotToken, {
|
||||
token: passwordForgotToken,
|
||||
code: passwordForgotToken.passCode,
|
||||
service: service,
|
||||
redirectTo: request.payload.redirectTo,
|
||||
resume: request.payload.resume,
|
||||
acceptLanguage: request.app.acceptLanguage,
|
||||
flowId: flowId,
|
||||
flowBeginTime: flowBeginTime,
|
||||
ip: ip,
|
||||
location: geoData.location,
|
||||
timeZone: geoData.timeZone,
|
||||
uaBrowser,
|
||||
uaBrowserVersion,
|
||||
uaOS,
|
||||
uaOSVersion,
|
||||
uaDeviceType,
|
||||
uid: passwordForgotToken.uid
|
||||
})
|
||||
})
|
||||
.then(
|
||||
function () {
|
||||
return request.emitMetricsEvent('password.forgot.send_code.completed')
|
||||
}
|
||||
)
|
||||
.then(
|
||||
function () {
|
||||
return passwordForgotToken
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
.then(
|
||||
function (passwordForgotToken) {
|
||||
return {
|
||||
passwordForgotToken: passwordForgotToken.data,
|
||||
ttl: passwordForgotToken.ttl(),
|
||||
codeLength: passwordForgotToken.passCode.length,
|
||||
tries: passwordForgotToken.tries
|
||||
}
|
||||
},
|
||||
|
||||
)
|
||||
return mailer.sendRecoveryCode(emails, passwordForgotToken, {
|
||||
token: passwordForgotToken,
|
||||
code: passwordForgotToken.passCode,
|
||||
service: service,
|
||||
redirectTo: request.payload.redirectTo,
|
||||
resume: request.payload.resume,
|
||||
acceptLanguage: request.app.acceptLanguage,
|
||||
flowId: flowId,
|
||||
flowBeginTime: flowBeginTime,
|
||||
ip: ip,
|
||||
location: geoData.location,
|
||||
timeZone: geoData.timeZone,
|
||||
uaBrowser,
|
||||
uaBrowserVersion,
|
||||
uaOS,
|
||||
uaOSVersion,
|
||||
uaDeviceType,
|
||||
uid: passwordForgotToken.uid
|
||||
})
|
||||
})
|
||||
.then(() => request.emitMetricsEvent('password.forgot.send_code.completed'))
|
||||
.then(() => ({
|
||||
passwordForgotToken: passwordForgotToken.data,
|
||||
ttl: passwordForgotToken.ttl(),
|
||||
codeLength: passwordForgotToken.passCode.length,
|
||||
tries: passwordForgotToken.tries
|
||||
}))
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -577,13 +573,13 @@ module.exports = function (
|
|||
return mailer.sendRecoveryCode(emails, passwordForgotToken, {
|
||||
code: passwordForgotToken.passCode,
|
||||
token: passwordForgotToken,
|
||||
service: service,
|
||||
service,
|
||||
redirectTo: request.payload.redirectTo,
|
||||
resume: request.payload.resume,
|
||||
acceptLanguage: request.app.acceptLanguage,
|
||||
flowId: flowId,
|
||||
flowBeginTime: flowBeginTime,
|
||||
ip: ip,
|
||||
flowId,
|
||||
flowBeginTime,
|
||||
ip,
|
||||
location: geoData.location,
|
||||
timeZone: geoData.timeZone,
|
||||
uaBrowser,
|
||||
|
@ -648,72 +644,56 @@ module.exports = function (
|
|||
flowBeginTime = request.payload.metricsContext.flowBeginTime
|
||||
}
|
||||
|
||||
let accountResetToken
|
||||
|
||||
return P.all([
|
||||
request.emitMetricsEvent('password.forgot.verify_code.start'),
|
||||
customs.check(request, passwordForgotToken.email, 'passwordForgotVerifyCode')
|
||||
])
|
||||
.then(
|
||||
function () {
|
||||
if (butil.buffersAreEqual(passwordForgotToken.passCode, code) &&
|
||||
passwordForgotToken.ttl() > 0) {
|
||||
return db.forgotPasswordVerified(passwordForgotToken)
|
||||
.then(
|
||||
function (accountResetToken) {
|
||||
return db.accountEmails(passwordForgotToken.uid)
|
||||
.then((emails) => {
|
||||
|
||||
if (accountResetWithRecoveryKey) {
|
||||
// To prevent multiple password change emails being sent to a user,
|
||||
// we check for a flag to see if this is a reset using an account recovery key.
|
||||
// If it is, then the notification email will be sent in `/account/reset`
|
||||
return P.resolve()
|
||||
}
|
||||
|
||||
return mailer.sendPasswordResetNotification(
|
||||
emails,
|
||||
passwordForgotToken,
|
||||
{
|
||||
code: code,
|
||||
acceptLanguage: request.app.acceptLanguage,
|
||||
flowId: flowId,
|
||||
flowBeginTime: flowBeginTime,
|
||||
uid: passwordForgotToken.uid
|
||||
}
|
||||
)
|
||||
})
|
||||
.then(
|
||||
function () {
|
||||
return request.emitMetricsEvent('password.forgot.verify_code.completed')
|
||||
}
|
||||
)
|
||||
.then(
|
||||
function () {
|
||||
return accountResetToken
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
.then(
|
||||
function (accountResetToken) {
|
||||
return {
|
||||
accountResetToken: accountResetToken.data
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
else {
|
||||
return failVerifyAttempt(passwordForgotToken)
|
||||
.then(
|
||||
function () {
|
||||
throw error.invalidVerificationCode({
|
||||
tries: passwordForgotToken.tries,
|
||||
ttl: passwordForgotToken.ttl()
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
.then(() => {
|
||||
if (butil.buffersAreEqual(passwordForgotToken.passCode, code) && passwordForgotToken.ttl() > 0) {
|
||||
return db.forgotPasswordVerified(passwordForgotToken)
|
||||
}
|
||||
)
|
||||
|
||||
return failVerifyAttempt(passwordForgotToken)
|
||||
.then(() => {
|
||||
throw error.invalidVerificationCode({
|
||||
tries: passwordForgotToken.tries,
|
||||
ttl: passwordForgotToken.ttl()
|
||||
})
|
||||
})
|
||||
})
|
||||
.then(result => {
|
||||
accountResetToken = result
|
||||
return P.all([
|
||||
request.propagateMetricsContext(passwordForgotToken, accountResetToken),
|
||||
db.accountEmails(passwordForgotToken.uid)
|
||||
])
|
||||
})
|
||||
.then(([_, emails]) => {
|
||||
if (accountResetWithRecoveryKey) {
|
||||
// To prevent multiple password change emails being sent to a user,
|
||||
// we check for a flag to see if this is a reset using an account recovery key.
|
||||
// If it is, then the notification email will be sent in `/account/reset`
|
||||
return P.resolve()
|
||||
}
|
||||
|
||||
return mailer.sendPasswordResetNotification(
|
||||
emails,
|
||||
passwordForgotToken,
|
||||
{
|
||||
code,
|
||||
acceptLanguage: request.app.acceptLanguage,
|
||||
flowId,
|
||||
flowBeginTime,
|
||||
uid: passwordForgotToken.uid
|
||||
}
|
||||
)
|
||||
})
|
||||
.then(() => request.emitMetricsEvent('password.forgot.verify_code.completed'))
|
||||
.then(() => ({
|
||||
accountResetToken: accountResetToken.data
|
||||
}))
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -263,6 +263,7 @@ async function create (log, error, config, routes, db, translator) {
|
|||
const metricsContext = require('./metrics/context')(log, config)
|
||||
server.decorate('request', 'stashMetricsContext', metricsContext.stash)
|
||||
server.decorate('request', 'gatherMetricsContext', metricsContext.gather)
|
||||
server.decorate('request', 'propagateMetricsContext', metricsContext.propagate)
|
||||
server.decorate('request', 'clearMetricsContext', metricsContext.clear)
|
||||
server.decorate('request', 'validateMetricsContext', metricsContext.validate)
|
||||
server.decorate('request', 'setMetricsFlowCompleteSignal', metricsContext.setFlowCompleteSignal)
|
||||
|
|
|
@ -53,7 +53,7 @@ describe('metricsContext', () => {
|
|||
|
||||
assert.equal(typeof metricsContext, 'object', 'metricsContext is object')
|
||||
assert.notEqual(metricsContext, null, 'metricsContext is not null')
|
||||
assert.equal(Object.keys(metricsContext).length, 5, 'metricsContext has 5 properties')
|
||||
assert.lengthOf(Object.keys(metricsContext), 6)
|
||||
|
||||
assert.equal(typeof metricsContext.stash, 'function', 'metricsContext.stash is function')
|
||||
assert.equal(metricsContext.stash.length, 1, 'metricsContext.stash expects 1 argument')
|
||||
|
@ -61,6 +61,9 @@ describe('metricsContext', () => {
|
|||
assert.equal(typeof metricsContext.gather, 'function', 'metricsContext.gather is function')
|
||||
assert.equal(metricsContext.gather.length, 1, 'metricsContext.gather expects 1 argument')
|
||||
|
||||
assert.isFunction(metricsContext.propagate)
|
||||
assert.lengthOf(metricsContext.propagate, 2)
|
||||
|
||||
assert.equal(typeof metricsContext.clear, 'function', 'metricsContext.clear is function')
|
||||
assert.equal(metricsContext.clear.length, 0, 'metricsContext.gather expects no arguments')
|
||||
|
||||
|
@ -512,6 +515,78 @@ describe('metricsContext', () => {
|
|||
}
|
||||
)
|
||||
|
||||
it('metricsContext.propagate', () => {
|
||||
results.get = P.resolve('wibble')
|
||||
results.add = P.resolve()
|
||||
const oldToken = {
|
||||
uid: Array(64).fill('c').join(''),
|
||||
id: 'foo'
|
||||
}
|
||||
const newToken = {
|
||||
uid: Array(64).fill('d').join(''),
|
||||
id: 'bar'
|
||||
}
|
||||
return metricsContext.propagate(oldToken, newToken)
|
||||
.then(() => {
|
||||
assert.equal(cache.get.callCount, 1)
|
||||
let args = cache.get.args[0]
|
||||
assert.lengthOf(args, 1)
|
||||
assert.equal(args[0], hashToken(oldToken))
|
||||
|
||||
assert.equal(cache.add.callCount, 1)
|
||||
args = cache.add.args[0]
|
||||
assert.lengthOf(args, 2)
|
||||
assert.equal(args[0], hashToken(newToken))
|
||||
assert.equal(args[1], 'wibble')
|
||||
|
||||
assert.equal(cache.del.callCount, 0)
|
||||
assert.equal(log.warn.callCount, 0)
|
||||
assert.equal(log.error.callCount, 0)
|
||||
})
|
||||
})
|
||||
|
||||
it('metricsContext.propagate with clashing data', () => {
|
||||
results.get = P.resolve('wibble')
|
||||
results.add = P.reject('blee')
|
||||
const oldToken = {
|
||||
uid: Array(64).fill('c').join(''),
|
||||
id: 'foo'
|
||||
}
|
||||
const newToken = {
|
||||
uid: Array(64).fill('d').join(''),
|
||||
id: 'bar'
|
||||
}
|
||||
return metricsContext.propagate(oldToken, newToken)
|
||||
.then(() => {
|
||||
assert.equal(cache.get.callCount, 1)
|
||||
assert.equal(cache.add.callCount, 1)
|
||||
assert.equal(log.warn.callCount, 1)
|
||||
assert.equal(cache.del.callCount, 0)
|
||||
assert.equal(log.error.callCount, 0)
|
||||
})
|
||||
})
|
||||
|
||||
it('metricsContext.propagate with get error', () => {
|
||||
results.get = P.reject('wibble')
|
||||
results.add = P.resolve()
|
||||
const oldToken = {
|
||||
uid: Array(64).fill('c').join(''),
|
||||
id: 'foo'
|
||||
}
|
||||
const newToken = {
|
||||
uid: Array(64).fill('d').join(''),
|
||||
id: 'bar'
|
||||
}
|
||||
return metricsContext.propagate(oldToken, newToken)
|
||||
.then(() => {
|
||||
assert.equal(cache.get.callCount, 1)
|
||||
assert.equal(log.error.callCount, 1)
|
||||
assert.equal(cache.add.callCount, 0)
|
||||
assert.equal(log.warn.callCount, 0)
|
||||
assert.equal(cache.del.callCount, 0)
|
||||
})
|
||||
})
|
||||
|
||||
it(
|
||||
'metricsContext.clear with token',
|
||||
() => {
|
||||
|
|
|
@ -49,75 +49,86 @@ function runRoute(routes, name, request) {
|
|||
}
|
||||
|
||||
describe('/password', () => {
|
||||
it(
|
||||
'/forgot/send_code',
|
||||
() => {
|
||||
var mockCustoms = mocks.mockCustoms()
|
||||
var uid = uuid.v4('binary').toString('hex')
|
||||
var mockDB = mocks.mockDB({
|
||||
email: TEST_EMAIL,
|
||||
passCode: 'foo',
|
||||
passwordForgotTokenId: crypto.randomBytes(16).toString('hex'),
|
||||
uid: uid
|
||||
})
|
||||
var mockMailer = mocks.mockMailer()
|
||||
var mockMetricsContext = mocks.mockMetricsContext()
|
||||
var mockLog = log('ERROR', 'test', {
|
||||
stdout: {
|
||||
on: sinon.spy(),
|
||||
write: sinon.spy()
|
||||
},
|
||||
stderr: {
|
||||
on: sinon.spy(),
|
||||
write: sinon.spy()
|
||||
}
|
||||
})
|
||||
mockLog.flowEvent = sinon.spy(() => {
|
||||
return P.resolve()
|
||||
})
|
||||
var passwordRoutes = makeRoutes({
|
||||
customs: mockCustoms,
|
||||
db: mockDB,
|
||||
mailer : mockMailer,
|
||||
metricsContext: mockMetricsContext,
|
||||
log: mockLog
|
||||
})
|
||||
it('/forgot/send_code', () => {
|
||||
const mockCustoms = mocks.mockCustoms()
|
||||
const uid = uuid.v4('binary').toString('hex')
|
||||
const passwordForgotTokenId = crypto.randomBytes(16).toString('hex')
|
||||
const mockDB = mocks.mockDB({
|
||||
email: TEST_EMAIL,
|
||||
passCode: 'foo',
|
||||
passwordForgotTokenId,
|
||||
uid
|
||||
})
|
||||
const mockMailer = mocks.mockMailer()
|
||||
const mockMetricsContext = mocks.mockMetricsContext()
|
||||
const mockLog = log('ERROR', 'test', {
|
||||
stdout: {
|
||||
on: sinon.spy(),
|
||||
write: sinon.spy()
|
||||
},
|
||||
stderr: {
|
||||
on: sinon.spy(),
|
||||
write: sinon.spy()
|
||||
}
|
||||
})
|
||||
mockLog.flowEvent = sinon.spy(() => {
|
||||
return P.resolve()
|
||||
})
|
||||
const passwordRoutes = makeRoutes({
|
||||
customs: mockCustoms,
|
||||
db: mockDB,
|
||||
mailer : mockMailer,
|
||||
metricsContext: mockMetricsContext,
|
||||
log: mockLog
|
||||
})
|
||||
|
||||
var mockRequest = mocks.mockRequest({
|
||||
log: mockLog,
|
||||
payload: {
|
||||
email: TEST_EMAIL,
|
||||
metricsContext: {
|
||||
flowId: 'F1031DF1031DF1031DF1031DF1031DF1031DF1031DF1031DF1031DF1031DF103',
|
||||
flowBeginTime: Date.now() - 1
|
||||
}
|
||||
},
|
||||
query: {},
|
||||
metricsContext: mockMetricsContext
|
||||
})
|
||||
return runRoute(passwordRoutes, '/password/forgot/send_code', mockRequest)
|
||||
.then(function(response) {
|
||||
const mockRequest = mocks.mockRequest({
|
||||
log: mockLog,
|
||||
payload: {
|
||||
email: TEST_EMAIL,
|
||||
metricsContext: {
|
||||
flowId: 'F1031DF1031DF1031DF1031DF1031DF1031DF1031DF1031DF1031DF1031DF103',
|
||||
flowBeginTime: Date.now() - 1
|
||||
}
|
||||
},
|
||||
query: {},
|
||||
metricsContext: mockMetricsContext
|
||||
})
|
||||
return runRoute(passwordRoutes, '/password/forgot/send_code', mockRequest)
|
||||
.then(response => {
|
||||
assert.equal(mockDB.accountRecord.callCount, 1, 'db.emailRecord was called once')
|
||||
|
||||
assert.equal(mockDB.createPasswordForgotToken.callCount, 1, 'db.createPasswordForgotToken was called once')
|
||||
var args = mockDB.createPasswordForgotToken.args[0]
|
||||
let args = mockDB.createPasswordForgotToken.args[0]
|
||||
assert.equal(args.length, 1, 'db.createPasswordForgotToken was passed one argument')
|
||||
assert.deepEqual(args[0].uid, uid, 'db.createPasswordForgotToken was passed the correct uid')
|
||||
assert.equal(args[0].createdAt, undefined, 'db.createPasswordForgotToken was not passed a createdAt timestamp')
|
||||
|
||||
assert.equal(mockRequest.validateMetricsContext.callCount, 1, 'validateMetricsContext was called')
|
||||
|
||||
assert.equal(mockMetricsContext.setFlowCompleteSignal.callCount, 1)
|
||||
args = mockMetricsContext.setFlowCompleteSignal.args[0]
|
||||
assert.lengthOf(args, 1)
|
||||
assert.equal(args[0], 'account.reset')
|
||||
|
||||
assert.equal(mockMetricsContext.stash.callCount, 1)
|
||||
args = mockMetricsContext.stash.args[0]
|
||||
assert.lengthOf(args, 1)
|
||||
assert.equal(args[0].id, passwordForgotTokenId)
|
||||
assert.equal(args[0].uid, uid)
|
||||
|
||||
assert.equal(mockLog.flowEvent.callCount, 2, 'log.flowEvent was called twice')
|
||||
assert.equal(mockLog.flowEvent.args[0][0].event, 'password.forgot.send_code.start', 'password.forgot.send_code.start event was logged')
|
||||
assert.equal(mockLog.flowEvent.args[1][0].event, 'password.forgot.send_code.completed', 'password.forgot.send_code.completed event was logged')
|
||||
|
||||
assert.equal(mockMailer.sendRecoveryCode.callCount, 1, 'mailer.sendRecoveryCode was called once')
|
||||
assert.equal(mockMailer.sendRecoveryCode.getCall(0).args[2].location.city, 'Mountain View')
|
||||
assert.equal(mockMailer.sendRecoveryCode.getCall(0).args[2].location.country, 'United States')
|
||||
assert.equal(mockMailer.sendRecoveryCode.getCall(0).args[2].timeZone, 'America/Los_Angeles')
|
||||
assert.equal(mockMailer.sendRecoveryCode.getCall(0).args[2].uid, uid)
|
||||
args = mockMailer.sendRecoveryCode.args[0]
|
||||
assert.equal(args[2].location.city, 'Mountain View')
|
||||
assert.equal(args[2].location.country, 'United States')
|
||||
assert.equal(args[2].timeZone, 'America/Los_Angeles')
|
||||
assert.equal(args[2].uid, uid)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it(
|
||||
'/forgot/resend_code',
|
||||
|
@ -157,6 +168,7 @@ describe('/password', () => {
|
|||
uid: uid
|
||||
},
|
||||
log: mockLog,
|
||||
metricsContext: mockMetricsContext,
|
||||
payload: {
|
||||
email: TEST_EMAIL,
|
||||
metricsContext: {
|
||||
|
@ -164,8 +176,7 @@ describe('/password', () => {
|
|||
flowBeginTime: Date.now() - 1
|
||||
}
|
||||
},
|
||||
query: {},
|
||||
metricsContext: mockMetricsContext
|
||||
query: {}
|
||||
})
|
||||
return runRoute(passwordRoutes, '/password/forgot/resend_code', mockRequest)
|
||||
.then(function(response) {
|
||||
|
@ -180,69 +191,72 @@ describe('/password', () => {
|
|||
}
|
||||
)
|
||||
|
||||
it(
|
||||
'/forgot/verify_code',
|
||||
() => {
|
||||
var mockCustoms = mocks.mockCustoms()
|
||||
var uid = uuid.v4('binary').toString('hex')
|
||||
var accountResetToken = {
|
||||
data: crypto.randomBytes(16).toString('hex')
|
||||
it('/forgot/verify_code', () => {
|
||||
const mockCustoms = mocks.mockCustoms()
|
||||
const uid = uuid.v4('binary').toString('hex')
|
||||
const accountResetToken = {
|
||||
data: crypto.randomBytes(16).toString('hex'),
|
||||
id: crypto.randomBytes(16).toString('hex'),
|
||||
uid
|
||||
}
|
||||
const passwordForgotTokenId = crypto.randomBytes(16).toString('hex')
|
||||
const mockDB = mocks.mockDB({
|
||||
accountResetToken: accountResetToken,
|
||||
email: TEST_EMAIL,
|
||||
passCode: 'abcdef',
|
||||
passwordForgotTokenId,
|
||||
uid
|
||||
})
|
||||
const mockMailer = mocks.mockMailer()
|
||||
const mockMetricsContext = mocks.mockMetricsContext()
|
||||
const mockLog = log('ERROR', 'test', {
|
||||
stdout: {
|
||||
on: sinon.spy(),
|
||||
write: sinon.spy()
|
||||
},
|
||||
stderr: {
|
||||
on: sinon.spy(),
|
||||
write: sinon.spy()
|
||||
}
|
||||
var mockDB = mocks.mockDB({
|
||||
accountResetToken: accountResetToken,
|
||||
email: TEST_EMAIL,
|
||||
passCode: 'abcdef',
|
||||
passwordForgotTokenId: crypto.randomBytes(16).toString('hex'),
|
||||
uid: uid
|
||||
})
|
||||
var mockMailer = mocks.mockMailer()
|
||||
var mockMetricsContext = mocks.mockMetricsContext()
|
||||
var mockLog = log('ERROR', 'test', {
|
||||
stdout: {
|
||||
on: sinon.spy(),
|
||||
write: sinon.spy()
|
||||
},
|
||||
stderr: {
|
||||
on: sinon.spy(),
|
||||
write: sinon.spy()
|
||||
}
|
||||
})
|
||||
mockLog.flowEvent = sinon.spy(() => {
|
||||
return P.resolve()
|
||||
})
|
||||
var passwordRoutes = makeRoutes({
|
||||
customs: mockCustoms,
|
||||
db: mockDB,
|
||||
mailer: mockMailer,
|
||||
metricsContext: mockMetricsContext
|
||||
})
|
||||
})
|
||||
mockLog.flowEvent = sinon.spy(() => {
|
||||
return P.resolve()
|
||||
})
|
||||
const passwordRoutes = makeRoutes({
|
||||
customs: mockCustoms,
|
||||
db: mockDB,
|
||||
mailer: mockMailer,
|
||||
metricsContext: mockMetricsContext
|
||||
})
|
||||
|
||||
var mockRequest = mocks.mockRequest({
|
||||
log: mockLog,
|
||||
credentials: {
|
||||
email: TEST_EMAIL,
|
||||
passCode: Buffer.from('abcdef', 'hex'),
|
||||
ttl: function () { return 17 },
|
||||
uid: uid
|
||||
},
|
||||
payload: {
|
||||
code: 'abcdef',
|
||||
metricsContext: {
|
||||
flowId: 'F1031DF1031DF1031DF1031DF1031DF1031DF1031DF1031DF1031DF1031DF103',
|
||||
flowBeginTime: Date.now() - 1
|
||||
}
|
||||
},
|
||||
query: {}
|
||||
})
|
||||
return runRoute(passwordRoutes, '/password/forgot/verify_code', mockRequest)
|
||||
.then(function(response) {
|
||||
const mockRequest = mocks.mockRequest({
|
||||
log: mockLog,
|
||||
credentials: {
|
||||
email: TEST_EMAIL,
|
||||
id: passwordForgotTokenId,
|
||||
passCode: Buffer.from('abcdef', 'hex'),
|
||||
ttl: function () { return 17 },
|
||||
uid
|
||||
},
|
||||
metricsContext: mockMetricsContext,
|
||||
payload: {
|
||||
code: 'abcdef',
|
||||
metricsContext: {
|
||||
flowId: 'F1031DF1031DF1031DF1031DF1031DF1031DF1031DF1031DF1031DF1031DF103',
|
||||
flowBeginTime: Date.now() - 1
|
||||
}
|
||||
},
|
||||
query: {}
|
||||
})
|
||||
return runRoute(passwordRoutes, '/password/forgot/verify_code', mockRequest)
|
||||
.then(response => {
|
||||
assert.deepEqual(Object.keys(response), ['accountResetToken'], 'an accountResetToken was returned')
|
||||
assert.equal(response.accountResetToken, accountResetToken.data.toString('hex'), 'correct accountResetToken was returned')
|
||||
|
||||
assert.equal(mockCustoms.check.callCount, 1, 'customs.check was called once')
|
||||
|
||||
assert.equal(mockDB.forgotPasswordVerified.callCount, 1, 'db.passwordForgotVerified was called once')
|
||||
var args = mockDB.forgotPasswordVerified.args[0]
|
||||
let args = mockDB.forgotPasswordVerified.args[0]
|
||||
assert.equal(args.length, 1, 'db.passwordForgotVerified was passed one argument')
|
||||
assert.deepEqual(args[0].uid, uid, 'db.forgotPasswordVerified was passed the correct token')
|
||||
|
||||
|
@ -251,6 +265,14 @@ describe('/password', () => {
|
|||
assert.equal(mockLog.flowEvent.args[0][0].event, 'password.forgot.verify_code.start', 'password.forgot.verify_code.start event was logged')
|
||||
assert.equal(mockLog.flowEvent.args[1][0].event, 'password.forgot.verify_code.completed', 'password.forgot.verify_code.completed event was logged')
|
||||
|
||||
assert.equal(mockMetricsContext.propagate.callCount, 1)
|
||||
args = mockMetricsContext.propagate.args[0]
|
||||
assert.lengthOf(args, 2)
|
||||
assert.equal(args[0].id, passwordForgotTokenId)
|
||||
assert.equal(args[0].uid, uid)
|
||||
assert.equal(args[1].id, accountResetToken.id)
|
||||
assert.equal(args[1].uid, uid)
|
||||
|
||||
assert.equal(mockMailer.sendPasswordResetNotification.callCount, 1, 'mailer.sendPasswordResetNotification was called once')
|
||||
assert.equal(mockMailer.sendPasswordResetNotification.args[0][2].uid, uid, 'mailer.sendPasswordResetNotification was passed uid')
|
||||
})
|
||||
|
|
|
@ -121,6 +121,7 @@ const MAILER_METHOD_NAMES = [
|
|||
const METRICS_CONTEXT_METHOD_NAMES = [
|
||||
'clear',
|
||||
'gather',
|
||||
'propagate',
|
||||
'setFlowCompleteSignal',
|
||||
'stash',
|
||||
'validate'
|
||||
|
@ -566,10 +567,11 @@ function mockRequest (data, errors) {
|
|||
info: {
|
||||
received: data.received || Date.now() - 1
|
||||
},
|
||||
path: data.path,
|
||||
params: data.params || {},
|
||||
method: data.method || undefined,
|
||||
params: data.params || {},
|
||||
path: data.path,
|
||||
payload: data.payload || {},
|
||||
propagateMetricsContext: metricsContext.propagate,
|
||||
query: data.query || {},
|
||||
setMetricsFlowCompleteSignal: metricsContext.setFlowCompleteSignal,
|
||||
stashMetricsContext: metricsContext.stash,
|
||||
|
|
Загрузка…
Ссылка в новой задаче