fxa-auth-server/test/mocks.js

381 строка
9.8 KiB
JavaScript

/* 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/. */
/*
* Shared helpers for mocking things out in the tests.
*/
const sinon = require('sinon')
const extend = require('util')._extend
const P = require('../lib/promise')
const crypto = require('crypto')
const config = require('../config').getProperties()
const CUSTOMS_METHOD_NAMES = [
'check',
'checkAuthenticated',
'flag',
'reset'
]
const DB_METHOD_NAMES = [
'account',
'accountResetToken',
'consumeUnblockCode',
'createAccount',
'createDevice',
'createEmailBounce',
'createKeyFetchToken',
'createPasswordForgotToken',
'createSessionToken',
'createUnblockCode',
'createVerificationReminder',
'deleteAccount',
'deleteDevice',
'deleteKeyFetchToken',
'deletePasswordChangeToken',
'deleteSessionToken',
'deleteVerificationReminder',
'deviceFromTokenVerificationId',
'devices',
'emailBounces',
'emailRecord',
'forgotPasswordVerified',
'keyFetchToken',
'keyFetchTokenWithVerificationStatus',
'passwordChangeToken',
'passwordForgotToken',
'resetAccount',
'securityEvent',
'securityEvents',
'sessions',
'sessionToken',
'sessionTokenWithVerificationStatus',
'sessionWithDevice',
'updateDevice',
'updateLocale',
'updateSessionToken',
'verifyEmail',
'verifyTokens'
]
const LOG_METHOD_NAMES = [
'activityEvent',
'begin',
'error',
'flowEvent',
'increment',
'info',
'notifyAttachedServices',
'warn',
'summary',
'timing',
'trace'
]
const MAILER_METHOD_NAMES = [
'sendNewDeviceLoginNotification',
'sendPasswordChangedNotification',
'sendPasswordResetNotification',
'sendPostVerifyEmail',
'sendUnblockCode',
'sendVerifyCode',
'sendVerifyLoginEmail',
'sendRecoveryCode'
]
const METRICS_CONTEXT_METHOD_NAMES = [
'clear',
'gather',
'setFlowCompleteSignal',
'stash',
'validate'
]
const PUSH_METHOD_NAMES = [
'notifyDeviceConnected',
'notifyDeviceDisconnected',
'notifyPasswordChanged',
'notifyPasswordReset',
'notifyUpdate',
'pushToAllDevices',
'pushToDevices'
]
module.exports = {
generateMetricsContext: generateMetricsContext,
mockCustoms: mockObject(CUSTOMS_METHOD_NAMES),
mockDB: mockDB,
mockDevices: mockDevices,
mockLog: mockLog,
spyLog: spyLog,
mockMailer: mockObject(MAILER_METHOD_NAMES),
mockMetricsContext: mockMetricsContext,
mockPush: mockObject(PUSH_METHOD_NAMES),
mockRequest: mockRequest
}
function mockDB (data, errors) {
data = data || {}
errors = errors || {}
return mockObject(DB_METHOD_NAMES)({
account: sinon.spy(() => {
return P.resolve({
email: data.email,
emailCode: data.emailCode,
emailVerified: data.emailVerified,
uid: data.uid,
verifierSetAt: Date.now(),
wrapWrapKb: data.wrapWrapKb
})
}),
createAccount: sinon.spy(() => {
return P.resolve({
uid: data.uid,
email: data.email,
emailCode: data.emailCode,
emailVerified: data.emailVerified,
locale: data.locale,
wrapWrapKb: data.wrapWrapKb
})
}),
createDevice: sinon.spy(() => {
return P.resolve(Object.keys(data.device).reduce((result, key) => {
result[key] = data.device[key]
return result
}, {
id: data.deviceId,
createdAt: data.deviceCreatedAt
}))
}),
createKeyFetchToken: sinon.spy(() => {
return P.resolve({
data: crypto.randomBytes(32),
tokenId: data.keyFetchTokenId,
uid: data.uid
})
}),
createPasswordForgotToken: sinon.spy(() => {
return P.resolve({
data: crypto.randomBytes(32),
passCode: data.passCode,
tokenId: data.passwordForgotTokenId,
uid: data.uid,
ttl: function () {
return data.passwordForgotTokenTTL || 100
}
})
}),
createSessionToken: sinon.spy(() => {
return P.resolve({
data: crypto.randomBytes(32),
email: data.email,
emailVerified: data.emailVerified,
lastAuthAt: () => {
return Date.now()
},
tokenId: data.sessionTokenId,
tokenVerificationId: data.tokenVerificationId,
tokenVerified: ! data.tokenVerificationId,
uaBrowser: data.uaBrowser,
uaBrowserVersion: data.uaBrowserVersion,
uaOS: data.uaOS,
uaOSVersion: data.uaOSVersion,
uaDeviceType: data.uaDeviceType,
uid: data.uid
})
}),
devices: sinon.spy(() => {
return P.resolve(data.devices || [])
}),
deleteSessionToken: sinon.spy(() => {
return P.resolve()
}),
emailRecord: sinon.spy(() => {
if (errors.emailRecord) {
return P.reject(errors.emailRecord)
}
return P.resolve({
authSalt: crypto.randomBytes(32),
createdAt: data.createdAt || Date.now(),
data: crypto.randomBytes(32),
email: data.email,
emailVerified: data.emailVerified,
kA: crypto.randomBytes(32),
lastAuthAt: () => {
return Date.now()
},
uid: data.uid,
wrapWrapKb: crypto.randomBytes(32)
})
}),
forgotPasswordVerified: sinon.spy(() => {
return P.resolve(data.accountResetToken)
}),
securityEvents: sinon.spy(() => {
return P.resolve([])
}),
sessions: sinon.spy(() => {
return P.resolve(data.sessions || [])
}),
updateDevice: sinon.spy((uid, sessionTokenId, device) => {
return P.resolve(device)
}),
sessionTokenWithVerificationStatus: sinon.spy(() => {
return P.resolve({
tokenVerified: true,
uaBrowser: data.uaBrowser,
uaBrowserVersion: data.uaBrowserVersion,
uaOS: data.uaOS,
uaOSVersion: data.uaOSVersion,
uaDeviceType: data.uaDeviceType
})
}),
verifyTokens: sinon.spy(() => {
if (errors.verifyTokens) {
return P.reject(errors.verifyTokens)
}
return P.resolve()
})
})
}
function mockObject (methodNames) {
return methods => {
return methodNames.reduce((object, name) => {
object[name] = methods && methods[name] || sinon.spy(() => P.resolve())
return object
}, {})
}
}
function mockDevices (data) {
data = data || {}
return {
upsert: sinon.spy(() => {
return P.resolve({
id: data.deviceId || crypto.randomBytes(16),
name: data.deviceName || 'mock device name',
type: data.deviceType || 'desktop'
})
}),
synthesizeName: sinon.spy(() => {
return data.deviceName || null
})
}
}
// A logging mock that doesn't capture anything.
// You can pass in an object of custom logging methods
// if you need to e.g. make assertions about logged values.
function mockLog (methods) {
const log = extend({}, methods)
LOG_METHOD_NAMES.forEach((name) => {
if (! log[name]) {
log[name] = function() {}
}
})
return log
}
// A logging mock where all logging methods are sinon spys,
// and we capture a log of all their calls in order.
function spyLog (methods) {
methods = extend({}, methods)
methods.messages = methods.messages || []
LOG_METHOD_NAMES.forEach(name => {
if (! methods[name]) {
// arrow function would alter `this` inside the method
methods[name] = function() {
this.messages.push({
level: name,
args: Array.prototype.slice.call(arguments)
})
}
}
methods[name] = sinon.spy(methods[name])
})
return mockLog(methods)
}
function mockMetricsContext (methods) {
methods = methods || {}
return mockObject(METRICS_CONTEXT_METHOD_NAMES)({
gather: methods.gather || sinon.spy(function (data) {
const time = Date.now()
return P.resolve()
.then(() => {
if (this.payload && this.payload.metricsContext) {
return Object.assign(data, {
time: time,
flow_id: this.payload.metricsContext.flowId,
flow_time: time - this.payload.metricsContext.flowBeginTime,
flowCompleteSignal: this.payload.metricsContext.flowCompleteSignal
})
}
return data
})
}),
setFlowCompleteSignal: sinon.spy(function (flowCompleteSignal) {
if (this.payload && this.payload.metricsContext) {
this.payload.metricsContext.flowCompleteSignal = flowCompleteSignal
}
}),
validate: methods.validate || sinon.spy(() => true)
})
}
function generateMetricsContext(){
const randomBytes = crypto.randomBytes(16).toString('hex')
const flowBeginTime = Date.now()
const flowSignature = crypto.createHmac('sha256', config.metrics.flow_id_key)
.update([
randomBytes,
flowBeginTime.toString(16),
undefined
].join('\n'))
.digest('hex')
.substr(0, 32)
return {
flowBeginTime: flowBeginTime,
flowId: randomBytes + flowSignature
}
}
function mockRequest (data) {
const events = require('../lib/metrics/events')(data.log || module.exports.mockLog())
const metricsContext = data.metricsContext || module.exports.mockMetricsContext()
return {
app: {
acceptLanguage: 'en-US',
clientAddress: data.clientAddress || '63.245.221.32',
isLocaleAcceptable: true,
locale: data.locale || 'en-US'
},
auth: {
credentials: data.credentials
},
clearMetricsContext: metricsContext.clear,
emitMetricsEvent: events.emit,
emitRouteFlowEvent: events.emitRouteFlowEvent,
gatherMetricsContext: metricsContext.gather,
headers: data.headers || {
'user-agent': 'test user-agent'
},
path: data.path,
payload: data.payload,
query: data.query,
setMetricsFlowCompleteSignal: metricsContext.setFlowCompleteSignal,
stashMetricsContext: metricsContext.stash,
validateMetricsContext: metricsContext.validate
}
}