diff --git a/packages/fxa-js-client/Gruntfile.js b/packages/fxa-js-client/Gruntfile.js index 7a88b65a2d8..5623b4ab167 100644 --- a/packages/fxa-js-client/Gruntfile.js +++ b/packages/fxa-js-client/Gruntfile.js @@ -5,8 +5,6 @@ module.exports = function(grunt) { // load all grunt tasks matching the `grunt-*` pattern require('load-grunt-tasks')(grunt); - // load the Intern tasks - grunt.loadNpmTasks('intern-geezer'); var pkg = grunt.file.readJSON('package.json'); @@ -25,11 +23,6 @@ module.exports = function(grunt) { 'bytesize', ]); - grunt.registerTask('test', 'Run tests via node', [ - 'intern:node', - 'intern:native_node', - ]); - grunt.registerTask('lint', 'Alias for eslint', ['eslint']); grunt.registerTask('default', ['build']); diff --git a/packages/fxa-js-client/client/FxAccountClient.js b/packages/fxa-js-client/client/FxAccountClient.js index a3f8eddfdf3..f7c51caf5e8 100644 --- a/packages/fxa-js-client/client/FxAccountClient.js +++ b/packages/fxa-js-client/client/FxAccountClient.js @@ -1,374 +1,471 @@ /* 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/. */ -define([ - 'es6-promise', - 'sjcl', - './lib/credentials', - './lib/errors', - './lib/hawkCredentials', - './lib/metricsContext', - './lib/request', -], function( - ES6Promise, - sjcl, - credentials, - ERRORS, - hawkCredentials, - metricsContext, - Request -) { - 'use strict'; - - // polyfill ES6 promises on browsers that do not support them. - ES6Promise.polyfill(); - var VERSION = 'v1'; - var uriVersionRegExp = new RegExp('/' + VERSION + '$'); - var HKDF_SIZE = 2 * 32; - - function isUndefined(val) { - return typeof val === 'undefined'; +'use strict'; + +const ES6Promise = require('es6-promise'); +const sjcl = require('sjcl'); +const credentials = require('./lib/credentials'); +const ERRORS = require('./lib/errors'); +const hawkCredentials = require('./lib/hawkCredentials'); +const metricsContext = require('./lib/metricsContext'); +const Request = require('./lib/request'); + +// polyfill ES6 promises on browsers that do not support them. +ES6Promise.polyfill(); + +var VERSION = 'v1'; +var uriVersionRegExp = new RegExp('/' + VERSION + '$'); +var HKDF_SIZE = 2 * 32; + +function isUndefined(val) { + return typeof val === 'undefined'; +} + +function isNull(val) { + return val === null; +} + +function isEmptyObject(val) { + return ( + Object.prototype.toString.call(val) === '[object Object]' && + !Object.keys(val).length + ); +} + +function isEmptyString(val) { + return val === ''; +} + +function required(val, name) { + if ( + isUndefined(val) || + isNull(val) || + isEmptyObject(val) || + isEmptyString(val) + ) { + throw new Error('Missing ' + name); + } +} + +/** + * @class FxAccountClient + * @constructor + * @param {String} uri Auth Server URI + * @param {Object} config Configuration + */ +function FxAccountClient(uri, config) { + if (!uri && !config) { + throw new Error( + 'Firefox Accounts auth server endpoint or configuration object required.' + ); } - function isNull(val) { - return val === null; + if (typeof uri !== 'string') { + config = uri || {}; + uri = config.uri; } - function isEmptyObject(val) { - return ( - Object.prototype.toString.call(val) === '[object Object]' && - !Object.keys(val).length - ); + if (typeof config === 'undefined') { + config = {}; } - function isEmptyString(val) { - return val === ''; + if (!uri) { + throw new Error('FxA auth server uri not set.'); } - function required(val, name) { - if ( - isUndefined(val) || - isNull(val) || - isEmptyObject(val) || - isEmptyString(val) - ) { - throw new Error('Missing ' + name); - } + if (!uriVersionRegExp.test(uri)) { + uri = uri + '/' + VERSION; } - /** - * @class FxAccountClient - * @constructor - * @param {String} uri Auth Server URI - * @param {Object} config Configuration - */ - function FxAccountClient(uri, config) { - if (!uri && !config) { - throw new Error( - 'Firefox Accounts auth server endpoint or configuration object required.' - ); - } + this.request = new Request(uri, config.xhr, { + localtimeOffsetMsec: config.localtimeOffsetMsec, + }); +} + +FxAccountClient.VERSION = VERSION; + +/** + * @method signUp + * @param {String} email Email input + * @param {String} password Password input + * @param {Object} [options={}] Options + * @param {Boolean} [options.keys] + * If `true`, calls the API with `?keys=true` to get the keyFetchToken + * @param {String} [options.service] + * Opaque alphanumeric token to be included in verification links + * @param {String} [options.redirectTo] + * a URL that the client should be redirected to after handling the request + * @param {String} [options.preVerified] + * set email to be verified if possible + * @param {String} [options.resume] + * Opaque url-encoded string that will be included in the verification link + * as a querystring parameter, useful for continuing an OAuth flow for + * example. + * @param {String} [options.lang] + * set the language for the 'Accept-Language' header + * @param {String} [options.style] + * Specify the style of confirmation emails + * @param {Object} [options.metricsContext={}] Metrics context metadata + * @param {String} options.metricsContext.deviceId identifier for the current device + * @param {String} options.metricsContext.flowId identifier for the current event flow + * @param {Number} options.metricsContext.flowBeginTime flow.begin event time + * @param {Number} options.metricsContext.utmCampaign marketing campaign identifier + * @param {Number} options.metricsContext.utmContent content identifier + * @param {Number} options.metricsContext.utmMedium acquisition medium + * @param {Number} options.metricsContext.utmSource traffic source + * @param {Number} options.metricsContext.utmTerm search terms + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.signUp = function(email, password, options) { + var self = this; + + return Promise.resolve() + .then(function() { + required(email, 'email'); + required(password, 'password'); - if (typeof uri !== 'string') { - config = uri || {}; - uri = config.uri; - } + return credentials.setup(email, password); + }) + .then(function(result) { + /*eslint complexity: [2, 13] */ + var endpoint = '/account/create'; + var data = { + email: result.emailUTF8, + authPW: sjcl.codec.hex.fromBits(result.authPW), + }; + var requestOpts = {}; - if (typeof config === 'undefined') { - config = {}; - } + if (options) { + if (options.service) { + data.service = options.service; + } - if (!uri) { - throw new Error('FxA auth server uri not set.'); - } + if (options.redirectTo) { + data.redirectTo = options.redirectTo; + } - if (!uriVersionRegExp.test(uri)) { - uri = uri + '/' + VERSION; - } + // preVerified is used for unit/functional testing + if (options.preVerified) { + data.preVerified = options.preVerified; + } - this.request = new Request(uri, config.xhr, { - localtimeOffsetMsec: config.localtimeOffsetMsec, - }); - } + if (options.resume) { + data.resume = options.resume; + } - FxAccountClient.VERSION = VERSION; - - /** - * @method signUp - * @param {String} email Email input - * @param {String} password Password input - * @param {Object} [options={}] Options - * @param {Boolean} [options.keys] - * If `true`, calls the API with `?keys=true` to get the keyFetchToken - * @param {String} [options.service] - * Opaque alphanumeric token to be included in verification links - * @param {String} [options.redirectTo] - * a URL that the client should be redirected to after handling the request - * @param {String} [options.preVerified] - * set email to be verified if possible - * @param {String} [options.resume] - * Opaque url-encoded string that will be included in the verification link - * as a querystring parameter, useful for continuing an OAuth flow for - * example. - * @param {String} [options.lang] - * set the language for the 'Accept-Language' header - * @param {String} [options.style] - * Specify the style of confirmation emails - * @param {Object} [options.metricsContext={}] Metrics context metadata - * @param {String} options.metricsContext.deviceId identifier for the current device - * @param {String} options.metricsContext.flowId identifier for the current event flow - * @param {Number} options.metricsContext.flowBeginTime flow.begin event time - * @param {Number} options.metricsContext.utmCampaign marketing campaign identifier - * @param {Number} options.metricsContext.utmContent content identifier - * @param {Number} options.metricsContext.utmMedium acquisition medium - * @param {Number} options.metricsContext.utmSource traffic source - * @param {Number} options.metricsContext.utmTerm search terms - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.signUp = function(email, password, options) { - var self = this; - - return Promise.resolve() - .then(function() { - required(email, 'email'); - required(password, 'password'); - - return credentials.setup(email, password); - }) - .then(function(result) { - /*eslint complexity: [2, 13] */ - var endpoint = '/account/create'; - var data = { - email: result.emailUTF8, - authPW: sjcl.codec.hex.fromBits(result.authPW), - }; - var requestOpts = {}; + if (options.keys) { + endpoint += '?keys=true'; + } - if (options) { - if (options.service) { - data.service = options.service; - } + if (options.lang) { + requestOpts.headers = { + 'Accept-Language': options.lang, + }; + } - if (options.redirectTo) { - data.redirectTo = options.redirectTo; - } + if (options.metricsContext) { + data.metricsContext = metricsContext.marshall(options.metricsContext); + } - // preVerified is used for unit/functional testing - if (options.preVerified) { - data.preVerified = options.preVerified; - } + if (options.style) { + data.style = options.style; + } + } - if (options.resume) { - data.resume = options.resume; + return self.request + .send(endpoint, 'POST', null, data, requestOpts) + .then(function(accountData) { + if (options && options.keys) { + accountData.unwrapBKey = sjcl.codec.hex.fromBits(result.unwrapBKey); } + return accountData; + }); + }); +}; + +/** + * @method signIn + * @param {String} email Email input + * @param {String} password Password input + * @param {Object} [options={}] Options + * @param {Boolean} [options.keys] + * If `true`, calls the API with `?keys=true` to get the keyFetchToken + * @param {Boolean} [options.skipCaseError] + * If `true`, the request will skip the incorrect case error + * @param {String} [options.service] + * Service being signed into + * @param {String} [options.reason] + * Reason for sign in. Can be one of: `signin`, `password_check`, + * `password_change`, `password_reset` + * @param {String} [options.redirectTo] + * a URL that the client should be redirected to after handling the request + * @param {String} [options.resume] + * Opaque url-encoded string that will be included in the verification link + * as a querystring parameter, useful for continuing an OAuth flow for + * example. + * @param {String} [options.originalLoginEmail] + * If retrying after an "incorrect email case" error, this specifies + * the email address as originally entered by the user. + * @param {String} [options.verificationMethod] + * Request a specific verification method be used for verifying the session, + * e.g. 'email-2fa' or 'totp-2fa'. + * @param {Object} [options.metricsContext={}] Metrics context metadata + * @param {String} options.metricsContext.deviceId identifier for the current device + * @param {String} options.metricsContext.flowId identifier for the current event flow + * @param {Number} options.metricsContext.flowBeginTime flow.begin event time + * @param {Number} options.metricsContext.utmCampaign marketing campaign identifier + * @param {Number} options.metricsContext.utmContent content identifier + * @param {Number} options.metricsContext.utmMedium acquisition medium + * @param {Number} options.metricsContext.utmSource traffic source + * @param {Number} options.metricsContext.utmTerm search terms + * @param {String} [options.unblockCode] + * Login unblock code. + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.signIn = function(email, password, options) { + var self = this; + options = options || {}; + + return Promise.resolve() + .then(function() { + required(email, 'email'); + required(password, 'password'); - if (options.keys) { - endpoint += '?keys=true'; - } + return credentials.setup(email, password); + }) + .then(function(result) { + var endpoint = '/account/login'; - if (options.lang) { - requestOpts.headers = { - 'Accept-Language': options.lang, - }; - } + if (options.keys) { + endpoint += '?keys=true'; + } - if (options.metricsContext) { - data.metricsContext = metricsContext.marshall( - options.metricsContext - ); - } + var data = { + email: result.emailUTF8, + authPW: sjcl.codec.hex.fromBits(result.authPW), + }; - if (options.style) { - data.style = options.style; - } - } + if (options.metricsContext) { + data.metricsContext = metricsContext.marshall(options.metricsContext); + } - return self.request - .send(endpoint, 'POST', null, data, requestOpts) - .then(function(accountData) { - if (options && options.keys) { - accountData.unwrapBKey = sjcl.codec.hex.fromBits( - result.unwrapBKey - ); - } - return accountData; - }); - }); - }; + if (options.reason) { + data.reason = options.reason; + } - /** - * @method signIn - * @param {String} email Email input - * @param {String} password Password input - * @param {Object} [options={}] Options - * @param {Boolean} [options.keys] - * If `true`, calls the API with `?keys=true` to get the keyFetchToken - * @param {Boolean} [options.skipCaseError] - * If `true`, the request will skip the incorrect case error - * @param {String} [options.service] - * Service being signed into - * @param {String} [options.reason] - * Reason for sign in. Can be one of: `signin`, `password_check`, - * `password_change`, `password_reset` - * @param {String} [options.redirectTo] - * a URL that the client should be redirected to after handling the request - * @param {String} [options.resume] - * Opaque url-encoded string that will be included in the verification link - * as a querystring parameter, useful for continuing an OAuth flow for - * example. - * @param {String} [options.originalLoginEmail] - * If retrying after an "incorrect email case" error, this specifies - * the email address as originally entered by the user. - * @param {String} [options.verificationMethod] - * Request a specific verification method be used for verifying the session, - * e.g. 'email-2fa' or 'totp-2fa'. - * @param {Object} [options.metricsContext={}] Metrics context metadata - * @param {String} options.metricsContext.deviceId identifier for the current device - * @param {String} options.metricsContext.flowId identifier for the current event flow - * @param {Number} options.metricsContext.flowBeginTime flow.begin event time - * @param {Number} options.metricsContext.utmCampaign marketing campaign identifier - * @param {Number} options.metricsContext.utmContent content identifier - * @param {Number} options.metricsContext.utmMedium acquisition medium - * @param {Number} options.metricsContext.utmSource traffic source - * @param {Number} options.metricsContext.utmTerm search terms - * @param {String} [options.unblockCode] - * Login unblock code. - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.signIn = function(email, password, options) { - var self = this; - options = options || {}; - - return Promise.resolve() - .then(function() { - required(email, 'email'); - required(password, 'password'); - - return credentials.setup(email, password); - }) - .then(function(result) { - var endpoint = '/account/login'; + if (options.redirectTo) { + data.redirectTo = options.redirectTo; + } - if (options.keys) { - endpoint += '?keys=true'; - } + if (options.resume) { + data.resume = options.resume; + } - var data = { - email: result.emailUTF8, - authPW: sjcl.codec.hex.fromBits(result.authPW), - }; + if (options.service) { + data.service = options.service; + } - if (options.metricsContext) { - data.metricsContext = metricsContext.marshall(options.metricsContext); - } + if (options.unblockCode) { + data.unblockCode = options.unblockCode; + } - if (options.reason) { - data.reason = options.reason; - } + if (options.originalLoginEmail) { + data.originalLoginEmail = options.originalLoginEmail; + } - if (options.redirectTo) { - data.redirectTo = options.redirectTo; - } + if (options.verificationMethod) { + data.verificationMethod = options.verificationMethod; + } - if (options.resume) { - data.resume = options.resume; + return self.request.send(endpoint, 'POST', null, data).then( + function(accountData) { + if (options.keys) { + accountData.unwrapBKey = sjcl.codec.hex.fromBits(result.unwrapBKey); + } + return accountData; + }, + function(error) { + if ( + error && + error.email && + error.errno === ERRORS.INCORRECT_EMAIL_CASE && + !options.skipCaseError + ) { + options.skipCaseError = true; + options.originalLoginEmail = email; + + return self.signIn(error.email, password, options); + } else { + throw error; + } } + ); + }); +}; + +/** + * @method verifyCode + * @param {String} uid Account ID + * @param {String} code Verification code + * @param {Object} [options={}] Options + * @param {String} [options.service] + * Service being signed into + * @param {String} [options.reminder] + * Reminder that was used to verify the account + * @param {String} [options.type] + * Type of code being verified, only supports `secondary` otherwise will verify account/sign-in + * @param {Boolean} [options.marketingOptIn] + * If `true`, notifies marketing of opt-in intent. + * @param {Array} [options.newsletters] + * Array of newsletters to opt in or out of. + * @param {String} [options.style] + * Specify the style of email to send. + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.verifyCode = function(uid, code, options) { + var self = this; + + return Promise.resolve().then(function() { + required(uid, 'uid'); + required(code, 'verify code'); - if (options.service) { - data.service = options.service; - } + var data = { + uid: uid, + code: code, + }; - if (options.unblockCode) { - data.unblockCode = options.unblockCode; - } + if (options) { + if (options.service) { + data.service = options.service; + } - if (options.originalLoginEmail) { - data.originalLoginEmail = options.originalLoginEmail; - } + if (options.reminder) { + data.reminder = options.reminder; + } - if (options.verificationMethod) { - data.verificationMethod = options.verificationMethod; - } + if (options.type) { + data.type = options.type; + } - return self.request.send(endpoint, 'POST', null, data).then( - function(accountData) { - if (options.keys) { - accountData.unwrapBKey = sjcl.codec.hex.fromBits( - result.unwrapBKey - ); - } - return accountData; - }, - function(error) { - if ( - error && - error.email && - error.errno === ERRORS.INCORRECT_EMAIL_CASE && - !options.skipCaseError - ) { - options.skipCaseError = true; - options.originalLoginEmail = email; + if (options.marketingOptIn) { + data.marketingOptIn = true; + } - return self.signIn(error.email, password, options); - } else { - throw error; - } - } - ); - }); - }; + if (options.newsletters) { + data.newsletters = options.newsletters; + } + + if (options.style) { + data.style = options.style; + } + } + + return self.request.send('/recovery_email/verify_code', 'POST', null, data); + }); +}; + +FxAccountClient.prototype.verifyTokenCode = function(sessionToken, uid, code) { + var self = this; - /** - * @method verifyCode - * @param {String} uid Account ID - * @param {String} code Verification code - * @param {Object} [options={}] Options - * @param {String} [options.service] - * Service being signed into - * @param {String} [options.reminder] - * Reminder that was used to verify the account - * @param {String} [options.type] - * Type of code being verified, only supports `secondary` otherwise will verify account/sign-in - * @param {Boolean} [options.marketingOptIn] - * If `true`, notifies marketing of opt-in intent. - * @param {Array} [options.newsletters] - * Array of newsletters to opt in or out of. - * @param {String} [options.style] - * Specify the style of email to send. - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.verifyCode = function(uid, code, options) { - var self = this; - - return Promise.resolve().then(function() { - required(uid, 'uid'); - required(code, 'verify code'); + required(uid, 'uid'); + required(code, 'verify token code'); + required(sessionToken, 'sessionToken'); + return Promise.resolve() + .then(function() { + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { var data = { uid: uid, code: code, }; + return self.request.send('/session/verify/token', 'POST', creds, data); + }); +}; + +/** + * @method recoveryEmailStatus + * @param {String} sessionToken sessionToken obtained from signIn + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.recoveryEmailStatus = function(sessionToken) { + var self = this; + + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + return self.request.send('/recovery_email/status', 'GET', creds); + }); +}; + +/** + * Re-sends a verification code to the account's recovery email address. + * + * @method recoveryEmailResendCode + * @param {String} sessionToken sessionToken obtained from signIn + * @param {Object} [options={}] Options + * @param {String} [options.email] + * Code will be resent to this email, only used for secondary email codes + * @param {String} [options.service] + * Opaque alphanumeric token to be included in verification links + * @param {String} [options.redirectTo] + * a URL that the client should be redirected to after handling the request + * @param {String} [options.resume] + * Opaque url-encoded string that will be included in the verification link + * as a querystring parameter, useful for continuing an OAuth flow for + * example. + * @param {String} [options.type] + * Specifies the type of code to send, currently only supported type is + * `upgradeSession`. + * @param {String} [options.lang] + * set the language for the 'Accept-Language' header + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.recoveryEmailResendCode = function( + sessionToken, + options +) { + var self = this; + var data = {}; + var requestOpts = {}; + + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + if (options) { + if (options.email) { + data.email = options.email; + } + if (options.service) { data.service = options.service; } - if (options.reminder) { - data.reminder = options.reminder; + if (options.redirectTo) { + data.redirectTo = options.redirectTo; } - if (options.type) { - data.type = options.type; + if (options.resume) { + data.resume = options.resume; } - if (options.marketingOptIn) { - data.marketingOptIn = true; + if (options.type) { + data.type = options.type; } - if (options.newsletters) { - data.newsletters = options.newsletters; + if (options.lang) { + requestOpts.headers = { + 'Accept-Language': options.lang, + }; } if (options.style) { @@ -376,176 +473,126 @@ define([ } } + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { return self.request.send( - '/recovery_email/verify_code', + '/recovery_email/resend_code', 'POST', - null, - data + creds, + data, + requestOpts ); }); +}; + +/** + * Used to ask the server to send a recovery code. + * The API returns passwordForgotToken to the client. + * + * @method passwordForgotSendCode + * @param {String} email + * @param {Object} [options={}] Options + * @param {String} [options.service] + * Opaque alphanumeric token to be included in verification links + * @param {String} [options.redirectTo] + * a URL that the client should be redirected to after handling the request + * @param {String} [options.resume] + * Opaque url-encoded string that will be included in the verification link + * as a querystring parameter, useful for continuing an OAuth flow for + * example. + * @param {String} [options.lang] + * set the language for the 'Accept-Language' header + * @param {Object} [options.metricsContext={}] Metrics context metadata + * @param {String} options.metricsContext.deviceId identifier for the current device + * @param {String} options.metricsContext.flowId identifier for the current event flow + * @param {Number} options.metricsContext.flowBeginTime flow.begin event time + * @param {Number} options.metricsContext.utmCampaign marketing campaign identifier + * @param {Number} options.metricsContext.utmContent content identifier + * @param {Number} options.metricsContext.utmMedium acquisition medium + * @param {Number} options.metricsContext.utmSource traffic source + * @param {Number} options.metricsContext.utmTerm search terms + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.passwordForgotSendCode = function(email, options) { + var self = this; + var data = { + email: email, }; + var requestOpts = {}; - FxAccountClient.prototype.verifyTokenCode = function( - sessionToken, - uid, - code - ) { - var self = this; - - required(uid, 'uid'); - required(code, 'verify token code'); - required(sessionToken, 'sessionToken'); - - return Promise.resolve() - .then(function() { - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - var data = { - uid: uid, - code: code, - }; - - return self.request.send('/session/verify/token', 'POST', creds, data); - }); - }; - - /** - * @method recoveryEmailStatus - * @param {String} sessionToken sessionToken obtained from signIn - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.recoveryEmailStatus = function(sessionToken) { - var self = this; - - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - return self.request.send('/recovery_email/status', 'GET', creds); - }); - }; - - /** - * Re-sends a verification code to the account's recovery email address. - * - * @method recoveryEmailResendCode - * @param {String} sessionToken sessionToken obtained from signIn - * @param {Object} [options={}] Options - * @param {String} [options.email] - * Code will be resent to this email, only used for secondary email codes - * @param {String} [options.service] - * Opaque alphanumeric token to be included in verification links - * @param {String} [options.redirectTo] - * a URL that the client should be redirected to after handling the request - * @param {String} [options.resume] - * Opaque url-encoded string that will be included in the verification link - * as a querystring parameter, useful for continuing an OAuth flow for - * example. - * @param {String} [options.type] - * Specifies the type of code to send, currently only supported type is - * `upgradeSession`. - * @param {String} [options.lang] - * set the language for the 'Accept-Language' header - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.recoveryEmailResendCode = function( - sessionToken, - options - ) { - var self = this; - var data = {}; - var requestOpts = {}; - - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - - if (options) { - if (options.email) { - data.email = options.email; - } - - if (options.service) { - data.service = options.service; - } + return Promise.resolve().then(function() { + required(email, 'email'); - if (options.redirectTo) { - data.redirectTo = options.redirectTo; - } + if (options) { + if (options.service) { + data.service = options.service; + } - if (options.resume) { - data.resume = options.resume; - } + if (options.redirectTo) { + data.redirectTo = options.redirectTo; + } - if (options.type) { - data.type = options.type; - } + if (options.resume) { + data.resume = options.resume; + } - if (options.lang) { - requestOpts.headers = { - 'Accept-Language': options.lang, - }; - } + if (options.lang) { + requestOpts.headers = { + 'Accept-Language': options.lang, + }; + } - if (options.style) { - data.style = options.style; - } - } + if (options.metricsContext) { + data.metricsContext = metricsContext.marshall(options.metricsContext); + } + } - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - return self.request.send( - '/recovery_email/resend_code', - 'POST', - creds, - data, - requestOpts - ); - }); + return self.request.send( + '/password/forgot/send_code', + 'POST', + null, + data, + requestOpts + ); + }); +}; + +/** + * Re-sends a verification code to the account's recovery email address. + * HAWK-authenticated with the passwordForgotToken. + * + * @method passwordForgotResendCode + * @param {String} email + * @param {String} passwordForgotToken + * @param {Object} [options={}] Options + * @param {String} [options.service] + * Opaque alphanumeric token to be included in verification links + * @param {String} [options.redirectTo] + * a URL that the client should be redirected to after handling the request + * @param {String} [options.resume] + * Opaque url-encoded string that will be included in the verification link + * as a querystring parameter, useful for continuing an OAuth flow for + * example. + * @param {String} [options.lang] + * set the language for the 'Accept-Language' header + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.passwordForgotResendCode = function( + email, + passwordForgotToken, + options +) { + var self = this; + var data = { + email: email, }; + var requestOpts = {}; - /** - * Used to ask the server to send a recovery code. - * The API returns passwordForgotToken to the client. - * - * @method passwordForgotSendCode - * @param {String} email - * @param {Object} [options={}] Options - * @param {String} [options.service] - * Opaque alphanumeric token to be included in verification links - * @param {String} [options.redirectTo] - * a URL that the client should be redirected to after handling the request - * @param {String} [options.resume] - * Opaque url-encoded string that will be included in the verification link - * as a querystring parameter, useful for continuing an OAuth flow for - * example. - * @param {String} [options.lang] - * set the language for the 'Accept-Language' header - * @param {Object} [options.metricsContext={}] Metrics context metadata - * @param {String} options.metricsContext.deviceId identifier for the current device - * @param {String} options.metricsContext.flowId identifier for the current event flow - * @param {Number} options.metricsContext.flowBeginTime flow.begin event time - * @param {Number} options.metricsContext.utmCampaign marketing campaign identifier - * @param {Number} options.metricsContext.utmContent content identifier - * @param {Number} options.metricsContext.utmMedium acquisition medium - * @param {Number} options.metricsContext.utmSource traffic source - * @param {Number} options.metricsContext.utmTerm search terms - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.passwordForgotSendCode = function(email, options) { - var self = this; - var data = { - email: email, - }; - var requestOpts = {}; - - return Promise.resolve().then(function() { + return Promise.resolve() + .then(function() { required(email, 'email'); + required(passwordForgotToken, 'passwordForgotToken'); if (options) { if (options.service) { @@ -565,341 +612,527 @@ define([ 'Accept-Language': options.lang, }; } - - if (options.metricsContext) { - data.metricsContext = metricsContext.marshall(options.metricsContext); - } } + return hawkCredentials( + passwordForgotToken, + 'passwordForgotToken', + HKDF_SIZE + ); + }) + .then(function(creds) { return self.request.send( - '/password/forgot/send_code', + '/password/forgot/resend_code', 'POST', - null, + creds, data, requestOpts ); }); - }; - - /** - * Re-sends a verification code to the account's recovery email address. - * HAWK-authenticated with the passwordForgotToken. - * - * @method passwordForgotResendCode - * @param {String} email - * @param {String} passwordForgotToken - * @param {Object} [options={}] Options - * @param {String} [options.service] - * Opaque alphanumeric token to be included in verification links - * @param {String} [options.redirectTo] - * a URL that the client should be redirected to after handling the request - * @param {String} [options.resume] - * Opaque url-encoded string that will be included in the verification link - * as a querystring parameter, useful for continuing an OAuth flow for - * example. - * @param {String} [options.lang] - * set the language for the 'Accept-Language' header - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.passwordForgotResendCode = function( - email, - passwordForgotToken, - options - ) { - var self = this; - var data = { - email: email, - }; - var requestOpts = {}; - - return Promise.resolve() - .then(function() { - required(email, 'email'); - required(passwordForgotToken, 'passwordForgotToken'); - - if (options) { - if (options.service) { - data.service = options.service; - } +}; + +/** + * Submits the verification token to the server. + * The API returns accountResetToken to the client. + * HAWK-authenticated with the passwordForgotToken. + * + * @method passwordForgotVerifyCode + * @param {String} code + * @param {String} passwordForgotToken + * @param {Object} [options={}] Options + * @param {Boolean} [options.accountResetWithRecoveryKey] verifying code to be use in account recovery + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.passwordForgotVerifyCode = function( + code, + passwordForgotToken, + options +) { + var self = this; - if (options.redirectTo) { - data.redirectTo = options.redirectTo; - } + return Promise.resolve() + .then(function() { + required(code, 'reset code'); + required(passwordForgotToken, 'passwordForgotToken'); - if (options.resume) { - data.resume = options.resume; - } + return hawkCredentials( + passwordForgotToken, + 'passwordForgotToken', + HKDF_SIZE + ); + }) + .then(function(creds) { + var data = { + code: code, + }; - if (options.lang) { - requestOpts.headers = { - 'Accept-Language': options.lang, - }; - } - } + if (options && options.accountResetWithRecoveryKey) { + data.accountResetWithRecoveryKey = options.accountResetWithRecoveryKey; + } - return hawkCredentials( - passwordForgotToken, - 'passwordForgotToken', - HKDF_SIZE - ); - }) - .then(function(creds) { - return self.request.send( - '/password/forgot/resend_code', - 'POST', - creds, - data, - requestOpts - ); - }); - }; + return self.request.send( + '/password/forgot/verify_code', + 'POST', + creds, + data + ); + }); +}; + +/** + * Returns the status for the passwordForgotToken. + * If the request returns a success response, the token has not yet been consumed. + + * @method passwordForgotStatus + * @param {String} passwordForgotToken + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.passwordForgotStatus = function(passwordForgotToken) { + var self = this; + + return Promise.resolve() + .then(function() { + required(passwordForgotToken, 'passwordForgotToken'); + + return hawkCredentials( + passwordForgotToken, + 'passwordForgotToken', + HKDF_SIZE + ); + }) + .then(function(creds) { + return self.request.send('/password/forgot/status', 'GET', creds); + }); +}; + +/** + * The API returns reset result to the client. + * HAWK-authenticated with accountResetToken + * + * @method accountReset + * @param {String} email + * @param {String} newPassword + * @param {String} accountResetToken + * @param {Object} [options={}] Options + * @param {Boolean} [options.keys] + * If `true`, a new `keyFetchToken` is provisioned. `options.sessionToken` + * is required if `options.keys` is true. + * @param {Boolean} [options.sessionToken] + * If `true`, a new `sessionToken` is provisioned. + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.accountReset = function( + email, + newPassword, + accountResetToken, + options +) { + var self = this; + var data = {}; + var unwrapBKey; - /** - * Submits the verification token to the server. - * The API returns accountResetToken to the client. - * HAWK-authenticated with the passwordForgotToken. - * - * @method passwordForgotVerifyCode - * @param {String} code - * @param {String} passwordForgotToken - * @param {Object} [options={}] Options - * @param {Boolean} [options.accountResetWithRecoveryKey] verifying code to be use in account recovery - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.passwordForgotVerifyCode = function( - code, - passwordForgotToken, - options - ) { - var self = this; + options = options || {}; - return Promise.resolve() - .then(function() { - required(code, 'reset code'); - required(passwordForgotToken, 'passwordForgotToken'); + if (options.sessionToken) { + data.sessionToken = options.sessionToken; + } - return hawkCredentials( - passwordForgotToken, - 'passwordForgotToken', - HKDF_SIZE - ); - }) - .then(function(creds) { - var data = { - code: code, - }; + return Promise.resolve() + .then(function() { + required(email, 'email'); + required(newPassword, 'new password'); + required(accountResetToken, 'accountResetToken'); - if (options && options.accountResetWithRecoveryKey) { - data.accountResetWithRecoveryKey = - options.accountResetWithRecoveryKey; - } + if (options.keys) { + required(options.sessionToken, 'sessionToken'); + } - return self.request.send( - '/password/forgot/verify_code', - 'POST', - creds, - data - ); - }); - }; + return credentials.setup(email, newPassword); + }) + .then(function(result) { + if (options.keys) { + unwrapBKey = sjcl.codec.hex.fromBits(result.unwrapBKey); + } - /** - * Returns the status for the passwordForgotToken. - * If the request returns a success response, the token has not yet been consumed. + data.authPW = sjcl.codec.hex.fromBits(result.authPW); - * @method passwordForgotStatus - * @param {String} passwordForgotToken - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.passwordForgotStatus = function( - passwordForgotToken - ) { - var self = this; + return hawkCredentials(accountResetToken, 'accountResetToken', HKDF_SIZE); + }) + .then(function(creds) { + var queryParams = ''; + if (options.keys) { + queryParams = '?keys=true'; + } - return Promise.resolve() - .then(function() { - required(passwordForgotToken, 'passwordForgotToken'); + var endpoint = '/account/reset' + queryParams; + return self.request + .send(endpoint, 'POST', creds, data) + .then(function(accountData) { + if (options.keys && accountData.keyFetchToken) { + accountData.unwrapBKey = unwrapBKey; + } - return hawkCredentials( - passwordForgotToken, - 'passwordForgotToken', - HKDF_SIZE - ); - }) - .then(function(creds) { - return self.request.send('/password/forgot/status', 'GET', creds); - }); - }; - - /** - * The API returns reset result to the client. - * HAWK-authenticated with accountResetToken - * - * @method accountReset - * @param {String} email - * @param {String} newPassword - * @param {String} accountResetToken - * @param {Object} [options={}] Options - * @param {Boolean} [options.keys] - * If `true`, a new `keyFetchToken` is provisioned. `options.sessionToken` - * is required if `options.keys` is true. - * @param {Boolean} [options.sessionToken] - * If `true`, a new `sessionToken` is provisioned. - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.accountReset = function( - email, - newPassword, - accountResetToken, - options - ) { - var self = this; - var data = {}; - var unwrapBKey; + return accountData; + }); + }); +}; + +/** + * Get the base16 bundle of encrypted kA|wrapKb. + * + * @method accountKeys + * @param {String} keyFetchToken + * @param {String} oldUnwrapBKey + * @return {Promise} A promise that will be fulfilled with JSON of {kA, kB} of the key bundle + */ +FxAccountClient.prototype.accountKeys = function(keyFetchToken, oldUnwrapBKey) { + var self = this; + + return Promise.resolve() + .then(function() { + required(keyFetchToken, 'keyFetchToken'); + required(oldUnwrapBKey, 'oldUnwrapBKey'); + + return hawkCredentials(keyFetchToken, 'keyFetchToken', 3 * 32); + }) + .then(function(creds) { + var bundleKey = sjcl.codec.hex.fromBits(creds.bundleKey); + + return self.request + .send('/account/keys', 'GET', creds) + .then(function(payload) { + return credentials.unbundleKeyFetchResponse( + bundleKey, + payload.bundle + ); + }); + }) + .then(function(keys) { + return { + kB: sjcl.codec.hex.fromBits( + credentials.xor( + sjcl.codec.hex.toBits(keys.wrapKB), + sjcl.codec.hex.toBits(oldUnwrapBKey) + ) + ), + kA: keys.kA, + }; + }); +}; + +/** + * This deletes the account completely. All stored data is erased. + * + * @method accountDestroy + * @param {String} email Email input + * @param {String} password Password input + * @param {Object} [options={}] Options + * @param {Boolean} [options.skipCaseError] + * If `true`, the request will skip the incorrect case error + * @param {String} sessionToken User session token + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.accountDestroy = function( + email, + password, + options, + sessionToken +) { + var self = this; + options = options || {}; - options = options || {}; + return Promise.resolve() + .then(function() { + required(email, 'email'); + required(password, 'password'); - if (options.sessionToken) { - data.sessionToken = options.sessionToken; - } + var defers = [credentials.setup(email, password)]; + if (sessionToken) { + defers.push(hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE)); + } - return Promise.resolve() - .then(function() { - required(email, 'email'); - required(newPassword, 'new password'); - required(accountResetToken, 'accountResetToken'); + return Promise.all(defers); + }) + .then(function(results) { + var auth = results[0]; + var creds = results[1]; + var data = { + email: auth.emailUTF8, + authPW: sjcl.codec.hex.fromBits(auth.authPW), + }; - if (options.keys) { - required(options.sessionToken, 'sessionToken'); + return self.request.send('/account/destroy', 'POST', creds, data).then( + function(response) { + return response; + }, + function(error) { + // if incorrect email case error + if ( + error && + error.email && + error.errno === ERRORS.INCORRECT_EMAIL_CASE && + !options.skipCaseError + ) { + options.skipCaseError = true; + + return self.accountDestroy( + error.email, + password, + options, + sessionToken + ); + } else { + throw error; + } } + ); + }); +}; + +/** + * Gets the status of an account by uid. + * + * @method accountStatus + * @param {String} uid User account id + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.accountStatus = function(uid) { + var self = this; + + return Promise.resolve().then(function() { + required(uid, 'uid'); - return credentials.setup(email, newPassword); - }) - .then(function(result) { - if (options.keys) { - unwrapBKey = sjcl.codec.hex.fromBits(result.unwrapBKey); - } + return self.request.send('/account/status?uid=' + uid, 'GET'); + }); +}; - data.authPW = sjcl.codec.hex.fromBits(result.authPW); +/** + * Gets the status of an account by email. + * + * @method accountStatusByEmail + * @param {String} email User account email + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.accountStatusByEmail = function(email) { + var self = this; - return hawkCredentials( - accountResetToken, - 'accountResetToken', - HKDF_SIZE - ); - }) - .then(function(creds) { - var queryParams = ''; - if (options.keys) { - queryParams = '?keys=true'; - } + return Promise.resolve().then(function() { + required(email, 'email'); - var endpoint = '/account/reset' + queryParams; - return self.request - .send(endpoint, 'POST', creds, data) - .then(function(accountData) { - if (options.keys && accountData.keyFetchToken) { - accountData.unwrapBKey = unwrapBKey; - } + return self.request.send('/account/status', 'POST', null, { + email: email, + }); + }); +}; + +/** + * Gets the account profile + * + * @method accountProfile + * @param {String} sessionToken User session token + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.accountProfile = function(sessionToken) { + var self = this; + + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + return self.request.send('/account/profile', 'GET', creds); + }); +}; + +/** + * Gets aggregated account data, to be used instead of making + * multiple calls to disparate `/status` endpoints. + * + * @method account + * @param {String} sessionToken User session token + * @return {Promise} A promise that will be fulfilled with JSON + */ +FxAccountClient.prototype.account = function(sessionToken) { + var self = this; + + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + return self.request.send('/account', 'GET', creds); + }); +}; + +/** + * Destroys this session, by invalidating the sessionToken. + * + * @method sessionDestroy + * @param {String} sessionToken User session token + * @param {Object} [options={}] Options + * @param {String} [options.customSessionToken] Override which session token to destroy for this same user + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.sessionDestroy = function(sessionToken, options) { + var self = this; + var data = {}; + options = options || {}; + + if (options.customSessionToken) { + data.customSessionToken = options.customSessionToken; + } - return accountData; - }); - }); - }; + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); - /** - * Get the base16 bundle of encrypted kA|wrapKb. - * - * @method accountKeys - * @param {String} keyFetchToken - * @param {String} oldUnwrapBKey - * @return {Promise} A promise that will be fulfilled with JSON of {kA, kB} of the key bundle - */ - FxAccountClient.prototype.accountKeys = function( - keyFetchToken, - oldUnwrapBKey - ) { - var self = this; - - return Promise.resolve() - .then(function() { - required(keyFetchToken, 'keyFetchToken'); - required(oldUnwrapBKey, 'oldUnwrapBKey'); - - return hawkCredentials(keyFetchToken, 'keyFetchToken', 3 * 32); - }) - .then(function(creds) { - var bundleKey = sjcl.codec.hex.fromBits(creds.bundleKey); - - return self.request - .send('/account/keys', 'GET', creds) - .then(function(payload) { - return credentials.unbundleKeyFetchResponse( - bundleKey, - payload.bundle - ); - }); - }) - .then(function(keys) { - return { - kB: sjcl.codec.hex.fromBits( - credentials.xor( - sjcl.codec.hex.toBits(keys.wrapKB), - sjcl.codec.hex.toBits(oldUnwrapBKey) - ) - ), - kA: keys.kA, - }; - }); - }; + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + return self.request.send('/session/destroy', 'POST', creds, data); + }); +}; + +/** + * Responds successfully if the session status is valid, requires the sessionToken. + * + * @method sessionStatus + * @param {String} sessionToken User session token + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.sessionStatus = function(sessionToken) { + var self = this; + + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + return self.request.send('/session/status', 'GET', creds); + }); +}; + +/** + * @method sessionReauth + * @param {String} sessionToken sessionToken obtained from signIn + * @param {String} email Email input + * @param {String} password Password input + * @param {Object} [options={}] Options + * @param {Boolean} [options.keys] + * If `true`, calls the API with `?keys=true` to get the keyFetchToken + * @param {Boolean} [options.skipCaseError] + * If `true`, the request will skip the incorrect case error + * @param {String} [options.service] + * Service being accessed that needs reauthentication + * @param {String} [options.reason] + * Reason for reauthentication. Can be one of: `signin`, `password_check`, + * `password_change`, `password_reset` + * @param {String} [options.redirectTo] + * a URL that the client should be redirected to after handling the request + * @param {String} [options.resume] + * Opaque url-encoded string that will be included in the verification link + * as a querystring parameter, useful for continuing an OAuth flow for + * example. + * @param {String} [options.originalLoginEmail] + * If retrying after an "incorrect email case" error, this specifies + * the email address as originally entered by the user. + * @param {String} [options.verificationMethod] + * Request a specific verification method be used for verifying the session, + * e.g. 'email-2fa' or 'totp-2fa'. + * @param {Object} [options.metricsContext={}] Metrics context metadata + * @param {String} options.metricsContext.deviceId identifier for the current device + * @param {String} options.metricsContext.flowId identifier for the current event flow + * @param {Number} options.metricsContext.flowBeginTime flow.begin event time + * @param {Number} options.metricsContext.utmCampaign marketing campaign identifier + * @param {Number} options.metricsContext.utmContent content identifier + * @param {Number} options.metricsContext.utmMedium acquisition medium + * @param {Number} options.metricsContext.utmSource traffic source + * @param {Number} options.metricsContext.utmTerm search terms + * @param {String} [options.unblockCode] + * Login unblock code. + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.sessionReauth = function( + sessionToken, + email, + password, + options +) { + var self = this; + options = options || {}; - /** - * This deletes the account completely. All stored data is erased. - * - * @method accountDestroy - * @param {String} email Email input - * @param {String} password Password input - * @param {Object} [options={}] Options - * @param {Boolean} [options.skipCaseError] - * If `true`, the request will skip the incorrect case error - * @param {String} sessionToken User session token - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.accountDestroy = function( - email, - password, - options, - sessionToken - ) { - var self = this; - options = options || {}; + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + required(email, 'email'); + required(password, 'password'); - return Promise.resolve() - .then(function() { - required(email, 'email'); - required(password, 'password'); + return credentials.setup(email, password); + }) + .then(function(result) { + var endpoint = '/session/reauth'; - var defers = [credentials.setup(email, password)]; - if (sessionToken) { - defers.push(hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE)); - } + if (options.keys) { + endpoint += '?keys=true'; + } - return Promise.all(defers); - }) - .then(function(results) { - var auth = results[0]; - var creds = results[1]; - var data = { - email: auth.emailUTF8, - authPW: sjcl.codec.hex.fromBits(auth.authPW), - }; + var data = { + email: result.emailUTF8, + authPW: sjcl.codec.hex.fromBits(result.authPW), + }; + + if (options.metricsContext) { + data.metricsContext = metricsContext.marshall(options.metricsContext); + } + + if (options.reason) { + data.reason = options.reason; + } + + if (options.redirectTo) { + data.redirectTo = options.redirectTo; + } + + if (options.resume) { + data.resume = options.resume; + } + + if (options.service) { + data.service = options.service; + } + + if (options.unblockCode) { + data.unblockCode = options.unblockCode; + } + + if (options.originalLoginEmail) { + data.originalLoginEmail = options.originalLoginEmail; + } + + if (options.verificationMethod) { + data.verificationMethod = options.verificationMethod; + } - return self.request.send('/account/destroy', 'POST', creds, data).then( - function(response) { - return response; + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE) + .then(function(creds) { + return self.request.send(endpoint, 'POST', creds, data); + }) + .then( + function(accountData) { + if (options.keys) { + accountData.unwrapBKey = sjcl.codec.hex.fromBits( + result.unwrapBKey + ); + } + return accountData; }, function(error) { - // if incorrect email case error if ( error && error.email && @@ -907,1762 +1140,1475 @@ define([ !options.skipCaseError ) { options.skipCaseError = true; + options.originalLoginEmail = email; - return self.accountDestroy( + return self.sessionReauth( + sessionToken, error.email, password, - options, - sessionToken + options ); } else { throw error; } } ); - }); + }); +}; + +/** + * Sign a BrowserID public key + * + * @method certificateSign + * @param {String} sessionToken User session token + * @param {Object} publicKey The key to sign + * @param {int} duration Time interval from now when the certificate will expire in milliseconds + * @param {Object} [options={}] Options + * @param {String} [service=''] The requesting service, sent via the query string + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.certificateSign = function( + sessionToken, + publicKey, + duration, + options +) { + var self = this; + var data = { + publicKey: publicKey, + duration: duration, }; - /** - * Gets the status of an account by uid. - * - * @method accountStatus - * @param {String} uid User account id - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.accountStatus = function(uid) { - var self = this; + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + required(publicKey, 'publicKey'); + required(duration, 'duration'); + + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + options = options || {}; - return Promise.resolve().then(function() { - required(uid, 'uid'); + var queryString = ''; + if (options.service) { + queryString = '?service=' + encodeURIComponent(options.service); + } - return self.request.send('/account/status?uid=' + uid, 'GET'); + return self.request.send( + '/certificate/sign' + queryString, + 'POST', + creds, + data + ); }); - }; +}; + +/** + * Change the password from one known value to another. + * + * @method passwordChange + * @param {String} email + * @param {String} oldPassword + * @param {String} newPassword + * @param {Object} [options={}] Options + * @param {Boolean} [options.keys] + * If `true`, calls the API with `?keys=true` to get a new keyFetchToken + * @param {String} [options.sessionToken] + * If a `sessionToken` is passed, a new sessionToken will be returned + * with the same `verified` status as the existing sessionToken. + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.passwordChange = function( + email, + oldPassword, + newPassword, + options +) { + var self = this; + options = options || {}; - /** - * Gets the status of an account by email. - * - * @method accountStatusByEmail - * @param {String} email User account email - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.accountStatusByEmail = function(email) { - var self = this; - - return Promise.resolve().then(function() { + return Promise.resolve() + .then(function() { required(email, 'email'); - - return self.request.send('/account/status', 'POST', null, { - email: email, + required(oldPassword, 'old password'); + required(newPassword, 'new password'); + + return self._passwordChangeStart(email, oldPassword); + }) + .then(function(credentials) { + var oldCreds = credentials; + var emailToHashWith = credentials.emailToHashWith || email; + + return self._passwordChangeKeys(oldCreds).then(function(keys) { + return self._passwordChangeFinish( + emailToHashWith, + newPassword, + oldCreds, + keys, + options + ); }); }); - }; - - /** - * Gets the account profile - * - * @method accountProfile - * @param {String} sessionToken User session token - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.accountProfile = function(sessionToken) { - var self = this; - - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - return self.request.send('/account/profile', 'GET', creds); - }); - }; - - /** - * Gets aggregated account data, to be used instead of making - * multiple calls to disparate `/status` endpoints. - * - * @method account - * @param {String} sessionToken User session token - * @return {Promise} A promise that will be fulfilled with JSON - */ - FxAccountClient.prototype.account = function(sessionToken) { - var self = this; - - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - return self.request.send('/account', 'GET', creds); - }); - }; +}; + +/** + * First step to change the password. + * + * @method passwordChangeStart + * @private + * @param {String} email + * @param {String} oldPassword + * @param {Object} [options={}] Options + * @param {Boolean} [options.skipCaseError] + * If `true`, the request will skip the incorrect case error + * @return {Promise} A promise that will be fulfilled with JSON of `xhr.responseText` and `oldUnwrapBKey` + */ +FxAccountClient.prototype._passwordChangeStart = function( + email, + oldPassword, + options +) { + var self = this; + options = options || {}; - /** - * Destroys this session, by invalidating the sessionToken. - * - * @method sessionDestroy - * @param {String} sessionToken User session token - * @param {Object} [options={}] Options - * @param {String} [options.customSessionToken] Override which session token to destroy for this same user - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.sessionDestroy = function(sessionToken, options) { - var self = this; - var data = {}; - options = options || {}; - - if (options.customSessionToken) { - data.customSessionToken = options.customSessionToken; - } + return Promise.resolve() + .then(function() { + required(email, 'email'); + required(oldPassword, 'old password'); - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); + return credentials.setup(email, oldPassword); + }) + .then(function(oldCreds) { + var data = { + email: oldCreds.emailUTF8, + oldAuthPW: sjcl.codec.hex.fromBits(oldCreds.authPW), + }; - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - return self.request.send('/session/destroy', 'POST', creds, data); - }); - }; + return self.request + .send('/password/change/start', 'POST', null, data) + .then( + function(passwordData) { + passwordData.oldUnwrapBKey = sjcl.codec.hex.fromBits( + oldCreds.unwrapBKey + ); - /** - * Responds successfully if the session status is valid, requires the sessionToken. - * - * @method sessionStatus - * @param {String} sessionToken User session token - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.sessionStatus = function(sessionToken) { - var self = this; - - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - return self.request.send('/session/status', 'GET', creds); - }); - }; + // Similar to password reset, this keeps the contract that we always + // hash passwords with the original account email. + passwordData.emailToHashWith = email; + return passwordData; + }, + function(error) { + // if incorrect email case error + if ( + error && + error.email && + error.errno === ERRORS.INCORRECT_EMAIL_CASE && + !options.skipCaseError + ) { + options.skipCaseError = true; - /** - * @method sessionReauth - * @param {String} sessionToken sessionToken obtained from signIn - * @param {String} email Email input - * @param {String} password Password input - * @param {Object} [options={}] Options - * @param {Boolean} [options.keys] - * If `true`, calls the API with `?keys=true` to get the keyFetchToken - * @param {Boolean} [options.skipCaseError] - * If `true`, the request will skip the incorrect case error - * @param {String} [options.service] - * Service being accessed that needs reauthentication - * @param {String} [options.reason] - * Reason for reauthentication. Can be one of: `signin`, `password_check`, - * `password_change`, `password_reset` - * @param {String} [options.redirectTo] - * a URL that the client should be redirected to after handling the request - * @param {String} [options.resume] - * Opaque url-encoded string that will be included in the verification link - * as a querystring parameter, useful for continuing an OAuth flow for - * example. - * @param {String} [options.originalLoginEmail] - * If retrying after an "incorrect email case" error, this specifies - * the email address as originally entered by the user. - * @param {String} [options.verificationMethod] - * Request a specific verification method be used for verifying the session, - * e.g. 'email-2fa' or 'totp-2fa'. - * @param {Object} [options.metricsContext={}] Metrics context metadata - * @param {String} options.metricsContext.deviceId identifier for the current device - * @param {String} options.metricsContext.flowId identifier for the current event flow - * @param {Number} options.metricsContext.flowBeginTime flow.begin event time - * @param {Number} options.metricsContext.utmCampaign marketing campaign identifier - * @param {Number} options.metricsContext.utmContent content identifier - * @param {Number} options.metricsContext.utmMedium acquisition medium - * @param {Number} options.metricsContext.utmSource traffic source - * @param {Number} options.metricsContext.utmTerm search terms - * @param {String} [options.unblockCode] - * Login unblock code. - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.sessionReauth = function( - sessionToken, - email, - password, - options - ) { - var self = this; - options = options || {}; + return self._passwordChangeStart( + error.email, + oldPassword, + options + ); + } else { + throw error; + } + } + ); + }); +}; + +function checkCreds(creds) { + required(creds, 'credentials'); + required(creds.oldUnwrapBKey, 'credentials.oldUnwrapBKey'); + required(creds.keyFetchToken, 'credentials.keyFetchToken'); + required(creds.passwordChangeToken, 'credentials.passwordChangeToken'); +} + +/** + * Second step to change the password. + * + * @method _passwordChangeKeys + * @private + * @param {Object} oldCreds This object should consists of `oldUnwrapBKey`, `keyFetchToken` and `passwordChangeToken`. + * @return {Promise} A promise that will be fulfilled with JSON of `xhr.responseText` + */ +FxAccountClient.prototype._passwordChangeKeys = function(oldCreds) { + var self = this; + + return Promise.resolve() + .then(function() { + checkCreds(oldCreds); + }) + .then(function() { + return self.accountKeys(oldCreds.keyFetchToken, oldCreds.oldUnwrapBKey); + }); +}; + +/** + * Third step to change the password. + * + * @method _passwordChangeFinish + * @private + * @param {String} email + * @param {String} newPassword + * @param {Object} oldCreds This object should consists of `oldUnwrapBKey`, `keyFetchToken` and `passwordChangeToken`. + * @param {Object} keys This object should contain the unbundled keys + * @param {Object} [options={}] Options + * @param {Boolean} [options.keys] + * If `true`, calls the API with `?keys=true` to get the keyFetchToken + * @param {String} [options.sessionToken] + * If a `sessionToken` is passed, a new sessionToken will be returned + * with the same `verified` status as the existing sessionToken. + * @return {Promise} A promise that will be fulfilled with JSON of `xhr.responseText` + */ +FxAccountClient.prototype._passwordChangeFinish = function( + email, + newPassword, + oldCreds, + keys, + options +) { + options = options || {}; + var self = this; - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - required(email, 'email'); - required(password, 'password'); + return Promise.resolve() + .then(function() { + required(email, 'email'); + required(newPassword, 'new password'); + checkCreds(oldCreds); + required(keys, 'keys'); + required(keys.kB, 'keys.kB'); + + var defers = []; + defers.push(credentials.setup(email, newPassword)); + defers.push( + hawkCredentials( + oldCreds.passwordChangeToken, + 'passwordChangeToken', + HKDF_SIZE + ) + ); - return credentials.setup(email, password); - }) - .then(function(result) { - var endpoint = '/session/reauth'; + if (options.sessionToken) { + // Unbundle session data to get session id + defers.push( + hawkCredentials(options.sessionToken, 'sessionToken', HKDF_SIZE) + ); + } - if (options.keys) { - endpoint += '?keys=true'; - } + return Promise.all(defers); + }) + .then(function(results) { + var newCreds = results[0]; + var hawkCreds = results[1]; + var sessionData = results[2]; + var newWrapKb = sjcl.codec.hex.fromBits( + credentials.xor(sjcl.codec.hex.toBits(keys.kB), newCreds.unwrapBKey) + ); - var data = { - email: result.emailUTF8, - authPW: sjcl.codec.hex.fromBits(result.authPW), - }; + var queryParams = ''; + if (options.keys) { + queryParams = '?keys=true'; + } - if (options.metricsContext) { - data.metricsContext = metricsContext.marshall(options.metricsContext); - } + var sessionTokenId; + if (sessionData && sessionData.id) { + sessionTokenId = sessionData.id; + } - if (options.reason) { - data.reason = options.reason; - } + return self.request + .send('/password/change/finish' + queryParams, 'POST', hawkCreds, { + wrapKb: newWrapKb, + authPW: sjcl.codec.hex.fromBits(newCreds.authPW), + sessionToken: sessionTokenId, + }) + .then(function(accountData) { + if (options.keys && accountData.keyFetchToken) { + accountData.unwrapBKey = sjcl.codec.hex.fromBits( + newCreds.unwrapBKey + ); + } + return accountData; + }); + }); +}; + +/** + * Get 32 bytes of random data. This should be combined with locally-sourced entropy when creating salts, etc. + * + * @method getRandomBytes + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.getRandomBytes = function() { + return this.request.send('/get_random_bytes', 'POST'); +}; + +/** + * Add a new device + * + * @method deviceRegister + * @param {String} sessionToken User session token + * @param {String} deviceName Name of device + * @param {String} deviceType Type of device (mobile|desktop) + * @param {Object} [options={}] Options + * @param {string} [options.deviceCallback] Device's push endpoint. + * @param {string} [options.devicePublicKey] Public key used to encrypt push messages. + * @param {string} [options.deviceAuthKey] Authentication secret used to encrypt push messages. + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.deviceRegister = function( + sessionToken, + deviceName, + deviceType, + options +) { + var request = this.request; + options = options || {}; + + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + required(deviceName, 'deviceName'); + required(deviceType, 'deviceType'); + + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + var data = { + name: deviceName, + type: deviceType, + }; - if (options.redirectTo) { - data.redirectTo = options.redirectTo; - } + if (options.deviceCallback) { + data.pushCallback = options.deviceCallback; + } - if (options.resume) { - data.resume = options.resume; - } + if (options.devicePublicKey && options.deviceAuthKey) { + data.pushPublicKey = options.devicePublicKey; + data.pushAuthKey = options.deviceAuthKey; + } - if (options.service) { - data.service = options.service; - } + return request.send('/account/device', 'POST', creds, data); + }); +}; + +/** + * Update the name of an existing device + * + * @method deviceUpdate + * @param {String} sessionToken User session token + * @param {String} deviceId User-unique identifier of device + * @param {String} deviceName Name of device + * @param {Object} [options={}] Options + * @param {string} [options.deviceCallback] Device's push endpoint. + * @param {string} [options.devicePublicKey] Public key used to encrypt push messages. + * @param {string} [options.deviceAuthKey] Authentication secret used to encrypt push messages. + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.deviceUpdate = function( + sessionToken, + deviceId, + deviceName, + options +) { + var request = this.request; + options = options || {}; + + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + required(deviceId, 'deviceId'); + required(deviceName, 'deviceName'); + + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + var data = { + id: deviceId, + name: deviceName, + }; - if (options.unblockCode) { - data.unblockCode = options.unblockCode; - } + if (options.deviceCallback) { + data.pushCallback = options.deviceCallback; + } - if (options.originalLoginEmail) { - data.originalLoginEmail = options.originalLoginEmail; - } + if (options.devicePublicKey && options.deviceAuthKey) { + data.pushPublicKey = options.devicePublicKey; + data.pushAuthKey = options.deviceAuthKey; + } - if (options.verificationMethod) { - data.verificationMethod = options.verificationMethod; - } + return request.send('/account/device', 'POST', creds, data); + }); +}; + +/** + * Unregister an existing device + * + * @method deviceDestroy + * @param {String} sessionToken Session token obtained from signIn + * @param {String} deviceId User-unique identifier of device + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.deviceDestroy = function(sessionToken, deviceId) { + var request = this.request; + + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + required(deviceId, 'deviceId'); + + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + var data = { + id: deviceId, + }; - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE) - .then(function(creds) { - return self.request.send(endpoint, 'POST', creds, data); - }) - .then( - function(accountData) { - if (options.keys) { - accountData.unwrapBKey = sjcl.codec.hex.fromBits( - result.unwrapBKey - ); - } - return accountData; - }, - function(error) { - if ( - error && - error.email && - error.errno === ERRORS.INCORRECT_EMAIL_CASE && - !options.skipCaseError - ) { - options.skipCaseError = true; - options.originalLoginEmail = email; - - return self.sessionReauth( - sessionToken, - error.email, - password, - options - ); - } else { - throw error; - } - } - ); - }); + return request.send('/account/device/destroy', 'POST', creds, data); + }); +}; + +/** + * Get a list of all devices for a user + * + * @method deviceList + * @param {String} sessionToken sessionToken obtained from signIn + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.deviceList = function(sessionToken) { + var request = this.request; + + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + return request.send('/account/devices', 'GET', creds); + }); +}; + +/** + * Get a list of user's sessions + * + * @method sessions + * @param {String} sessionToken sessionToken obtained from signIn + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.sessions = function(sessionToken) { + var request = this.request; + + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + return request.send('/account/sessions', 'GET', creds); + }); +}; + +/** + * Get a list of user's security events + * + * @method securityEvents + * @param {String} sessionToken sessionToken obtained from signIn + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.securityEvents = function(sessionToken) { + let request = this.request; + + return Promise.resolve() + .then(() => { + required(sessionToken, 'sessionToken'); + + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(creds => { + return request.send('/securityEvents', 'GET', creds); + }); +}; + +/** + * Delete user's security events + * + * @method deleteSecurityEvents + * @param {String} sessionToken sessionToken obtained from signIn + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.deleteSecurityEvents = function(sessionToken) { + let request = this.request; + + return Promise.resolve() + .then(() => { + required(sessionToken, 'sessionToken'); + + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(creds => { + return request.send('/securityEvents', 'DELETE', creds, {}); + }); +}; + +/** + * Get a list of user's attached clients + * + * @method attachedClients + * @param {String} sessionToken sessionToken obtained from signIn + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.attachedClients = function(sessionToken) { + var request = this.request; + + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + return request.send('/account/attached_clients', 'GET', creds); + }); +}; + +/** + * Destroys all tokens held by an attached client. + * + * @method attachedClientDestroy + * @param {String} sessionToken User session token + * @param {Object} clientInfo Attached client info, as returned by `attachedClients` method + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.attachedClientDestroy = function( + sessionToken, + clientInfo +) { + var self = this; + var data = { + clientId: clientInfo.clientId, + deviceId: clientInfo.deviceId, + refreshTokenId: clientInfo.refreshTokenId, + sessionTokenId: clientInfo.sessionTokenId, }; - /** - * Sign a BrowserID public key - * - * @method certificateSign - * @param {String} sessionToken User session token - * @param {Object} publicKey The key to sign - * @param {int} duration Time interval from now when the certificate will expire in milliseconds - * @param {Object} [options={}] Options - * @param {String} [service=''] The requesting service, sent via the query string - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.certificateSign = function( - sessionToken, - publicKey, - duration, - options - ) { - var self = this; - var data = { - publicKey: publicKey, - duration: duration, - }; - - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - required(publicKey, 'publicKey'); - required(duration, 'duration'); + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - options = options || {}; + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + return self.request.send( + '/account/attached_client/destroy', + 'POST', + creds, + data + ); + }); +}; + +/** + * Get a list of user's attached clients + * + * @method attachedClients + * @param {String} sessionToken sessionToken obtained from signIn + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.attachedClients = function(sessionToken) { + var request = this.request; + + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + return request.send('/account/attached_clients', 'GET', creds); + }); +}; + +/** + * Destroys all tokens held by an attached client. + * + * @method attachedClientDestroy + * @param {String} sessionToken User session token + * @param {Object} clientInfo Attached client info, as returned by `attachedClients` method + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.attachedClientDestroy = function( + sessionToken, + clientInfo +) { + var self = this; + var data = { + clientId: clientInfo.clientId, + deviceId: clientInfo.deviceId, + refreshTokenId: clientInfo.refreshTokenId, + sessionTokenId: clientInfo.sessionTokenId, + }; - var queryString = ''; - if (options.service) { - queryString = '?service=' + encodeURIComponent(options.service); - } + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); - return self.request.send( - '/certificate/sign' + queryString, - 'POST', - creds, - data - ); - }); - }; + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + return self.request.send( + '/account/attached_client/destroy', + 'POST', + creds, + data + ); + }); +}; + +/** + * Send an unblock code + * + * @method sendUnblockCode + * @param {String} email email where to send the login authorization code + * @param {Object} [options={}] Options + * @param {Object} [options.metricsContext={}] Metrics context metadata + * @param {String} options.metricsContext.deviceId identifier for the current device + * @param {String} options.metricsContext.flowId identifier for the current event flow + * @param {Number} options.metricsContext.flowBeginTime flow.begin event time + * @param {Number} options.metricsContext.utmCampaign marketing campaign identifier + * @param {Number} options.metricsContext.utmContent content identifier + * @param {Number} options.metricsContext.utmMedium acquisition medium + * @param {Number} options.metricsContext.utmSource traffic source + * @param {Number} options.metricsContext.utmTerm search terms + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.sendUnblockCode = function(email, options) { + var self = this; + + return Promise.resolve().then(function() { + required(email, 'email'); - /** - * Change the password from one known value to another. - * - * @method passwordChange - * @param {String} email - * @param {String} oldPassword - * @param {String} newPassword - * @param {Object} [options={}] Options - * @param {Boolean} [options.keys] - * If `true`, calls the API with `?keys=true` to get a new keyFetchToken - * @param {String} [options.sessionToken] - * If a `sessionToken` is passed, a new sessionToken will be returned - * with the same `verified` status as the existing sessionToken. - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.passwordChange = function( - email, - oldPassword, - newPassword, - options - ) { - var self = this; - options = options || {}; - - return Promise.resolve() - .then(function() { - required(email, 'email'); - required(oldPassword, 'old password'); - required(newPassword, 'new password'); - - return self._passwordChangeStart(email, oldPassword); - }) - .then(function(credentials) { - var oldCreds = credentials; - var emailToHashWith = credentials.emailToHashWith || email; - - return self._passwordChangeKeys(oldCreds).then(function(keys) { - return self._passwordChangeFinish( - emailToHashWith, - newPassword, - oldCreds, - keys, - options - ); - }); - }); - }; + var data = { + email: email, + }; - /** - * First step to change the password. - * - * @method passwordChangeStart - * @private - * @param {String} email - * @param {String} oldPassword - * @param {Object} [options={}] Options - * @param {Boolean} [options.skipCaseError] - * If `true`, the request will skip the incorrect case error - * @return {Promise} A promise that will be fulfilled with JSON of `xhr.responseText` and `oldUnwrapBKey` - */ - FxAccountClient.prototype._passwordChangeStart = function( - email, - oldPassword, - options - ) { - var self = this; - options = options || {}; - - return Promise.resolve() - .then(function() { - required(email, 'email'); - required(oldPassword, 'old password'); - - return credentials.setup(email, oldPassword); - }) - .then(function(oldCreds) { - var data = { - email: oldCreds.emailUTF8, - oldAuthPW: sjcl.codec.hex.fromBits(oldCreds.authPW), - }; + if (options && options.metricsContext) { + data.metricsContext = metricsContext.marshall(options.metricsContext); + } - return self.request - .send('/password/change/start', 'POST', null, data) - .then( - function(passwordData) { - passwordData.oldUnwrapBKey = sjcl.codec.hex.fromBits( - oldCreds.unwrapBKey - ); + return self.request.send( + '/account/login/send_unblock_code', + 'POST', + null, + data + ); + }); +}; + +/** + * Reject a login unblock code. Code will be deleted from the server + * and will not be able to be used again. + * + * @method rejectLoginAuthorizationCode + * @param {String} uid Account ID + * @param {String} unblockCode unblock code + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +FxAccountClient.prototype.rejectUnblockCode = function(uid, unblockCode) { + var self = this; + + return Promise.resolve().then(function() { + required(uid, 'uid'); + required(unblockCode, 'unblockCode'); - // Similar to password reset, this keeps the contract that we always - // hash passwords with the original account email. - passwordData.emailToHashWith = email; - return passwordData; - }, - function(error) { - // if incorrect email case error - if ( - error && - error.email && - error.errno === ERRORS.INCORRECT_EMAIL_CASE && - !options.skipCaseError - ) { - options.skipCaseError = true; - - return self._passwordChangeStart( - error.email, - oldPassword, - options - ); - } else { - throw error; - } - } - ); - }); - }; + var data = { + uid: uid, + unblockCode: unblockCode, + }; - function checkCreds(creds) { - required(creds, 'credentials'); - required(creds.oldUnwrapBKey, 'credentials.oldUnwrapBKey'); - required(creds.keyFetchToken, 'credentials.keyFetchToken'); - required(creds.passwordChangeToken, 'credentials.passwordChangeToken'); - } + return self.request.send( + '/account/login/reject_unblock_code', + 'POST', + null, + data + ); + }); +}; + +/** + * Send an sms. + * + * @method sendSms + * @param {String} sessionToken SessionToken obtained from signIn + * @param {String} phoneNumber Phone number sms will be sent to + * @param {String} messageId Corresponding message id that will be sent + * @param {Object} [options={}] Options + * @param {String} [options.lang] Language that sms will be sent in + * @param {Array} [options.features] Array of features to be enabled for the request + * @param {Object} [options.metricsContext={}] Metrics context metadata + * @param {String} options.metricsContext.deviceId identifier for the current device + * @param {String} options.metricsContext.flowId identifier for the current event flow + * @param {Number} options.metricsContext.flowBeginTime flow.begin event time + * @param {Number} options.metricsContext.utmCampaign marketing campaign identifier + * @param {Number} options.metricsContext.utmContent content identifier + * @param {Number} options.metricsContext.utmMedium acquisition medium + * @param {Number} options.metricsContext.utmSource traffic source + * @param {Number} options.metricsContext.utmTerm search terms + */ +FxAccountClient.prototype.sendSms = function( + sessionToken, + phoneNumber, + messageId, + options +) { + var request = this.request; - /** - * Second step to change the password. - * - * @method _passwordChangeKeys - * @private - * @param {Object} oldCreds This object should consists of `oldUnwrapBKey`, `keyFetchToken` and `passwordChangeToken`. - * @return {Promise} A promise that will be fulfilled with JSON of `xhr.responseText` - */ - FxAccountClient.prototype._passwordChangeKeys = function(oldCreds) { - var self = this; - - return Promise.resolve() - .then(function() { - checkCreds(oldCreds); - }) - .then(function() { - return self.accountKeys(oldCreds.keyFetchToken, oldCreds.oldUnwrapBKey); - }); - }; + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + required(phoneNumber, 'phoneNumber'); + required(messageId, 'messageId'); - /** - * Third step to change the password. - * - * @method _passwordChangeFinish - * @private - * @param {String} email - * @param {String} newPassword - * @param {Object} oldCreds This object should consists of `oldUnwrapBKey`, `keyFetchToken` and `passwordChangeToken`. - * @param {Object} keys This object should contain the unbundled keys - * @param {Object} [options={}] Options - * @param {Boolean} [options.keys] - * If `true`, calls the API with `?keys=true` to get the keyFetchToken - * @param {String} [options.sessionToken] - * If a `sessionToken` is passed, a new sessionToken will be returned - * with the same `verified` status as the existing sessionToken. - * @return {Promise} A promise that will be fulfilled with JSON of `xhr.responseText` - */ - FxAccountClient.prototype._passwordChangeFinish = function( - email, - newPassword, - oldCreds, - keys, - options - ) { - options = options || {}; - var self = this; - - return Promise.resolve() - .then(function() { - required(email, 'email'); - required(newPassword, 'new password'); - checkCreds(oldCreds); - required(keys, 'keys'); - required(keys.kB, 'keys.kB'); - - var defers = []; - defers.push(credentials.setup(email, newPassword)); - defers.push( - hawkCredentials( - oldCreds.passwordChangeToken, - 'passwordChangeToken', - HKDF_SIZE - ) - ); + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + var data = { + phoneNumber: phoneNumber, + messageId: messageId, + }; + var requestOpts = {}; - if (options.sessionToken) { - // Unbundle session data to get session id - defers.push( - hawkCredentials(options.sessionToken, 'sessionToken', HKDF_SIZE) - ); + if (options) { + if (options.lang) { + requestOpts.headers = { + 'Accept-Language': options.lang, + }; } - return Promise.all(defers); - }) - .then(function(results) { - var newCreds = results[0]; - var hawkCreds = results[1]; - var sessionData = results[2]; - var newWrapKb = sjcl.codec.hex.fromBits( - credentials.xor(sjcl.codec.hex.toBits(keys.kB), newCreds.unwrapBKey) - ); - - var queryParams = ''; - if (options.keys) { - queryParams = '?keys=true'; + if (options.features) { + data.features = options.features; } - var sessionTokenId; - if (sessionData && sessionData.id) { - sessionTokenId = sessionData.id; + if (options.metricsContext) { + data.metricsContext = metricsContext.marshall(options.metricsContext); } + } - return self.request - .send('/password/change/finish' + queryParams, 'POST', hawkCreds, { - wrapKb: newWrapKb, - authPW: sjcl.codec.hex.fromBits(newCreds.authPW), - sessionToken: sessionTokenId, - }) - .then(function(accountData) { - if (options.keys && accountData.keyFetchToken) { - accountData.unwrapBKey = sjcl.codec.hex.fromBits( - newCreds.unwrapBKey - ); - } - return accountData; - }); - }); - }; - - /** - * Get 32 bytes of random data. This should be combined with locally-sourced entropy when creating salts, etc. - * - * @method getRandomBytes - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.getRandomBytes = function() { - return this.request.send('/get_random_bytes', 'POST'); - }; - - /** - * Add a new device - * - * @method deviceRegister - * @param {String} sessionToken User session token - * @param {String} deviceName Name of device - * @param {String} deviceType Type of device (mobile|desktop) - * @param {Object} [options={}] Options - * @param {string} [options.deviceCallback] Device's push endpoint. - * @param {string} [options.devicePublicKey] Public key used to encrypt push messages. - * @param {string} [options.deviceAuthKey] Authentication secret used to encrypt push messages. - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.deviceRegister = function( - sessionToken, - deviceName, - deviceType, - options - ) { - var request = this.request; - options = options || {}; - - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - required(deviceName, 'deviceName'); - required(deviceType, 'deviceType'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - var data = { - name: deviceName, - type: deviceType, - }; - - if (options.deviceCallback) { - data.pushCallback = options.deviceCallback; - } - - if (options.devicePublicKey && options.deviceAuthKey) { - data.pushPublicKey = options.devicePublicKey; - data.pushAuthKey = options.deviceAuthKey; - } - - return request.send('/account/device', 'POST', creds, data); - }); - }; - - /** - * Update the name of an existing device - * - * @method deviceUpdate - * @param {String} sessionToken User session token - * @param {String} deviceId User-unique identifier of device - * @param {String} deviceName Name of device - * @param {Object} [options={}] Options - * @param {string} [options.deviceCallback] Device's push endpoint. - * @param {string} [options.devicePublicKey] Public key used to encrypt push messages. - * @param {string} [options.deviceAuthKey] Authentication secret used to encrypt push messages. - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.deviceUpdate = function( - sessionToken, - deviceId, - deviceName, - options - ) { - var request = this.request; - options = options || {}; - - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - required(deviceId, 'deviceId'); - required(deviceName, 'deviceName'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - var data = { - id: deviceId, - name: deviceName, - }; - - if (options.deviceCallback) { - data.pushCallback = options.deviceCallback; - } - - if (options.devicePublicKey && options.deviceAuthKey) { - data.pushPublicKey = options.devicePublicKey; - data.pushAuthKey = options.deviceAuthKey; - } - - return request.send('/account/device', 'POST', creds, data); - }); - }; - - /** - * Unregister an existing device - * - * @method deviceDestroy - * @param {String} sessionToken Session token obtained from signIn - * @param {String} deviceId User-unique identifier of device - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.deviceDestroy = function(sessionToken, deviceId) { - var request = this.request; - - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - required(deviceId, 'deviceId'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - var data = { - id: deviceId, - }; - - return request.send('/account/device/destroy', 'POST', creds, data); - }); - }; - - /** - * Get a list of all devices for a user - * - * @method deviceList - * @param {String} sessionToken sessionToken obtained from signIn - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.deviceList = function(sessionToken) { - var request = this.request; - - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - return request.send('/account/devices', 'GET', creds); - }); - }; - - /** - * Get a list of user's sessions - * - * @method sessions - * @param {String} sessionToken sessionToken obtained from signIn - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.sessions = function(sessionToken) { - var request = this.request; - - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - return request.send('/account/sessions', 'GET', creds); - }); - }; - - /** - * Get a list of user's security events - * - * @method securityEvents - * @param {String} sessionToken sessionToken obtained from signIn - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.securityEvents = function(sessionToken) { - let request = this.request; - - return Promise.resolve() - .then(() => { - required(sessionToken, 'sessionToken'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(creds => { - return request.send('/securityEvents', 'GET', creds); - }); - }; - - /** - * Delete user's security events - * - * @method deleteSecurityEvents - * @param {String} sessionToken sessionToken obtained from signIn - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.deleteSecurityEvents = function(sessionToken) { - let request = this.request; - - return Promise.resolve() - .then(() => { - required(sessionToken, 'sessionToken'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(creds => { - return request.send('/securityEvents', 'DELETE', creds, {}); - }); - }; - - /** - * Get a list of user's attached clients - * - * @method attachedClients - * @param {String} sessionToken sessionToken obtained from signIn - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.attachedClients = function(sessionToken) { - var request = this.request; - - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - return request.send('/account/attached_clients', 'GET', creds); - }); - }; - - /** - * Destroys all tokens held by an attached client. - * - * @method attachedClientDestroy - * @param {String} sessionToken User session token - * @param {Object} clientInfo Attached client info, as returned by `attachedClients` method - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.attachedClientDestroy = function( - sessionToken, - clientInfo - ) { - var self = this; - var data = { - clientId: clientInfo.clientId, - deviceId: clientInfo.deviceId, - refreshTokenId: clientInfo.refreshTokenId, - sessionTokenId: clientInfo.sessionTokenId, - }; - - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - return self.request.send( - '/account/attached_client/destroy', - 'POST', - creds, - data - ); - }); - }; - - /** - * Get a list of user's attached clients - * - * @method attachedClients - * @param {String} sessionToken sessionToken obtained from signIn - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.attachedClients = function(sessionToken) { - var request = this.request; - - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - return request.send('/account/attached_clients', 'GET', creds); - }); - }; - - /** - * Destroys all tokens held by an attached client. - * - * @method attachedClientDestroy - * @param {String} sessionToken User session token - * @param {Object} clientInfo Attached client info, as returned by `attachedClients` method - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.attachedClientDestroy = function( - sessionToken, - clientInfo - ) { - var self = this; - var data = { - clientId: clientInfo.clientId, - deviceId: clientInfo.deviceId, - refreshTokenId: clientInfo.refreshTokenId, - sessionTokenId: clientInfo.sessionTokenId, - }; - - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - return self.request.send( - '/account/attached_client/destroy', - 'POST', - creds, - data - ); - }); - }; + return request.send('/sms', 'POST', creds, data, requestOpts); + }); +}; + +/** + * Get SMS status for the current user. + * + * @method smsStatus + * @param {String} sessionToken SessionToken obtained from signIn + * @param {Object} [options={}] Options + * @param {String} [options.country] country Country to force for testing. + */ +FxAccountClient.prototype.smsStatus = function(sessionToken, options) { + var request = this.request; + options = options || {}; + + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + var url = '/sms/status'; + if (options.country) { + url += '?country=' + encodeURIComponent(options.country); + } + return request.send(url, 'GET', creds); + }); +}; + +/** + * Consume a signinCode. + * + * @method consumeSigninCode + * @param {String} code The signinCode entered by the user + * @param {String} flowId Identifier for the current event flow + * @param {Number} flowBeginTime Timestamp for the flow.begin event + * @param {String} [deviceId] Identifier for the current device + */ +FxAccountClient.prototype.consumeSigninCode = function( + code, + flowId, + flowBeginTime, + deviceId +) { + var self = this; + + return Promise.resolve().then(function() { + required(code, 'code'); + required(flowId, 'flowId'); + required(flowBeginTime, 'flowBeginTime'); + + return self.request.send('/signinCodes/consume', 'POST', null, { + code: code, + metricsContext: { + deviceId: deviceId, + flowId: flowId, + flowBeginTime: flowBeginTime, + }, + }); + }); +}; + +/** + * Get the recovery emails associated with the signed in account. + * + * @method recoveryEmails + * @param {String} sessionToken SessionToken obtained from signIn + */ +FxAccountClient.prototype.recoveryEmails = function(sessionToken) { + var request = this.request; + + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + return request.send('/recovery_emails', 'GET', creds); + }); +}; + +/** + * Create a new recovery email for the signed in account. + * + * @method recoveryEmailCreate + * @param {String} sessionToken SessionToken obtained from signIn + * @param {String} email new email to be added + */ +FxAccountClient.prototype.recoveryEmailCreate = function(sessionToken, email) { + var request = this.request; + + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + required(sessionToken, 'email'); + + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + var data = { + email: email, + }; - /** - * Send an unblock code - * - * @method sendUnblockCode - * @param {String} email email where to send the login authorization code - * @param {Object} [options={}] Options - * @param {Object} [options.metricsContext={}] Metrics context metadata - * @param {String} options.metricsContext.deviceId identifier for the current device - * @param {String} options.metricsContext.flowId identifier for the current event flow - * @param {Number} options.metricsContext.flowBeginTime flow.begin event time - * @param {Number} options.metricsContext.utmCampaign marketing campaign identifier - * @param {Number} options.metricsContext.utmContent content identifier - * @param {Number} options.metricsContext.utmMedium acquisition medium - * @param {Number} options.metricsContext.utmSource traffic source - * @param {Number} options.metricsContext.utmTerm search terms - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.sendUnblockCode = function(email, options) { - var self = this; - - return Promise.resolve().then(function() { - required(email, 'email'); + return request.send('/recovery_email', 'POST', creds, data); + }); +}; + +/** + * Remove the recovery email for the signed in account. + * + * @method recoveryEmailDestroy + * @param {String} sessionToken SessionToken obtained from signIn + * @param {String} email email to be removed + */ +FxAccountClient.prototype.recoveryEmailDestroy = function(sessionToken, email) { + var request = this.request; + + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + required(sessionToken, 'email'); + + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + var data = { + email: email, + }; + return request.send('/recovery_email/destroy', 'POST', creds, data); + }); +}; + +/** + * Changes user's primary email address. + * + * @method recoveryEmailSetPrimaryEmail + * @param {String} sessionToken SessionToken obtained from signIn + * @param {String} email Email that will be the new primary email for user + */ +FxAccountClient.prototype.recoveryEmailSetPrimaryEmail = function( + sessionToken, + email +) { + var request = this.request; + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { var data = { email: email, }; + return request.send('/recovery_email/set_primary', 'POST', creds, data); + }); +}; + +/** + * Creates a new TOTP token for the user associated with this session. + * + * @method createTotpToken + * @param {String} sessionToken SessionToken obtained from signIn + * @param {Object} [options.metricsContext={}] Metrics context metadata + * @param {String} options.metricsContext.deviceId identifier for the current device + * @param {String} options.metricsContext.flowId identifier for the current event flow + * @param {Number} options.metricsContext.flowBeginTime flow.begin event time + * @param {Number} options.metricsContext.utmCampaign marketing campaign identifier + * @param {Number} options.metricsContext.utmContent content identifier + * @param {Number} options.metricsContext.utmMedium acquisition medium + * @param {Number} options.metricsContext.utmSource traffic source + * @param {Number} options.metricsContext.utmTerm search terms + */ +FxAccountClient.prototype.createTotpToken = function(sessionToken, options) { + var request = this.request; + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + var data = {}; if (options && options.metricsContext) { data.metricsContext = metricsContext.marshall(options.metricsContext); } - return self.request.send( - '/account/login/send_unblock_code', - 'POST', - null, - data - ); + return request.send('/totp/create', 'POST', creds, data); }); - }; - - /** - * Reject a login unblock code. Code will be deleted from the server - * and will not be able to be used again. - * - * @method rejectLoginAuthorizationCode - * @param {String} uid Account ID - * @param {String} unblockCode unblock code - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - FxAccountClient.prototype.rejectUnblockCode = function(uid, unblockCode) { - var self = this; - - return Promise.resolve().then(function() { - required(uid, 'uid'); - required(unblockCode, 'unblockCode'); +}; + +/** + * Deletes this user's TOTP token. + * + * @method deleteTotpToken + * @param {String} sessionToken SessionToken obtained from signIn + */ +FxAccountClient.prototype.deleteTotpToken = function(sessionToken) { + var request = this.request; + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + return request.send('/totp/destroy', 'POST', creds, {}); + }); +}; + +/** + * Check to see if the current user has a TOTP token associated with + * their account. + * + * @method checkTotpTokenExists + * @param {String} sessionToken SessionToken obtained from signIn + */ +FxAccountClient.prototype.checkTotpTokenExists = function(sessionToken) { + var request = this.request; + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + return request.send('/totp/exists', 'GET', creds); + }); +}; + +/** + * Verify tokens if using a valid TOTP code. + * + * @method verifyTotpCode + * @param {String} sessionToken SessionToken obtained from signIn + * @param {String} code TOTP code to verif + * @param {String} [options.service] Service being used + */ +FxAccountClient.prototype.verifyTotpCode = function( + sessionToken, + code, + options +) { + var request = this.request; + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + required(code, 'code'); + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { var data = { - uid: uid, - unblockCode: unblockCode, + code: code, }; - return self.request.send( - '/account/login/reject_unblock_code', - 'POST', - null, - data - ); - }); - }; - - /** - * Send an sms. - * - * @method sendSms - * @param {String} sessionToken SessionToken obtained from signIn - * @param {String} phoneNumber Phone number sms will be sent to - * @param {String} messageId Corresponding message id that will be sent - * @param {Object} [options={}] Options - * @param {String} [options.lang] Language that sms will be sent in - * @param {Array} [options.features] Array of features to be enabled for the request - * @param {Object} [options.metricsContext={}] Metrics context metadata - * @param {String} options.metricsContext.deviceId identifier for the current device - * @param {String} options.metricsContext.flowId identifier for the current event flow - * @param {Number} options.metricsContext.flowBeginTime flow.begin event time - * @param {Number} options.metricsContext.utmCampaign marketing campaign identifier - * @param {Number} options.metricsContext.utmContent content identifier - * @param {Number} options.metricsContext.utmMedium acquisition medium - * @param {Number} options.metricsContext.utmSource traffic source - * @param {Number} options.metricsContext.utmTerm search terms - */ - FxAccountClient.prototype.sendSms = function( - sessionToken, - phoneNumber, - messageId, - options - ) { - var request = this.request; - - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - required(phoneNumber, 'phoneNumber'); - required(messageId, 'messageId'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - var data = { - phoneNumber: phoneNumber, - messageId: messageId, - }; - var requestOpts = {}; - - if (options) { - if (options.lang) { - requestOpts.headers = { - 'Accept-Language': options.lang, - }; - } - - if (options.features) { - data.features = options.features; - } - - if (options.metricsContext) { - data.metricsContext = metricsContext.marshall( - options.metricsContext - ); - } - } - - return request.send('/sms', 'POST', creds, data, requestOpts); - }); - }; - - /** - * Get SMS status for the current user. - * - * @method smsStatus - * @param {String} sessionToken SessionToken obtained from signIn - * @param {Object} [options={}] Options - * @param {String} [options.country] country Country to force for testing. - */ - FxAccountClient.prototype.smsStatus = function(sessionToken, options) { - var request = this.request; - options = options || {}; - - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - var url = '/sms/status'; - if (options.country) { - url += '?country=' + encodeURIComponent(options.country); - } - return request.send(url, 'GET', creds); - }); - }; - - /** - * Consume a signinCode. - * - * @method consumeSigninCode - * @param {String} code The signinCode entered by the user - * @param {String} flowId Identifier for the current event flow - * @param {Number} flowBeginTime Timestamp for the flow.begin event - * @param {String} [deviceId] Identifier for the current device - */ - FxAccountClient.prototype.consumeSigninCode = function( - code, - flowId, - flowBeginTime, - deviceId - ) { - var self = this; + if (options && options.service) { + data.service = options.service; + } - return Promise.resolve().then(function() { + return request.send('/session/verify/totp', 'POST', creds, data); + }); +}; + +/** + * Replace user's recovery codes. + * + * @method replaceRecoveryCodes + * @param {String} sessionToken SessionToken obtained from signIn + */ +FxAccountClient.prototype.replaceRecoveryCodes = function(sessionToken) { + var request = this.request; + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + return request.send('/recoveryCodes', 'GET', creds); + }); +}; + +/** + * Consume recovery code. + * + * @method consumeRecoveryCode + * @param {String} sessionToken SessionToken obtained from signIn + * @param {String} code recovery code + */ +FxAccountClient.prototype.consumeRecoveryCode = function(sessionToken, code) { + var request = this.request; + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); required(code, 'code'); - required(flowId, 'flowId'); - required(flowBeginTime, 'flowBeginTime'); - return self.request.send('/signinCodes/consume', 'POST', null, { + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + var data = { code: code, - metricsContext: { - deviceId: deviceId, - flowId: flowId, - flowBeginTime: flowBeginTime, - }, - }); - }); - }; - - /** - * Get the recovery emails associated with the signed in account. - * - * @method recoveryEmails - * @param {String} sessionToken SessionToken obtained from signIn - */ - FxAccountClient.prototype.recoveryEmails = function(sessionToken) { - var request = this.request; - - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - return request.send('/recovery_emails', 'GET', creds); - }); - }; - - /** - * Create a new recovery email for the signed in account. - * - * @method recoveryEmailCreate - * @param {String} sessionToken SessionToken obtained from signIn - * @param {String} email new email to be added - */ - FxAccountClient.prototype.recoveryEmailCreate = function( - sessionToken, - email - ) { - var request = this.request; - - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - required(sessionToken, 'email'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - var data = { - email: email, - }; - - return request.send('/recovery_email', 'POST', creds, data); - }); - }; - - /** - * Remove the recovery email for the signed in account. - * - * @method recoveryEmailDestroy - * @param {String} sessionToken SessionToken obtained from signIn - * @param {String} email email to be removed - */ - FxAccountClient.prototype.recoveryEmailDestroy = function( - sessionToken, - email - ) { - var request = this.request; - - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - required(sessionToken, 'email'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - var data = { - email: email, - }; - - return request.send('/recovery_email/destroy', 'POST', creds, data); - }); - }; - - /** - * Changes user's primary email address. - * - * @method recoveryEmailSetPrimaryEmail - * @param {String} sessionToken SessionToken obtained from signIn - * @param {String} email Email that will be the new primary email for user - */ - FxAccountClient.prototype.recoveryEmailSetPrimaryEmail = function( - sessionToken, - email - ) { - var request = this.request; - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - var data = { - email: email, - }; - return request.send('/recovery_email/set_primary', 'POST', creds, data); - }); - }; - - /** - * Creates a new TOTP token for the user associated with this session. - * - * @method createTotpToken - * @param {String} sessionToken SessionToken obtained from signIn - * @param {Object} [options.metricsContext={}] Metrics context metadata - * @param {String} options.metricsContext.deviceId identifier for the current device - * @param {String} options.metricsContext.flowId identifier for the current event flow - * @param {Number} options.metricsContext.flowBeginTime flow.begin event time - * @param {Number} options.metricsContext.utmCampaign marketing campaign identifier - * @param {Number} options.metricsContext.utmContent content identifier - * @param {Number} options.metricsContext.utmMedium acquisition medium - * @param {Number} options.metricsContext.utmSource traffic source - * @param {Number} options.metricsContext.utmTerm search terms - */ - FxAccountClient.prototype.createTotpToken = function(sessionToken, options) { - var request = this.request; - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - var data = {}; - - if (options && options.metricsContext) { - data.metricsContext = metricsContext.marshall(options.metricsContext); - } - - return request.send('/totp/create', 'POST', creds, data); - }); - }; - - /** - * Deletes this user's TOTP token. - * - * @method deleteTotpToken - * @param {String} sessionToken SessionToken obtained from signIn - */ - FxAccountClient.prototype.deleteTotpToken = function(sessionToken) { - var request = this.request; - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - return request.send('/totp/destroy', 'POST', creds, {}); - }); - }; - - /** - * Check to see if the current user has a TOTP token associated with - * their account. - * - * @method checkTotpTokenExists - * @param {String} sessionToken SessionToken obtained from signIn - */ - FxAccountClient.prototype.checkTotpTokenExists = function(sessionToken) { - var request = this.request; - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - return request.send('/totp/exists', 'GET', creds); - }); - }; - - /** - * Verify tokens if using a valid TOTP code. - * - * @method verifyTotpCode - * @param {String} sessionToken SessionToken obtained from signIn - * @param {String} code TOTP code to verif - * @param {String} [options.service] Service being used - */ - FxAccountClient.prototype.verifyTotpCode = function( - sessionToken, - code, - options - ) { - var request = this.request; - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - required(code, 'code'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - var data = { - code: code, - }; - - if (options && options.service) { - data.service = options.service; - } - - return request.send('/session/verify/totp', 'POST', creds, data); - }); - }; - - /** - * Replace user's recovery codes. - * - * @method replaceRecoveryCodes - * @param {String} sessionToken SessionToken obtained from signIn - */ - FxAccountClient.prototype.replaceRecoveryCodes = function(sessionToken) { - var request = this.request; - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - return request.send('/recoveryCodes', 'GET', creds); - }); - }; - - /** - * Consume recovery code. - * - * @method consumeRecoveryCode - * @param {String} sessionToken SessionToken obtained from signIn - * @param {String} code recovery code - */ - FxAccountClient.prototype.consumeRecoveryCode = function(sessionToken, code) { - var request = this.request; - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - required(code, 'code'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - var data = { - code: code, - }; - - return request.send( - '/session/verify/recoveryCode', - 'POST', - creds, - data - ); - }); - }; - - /** - * Creates a new recovery key for the account. The recovery key contains encrypted - * data the corresponds the the accounts current `kB`. This data can be used during - * the password reset process to avoid regenerating the `kB`. - * - * @param sessionToken - * @param recoveryKeyId The recoveryKeyId that can be used to retrieve saved bundle - * @param bundle The encrypted recovery bundle to store - * @returns {Promise} A promise that will be fulfilled with decoded recovery data (`kB`) - */ - FxAccountClient.prototype.createRecoveryKey = function( - sessionToken, - recoveryKeyId, - bundle - ) { - var request = this.request; - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - required(recoveryKeyId, 'recoveryKeyId'); - required(bundle, 'bundle'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - var data = { - recoveryKeyId: recoveryKeyId, - recoveryData: bundle, - }; - - return request.send('/recoveryKey', 'POST', creds, data); - }); - }; - - /** - * Retrieves the encrypted recovery data that corresponds to the recovery key which - * then gets decoded into the stored `kB`. - * - * @param accountResetToken - * @param recoveryKeyId The recovery key id to retrieve encrypted bundle - * @returns {Promise} A promise that will be fulfilled with decoded recovery data (`kB`) - */ - FxAccountClient.prototype.getRecoveryKey = function( - accountResetToken, - recoveryKeyId - ) { - var request = this.request; - return Promise.resolve() - .then(function() { - required(accountResetToken, 'accountResetToken'); - required(recoveryKeyId, 'recoveryKeyId'); - - return hawkCredentials( - accountResetToken, - 'accountResetToken', - HKDF_SIZE - ); - }) - .then(function(creds) { - return request.send('/recoveryKey/' + recoveryKeyId, 'GET', creds); - }); - }; - - /** - * Reset a user's account using keys (kB) derived from a recovery key. This - * process can be used to maintain the account's original kB. - * - * @param accountResetToken The account reset token - * @param email The current email of the account - * @param newPassword The new password of the account - * @param recoveryKeyId The recovery key id used for account recovery - * @param keys Keys used to create the new wrapKb - * @param {Object} [options={}] Options - * @param {Boolean} [options.keys] - * If `true`, a new `keyFetchToken` is provisioned. `options.sessionToken` - * is required if `options.keys` is true. - * @param {Boolean} [options.sessionToken] - * If `true`, a new `sessionToken` is provisioned. - * @returns {Promise} A promise that will be fulfilled with updated account data - */ - FxAccountClient.prototype.resetPasswordWithRecoveryKey = function( - accountResetToken, - email, - newPassword, - recoveryKeyId, - keys, - options - ) { - options = options || {}; - var request = this.request; - return Promise.resolve() - .then(function() { - required(email, 'email'); - required(newPassword, 'new password'); - required(keys, 'keys'); - required(keys.kB, 'keys.kB'); - required(accountResetToken, 'accountResetToken'); - required(recoveryKeyId, 'recoveryKeyId'); - - var defers = []; - defers.push(credentials.setup(email, newPassword)); - defers.push( - hawkCredentials(accountResetToken, 'accountResetToken', HKDF_SIZE) - ); - - return Promise.all(defers); - }) - .then(function(results) { - var newCreds = results[0]; - var hawkCreds = results[1]; - var newWrapKb = sjcl.codec.hex.fromBits( - credentials.xor(sjcl.codec.hex.toBits(keys.kB), newCreds.unwrapBKey) - ); + }; - var data = { - wrapKb: newWrapKb, - authPW: sjcl.codec.hex.fromBits(newCreds.authPW), - recoveryKeyId: recoveryKeyId, - }; + return request.send('/session/verify/recoveryCode', 'POST', creds, data); + }); +}; + +/** + * Creates a new recovery key for the account. The recovery key contains encrypted + * data the corresponds the the accounts current `kB`. This data can be used during + * the password reset process to avoid regenerating the `kB`. + * + * @param sessionToken + * @param recoveryKeyId The recoveryKeyId that can be used to retrieve saved bundle + * @param bundle The encrypted recovery bundle to store + * @returns {Promise} A promise that will be fulfilled with decoded recovery data (`kB`) + */ +FxAccountClient.prototype.createRecoveryKey = function( + sessionToken, + recoveryKeyId, + bundle +) { + var request = this.request; + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + required(recoveryKeyId, 'recoveryKeyId'); + required(bundle, 'bundle'); + + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + var data = { + recoveryKeyId: recoveryKeyId, + recoveryData: bundle, + }; - if (options.sessionToken) { - data.sessionToken = options.sessionToken; - } + return request.send('/recoveryKey', 'POST', creds, data); + }); +}; + +/** + * Retrieves the encrypted recovery data that corresponds to the recovery key which + * then gets decoded into the stored `kB`. + * + * @param accountResetToken + * @param recoveryKeyId The recovery key id to retrieve encrypted bundle + * @returns {Promise} A promise that will be fulfilled with decoded recovery data (`kB`) + */ +FxAccountClient.prototype.getRecoveryKey = function( + accountResetToken, + recoveryKeyId +) { + var request = this.request; + return Promise.resolve() + .then(function() { + required(accountResetToken, 'accountResetToken'); + required(recoveryKeyId, 'recoveryKeyId'); + + return hawkCredentials(accountResetToken, 'accountResetToken', HKDF_SIZE); + }) + .then(function(creds) { + return request.send('/recoveryKey/' + recoveryKeyId, 'GET', creds); + }); +}; + +/** + * Reset a user's account using keys (kB) derived from a recovery key. This + * process can be used to maintain the account's original kB. + * + * @param accountResetToken The account reset token + * @param email The current email of the account + * @param newPassword The new password of the account + * @param recoveryKeyId The recovery key id used for account recovery + * @param keys Keys used to create the new wrapKb + * @param {Object} [options={}] Options + * @param {Boolean} [options.keys] + * If `true`, a new `keyFetchToken` is provisioned. `options.sessionToken` + * is required if `options.keys` is true. + * @param {Boolean} [options.sessionToken] + * If `true`, a new `sessionToken` is provisioned. + * @returns {Promise} A promise that will be fulfilled with updated account data + */ +FxAccountClient.prototype.resetPasswordWithRecoveryKey = function( + accountResetToken, + email, + newPassword, + recoveryKeyId, + keys, + options +) { + options = options || {}; + var request = this.request; + return Promise.resolve() + .then(function() { + required(email, 'email'); + required(newPassword, 'new password'); + required(keys, 'keys'); + required(keys.kB, 'keys.kB'); + required(accountResetToken, 'accountResetToken'); + required(recoveryKeyId, 'recoveryKeyId'); + + var defers = []; + defers.push(credentials.setup(email, newPassword)); + defers.push( + hawkCredentials(accountResetToken, 'accountResetToken', HKDF_SIZE) + ); - if (options.keys) { - required(options.sessionToken, 'sessionToken'); - } + return Promise.all(defers); + }) + .then(function(results) { + var newCreds = results[0]; + var hawkCreds = results[1]; + var newWrapKb = sjcl.codec.hex.fromBits( + credentials.xor(sjcl.codec.hex.toBits(keys.kB), newCreds.unwrapBKey) + ); - var queryParams = ''; - if (options.keys) { - queryParams = '?keys=true'; - } + var data = { + wrapKb: newWrapKb, + authPW: sjcl.codec.hex.fromBits(newCreds.authPW), + recoveryKeyId: recoveryKeyId, + }; - return request - .send('/account/reset' + queryParams, 'POST', hawkCreds, data) - .then(function(accountData) { - if (options.keys && accountData.keyFetchToken) { - accountData.unwrapBKey = sjcl.codec.hex.fromBits( - newCreds.unwrapBKey - ); - } - return accountData; - }); - }); - }; + if (options.sessionToken) { + data.sessionToken = options.sessionToken; + } - /** - * Deletes the recovery key associated with this user. - * - * @param sessionToken - */ - FxAccountClient.prototype.deleteRecoveryKey = function(sessionToken) { - var request = this.request; - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - return request.send('/recoveryKey', 'DELETE', creds, {}); - }); - }; + if (options.keys) { + required(options.sessionToken, 'sessionToken'); + } - /** - * This checks to see if a recovery key exists for a user. This check - * can be performed with either a sessionToken or an email. - * - * Typically, sessionToken is used when checking from within the `/settings` - * view. If it exists, we can give the user an option to revoke the key. - * - * Checking with an email is typically performed during the password reset - * flow. It is used to decide whether or not we can redirect a user to - * the `Reset password with recovery key` page or regular password reset page. - * - * @param sessionToken - * @param {String} email User's email - * @returns {Promise} A promise that will be fulfilled with whether or not account has recovery ket - */ - FxAccountClient.prototype.recoveryKeyExists = function(sessionToken, email) { - var request = this.request; - return Promise.resolve().then(function() { - if (sessionToken) { - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE).then( - function(creds) { - return request.send('/recoveryKey/exists', 'POST', creds, {}); - } - ); + var queryParams = ''; + if (options.keys) { + queryParams = '?keys=true'; } - return request.send('/recoveryKey/exists', 'POST', null, { - email: email, - }); + return request + .send('/account/reset' + queryParams, 'POST', hawkCreds, data) + .then(function(accountData) { + if (options.keys && accountData.keyFetchToken) { + accountData.unwrapBKey = sjcl.codec.hex.fromBits( + newCreds.unwrapBKey + ); + } + return accountData; + }); }); - }; +}; + +/** + * Deletes the recovery key associated with this user. + * + * @param sessionToken + */ +FxAccountClient.prototype.deleteRecoveryKey = function(sessionToken) { + var request = this.request; + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + return request.send('/recoveryKey', 'DELETE', creds, {}); + }); +}; + +/** + * This checks to see if a recovery key exists for a user. This check + * can be performed with either a sessionToken or an email. + * + * Typically, sessionToken is used when checking from within the `/settings` + * view. If it exists, we can give the user an option to revoke the key. + * + * Checking with an email is typically performed during the password reset + * flow. It is used to decide whether or not we can redirect a user to + * the `Reset password with recovery key` page or regular password reset page. + * + * @param sessionToken + * @param {String} email User's email + * @returns {Promise} A promise that will be fulfilled with whether or not account has recovery ket + */ +FxAccountClient.prototype.recoveryKeyExists = function(sessionToken, email) { + var request = this.request; + return Promise.resolve().then(function() { + if (sessionToken) { + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE).then( + function(creds) { + return request.send('/recoveryKey/exists', 'POST', creds, {}); + } + ); + } - /** - * Create an OAuth code using `sessionToken` - * - * @param {String} sessionToken - * @param {String} clientId - * @param {String} state - * @param {Object} [options={}] Options - * @param {String} [options.access_type=online] if `accessType=offline`, a refresh token - * will be issued when trading the code for an access token. - * @param {String} [options.acr_values] allowed ACR values - * @param {String} [options.keys_jwe] Keys used to encrypt - * @param {String} [options.redirect_uri] registered redirect URI to return to - * @param {String} [options.response_type=code] response type - * @param {String} [options.scope] requested scopes - * @param {String} [options.code_challenge_method] PKCE code challenge method - * @param {String} [options.code_challenge] PKCE code challenge - * @returns {Promise} A promise that will be fulfilled with: - * - `redirect` - redirect URI - * - `code` - authorization code - * - `state` - state token - */ - FxAccountClient.prototype.createOAuthCode = function( - sessionToken, - clientId, - state, - options - ) { - options = options || {}; - - var params = { - access_type: options.access_type, - acr_values: options.acr_values, - client_id: clientId, - code_challenge: options.code_challenge, - code_challenge_method: options.code_challenge_method, - keys_jwe: options.keys_jwe, - redirect_uri: options.redirect_uri, - response_type: options.response_type, - scope: options.scope, - state: state, - }; - var request = this.request; - - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - required(clientId, 'clientId'); - required(state, 'state'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - return request.send('/oauth/authorization', 'POST', creds, params); - }); + return request.send('/recoveryKey/exists', 'POST', null, { + email: email, + }); + }); +}; + +/** + * Create an OAuth code using `sessionToken` + * + * @param {String} sessionToken + * @param {String} clientId + * @param {String} state + * @param {Object} [options={}] Options + * @param {String} [options.access_type=online] if `accessType=offline`, a refresh token + * will be issued when trading the code for an access token. + * @param {String} [options.acr_values] allowed ACR values + * @param {String} [options.keys_jwe] Keys used to encrypt + * @param {String} [options.redirect_uri] registered redirect URI to return to + * @param {String} [options.response_type=code] response type + * @param {String} [options.scope] requested scopes + * @param {String} [options.code_challenge_method] PKCE code challenge method + * @param {String} [options.code_challenge] PKCE code challenge + * @returns {Promise} A promise that will be fulfilled with: + * - `redirect` - redirect URI + * - `code` - authorization code + * - `state` - state token + */ +FxAccountClient.prototype.createOAuthCode = function( + sessionToken, + clientId, + state, + options +) { + options = options || {}; + + var params = { + access_type: options.access_type, + acr_values: options.acr_values, + client_id: clientId, + code_challenge: options.code_challenge, + code_challenge_method: options.code_challenge_method, + keys_jwe: options.keys_jwe, + redirect_uri: options.redirect_uri, + response_type: options.response_type, + scope: options.scope, + state: state, }; - - /** - * Create an OAuth token using `sessionToken` - * - * @param {String} sessionToken - * @param {String} clientId - * @param {Object} [options={}] Options - * @param {String} [options.access_type=online] if `accessType=offline`, a refresh token - * will be issued when trading the code for an access token. - * @param {String} [options.scope] requested scopes - * @param {Number} [options.ttl] time to live, in seconds - * @returns {Promise} A promise that will be fulfilled with: - * - `access_token` - The access token - * - `refresh_token` - A refresh token, if `options.accessType=offline` - * - `id_token` - an OIDC ID token, returned if `scope` includes `openid` - * - `scope` - Requested scopes - * - `auth_at` - Time the user authenticated - * - `token_type` - The string `bearer` - * - `expires_in` - Time at which the token expires - */ - FxAccountClient.prototype.createOAuthToken = function( - sessionToken, - clientId, - options - ) { - options = options || {}; - - var params = { - grant_type: 'fxa-credentials', - access_type: options.access_type, - client_id: clientId, - scope: options.scope, - ttl: options.ttl, - }; - - var request = this.request; - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - required(clientId, 'clientId'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - return request.send('/oauth/token', 'POST', creds, params); - }); + var request = this.request; + + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + required(clientId, 'clientId'); + required(state, 'state'); + + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + return request.send('/oauth/authorization', 'POST', creds, params); + }); +}; + +/** + * Create an OAuth token using `sessionToken` + * + * @param {String} sessionToken + * @param {String} clientId + * @param {Object} [options={}] Options + * @param {String} [options.access_type=online] if `accessType=offline`, a refresh token + * will be issued when trading the code for an access token. + * @param {String} [options.scope] requested scopes + * @param {Number} [options.ttl] time to live, in seconds + * @returns {Promise} A promise that will be fulfilled with: + * - `access_token` - The access token + * - `refresh_token` - A refresh token, if `options.accessType=offline` + * - `id_token` - an OIDC ID token, returned if `scope` includes `openid` + * - `scope` - Requested scopes + * - `auth_at` - Time the user authenticated + * - `token_type` - The string `bearer` + * - `expires_in` - Time at which the token expires + */ +FxAccountClient.prototype.createOAuthToken = function( + sessionToken, + clientId, + options +) { + options = options || {}; + + var params = { + grant_type: 'fxa-credentials', + access_type: options.access_type, + client_id: clientId, + scope: options.scope, + ttl: options.ttl, }; - /** - * Use `sessionToken` to get scoped key data for the RP associated with `client_id` - * - * @param {String} sessionToken - * @param {String} clientId - * @param {String} scope - * @returns {Promise} A promise that will be fulfilled with: - * - `identifier` - * - `keyRotationSecret` - * - `keyRotationTimestamp` - */ - FxAccountClient.prototype.getOAuthScopedKeyData = function( - sessionToken, - clientId, - scope - ) { - var params = { - client_id: clientId, - scope: scope, - }; - - var request = this.request; - return Promise.resolve() - .then(function() { - required(sessionToken, 'sessionToken'); - required(clientId, 'clientId'); - required(scope, 'scope'); - - return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); - }) - .then(function(creds) { - return request.send('/account/scoped-key-data', 'POST', creds, params); - }); - }; + var request = this.request; + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + required(clientId, 'clientId'); - /** - * Get a user's list of active subscriptions. - * - * @param {String} token A token from the OAuth server. - * @returns {Promise} A promise that will be fulfilled with a list of active - * subscriptions. - */ - FxAccountClient.prototype.getActiveSubscriptions = function(token) { - var self = this; - - return Promise.resolve().then(function() { - required(token, 'token'); - const requestOptions = { - headers: { - Authorization: `Bearer ${token}`, - }, - }; - return self.request.send( - '/oauth/subscriptions/active', - 'GET', - null, - null, - requestOptions - ); + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + return request.send('/oauth/token', 'POST', creds, params); }); +}; + +/** + * Use `sessionToken` to get scoped key data for the RP associated with `client_id` + * + * @param {String} sessionToken + * @param {String} clientId + * @param {String} scope + * @returns {Promise} A promise that will be fulfilled with: + * - `identifier` + * - `keyRotationSecret` + * - `keyRotationTimestamp` + */ +FxAccountClient.prototype.getOAuthScopedKeyData = function( + sessionToken, + clientId, + scope +) { + var params = { + client_id: clientId, + scope: scope, }; - /** - * Submit a support ticket. - * - * @param {String} authorizationHeader A token from the OAuth server. - * @param {Object} [supportTicket={}] - * @param {String} [supportTicket.topic] - * @param {String} [supportTicket.subject] Optional subject - * @param {String} [supportTicket.message] - * @returns {Promise} A promise that will be fulfilled with: - * - `success` - * - `ticket` OR `error` - */ - FxAccountClient.prototype.createSupportTicket = function( - token, - supportTicket - ) { - var self = this; - - return Promise.resolve().then(function() { - required(token, 'token'); - required(supportTicket, 'supportTicket'); - const requestOptions = { - headers: { - Authorization: `Bearer ${token}`, - }, - }; - return self.request.send( - '/support/ticket', - 'POST', - null, - supportTicket, - requestOptions - ); + var request = this.request; + return Promise.resolve() + .then(function() { + required(sessionToken, 'sessionToken'); + required(clientId, 'clientId'); + required(scope, 'scope'); + + return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE); + }) + .then(function(creds) { + return request.send('/account/scoped-key-data', 'POST', creds, params); }); - }; - - /** - * Check for a required argument. Exposed for unit testing. - * - * @param {Value} val - value to check - * @param {String} name - name of value - * @throws {Error} if argument is falsey, or an empty object - */ - FxAccountClient.prototype._required = required; - - return FxAccountClient; -}); +}; + +/** + * Get a user's list of active subscriptions. + * + * @param {String} token A token from the OAuth server. + * @returns {Promise} A promise that will be fulfilled with a list of active + * subscriptions. + */ +FxAccountClient.prototype.getActiveSubscriptions = function(token) { + var self = this; + + return Promise.resolve().then(function() { + required(token, 'token'); + const requestOptions = { + headers: { + Authorization: `Bearer ${token}`, + }, + }; + return self.request.send( + '/oauth/subscriptions/active', + 'GET', + null, + null, + requestOptions + ); + }); +}; + +/** + * Submit a support ticket. + * + * @param {String} authorizationHeader A token from the OAuth server. + * @param {Object} [supportTicket={}] + * @param {String} [supportTicket.topic] + * @param {String} [supportTicket.subject] Optional subject + * @param {String} [supportTicket.message] + * @returns {Promise} A promise that will be fulfilled with: + * - `success` + * - `ticket` OR `error` + */ +FxAccountClient.prototype.createSupportTicket = function(token, supportTicket) { + var self = this; + + return Promise.resolve().then(function() { + required(token, 'token'); + required(supportTicket, 'supportTicket'); + const requestOptions = { + headers: { + Authorization: `Bearer ${token}`, + }, + }; + return self.request.send( + '/support/ticket', + 'POST', + null, + supportTicket, + requestOptions + ); + }); +}; + +/** + * Check for a required argument. Exposed for unit testing. + * + * @param {Value} val - value to check + * @param {String} name - name of value + * @throws {Error} if argument is falsey, or an empty object + */ +FxAccountClient.prototype._required = required; + +module.exports = FxAccountClient; diff --git a/packages/fxa-js-client/client/lib/credentials.js b/packages/fxa-js-client/client/lib/credentials.js index e01367204fb..216d4fc7829 100644 --- a/packages/fxa-js-client/client/lib/credentials.js +++ b/packages/fxa-js-client/client/lib/credentials.js @@ -1,163 +1,162 @@ /* 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/. */ -define(['./request', 'sjcl', './hkdf', './pbkdf2'], function( - Request, - sjcl, - hkdf, - pbkdf2 -) { - 'use strict'; - - // Key wrapping and stretching configuration. - var NAMESPACE = 'identity.mozilla.com/picl/v1/'; - var PBKDF2_ROUNDS = 1000; - var STRETCHED_PASS_LENGTH_BYTES = 32 * 8; - - var HKDF_SALT = sjcl.codec.hex.toBits('00'); - var HKDF_LENGTH = 32; +'use strict'; + +const Request = require('./request'); +const sjcl = require('sjcl'); +const hkdf = require('./hkdf'); +const pbkdf2 = require('./pbkdf2'); + +// Key wrapping and stretching configuration. +var NAMESPACE = 'identity.mozilla.com/picl/v1/'; +var PBKDF2_ROUNDS = 1000; +var STRETCHED_PASS_LENGTH_BYTES = 32 * 8; + +var HKDF_SALT = sjcl.codec.hex.toBits('00'); +var HKDF_LENGTH = 32; + +/** + * Key Wrapping with a name + * + * @method kw + * @static + * @param {String} name The name of the salt + * @return {bitArray} the salt combination with the namespace + */ +function kw(name) { + return sjcl.codec.utf8String.toBits(NAMESPACE + name); +} + +/** + * Key Wrapping with a name and an email + * + * @method kwe + * @static + * @param {String} name The name of the salt + * @param {String} email The email of the user. + * @return {bitArray} the salt combination with the namespace + */ +function kwe(name, email) { + return sjcl.codec.utf8String.toBits(NAMESPACE + name + ':' + email); +} + +/** + * @class credentials + * @constructor + */ +module.exports = { /** - * Key Wrapping with a name + * Setup credentials * - * @method kw - * @static - * @param {String} name The name of the salt - * @return {bitArray} the salt combination with the namespace + * @method setup + * @param {String} emailInput + * @param {String} passwordInput + * @return {Promise} A promise that will be fulfilled with `result` of generated credentials */ - function kw(name) { - return sjcl.codec.utf8String.toBits(NAMESPACE + name); - } + setup: function(emailInput, passwordInput) { + var result = {}; + var email = kwe('quickStretch', emailInput); + var password = sjcl.codec.utf8String.toBits(passwordInput); + + result.emailUTF8 = emailInput; + result.passwordUTF8 = passwordInput; + + return pbkdf2 + .derive(password, email, PBKDF2_ROUNDS, STRETCHED_PASS_LENGTH_BYTES) + .then(function(quickStretchedPW) { + result.quickStretchedPW = quickStretchedPW; + + return hkdf( + quickStretchedPW, + kw('authPW'), + HKDF_SALT, + HKDF_LENGTH + ).then(function(authPW) { + result.authPW = authPW; + return hkdf( + quickStretchedPW, + kw('unwrapBkey'), + HKDF_SALT, + HKDF_LENGTH + ); + }); + }) + .then(function(unwrapBKey) { + result.unwrapBKey = unwrapBKey; + return result; + }); + }, /** - * Key Wrapping with a name and an email + * Wrap * - * @method kwe - * @static - * @param {String} name The name of the salt - * @param {String} email The email of the user. - * @return {bitArray} the salt combination with the namespace + * @method wrap + * @param {bitArray} bitArray1 + * @param {bitArray} bitArray2 + * @return {bitArray} wrap result of the two bitArrays */ - function kwe(name, email) { - return sjcl.codec.utf8String.toBits(NAMESPACE + name + ':' + email); - } + xor: function(bitArray1, bitArray2) { + var result = []; + + for (var i = 0; i < bitArray1.length; i++) { + result[i] = bitArray1[i] ^ bitArray2[i]; + } + return result; + }, /** - * @class credentials - * @constructor + * Unbundle the WrapKB + * @param {String} key Bundle Key in hex + * @param {String} bundle Key bundle in hex + * @returns {*} */ - return { - /** - * Setup credentials - * - * @method setup - * @param {String} emailInput - * @param {String} passwordInput - * @return {Promise} A promise that will be fulfilled with `result` of generated credentials - */ - setup: function(emailInput, passwordInput) { - var result = {}; - var email = kwe('quickStretch', emailInput); - var password = sjcl.codec.utf8String.toBits(passwordInput); - - result.emailUTF8 = emailInput; - result.passwordUTF8 = passwordInput; - - return pbkdf2 - .derive(password, email, PBKDF2_ROUNDS, STRETCHED_PASS_LENGTH_BYTES) - .then(function(quickStretchedPW) { - result.quickStretchedPW = quickStretchedPW; - - return hkdf( - quickStretchedPW, - kw('authPW'), - HKDF_SALT, - HKDF_LENGTH - ).then(function(authPW) { - result.authPW = authPW; - - return hkdf( - quickStretchedPW, - kw('unwrapBkey'), - HKDF_SALT, - HKDF_LENGTH - ); - }); - }) - .then(function(unwrapBKey) { - result.unwrapBKey = unwrapBKey; - return result; - }); - }, - /** - * Wrap - * - * @method wrap - * @param {bitArray} bitArray1 - * @param {bitArray} bitArray2 - * @return {bitArray} wrap result of the two bitArrays - */ - xor: function(bitArray1, bitArray2) { - var result = []; - - for (var i = 0; i < bitArray1.length; i++) { - result[i] = bitArray1[i] ^ bitArray2[i]; + unbundleKeyFetchResponse: function(key, bundle) { + var self = this; + var bitBundle = sjcl.codec.hex.toBits(bundle); + + return this.deriveBundleKeys(key, 'account/keys').then(function(keys) { + var ciphertext = sjcl.bitArray.bitSlice(bitBundle, 0, 8 * 64); + var expectedHmac = sjcl.bitArray.bitSlice(bitBundle, 8 * -32); + var hmac = new sjcl.misc.hmac(keys.hmacKey, sjcl.hash.sha256); + hmac.update(ciphertext); + + if (!sjcl.bitArray.equal(hmac.digest(), expectedHmac)) { + throw new Error('Bad HMac'); } - return result; - }, - /** - * Unbundle the WrapKB - * @param {String} key Bundle Key in hex - * @param {String} bundle Key bundle in hex - * @returns {*} - */ - unbundleKeyFetchResponse: function(key, bundle) { - var self = this; - var bitBundle = sjcl.codec.hex.toBits(bundle); - - return this.deriveBundleKeys(key, 'account/keys').then(function(keys) { - var ciphertext = sjcl.bitArray.bitSlice(bitBundle, 0, 8 * 64); - var expectedHmac = sjcl.bitArray.bitSlice(bitBundle, 8 * -32); - var hmac = new sjcl.misc.hmac(keys.hmacKey, sjcl.hash.sha256); - hmac.update(ciphertext); - - if (!sjcl.bitArray.equal(hmac.digest(), expectedHmac)) { - throw new Error('Bad HMac'); - } - - var keyAWrapB = self.xor( - sjcl.bitArray.bitSlice(bitBundle, 0, 8 * 64), - keys.xorKey - ); - - return { - kA: sjcl.codec.hex.fromBits( - sjcl.bitArray.bitSlice(keyAWrapB, 0, 8 * 32) - ), - wrapKB: sjcl.codec.hex.fromBits( - sjcl.bitArray.bitSlice(keyAWrapB, 8 * 32) - ), - }; - }); - }, - /** - * Derive the HMAC and XOR keys required to encrypt a given size of payload. - * @param {String} key Hex Bundle Key - * @param {String} keyInfo Bundle Key Info - * @returns {Object} hmacKey, xorKey - */ - deriveBundleKeys: function(key, keyInfo) { - var bitKeyInfo = kw(keyInfo); - var salt = sjcl.codec.hex.toBits(''); - key = sjcl.codec.hex.toBits(key); - - return hkdf(key, bitKeyInfo, salt, 3 * 32).then(function(keyMaterial) { - return { - hmacKey: sjcl.bitArray.bitSlice(keyMaterial, 0, 8 * 32), - xorKey: sjcl.bitArray.bitSlice(keyMaterial, 8 * 32), - }; - }); - }, - }; -}); + var keyAWrapB = self.xor( + sjcl.bitArray.bitSlice(bitBundle, 0, 8 * 64), + keys.xorKey + ); + + return { + kA: sjcl.codec.hex.fromBits( + sjcl.bitArray.bitSlice(keyAWrapB, 0, 8 * 32) + ), + wrapKB: sjcl.codec.hex.fromBits( + sjcl.bitArray.bitSlice(keyAWrapB, 8 * 32) + ), + }; + }); + }, + /** + * Derive the HMAC and XOR keys required to encrypt a given size of payload. + * @param {String} key Hex Bundle Key + * @param {String} keyInfo Bundle Key Info + * @returns {Object} hmacKey, xorKey + */ + deriveBundleKeys: function(key, keyInfo) { + var bitKeyInfo = kw(keyInfo); + var salt = sjcl.codec.hex.toBits(''); + key = sjcl.codec.hex.toBits(key); + + return hkdf(key, bitKeyInfo, salt, 3 * 32).then(function(keyMaterial) { + return { + hmacKey: sjcl.bitArray.bitSlice(keyMaterial, 0, 8 * 32), + xorKey: sjcl.bitArray.bitSlice(keyMaterial, 8 * 32), + }; + }); + }, +}; diff --git a/packages/fxa-js-client/client/lib/errors.js b/packages/fxa-js-client/client/lib/errors.js index c3715594109..35a257d9587 100644 --- a/packages/fxa-js-client/client/lib/errors.js +++ b/packages/fxa-js-client/client/lib/errors.js @@ -1,9 +1,8 @@ /* 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/. */ -define([], function() { - return { - INVALID_TIMESTAMP: 111, - INCORRECT_EMAIL_CASE: 120, - }; -}); + +module.exports = { + INVALID_TIMESTAMP: 111, + INCORRECT_EMAIL_CASE: 120, +}; diff --git a/packages/fxa-js-client/client/lib/hawk.js b/packages/fxa-js-client/client/lib/hawk.js index 8b7ce2942b7..28184966edc 100644 --- a/packages/fxa-js-client/client/lib/hawk.js +++ b/packages/fxa-js-client/client/lib/hawk.js @@ -1,586 +1,585 @@ /* 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/. */ -define(['sjcl'], function(sjcl) { - 'use strict'; - /* - HTTP Hawk Authentication Scheme - Copyright (c) 2012-2013, Eran Hammer - MIT Licensed - */ - - // Declare namespace - - var hawk = {}; - - hawk.client = { - // Generate an Authorization header for a given request - - /* - uri: 'http://example.com/resource?a=b' - method: HTTP verb (e.g. 'GET', 'POST') - options: { - - // Required - - credentials: { - id: 'dh37fgj492je', - key: 'aoijedoaijsdlaksjdl', - algorithm: 'sha256' // 'sha1', 'sha256' - }, - - // Optional - - ext: 'application-specific', // Application specific data sent via the ext attribute - timestamp: Date.now() / 1000, // A pre-calculated timestamp in seconds - nonce: '2334f34f', // A pre-generated nonce - localtimeOffsetMsec: 400, // Time offset to sync with server time (ignored if timestamp provided) - payload: '{"some":"payload"}', // UTF-8 encoded string for body hash generation (ignored if hash provided) - contentType: 'application/json', // Payload content-type (ignored if hash provided) - hash: 'U4MKKSmiVxk37JCCrAVIjV=', // Pre-calculated payload hash - app: '24s23423f34dx', // Oz application id - dlg: '234sz34tww3sd' // Oz delegated-by application id - } - */ - // eslint-disable-next-line complexity - header: function(uri, method, options) { - /*eslint complexity: [2, 21] */ - var result = { - field: '', - artifacts: {}, - }; - - // Validate inputs - - if ( - !uri || - (typeof uri !== 'string' && typeof uri !== 'object') || - !method || - typeof method !== 'string' || - !options || - typeof options !== 'object' - ) { - result.err = 'Invalid argument type'; - return result; - } - - // Application time - - var timestamp = - options.timestamp || - Math.floor( - (hawk.utils.now() + (options.localtimeOffsetMsec || 0)) / 1000 - ); - - // Validate credentials - - var credentials = options.credentials; - if ( - !credentials || - !credentials.id || - !credentials.key || - !credentials.algorithm - ) { - result.err = 'Invalid credential object'; - return result; - } - - if ( - hawk.utils.baseIndexOf( - hawk.crypto.algorithms, - credentials.algorithm - ) === -1 - ) { - result.err = 'Unknown algorithm'; - return result; - } +'use strict'; - // Parse URI +const sjcl = require('sjcl'); - if (typeof uri === 'string') { - uri = hawk.utils.parseUri(uri); - } +/* + HTTP Hawk Authentication Scheme + Copyright (c) 2012-2013, Eran Hammer + MIT Licensed + */ - // Calculate signature - - var artifacts = { - ts: timestamp, - nonce: options.nonce || hawk.utils.randomString(6), - method: method, - resource: uri.relative, - host: uri.hostname, - port: uri.port, - hash: options.hash, - ext: options.ext, - app: options.app, - dlg: options.dlg, - }; - - result.artifacts = artifacts; - - // Calculate payload hash - - if (!artifacts.hash && options.hasOwnProperty('payload')) { - artifacts.hash = hawk.crypto.calculatePayloadHash( - options.payload, - credentials.algorithm, - options.contentType - ); - } +// Declare namespace - var mac = hawk.crypto.calculateMac('header', credentials, artifacts); - - // Construct header - - var hasExt = - artifacts.ext !== null && - artifacts.ext !== undefined && - artifacts.ext !== ''; // Other falsey values allowed - var header = - 'Hawk id="' + - credentials.id + - '", ts="' + - artifacts.ts + - '", nonce="' + - artifacts.nonce + - (artifacts.hash ? '", hash="' + artifacts.hash : '') + - (hasExt - ? '", ext="' + hawk.utils.escapeHeaderAttribute(artifacts.ext) - : '') + - '", mac="' + - mac + - '"'; +var hawk = {}; - if (artifacts.app) { - header += - ', app="' + - artifacts.app + - (artifacts.dlg ? '", dlg="' + artifacts.dlg : '') + - '"'; - } - - result.field = header; +hawk.client = { + // Generate an Authorization header for a given request + /* + uri: 'http://example.com/resource?a=b' + method: HTTP verb (e.g. 'GET', 'POST') + options: { + + // Required + + credentials: { + id: 'dh37fgj492je', + key: 'aoijedoaijsdlaksjdl', + algorithm: 'sha256' // 'sha1', 'sha256' + }, + + // Optional + + ext: 'application-specific', // Application specific data sent via the ext attribute + timestamp: Date.now() / 1000, // A pre-calculated timestamp in seconds + nonce: '2334f34f', // A pre-generated nonce + localtimeOffsetMsec: 400, // Time offset to sync with server time (ignored if timestamp provided) + payload: '{"some":"payload"}', // UTF-8 encoded string for body hash generation (ignored if hash provided) + contentType: 'application/json', // Payload content-type (ignored if hash provided) + hash: 'U4MKKSmiVxk37JCCrAVIjV=', // Pre-calculated payload hash + app: '24s23423f34dx', // Oz application id + dlg: '234sz34tww3sd' // Oz delegated-by application id + } + */ + // eslint-disable-next-line complexity + header: function(uri, method, options) { + /*eslint complexity: [2, 21] */ + var result = { + field: '', + artifacts: {}, + }; + + // Validate inputs + + if ( + !uri || + (typeof uri !== 'string' && typeof uri !== 'object') || + !method || + typeof method !== 'string' || + !options || + typeof options !== 'object' + ) { + result.err = 'Invalid argument type'; return result; - }, - - // Validate server response - - /* - request: object created via 'new XMLHttpRequest()' after response received - artifacts: object recieved from header().artifacts - options: { - payload: optional payload received - required: specifies if a Server-Authorization header is required. Defaults to 'false' - } - */ + } - authenticate: function(request, credentials, artifacts, options) { - options = options || {}; + // Application time - if (request.getResponseHeader('www-authenticate')) { - // Parse HTTP WWW-Authenticate header + var timestamp = + options.timestamp || + Math.floor( + (hawk.utils.now() + (options.localtimeOffsetMsec || 0)) / 1000 + ); - var attrsAuth = hawk.utils.parseAuthorizationHeader( - request.getResponseHeader('www-authenticate'), - ['ts', 'tsm', 'error'] - ); - if (!attrsAuth) { - return false; - } + // Validate credentials - if (attrsAuth.ts) { - var tsm = hawk.crypto.calculateTsMac(attrsAuth.ts, credentials); - if (tsm !== attrsAuth.tsm) { - return false; - } + var credentials = options.credentials; + if ( + !credentials || + !credentials.id || + !credentials.key || + !credentials.algorithm + ) { + result.err = 'Invalid credential object'; + return result; + } - hawk.utils.setNtpOffset( - attrsAuth.ts - Math.floor(new Date().getTime() / 1000) - ); // Keep offset at 1 second precision - } - } + if ( + hawk.utils.baseIndexOf(hawk.crypto.algorithms, credentials.algorithm) === + -1 + ) { + result.err = 'Unknown algorithm'; + return result; + } - // Parse HTTP Server-Authorization header + // Parse URI - if ( - !request.getResponseHeader('server-authorization') && - !options.required - ) { - return true; - } + if (typeof uri === 'string') { + uri = hawk.utils.parseUri(uri); + } - var attributes = hawk.utils.parseAuthorizationHeader( - request.getResponseHeader('server-authorization'), - ['mac', 'ext', 'hash'] - ); - if (!attributes) { - return false; - } + // Calculate signature - var modArtifacts = { - ts: artifacts.ts, - nonce: artifacts.nonce, - method: artifacts.method, - resource: artifacts.resource, - host: artifacts.host, - port: artifacts.port, - hash: attributes.hash, - ext: attributes.ext, - app: artifacts.app, - dlg: artifacts.dlg, - }; - - var mac = hawk.crypto.calculateMac('response', credentials, modArtifacts); - if (mac !== attributes.mac) { - return false; - } + var artifacts = { + ts: timestamp, + nonce: options.nonce || hawk.utils.randomString(6), + method: method, + resource: uri.relative, + host: uri.hostname, + port: uri.port, + hash: options.hash, + ext: options.ext, + app: options.app, + dlg: options.dlg, + }; - if (!options.hasOwnProperty('payload')) { - return true; - } + result.artifacts = artifacts; - if (!attributes.hash) { - return false; - } + // Calculate payload hash - var calculatedHash = hawk.crypto.calculatePayloadHash( + if (!artifacts.hash && options.hasOwnProperty('payload')) { + artifacts.hash = hawk.crypto.calculatePayloadHash( options.payload, credentials.algorithm, - request.getResponseHeader('content-type') + options.contentType ); - return calculatedHash === attributes.hash; - }, - - message: function(host, port, message, options) { - // Validate inputs - - if ( - !host || - typeof host !== 'string' || - !port || - typeof port !== 'number' || - message === null || - message === undefined || - typeof message !== 'string' || - !options || - typeof options !== 'object' - ) { - return null; - } - - // Application time - - var timestamp = - options.timestamp || - Math.floor( - (hawk.utils.now() + (options.localtimeOffsetMsec || 0)) / 1000 - ); - - // Validate credentials - - var credentials = options.credentials; - if ( - !credentials || - !credentials.id || - !credentials.key || - !credentials.algorithm - ) { - // Invalid credential object - return null; - } - - if (hawk.crypto.algorithms.indexOf(credentials.algorithm) === -1) { - return null; - } + } + + var mac = hawk.crypto.calculateMac('header', credentials, artifacts); + + // Construct header + + var hasExt = + artifacts.ext !== null && + artifacts.ext !== undefined && + artifacts.ext !== ''; // Other falsey values allowed + var header = + 'Hawk id="' + + credentials.id + + '", ts="' + + artifacts.ts + + '", nonce="' + + artifacts.nonce + + (artifacts.hash ? '", hash="' + artifacts.hash : '') + + (hasExt + ? '", ext="' + hawk.utils.escapeHeaderAttribute(artifacts.ext) + : '') + + '", mac="' + + mac + + '"'; + + if (artifacts.app) { + header += + ', app="' + + artifacts.app + + (artifacts.dlg ? '", dlg="' + artifacts.dlg : '') + + '"'; + } - // Calculate signature + result.field = header; - var artifacts = { - ts: timestamp, - nonce: options.nonce || hawk.utils.randomString(6), - host: host, - port: port, - hash: hawk.crypto.calculatePayloadHash(message, credentials.algorithm), - }; + return result; + }, - // Construct authorization + // Validate server response - var result = { - id: credentials.id, - ts: artifacts.ts, - nonce: artifacts.nonce, - hash: artifacts.hash, - mac: hawk.crypto.calculateMac('message', credentials, artifacts), - }; + /* + request: object created via 'new XMLHttpRequest()' after response received + artifacts: object recieved from header().artifacts + options: { + payload: optional payload received + required: specifies if a Server-Authorization header is required. Defaults to 'false' + } + */ - return result; - }, + authenticate: function(request, credentials, artifacts, options) { + options = options || {}; - authenticateTimestamp: function(message, credentials, updateClock) { - // updateClock defaults to true + if (request.getResponseHeader('www-authenticate')) { + // Parse HTTP WWW-Authenticate header - var tsm = hawk.crypto.calculateTsMac(message.ts, credentials); - if (tsm !== message.tsm) { + var attrsAuth = hawk.utils.parseAuthorizationHeader( + request.getResponseHeader('www-authenticate'), + ['ts', 'tsm', 'error'] + ); + if (!attrsAuth) { return false; } - if (updateClock !== false) { + if (attrsAuth.ts) { + var tsm = hawk.crypto.calculateTsMac(attrsAuth.ts, credentials); + if (tsm !== attrsAuth.tsm) { + return false; + } + hawk.utils.setNtpOffset( - message.ts - Math.floor(new Date().getTime() / 1000) + attrsAuth.ts - Math.floor(new Date().getTime() / 1000) ); // Keep offset at 1 second precision } + } - return true; - }, - }; - - hawk.crypto = { - headerVersion: '1', - - algorithms: ['sha1', 'sha256'], - - calculateMac: function(type, credentials, options) { - var normalized = hawk.crypto.generateNormalizedString(type, options); - var hmac = new sjcl.misc.hmac(credentials.key, sjcl.hash.sha256); - hmac.update(normalized); - - return sjcl.codec.base64.fromBits(hmac.digest()); - }, - - generateNormalizedString: function(type, options) { - var normalized = - 'hawk.' + - hawk.crypto.headerVersion + - '.' + - type + - '\n' + - options.ts + - '\n' + - options.nonce + - '\n' + - (options.method || '').toUpperCase() + - '\n' + - (options.resource || '') + - '\n' + - options.host.toLowerCase() + - '\n' + - options.port + - '\n' + - (options.hash || '') + - '\n'; - - if (options.ext) { - normalized += options.ext.replace('\\', '\\\\').replace('\n', '\\n'); - } - - normalized += '\n'; - - if (options.app) { - normalized += options.app + '\n' + (options.dlg || '') + '\n'; - } - - return normalized; - }, - - calculatePayloadHash: function(payload, algorithm, contentType) { - var hash = new sjcl.hash.sha256(); - hash - .update('hawk.' + hawk.crypto.headerVersion + '.payload\n') - .update(hawk.utils.parseContentType(contentType) + '\n') - .update(payload || '') - .update('\n'); - - return sjcl.codec.base64.fromBits(hash.finalize()); - }, - - calculateTsMac: function(ts, credentials) { - var hmac = new sjcl.misc.hmac(credentials.key, sjcl.hash.sha256); - hmac.update('hawk.' + hawk.crypto.headerVersion + '.ts\n' + ts + '\n'); - - return sjcl.codec.base64.fromBits(hmac.digest()); - }, - }; - - hawk.utils = { - storage: { - // localStorage compatible interface - _cache: {}, - setItem: function(key, value) { - hawk.utils.storage._cache[key] = value; - }, - getItem: function(key) { - return hawk.utils.storage._cache[key]; - }, - }, - - setStorage: function(storage) { - var ntpOffset = hawk.utils.getNtpOffset() || 0; - hawk.utils.storage = storage; - hawk.utils.setNtpOffset(ntpOffset); - }, - - setNtpOffset: function(offset) { - try { - hawk.utils.storage.setItem('hawk_ntp_offset', offset); - } catch (err) { - console.error('[hawk] could not write to storage.'); - console.error(err); - } - }, - - getNtpOffset: function() { - return parseInt(hawk.utils.storage.getItem('hawk_ntp_offset') || '0', 10); - }, + // Parse HTTP Server-Authorization header - now: function() { - return new Date().getTime() + hawk.utils.getNtpOffset(); - }, + if ( + !request.getResponseHeader('server-authorization') && + !options.required + ) { + return true; + } + + var attributes = hawk.utils.parseAuthorizationHeader( + request.getResponseHeader('server-authorization'), + ['mac', 'ext', 'hash'] + ); + if (!attributes) { + return false; + } + + var modArtifacts = { + ts: artifacts.ts, + nonce: artifacts.nonce, + method: artifacts.method, + resource: artifacts.resource, + host: artifacts.host, + port: artifacts.port, + hash: attributes.hash, + ext: attributes.ext, + app: artifacts.app, + dlg: artifacts.dlg, + }; + + var mac = hawk.crypto.calculateMac('response', credentials, modArtifacts); + if (mac !== attributes.mac) { + return false; + } + + if (!options.hasOwnProperty('payload')) { + return true; + } + + if (!attributes.hash) { + return false; + } + + var calculatedHash = hawk.crypto.calculatePayloadHash( + options.payload, + credentials.algorithm, + request.getResponseHeader('content-type') + ); + return calculatedHash === attributes.hash; + }, + + message: function(host, port, message, options) { + // Validate inputs + + if ( + !host || + typeof host !== 'string' || + !port || + typeof port !== 'number' || + message === null || + message === undefined || + typeof message !== 'string' || + !options || + typeof options !== 'object' + ) { + return null; + } + + // Application time + + var timestamp = + options.timestamp || + Math.floor( + (hawk.utils.now() + (options.localtimeOffsetMsec || 0)) / 1000 + ); - escapeHeaderAttribute: function(attribute) { - return attribute.replace(/\\/g, '\\\\').replace(/\"/g, '\\"'); + // Validate credentials + + var credentials = options.credentials; + if ( + !credentials || + !credentials.id || + !credentials.key || + !credentials.algorithm + ) { + // Invalid credential object + return null; + } + + if (hawk.crypto.algorithms.indexOf(credentials.algorithm) === -1) { + return null; + } + + // Calculate signature + + var artifacts = { + ts: timestamp, + nonce: options.nonce || hawk.utils.randomString(6), + host: host, + port: port, + hash: hawk.crypto.calculatePayloadHash(message, credentials.algorithm), + }; + + // Construct authorization + + var result = { + id: credentials.id, + ts: artifacts.ts, + nonce: artifacts.nonce, + hash: artifacts.hash, + mac: hawk.crypto.calculateMac('message', credentials, artifacts), + }; + + return result; + }, + + authenticateTimestamp: function(message, credentials, updateClock) { + // updateClock defaults to true + + var tsm = hawk.crypto.calculateTsMac(message.ts, credentials); + if (tsm !== message.tsm) { + return false; + } + + if (updateClock !== false) { + hawk.utils.setNtpOffset( + message.ts - Math.floor(new Date().getTime() / 1000) + ); // Keep offset at 1 second precision + } + + return true; + }, +}; + +hawk.crypto = { + headerVersion: '1', + + algorithms: ['sha1', 'sha256'], + + calculateMac: function(type, credentials, options) { + var normalized = hawk.crypto.generateNormalizedString(type, options); + var hmac = new sjcl.misc.hmac(credentials.key, sjcl.hash.sha256); + hmac.update(normalized); + + return sjcl.codec.base64.fromBits(hmac.digest()); + }, + + generateNormalizedString: function(type, options) { + var normalized = + 'hawk.' + + hawk.crypto.headerVersion + + '.' + + type + + '\n' + + options.ts + + '\n' + + options.nonce + + '\n' + + (options.method || '').toUpperCase() + + '\n' + + (options.resource || '') + + '\n' + + options.host.toLowerCase() + + '\n' + + options.port + + '\n' + + (options.hash || '') + + '\n'; + + if (options.ext) { + normalized += options.ext.replace('\\', '\\\\').replace('\n', '\\n'); + } + + normalized += '\n'; + + if (options.app) { + normalized += options.app + '\n' + (options.dlg || '') + '\n'; + } + + return normalized; + }, + + calculatePayloadHash: function(payload, algorithm, contentType) { + var hash = new sjcl.hash.sha256(); + hash + .update('hawk.' + hawk.crypto.headerVersion + '.payload\n') + .update(hawk.utils.parseContentType(contentType) + '\n') + .update(payload || '') + .update('\n'); + + return sjcl.codec.base64.fromBits(hash.finalize()); + }, + + calculateTsMac: function(ts, credentials) { + var hmac = new sjcl.misc.hmac(credentials.key, sjcl.hash.sha256); + hmac.update('hawk.' + hawk.crypto.headerVersion + '.ts\n' + ts + '\n'); + + return sjcl.codec.base64.fromBits(hmac.digest()); + }, +}; + +hawk.utils = { + storage: { + // localStorage compatible interface + _cache: {}, + setItem: function(key, value) { + hawk.utils.storage._cache[key] = value; }, - - parseContentType: function(header) { - if (!header) { - return ''; - } - - return header - .split(';')[0] - .replace(/^\s+|\s+$/g, '') - .toLowerCase(); + getItem: function(key) { + return hawk.utils.storage._cache[key]; }, + }, + + setStorage: function(storage) { + var ntpOffset = hawk.utils.getNtpOffset() || 0; + hawk.utils.storage = storage; + hawk.utils.setNtpOffset(ntpOffset); + }, + + setNtpOffset: function(offset) { + try { + hawk.utils.storage.setItem('hawk_ntp_offset', offset); + } catch (err) { + console.error('[hawk] could not write to storage.'); + console.error(err); + } + }, + + getNtpOffset: function() { + return parseInt(hawk.utils.storage.getItem('hawk_ntp_offset') || '0', 10); + }, + + now: function() { + return new Date().getTime() + hawk.utils.getNtpOffset(); + }, + + escapeHeaderAttribute: function(attribute) { + return attribute.replace(/\\/g, '\\\\').replace(/\"/g, '\\"'); + }, + + parseContentType: function(header) { + if (!header) { + return ''; + } + + return header + .split(';')[0] + .replace(/^\s+|\s+$/g, '') + .toLowerCase(); + }, + + parseAuthorizationHeader: function(header, keys) { + if (!header) { + return null; + } + + var headerParts = header.match(/^(\w+)(?:\s+(.*))?$/); // Header: scheme[ something] + if (!headerParts) { + return null; + } + + var scheme = headerParts[1]; + if (scheme.toLowerCase() !== 'hawk') { + return null; + } + + var attributesString = headerParts[2]; + if (!attributesString) { + return null; + } + + var attributes = {}; + var verify = attributesString.replace( + /(\w+)="([^"\\]*)"\s*(?:,\s*|$)/g, + function($0, $1, $2) { + // Check valid attribute names + + if (keys.indexOf($1) === -1) { + return; + } - parseAuthorizationHeader: function(header, keys) { - if (!header) { - return null; - } - - var headerParts = header.match(/^(\w+)(?:\s+(.*))?$/); // Header: scheme[ something] - if (!headerParts) { - return null; - } - - var scheme = headerParts[1]; - if (scheme.toLowerCase() !== 'hawk') { - return null; - } - - var attributesString = headerParts[2]; - if (!attributesString) { - return null; - } - - var attributes = {}; - var verify = attributesString.replace( - /(\w+)="([^"\\]*)"\s*(?:,\s*|$)/g, - function($0, $1, $2) { - // Check valid attribute names - - if (keys.indexOf($1) === -1) { - return; - } - - // Allowed attribute value characters: !#$%&'()*+,-./:;<=>?@[]^_`{|}~ and space, a-z, A-Z, 0-9 - - if ( - $2.match( - /^[ \w\!#\$%&'\(\)\*\+,\-\.\/\:;<\=>\?@\[\]\^`\{\|\}~]+$/ - ) === null - ) { - return; - } + // Allowed attribute value characters: !#$%&'()*+,-./:;<=>?@[]^_`{|}~ and space, a-z, A-Z, 0-9 - // Check for duplicates + if ( + $2.match( + /^[ \w\!#\$%&'\(\)\*\+,\-\.\/\:;<\=>\?@\[\]\^`\{\|\}~]+$/ + ) === null + ) { + return; + } - if (attributes.hasOwnProperty($1)) { - return; - } + // Check for duplicates - attributes[$1] = $2; - return ''; + if (attributes.hasOwnProperty($1)) { + return; } - ); - if (verify !== '') { - return null; + attributes[$1] = $2; + return ''; } + ); - return attributes; - }, + if (verify !== '') { + return null; + } - randomString: function(size) { - var randomSource = - 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; - var len = randomSource.length; + return attributes; + }, - var result = []; - for (var i = 0; i < size; ++i) { - result[i] = randomSource[Math.floor(Math.random() * len)]; - } - - return result.join(''); - }, + randomString: function(size) { + var randomSource = + 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + var len = randomSource.length; - baseIndexOf: function(array, value, fromIndex) { - var index = (fromIndex || 0) - 1, - length = array ? array.length : 0; + var result = []; + for (var i = 0; i < size; ++i) { + result[i] = randomSource[Math.floor(Math.random() * len)]; + } - while (++index < length) { - if (array[index] === value) { - return index; - } - } - return -1; - }, + return result.join(''); + }, - parseUri: function(input) { - // Based on: parseURI 1.2.2 - // http://blog.stevenlevithan.com/archives/parseuri - // (c) Steven Levithan - // MIT License - - var keys = [ - 'source', - 'protocol', - 'authority', - 'userInfo', - 'user', - 'password', - 'hostname', - 'port', - 'resource', - 'relative', - 'pathname', - 'directory', - 'file', - 'query', - 'fragment', - ]; - - var uriRegex = /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?(((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?)(?:#(.*))?)/; - var uriByNumber = uriRegex.exec(input); - var uri = {}; - - var i = 15; - while (i--) { - uri[keys[i]] = uriByNumber[i] || ''; - } + baseIndexOf: function(array, value, fromIndex) { + var index = (fromIndex || 0) - 1, + length = array ? array.length : 0; - if (uri.port === null || uri.port === '') { - uri.port = - uri.protocol.toLowerCase() === 'http' - ? '80' - : uri.protocol.toLowerCase() === 'https' - ? '443' - : ''; + while (++index < length) { + if (array[index] === value) { + return index; } - - return uri; - }, - }; - - return hawk; -}); + } + return -1; + }, + + parseUri: function(input) { + // Based on: parseURI 1.2.2 + // http://blog.stevenlevithan.com/archives/parseuri + // (c) Steven Levithan + // MIT License + + var keys = [ + 'source', + 'protocol', + 'authority', + 'userInfo', + 'user', + 'password', + 'hostname', + 'port', + 'resource', + 'relative', + 'pathname', + 'directory', + 'file', + 'query', + 'fragment', + ]; + + var uriRegex = /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?(((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?)(?:#(.*))?)/; + var uriByNumber = uriRegex.exec(input); + var uri = {}; + + var i = 15; + while (i--) { + uri[keys[i]] = uriByNumber[i] || ''; + } + + if (uri.port === null || uri.port === '') { + uri.port = + uri.protocol.toLowerCase() === 'http' + ? '80' + : uri.protocol.toLowerCase() === 'https' + ? '443' + : ''; + } + + return uri; + }, +}; + +module.exports = hawk; diff --git a/packages/fxa-js-client/client/lib/hawkCredentials.js b/packages/fxa-js-client/client/lib/hawkCredentials.js index 11c5fc3d6ff..bd62b940568 100644 --- a/packages/fxa-js-client/client/lib/hawkCredentials.js +++ b/packages/fxa-js-client/client/lib/hawkCredentials.js @@ -1,37 +1,39 @@ /* 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/. */ -define(['sjcl', './hkdf'], function(sjcl, hkdf) { - 'use strict'; - var PREFIX_NAME = 'identity.mozilla.com/picl/v1/'; - var bitSlice = sjcl.bitArray.bitSlice; - var salt = sjcl.codec.hex.toBits(''); +'use strict'; - /** - * @class hawkCredentials - * @method deriveHawkCredentials - * @param {String} tokenHex - * @param {String} context - * @param {int} size - * @returns {Promise} - */ - function deriveHawkCredentials(tokenHex, context, size) { - var token = sjcl.codec.hex.toBits(tokenHex); - var info = sjcl.codec.utf8String.toBits(PREFIX_NAME + context); +const sjcl = require('sjcl'); +const hkdf = require('./hkdf'); - return hkdf(token, info, salt, size || 3 * 32).then(function(out) { - var authKey = bitSlice(out, 8 * 32, 8 * 64); - var bundleKey = bitSlice(out, 8 * 64); +var PREFIX_NAME = 'identity.mozilla.com/picl/v1/'; +var bitSlice = sjcl.bitArray.bitSlice; +var salt = sjcl.codec.hex.toBits(''); - return { - algorithm: 'sha256', - id: sjcl.codec.hex.fromBits(bitSlice(out, 0, 8 * 32)), - key: authKey, - bundleKey: bundleKey, - }; - }); - } +/** + * @class hawkCredentials + * @method deriveHawkCredentials + * @param {String} tokenHex + * @param {String} context + * @param {int} size + * @returns {Promise} + */ +function deriveHawkCredentials(tokenHex, context, size) { + var token = sjcl.codec.hex.toBits(tokenHex); + var info = sjcl.codec.utf8String.toBits(PREFIX_NAME + context); - return deriveHawkCredentials; -}); + return hkdf(token, info, salt, size || 3 * 32).then(function(out) { + var authKey = bitSlice(out, 8 * 32, 8 * 64); + var bundleKey = bitSlice(out, 8 * 64); + + return { + algorithm: 'sha256', + id: sjcl.codec.hex.fromBits(bitSlice(out, 0, 8 * 32)), + key: authKey, + bundleKey: bundleKey, + }; + }); +} + +module.exports = deriveHawkCredentials; diff --git a/packages/fxa-js-client/client/lib/hkdf.js b/packages/fxa-js-client/client/lib/hkdf.js index b7e64b04c4c..715edd24f40 100644 --- a/packages/fxa-js-client/client/lib/hkdf.js +++ b/packages/fxa-js-client/client/lib/hkdf.js @@ -1,54 +1,55 @@ /* 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/. */ -define(['sjcl'], function(sjcl) { - 'use strict'; - - /** - * hkdf - The HMAC-based Key Derivation Function - * based on https://github.com/mozilla/node-hkdf - * - * @class hkdf - * @param {bitArray} ikm Initial keying material - * @param {bitArray} info Key derivation data - * @param {bitArray} salt Salt - * @param {integer} length Length of the derived key in bytes - * @return promise object- It will resolve with `output` data - */ - function hkdf(ikm, info, salt, length) { - var mac = new sjcl.misc.hmac(salt, sjcl.hash.sha256); - mac.update(ikm); - - // compute the PRK - var prk = mac.digest(); - - // hash length is 32 because only sjcl.hash.sha256 is used at this moment - var hashLength = 32; - var num_blocks = Math.ceil(length / hashLength); - var prev = sjcl.codec.hex.toBits(''); - var output = ''; - - for (var i = 0; i < num_blocks; i++) { - var hmac = new sjcl.misc.hmac(prk, sjcl.hash.sha256); - - var input = sjcl.bitArray.concat( - sjcl.bitArray.concat(prev, info), - sjcl.codec.utf8String.toBits(String.fromCharCode(i + 1)) - ); - - hmac.update(input); - - prev = hmac.digest(); - output += sjcl.codec.hex.fromBits(prev); - } - - var truncated = sjcl.bitArray.clamp( - sjcl.codec.hex.toBits(output), - length * 8 + +'use strict'; + +const sjcl = require('sjcl'); + +/** + * hkdf - The HMAC-based Key Derivation Function + * based on https://github.com/mozilla/node-hkdf + * + * @class hkdf + * @param {bitArray} ikm Initial keying material + * @param {bitArray} info Key derivation data + * @param {bitArray} salt Salt + * @param {integer} length Length of the derived key in bytes + * @return promise object- It will resolve with `output` data + */ +function hkdf(ikm, info, salt, length) { + var mac = new sjcl.misc.hmac(salt, sjcl.hash.sha256); + mac.update(ikm); + + // compute the PRK + var prk = mac.digest(); + + // hash length is 32 because only sjcl.hash.sha256 is used at this moment + var hashLength = 32; + var num_blocks = Math.ceil(length / hashLength); + var prev = sjcl.codec.hex.toBits(''); + var output = ''; + + for (var i = 0; i < num_blocks; i++) { + var hmac = new sjcl.misc.hmac(prk, sjcl.hash.sha256); + + var input = sjcl.bitArray.concat( + sjcl.bitArray.concat(prev, info), + sjcl.codec.utf8String.toBits(String.fromCharCode(i + 1)) ); - return Promise.resolve(truncated); + hmac.update(input); + + prev = hmac.digest(); + output += sjcl.codec.hex.fromBits(prev); } - return hkdf; -}); + var truncated = sjcl.bitArray.clamp( + sjcl.codec.hex.toBits(output), + length * 8 + ); + + return Promise.resolve(truncated); +} + +module.exports = hkdf; diff --git a/packages/fxa-js-client/client/lib/metricsContext.js b/packages/fxa-js-client/client/lib/metricsContext.js index e00107a743e..62d096fc467 100644 --- a/packages/fxa-js-client/client/lib/metricsContext.js +++ b/packages/fxa-js-client/client/lib/metricsContext.js @@ -2,27 +2,22 @@ * 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/. */ -// This module does the handling for the metrics context -// activity event metadata. +'use strict'; -define([], function() { - 'use strict'; - - return { - marshall: function(data) { - return { - deviceId: data.deviceId, - entrypoint: data.entrypoint, - entrypointExperiment: data.entrypointExperiment, - entrypointVariation: data.entrypointVariation, - flowId: data.flowId, - flowBeginTime: data.flowBeginTime, - utmCampaign: data.utmCampaign, - utmContent: data.utmContent, - utmMedium: data.utmMedium, - utmSource: data.utmSource, - utmTerm: data.utmTerm, - }; - }, - }; -}); +module.exports = { + marshall: function(data) { + return { + deviceId: data.deviceId, + entrypoint: data.entrypoint, + entrypointExperiment: data.entrypointExperiment, + entrypointVariation: data.entrypointVariation, + flowId: data.flowId, + flowBeginTime: data.flowBeginTime, + utmCampaign: data.utmCampaign, + utmContent: data.utmContent, + utmMedium: data.utmMedium, + utmSource: data.utmSource, + utmTerm: data.utmTerm, + }; + }, +}; diff --git a/packages/fxa-js-client/client/lib/pbkdf2.js b/packages/fxa-js-client/client/lib/pbkdf2.js index d384abb2325..56b3f98621d 100644 --- a/packages/fxa-js-client/client/lib/pbkdf2.js +++ b/packages/fxa-js-client/client/lib/pbkdf2.js @@ -1,31 +1,26 @@ /* 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/. */ -define(['sjcl'], function(sjcl, P) { - 'use strict'; +'use strict'; + +const sjcl = require('sjcl'); + +/** + * @class pbkdf2 + * @constructor + */ +var pbkdf2 = { /** - * @class pbkdf2 - * @constructor + * @method derive + * @param {bitArray} input The password hex buffer. + * @param {bitArray} salt The salt string buffer. + * @return {int} iterations the derived key bit array. */ - var pbkdf2 = { - /** - * @method derive - * @param {bitArray} input The password hex buffer. - * @param {bitArray} salt The salt string buffer. - * @return {int} iterations the derived key bit array. - */ - derive: function(input, salt, iterations, len) { - var result = sjcl.misc.pbkdf2( - input, - salt, - iterations, - len, - sjcl.misc.hmac - ); - return Promise.resolve(result); - }, - }; + derive: function(input, salt, iterations, len) { + var result = sjcl.misc.pbkdf2(input, salt, iterations, len, sjcl.misc.hmac); + return Promise.resolve(result); + }, +}; - return pbkdf2; -}); +module.exports = pbkdf2; diff --git a/packages/fxa-js-client/client/lib/request.js b/packages/fxa-js-client/client/lib/request.js index eea7f201c4e..614a14664c9 100644 --- a/packages/fxa-js-client/client/lib/request.js +++ b/packages/fxa-js-client/client/lib/request.js @@ -1,141 +1,140 @@ /* 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/. */ -define(['./hawk', './errors'], function(hawk, ERRORS) { - 'use strict'; - /* global XMLHttpRequest */ - - /** - * @class Request - * @constructor - * @param {String} baseUri Base URI - * @param {Object} xhr XMLHttpRequest constructor - * @param {Object} [options={}] Options - * @param {Number} [options.localtimeOffsetMsec] - * Local time offset with the remote auth server's clock - */ - function Request(baseUri, xhr, options) { - if (!options) { - options = {}; - } - this.baseUri = baseUri; - this._localtimeOffsetMsec = options.localtimeOffsetMsec; - this.xhr = xhr || XMLHttpRequest; - this.timeout = options.timeout || 30 * 1000; + +'use strict'; + +const hawk = require('./hawk'); +const ERRORS = require('./errors'); +/* global XMLHttpRequest */ + +/** + * @class Request + * @constructor + * @param {String} baseUri Base URI + * @param {Object} xhr XMLHttpRequest constructor + * @param {Object} [options={}] Options + * @param {Number} [options.localtimeOffsetMsec] + * Local time offset with the remote auth server's clock + */ +function Request(baseUri, xhr, options) { + if (!options) { + options = {}; } + this.baseUri = baseUri; + this._localtimeOffsetMsec = options.localtimeOffsetMsec; + this.xhr = xhr || XMLHttpRequest; + this.timeout = options.timeout || 30 * 1000; +} - /** - * @method send - * @param {String} path Request path - * @param {String} method HTTP Method - * @param {Object} credentials HAWK Headers - * @param {Object} jsonPayload JSON Payload - * @param {Object} [options={}] Options - * @param {String} [options.retrying] - * Flag indicating if the request is a retry - * @param {Array} [options.headers] - * A set of extra headers to add to the request - * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request - */ - Request.prototype.send = function request( - path, - method, - credentials, - jsonPayload, - options - ) { - /*eslint complexity: [2, 8] */ - var xhr = new this.xhr(); - var uri = this.baseUri + path; - var payload = null; - var self = this; - options = options || {}; - - if (jsonPayload) { - payload = JSON.stringify(jsonPayload); - } +/** + * @method send + * @param {String} path Request path + * @param {String} method HTTP Method + * @param {Object} credentials HAWK Headers + * @param {Object} jsonPayload JSON Payload + * @param {Object} [options={}] Options + * @param {String} [options.retrying] + * Flag indicating if the request is a retry + * @param {Array} [options.headers] + * A set of extra headers to add to the request + * @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request + */ +Request.prototype.send = function request( + path, + method, + credentials, + jsonPayload, + options +) { + /*eslint complexity: [2, 8] */ + var xhr = new this.xhr(); + var uri = this.baseUri + path; + var payload = null; + var self = this; + options = options || {}; - try { - xhr.open(method, uri); - } catch (e) { - return Promise.reject({ - error: 'Unknown error', - message: e.toString(), - errno: 999, - }); - } + if (jsonPayload) { + payload = JSON.stringify(jsonPayload); + } - return new Promise(function(resolve, reject) { - xhr.timeout = self.timeout; - // eslint-disable-next-line complexity - xhr.onreadystatechange = function() { - if (xhr.readyState === 4) { - var result = xhr.responseText; - try { - result = JSON.parse(xhr.responseText); - } catch (e) {} - - if (result.errno) { - // Try to recover from a timeskew error and not already tried - if ( - result.errno === ERRORS.INVALID_TIMESTAMP && - !options.retrying - ) { - var serverTime = result.serverTime; - self._localtimeOffsetMsec = - serverTime * 1000 - new Date().getTime(); - - // add to options that the request is retrying - options.retrying = true; - - return self - .send(path, method, credentials, jsonPayload, options) - .then(resolve, reject); - } else { - return reject(result); - } - } + try { + xhr.open(method, uri); + } catch (e) { + return Promise.reject({ + error: 'Unknown error', + message: e.toString(), + errno: 999, + }); + } + + return new Promise(function(resolve, reject) { + xhr.timeout = self.timeout; + // eslint-disable-next-line complexity + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + var result = xhr.responseText; + try { + result = JSON.parse(xhr.responseText); + } catch (e) {} - if (typeof xhr.status === 'undefined' || xhr.status !== 200) { - if (result.length === 0) { - return reject({ error: 'Timeout error', errno: 999 }); - } else { - return reject({ - error: 'Unknown error', - message: result, - errno: 999, - code: xhr.status, - }); - } + if (result.errno) { + // Try to recover from a timeskew error and not already tried + if (result.errno === ERRORS.INVALID_TIMESTAMP && !options.retrying) { + var serverTime = result.serverTime; + self._localtimeOffsetMsec = + serverTime * 1000 - new Date().getTime(); + + // add to options that the request is retrying + options.retrying = true; + + return self + .send(path, method, credentials, jsonPayload, options) + .then(resolve, reject); + } else { + return reject(result); } + } - resolve(result); + if (typeof xhr.status === 'undefined' || xhr.status !== 200) { + if (result.length === 0) { + return reject({ error: 'Timeout error', errno: 999 }); + } else { + return reject({ + error: 'Unknown error', + message: result, + errno: 999, + code: xhr.status, + }); + } } - }; - - // calculate Hawk header if credentials are supplied - if (credentials) { - var hawkHeader = hawk.client.header(uri, method, { - credentials: credentials, - payload: payload, - contentType: 'application/json', - localtimeOffsetMsec: self._localtimeOffsetMsec || 0, - }); - xhr.setRequestHeader('authorization', hawkHeader.field); + + resolve(result); } + }; - xhr.setRequestHeader('Content-Type', 'application/json'); + // calculate Hawk header if credentials are supplied + if (credentials) { + var hawkHeader = hawk.client.header(uri, method, { + credentials: credentials, + payload: payload, + contentType: 'application/json', + localtimeOffsetMsec: self._localtimeOffsetMsec || 0, + }); + xhr.setRequestHeader('authorization', hawkHeader.field); + } - if (options && options.headers) { - // set extra headers for this request - for (var header in options.headers) { - xhr.setRequestHeader(header, options.headers[header]); - } + xhr.setRequestHeader('Content-Type', 'application/json'); + + if (options && options.headers) { + // set extra headers for this request + for (var header in options.headers) { + xhr.setRequestHeader(header, options.headers[header]); } + } - xhr.send(payload); - }); - }; + xhr.send(payload); + }); +}; - return Request; -}); +module.exports = Request; diff --git a/packages/fxa-js-client/package-lock.json b/packages/fxa-js-client/package-lock.json index 85b9d299aba..e7801b210f1 100644 --- a/packages/fxa-js-client/package-lock.json +++ b/packages/fxa-js-client/package-lock.json @@ -61,6 +61,50 @@ } } }, + "@sinonjs/commons": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz", + "integrity": "sha512-9jHK3YF/8HtJ9wCAbG+j8cD0i0+ATS9A7gXFqS36TblLPNy6rEEc+SB0imo91eCboGaBYGV/MT1/br/J+EE7Tw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/formatio": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz", + "integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^3.1.0" + } + }, + "@sinonjs/samsam": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.2.tgz", + "integrity": "sha512-ILO/rR8LfAb60Y1Yfp9vxfYAASK43NFC2mLzpvLUbCQY/Qu8YwReboseu8aheCEkyElZF2L2T9mHcR2bgdvZyA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.0.2", + "array-from": "^2.1.1", + "lodash": "^4.17.11" + }, + "dependencies": { + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + } + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, "JSONStream": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", @@ -122,12 +166,6 @@ "integrity": "sha1-anmQQ3ynNtXhKI25K9MmbV9csqo=", "dev": true }, - "adm-zip": { - "version": "0.4.13", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.13.tgz", - "integrity": "sha512-fERNJX8sOXfel6qCBCMPvZLzENBEhZTzKqg6vrOW5pvoEaQuJhRU4ndTAh6lHOxn1I6jnz2NHra56ZODM751uw==", - "dev": true - }, "ajv": { "version": "5.5.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", @@ -163,6 +201,12 @@ "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", "dev": true }, + "ansi-colors": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", + "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", + "dev": true + }, "ansi-escapes": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", @@ -256,6 +300,12 @@ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", "dev": true }, + "array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", + "dev": true + }, "array-ify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", @@ -340,6 +390,12 @@ "dev": true, "optional": true }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -462,15 +518,6 @@ "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", "dev": true }, - "block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "dev": true, - "requires": { - "inherits": "~2.0.0" - } - }, "bn.js": { "version": "4.11.8", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", @@ -576,6 +623,12 @@ "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", "dev": true }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, "browserify-aes": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", @@ -748,6 +801,20 @@ "lazy-cache": "^1.0.3" } }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, "chalk": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", @@ -767,10 +834,10 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, - "charm": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/charm/-/charm-0.2.0.tgz", - "integrity": "sha1-us0G2HF3WTYvemYqHpZ691N/2os=", + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", "dev": true }, "chokidar": { @@ -1301,92 +1368,13 @@ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", "dev": true }, - "decompress": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/decompress/-/decompress-0.2.3.tgz", - "integrity": "sha1-rUcaD8UbcZO3L2DJ4EDOGnpLn8w=", - "dev": true, - "requires": { - "adm-zip": "^0.4.3", - "extname": "^0.1.1", - "get-stdin": "^0.1.0", - "map-key": "^0.1.1", - "mkdirp": "^0.3.5", - "nopt": "^2.2.0", - "rimraf": "^2.2.2", - "stream-combiner": "^0.0.4", - "tar": "^0.1.18", - "tempfile": "^0.1.2" - }, - "dependencies": { - "get-stdin": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-0.1.0.tgz", - "integrity": "sha1-WZivJKr8gC0VyCxoVlfuuLENSpE=", - "dev": true - }, - "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "mkdirp": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", - "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=", - "dev": true - }, - "nopt": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-2.2.1.tgz", - "integrity": "sha1-KqCbfRdoSHs7ianFqlIzW/8Lrqc=", - "dev": true, - "requires": { - "abbrev": "1" - } - }, - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "tempfile": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/tempfile/-/tempfile-0.1.3.tgz", - "integrity": "sha1-fWtxAEcznTn4RzJ6BW2t8YMQMBA=", - "dev": true, - "requires": { - "uuid": "~1.4.0" - } - }, - "uuid": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-1.4.2.tgz", - "integrity": "sha1-RTAZ9oaWam34PNxSROfJkOzDMvw=", - "dev": true - } + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" } }, "deep-for-each": { @@ -1404,6 +1392,15 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, "define-property": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", @@ -1480,12 +1477,6 @@ "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", "dev": true }, - "diff": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-1.1.0.tgz", - "integrity": "sha1-eYpJOBqkZBUem08Ob/Kwmooa0j8=", - "dev": true - }, "diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", @@ -1497,24 +1488,6 @@ "randombytes": "^2.0.0" } }, - "digdug": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/digdug/-/digdug-1.4.0.tgz", - "integrity": "sha1-A71xKojRSEe5LXxuWv0luNn3Qno=", - "dev": true, - "requires": { - "decompress": "0.2.3", - "dojo": "2.0.0-alpha.7" - }, - "dependencies": { - "dojo": { - "version": "2.0.0-alpha.7", - "resolved": "https://registry.npmjs.org/dojo/-/dojo-2.0.0-alpha.7.tgz", - "integrity": "sha1-wrJdQ9j3LMycj+iaNJBqLSceXJE=", - "dev": true - } - } - }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -1524,12 +1497,6 @@ "esutils": "^2.0.2" } }, - "dojo": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/dojo/-/dojo-1.9.4.tgz", - "integrity": "sha1-cq/VxZUgbQjv8tsnk2/g9FQ/ebM=", - "dev": true - }, "domain-browser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", @@ -1545,12 +1512,6 @@ "is-obj": "^1.0.0" } }, - "duplexer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", - "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", - "dev": true - }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1590,6 +1551,15 @@ "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", "dev": true }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, "enhanced-resolve": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz", @@ -1634,6 +1604,31 @@ "is-arrayish": "^0.2.1" } }, + "es-abstract": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", + "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.0", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "is-callable": "^1.1.4", + "is-regex": "^1.0.4", + "object-keys": "^1.0.12" + } + }, + "es-to-primitive": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", + "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, "es5-ext": { "version": "0.10.49", "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.49.tgz", @@ -1722,38 +1717,6 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "dev": true }, - "escodegen": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.3.3.tgz", - "integrity": "sha1-8CQBb1qI4Eb9EgBQVek5gC5sXyM=", - "dev": true, - "requires": { - "esprima": "~1.1.1", - "estraverse": "~1.5.0", - "esutils": "~1.0.0", - "source-map": "~0.1.33" - }, - "dependencies": { - "esprima": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.1.1.tgz", - "integrity": "sha1-W28VR/TRAuZw4UDFCb5ncdautUk=", - "dev": true - }, - "estraverse": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.5.1.tgz", - "integrity": "sha1-hno+jlip+EYYr7bC3bzZFrfLr3E=", - "dev": true - }, - "esutils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.0.0.tgz", - "integrity": "sha1-gVHTWOIMisx/t0XnRywAJf5JZXA=", - "dev": true - } - } - }, "escope": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", @@ -2241,15 +2204,6 @@ } } }, - "ext-list": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-0.2.0.tgz", - "integrity": "sha1-NhTV8pn0pZKolinn3oJfF3TRmr0=", - "dev": true, - "requires": { - "got": "^0.2.0" - } - }, "extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", @@ -2364,25 +2318,6 @@ } } }, - "extname": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/extname/-/extname-0.1.5.tgz", - "integrity": "sha1-PLs4jQ2sYIbFMTPN+hCAdNinVS4=", - "dev": true, - "requires": { - "ext-list": "^0.2.0", - "map-key": "^0.1.1", - "underscore.string": "~2.3.3" - }, - "dependencies": { - "underscore.string": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", - "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", - "dev": true - } - } - }, "fast-deep-equal": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", @@ -2425,16 +2360,6 @@ "flat-cache": "^2.0.1" } }, - "fileset": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/fileset/-/fileset-0.1.8.tgz", - "integrity": "sha1-UGuRqTluqn4y+0KoQHfHoMc2t0E=", - "dev": true, - "requires": { - "glob": "3.x", - "minimatch": "0.x" - } - }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -2532,6 +2457,23 @@ } } }, + "flat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", + "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", + "dev": true, + "requires": { + "is-buffer": "~2.0.3" + }, + "dependencies": { + "is-buffer": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", + "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==", + "dev": true + } + } + }, "flat-cache": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", @@ -3198,28 +3140,11 @@ } } }, - "fstream": { - "version": "0.1.31", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-0.1.31.tgz", - "integrity": "sha1-czfwWPu7vvqMn1YaKMqwhJICyYg=", - "dev": true, - "requires": { - "graceful-fs": "~3.0.2", - "inherits": "~2.0.0", - "mkdirp": "0.5", - "rimraf": "2" - }, - "dependencies": { - "graceful-fs": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.11.tgz", - "integrity": "sha1-dhPHeKGv6mLyXGMKCG1/Osu92Bg=", - "dev": true, - "requires": { - "natives": "^1.1.0" - } - } - } + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true }, "functional-red-black-tree": { "version": "1.0.1", @@ -3242,6 +3167,12 @@ "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", "dev": true }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, "get-pkg-repo": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-pkg-repo/-/get-pkg-repo-0.1.0.tgz", @@ -3521,29 +3452,18 @@ } } }, - "got": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/got/-/got-0.2.0.tgz", - "integrity": "sha1-0Awkiyn9zK6pQN+coJlev/MbUaU=", - "dev": true, - "requires": { - "object-assign": "^0.3.0" - }, - "dependencies": { - "object-assign": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-0.3.1.tgz", - "integrity": "sha1-Bg4qKifXwNd+x3t48Rqkf9iACNI=", - "dev": true - } - } - }, "graceful-fs": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=", "dev": true }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, "grunt": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.2.tgz", @@ -3763,6 +3683,15 @@ "uglify-js": "^2.6" } }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, "has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", @@ -3778,6 +3707,12 @@ "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", "dev": true }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true + }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", @@ -3843,6 +3778,12 @@ "sntp": "0.2.x" } }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -4063,32 +4004,6 @@ } } }, - "intern-geezer": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/intern-geezer/-/intern-geezer-2.2.3.tgz", - "integrity": "sha1-F5xVT79k8gai2gQZXQHx7NAZ5a4=", - "dev": true, - "requires": { - "charm": "0.2.0", - "diff": "1.1.0", - "digdug": "1.4.0", - "dojo": "1.9.4", - "istanbul": "0.2.16", - "leadfoot": "1.6.6", - "source-map": "0.1.33" - }, - "dependencies": { - "source-map": { - "version": "0.1.33", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.33.tgz", - "integrity": "sha1-xlkpenOvGMBzsKoufMkeMWtcVww=", - "dev": true, - "requires": { - "amdefine": ">=0.0.4" - } - } - } - }, "interpret": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", @@ -4150,6 +4065,12 @@ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", "dev": true }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "dev": true + }, "is-data-descriptor": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", @@ -4159,6 +4080,12 @@ "kind-of": "^3.0.2" } }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true + }, "is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", @@ -4253,6 +4180,15 @@ "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", "dev": true }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "dev": true, + "requires": { + "has": "^1.0.1" + } + }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", @@ -4265,6 +4201,15 @@ "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=", "dev": true }, + "is-symbol": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", + "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.0" + } + }, "is-text-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", @@ -4310,128 +4255,6 @@ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "dev": true }, - "istanbul": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.2.16.tgz", - "integrity": "sha1-hwVFoNT0tM4WEDnp6AWpjCxwC9k=", - "dev": true, - "requires": { - "abbrev": "1.0.x", - "async": "0.9.x", - "escodegen": "1.3.x", - "esprima": "1.2.x", - "fileset": "0.1.x", - "handlebars": "1.3.x", - "js-yaml": "3.x", - "mkdirp": "0.5.x", - "nopt": "3.x", - "resolve": "0.7.x", - "which": "1.0.x", - "wordwrap": "0.0.x" - }, - "dependencies": { - "abbrev": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", - "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", - "dev": true - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "async": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", - "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=", - "dev": true - }, - "esprima": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.5.tgz", - "integrity": "sha1-CZNQL+r2aBODJXVvMPmlH+7sEek=", - "dev": true - }, - "handlebars": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-1.3.0.tgz", - "integrity": "sha1-npsTCpPjiUkTItl1zz7BgYw3zjQ=", - "dev": true, - "requires": { - "optimist": "~0.3", - "uglify-js": "~2.3" - } - }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "dependencies": { - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - } - } - }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "dev": true, - "requires": { - "abbrev": "1" - } - }, - "optimist": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", - "integrity": "sha1-yQlBrVnkJzMokjB00s8ufLxuwNk=", - "dev": true, - "requires": { - "wordwrap": "~0.0.2" - } - }, - "resolve": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-0.7.4.tgz", - "integrity": "sha1-OVqe+ehz+/4SvRRAi9kbuTYAPWk=", - "dev": true - }, - "uglify-js": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.3.6.tgz", - "integrity": "sha1-+gmEdwtCi3qbKoBY9GNV0U/vIRo=", - "dev": true, - "optional": true, - "requires": { - "async": "~0.2.6", - "optimist": "~0.3.5", - "source-map": "~0.1.7" - }, - "dependencies": { - "async": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", - "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=", - "dev": true, - "optional": true - } - } - } - } - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4517,14 +4340,11 @@ "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", "dev": true }, - "jszip": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-2.5.0.tgz", - "integrity": "sha1-dET9hVHd8+XacZj+oMkbyDCMwnQ=", - "dev": true, - "requires": { - "pako": "~0.2.5" - } + "just-extend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", + "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==", + "dev": true }, "kind-of": { "version": "3.2.2", @@ -4550,24 +4370,6 @@ "invert-kv": "^1.0.0" } }, - "leadfoot": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/leadfoot/-/leadfoot-1.6.6.tgz", - "integrity": "sha1-GdFGJKoRzT+hvD1hm6AqRO+YmSI=", - "dev": true, - "requires": { - "dojo": "2.0.0-alpha.7", - "jszip": "2.5.0" - }, - "dependencies": { - "dojo": { - "version": "2.0.0-alpha.7", - "resolved": "https://registry.npmjs.org/dojo/-/dojo-2.0.0-alpha.7.tgz", - "integrity": "sha1-wrJdQ9j3LMycj+iaNJBqLSceXJE=", - "dev": true - } - } - }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -4809,6 +4611,58 @@ "lodash.escape": "^3.0.0" } }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "requires": { + "chalk": "^2.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "lolex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.1.0.tgz", + "integrity": "sha512-BYxIEXiVq5lGIXeVHnsFzqa1TxN5acnKnPCdlZSpzm8viNEOhiigupA4vTQ9HEFQ6nLTQ9wQOgBknJgzUYQ9Aw==", + "dev": true + }, "longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", @@ -4831,36 +4685,21 @@ "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", "dev": true }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "requires": { + "p-defer": "^1.0.0" + } + }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", "dev": true }, - "map-key": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/map-key/-/map-key-0.1.5.tgz", - "integrity": "sha1-ivsTTyoyGk3im+/LWqujNIs3vfw=", - "dev": true, - "requires": { - "lodash": "^2.4.1", - "underscore.string": "^2.3.3" - }, - "dependencies": { - "lodash": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", - "dev": true - }, - "underscore.string": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz", - "integrity": "sha1-jN2PusTi0uoefi6Al8QvRCKA+Fs=", - "dev": true - } - } - }, "map-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", @@ -5098,28 +4937,414 @@ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", "dev": true, "requires": { - "is-plain-object": "^2.0.4" + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "mocha": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.2.0.tgz", + "integrity": "sha512-qwfFgY+7EKAAUAdv7VYMZQknI7YJSGesxHyhn6qD52DV8UcSZs5XwCifcZGMVIE4a5fbmhvbotxC0DLQ0oKohQ==", + "dev": true, + "requires": { + "ansi-colors": "3.2.3", + "browser-stdout": "1.3.1", + "debug": "3.2.6", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "find-up": "3.0.0", + "glob": "7.1.3", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.13.1", + "log-symbols": "2.2.0", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "ms": "2.1.1", + "node-environment-flags": "1.0.5", + "object.assign": "4.1.0", + "strip-json-comments": "2.0.1", + "supports-color": "6.0.0", + "which": "1.3.1", + "wide-align": "1.1.3", + "yargs": "13.2.2", + "yargs-parser": "13.0.0", + "yargs-unparser": "1.5.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "dev": true, + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + }, + "dependencies": { + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + } + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "dev": true, + "requires": { + "invert-kv": "^2.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "supports-color": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", + "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yargs": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz", + "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==", + "dev": true, + "requires": { + "cliui": "^4.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "os-locale": "^3.1.0", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.0.0" + } + }, + "yargs-parser": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz", + "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" } } } }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - } - } - }, "modify-values": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", @@ -5195,12 +5420,6 @@ } } }, - "natives": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/natives/-/natives-1.1.6.tgz", - "integrity": "sha512-6+TDFewD4yxY14ptjKaS63GVdtKiES1pTPyxn9Jb0rBqPMZ7VcCiooEhPNsr+mqHtMGxa/5c/HhcC4uPEUw/nA==", - "dev": true - }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -5231,6 +5450,54 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "nise": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.0.tgz", + "integrity": "sha512-Z3sfYEkLFzFmL8KY6xnSJLRxwQwYBjOXi/24lb62ZnZiGA0JUzGGTI6TBIgfCSMIDl9Jlu8SRmHNACLTemDHww==", + "dev": true, + "requires": { + "@sinonjs/formatio": "^3.1.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "lolex": "^4.1.0", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + } + } + }, + "node-environment-flags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", + "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==", + "dev": true, + "requires": { + "object.getownpropertydescriptors": "^2.0.3", + "semver": "^5.7.0" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + } + } + }, "node-libs-browser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.0.tgz", @@ -5384,6 +5651,12 @@ } } }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, "object-visit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", @@ -5393,6 +5666,28 @@ "isobject": "^3.0.0" } }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "object.getownpropertydescriptors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", + "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.5.1" + } + }, "object.pick": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", @@ -5510,12 +5805,24 @@ "thirty-two": "1.0.2" } }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "dev": true + }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", "dev": true }, + "p-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "dev": true + }, "p-limit": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", @@ -5540,12 +5847,6 @@ "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", "dev": true }, - "pako": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", - "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=", - "dev": true - }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5660,6 +5961,12 @@ } } }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, "pbkdf2": { "version": "3.0.17", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", @@ -5782,6 +6089,16 @@ "safe-buffer": "^5.1.2" } }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -6271,6 +6588,44 @@ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true }, + "sinon": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.3.2.tgz", + "integrity": "sha512-thErC1z64BeyGiPvF8aoSg0LEnptSaWE7YhdWWbWXgelOyThent7uKOnnEh9zBxDbKixtr5dEko+ws1sZMuFMA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.4.0", + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/samsam": "^3.3.1", + "diff": "^3.5.0", + "lolex": "^4.0.1", + "nise": "^1.4.10", + "supports-color": "^5.5.0" + }, + "dependencies": { + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "sjcl": { "version": "git://github.com/bitwiseshiftleft/sjcl.git#a03ea8ef32329bc8d7bc28a438372b5acb46616b", "from": "git://github.com/bitwiseshiftleft/sjcl.git#a03ea8e" @@ -6566,15 +6921,6 @@ "readable-stream": "^2.0.2" } }, - "stream-combiner": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", - "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", - "dev": true, - "requires": { - "duplexer": "~0.1.1" - } - }, "stream-http": { "version": "2.8.3", "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", @@ -6764,17 +7110,6 @@ "integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A==", "dev": true }, - "tar": { - "version": "0.1.20", - "resolved": "https://registry.npmjs.org/tar/-/tar-0.1.20.tgz", - "integrity": "sha1-QpQLrltfIsdEg2mRJvnz8nRJyxM=", - "dev": true, - "requires": { - "block-stream": "*", - "fstream": "~0.1.28", - "inherits": "2" - } - }, "tempfile": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/tempfile/-/tempfile-1.1.1.tgz", @@ -6932,6 +7267,12 @@ "prelude-ls": "~1.1.2" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "type-is": { "version": "1.6.16", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", @@ -7414,6 +7755,15 @@ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", "dev": true }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, "window-size": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", @@ -7524,6 +7874,231 @@ } } }, + "yargs-unparser": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.5.0.tgz", + "integrity": "sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw==", + "dev": true, + "requires": { + "flat": "^4.1.0", + "lodash": "^4.17.11", + "yargs": "^12.0.5" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "dev": true, + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "dev": true + }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "dev": true, + "requires": { + "invert-kv": "^2.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "yargs": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", + "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "dev": true, + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^11.1.1" + } + }, + "yargs-parser": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", + "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, "yui": { "version": "3.18.1", "resolved": "https://registry.npmjs.org/yui/-/yui-3.18.1.tgz", diff --git a/packages/fxa-js-client/package.json b/packages/fxa-js-client/package.json index b69ca7d9ce6..26badb28ede 100644 --- a/packages/fxa-js-client/package.json +++ b/packages/fxa-js-client/package.json @@ -6,8 +6,8 @@ "license": "MPL-2.0", "scripts": { "start": "grunt", - "test": "grunt test", - "test-local": "intern-client config=tests/intern auth_server=LOCAL", + "test": "mocha tests/lib --reporter dot --timeout 5000", + "test-local": "AUTH_SERVER_URL=http://127.0.0.1:9000 npm test", "setup": "npm install && grunt sjcl", "contributors": "git shortlog -s | cut -c8- | sort -f > CONTRIBUTORS.md", "format": "prettier '**' --write" @@ -36,6 +36,7 @@ "xhr2": "0.0.7" }, "devDependencies": { + "chai": "4.2.0", "eslint-config-prettier": "^5.0.0", "grunt": "0.4.2", "grunt-build-control": "git://github.com/robwierzbowski/grunt-build-control#274952", @@ -51,11 +52,12 @@ "grunt-open": "0.2.4", "grunt-webpack": "3.0.2", "http-proxy": "1.11.1", - "intern-geezer": "2.2.3", "jscs-jsdoc": "1.1.0", "load-grunt-tasks": "3.2.0", + "mocha": "6.2.0", "otplib": "7.1.0", "prettier": "^1.18.2", + "sinon": "7.3.2", "webpack": "3.10.0" } } diff --git a/packages/fxa-js-client/tasks/intern.js b/packages/fxa-js-client/tasks/intern.js deleted file mode 100644 index 2eef79eb300..00000000000 --- a/packages/fxa-js-client/tasks/intern.js +++ /dev/null @@ -1,41 +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/. */ - -module.exports = function(grunt) { - 'use strict'; - - grunt.config('intern', { - node: { - options: { - config: 'tests/intern', - reporters: ['console'], - suites: ['tests/all'], - }, - }, - native_node: { - options: { - config: 'tests/intern_native_node', - reporters: ['console'], - suites: ['tests/all'], - }, - }, - // local browser - browser: { - options: { - runType: 'runner', - config: 'tests/intern_browser', - suites: ['tests/all'], - }, - }, - sauce: { - options: { - runType: 'runner', - config: 'tests/intern_sauce', - suites: ['tests/all'], - sauceUsername: 'fxa-client', - sauceAccessKey: '863203af-38fd-4f1d-9332-adc8f60f157b', - }, - }, - }); -}; diff --git a/packages/fxa-js-client/tasks/sjcl.js b/packages/fxa-js-client/tasks/sjcl.js index 1d74cf70c00..559ace4232f 100644 --- a/packages/fxa-js-client/tasks/sjcl.js +++ b/packages/fxa-js-client/tasks/sjcl.js @@ -22,9 +22,7 @@ module.exports = function(grunt) { } var sjclBower = fs.readFileSync(src); - var sjclAmd = - 'define([], function () {' + sjclBower + ' return sjcl; });'; - fs.writeFileSync(dist, sjclAmd); + fs.writeFileSync(dist, sjclBower); process.chdir('../..'); done(); diff --git a/packages/fxa-js-client/tests/addons/accountHelper.js b/packages/fxa-js-client/tests/addons/accountHelper.js index 1faa069f5a7..a7a597ef8af 100644 --- a/packages/fxa-js-client/tests/addons/accountHelper.js +++ b/packages/fxa-js-client/tests/addons/accountHelper.js @@ -2,109 +2,109 @@ * 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/. */ -define(['tests/mocks/request'], function(RequestMocks) { - 'use strict'; +'use strict'; - function AccountHelper(client, mail, respond) { - this.client = client; - this.mail = mail; - this.respond = respond; +const RequestMocks = require('../mocks/request'); + +function AccountHelper(client, mail, respond) { + this.client = client; + this.mail = mail; + this.respond = respond; +} +AccountHelper.prototype.newVerifiedAccount = function(options) { + var username = 'testHelp1'; + var domain = '@restmail.net'; + + if (options && options.domain) { + domain = options.domain; } - AccountHelper.prototype.newVerifiedAccount = function(options) { - var username = 'testHelp1'; - var domain = '@restmail.net'; - - if (options && options.domain) { - domain = options.domain; - } - - if (options && options.username) { - username = options.username; - } - - var user = username + new Date().getTime(); - var email = user + domain; - var password = 'iliketurtles'; - var respond = this.respond; - var mail = this.mail; - var client = this.client; - var uid; - var result = { - input: { - user: user, - email: email, - password: password, - }, - }; - - return respond(client.signUp(email, password), RequestMocks.signUp) - .then(function(res) { - uid = res.uid; - result.signUp = res; - - return respond(mail.wait(user), RequestMocks.mail); - }) - .then(function(emails) { - var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; - - return respond(client.verifyCode(uid, code), RequestMocks.verifyCode); - }) - - .then(function(res) { - result.verifyCode = res; - - return respond( - client.signIn(email, password, { keys: true }), - RequestMocks.signInWithKeys - ); - }) - .then(function(res) { - result.signIn = res; - - return result; - }); + + if (options && options.username) { + username = options.username; + } + + var user = username + new Date().getTime(); + var email = user + domain; + var password = 'iliketurtles'; + var respond = this.respond; + var mail = this.mail; + var client = this.client; + var uid; + var result = { + input: { + user: user, + email: email, + password: password, + }, }; - AccountHelper.prototype.newUnverifiedAccount = function(options) { - var username = 'testHelp2'; - var domain = '@restmail.net'; - - if (options && options.domain) { - domain = options.domain; - } - - if (options && options.username) { - username = options.username; - } - - var user = username + new Date().getTime(); - var email = user + domain; - var password = 'iliketurtles'; - var respond = this.respond; - var client = this.client; - var result = { - input: { - user: user, - email: email, - password: password, - }, - }; - - return respond(client.signUp(email, password), RequestMocks.signUp) - .then(function(res) { - result.signUp = res; - - return respond( - client.signIn(email, password, { keys: true }), - RequestMocks.signInWithKeys - ); - }) - .then(function(res) { - result.signIn = res; - - return result; - }); + return respond(client.signUp(email, password), RequestMocks.signUp) + .then(function(res) { + uid = res.uid; + result.signUp = res; + + return respond(mail.wait(user), RequestMocks.mail); + }) + .then(function(emails) { + var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; + + return respond(client.verifyCode(uid, code), RequestMocks.verifyCode); + }) + + .then(function(res) { + result.verifyCode = res; + + return respond( + client.signIn(email, password, { keys: true }), + RequestMocks.signInWithKeys + ); + }) + .then(function(res) { + result.signIn = res; + + return result; + }); +}; + +AccountHelper.prototype.newUnverifiedAccount = function(options) { + var username = 'testHelp2'; + var domain = '@restmail.net'; + + if (options && options.domain) { + domain = options.domain; + } + + if (options && options.username) { + username = options.username; + } + + var user = username + new Date().getTime(); + var email = user + domain; + var password = 'iliketurtles'; + var respond = this.respond; + var client = this.client; + var result = { + input: { + user: user, + email: email, + password: password, + }, }; - return AccountHelper; -}); + return respond(client.signUp(email, password), RequestMocks.signUp) + .then(function(res) { + result.signUp = res; + + return respond( + client.signIn(email, password, { keys: true }), + RequestMocks.signInWithKeys + ); + }) + .then(function(res) { + result.signIn = res; + + return result; + }); +}; + +module.exports = AccountHelper; diff --git a/packages/fxa-js-client/tests/addons/environment.js b/packages/fxa-js-client/tests/addons/environment.js index eb30b17e616..4ebc0aaa538 100644 --- a/packages/fxa-js-client/tests/addons/environment.js +++ b/packages/fxa-js-client/tests/addons/environment.js @@ -1,93 +1,79 @@ /* 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/. */ +const XHR = require('xhr2'); +const Sinon = require('sinon'); +const FxAccountClient = require('../../client/FxAccountClient'); +const Restmail = require('../addons/restmail'); +const AccountHelper = require('../addons/accountHelper'); +const RequestMocks = require('../mocks/request'); +const ErrorMocks = require('../mocks/errors'); +function Environment() { + var self = this; + this.authServerUrl = process.env.AUTH_SERVER_URL || 'http://127.0.0.1:9000'; + this.useRemoteServer = !!process.env.AUTH_SERVER_URL; + this.mailServerUrl = this.authServerUrl.match(/^http:\/\/127/) + ? 'http://127.0.0.1:9001' + : 'http://restmail.net'; -define([ - 'tests/intern', - 'intern/node_modules/dojo/has!host-node?intern/node_modules/dojo/node!xhr2', - 'tests/addons/sinon', - 'client/FxAccountClient', - 'tests/addons/restmail', - 'tests/addons/accountHelper', - 'tests/mocks/request', - 'tests/mocks/errors', -], function( - config, - XHR, - Sinon, - FxAccountClient, - Restmail, - AccountHelper, - RequestMocks, - ErrorMocks -) { - function Environment() { - var self = this; - this.authServerUrl = config.AUTH_SERVER_URL || 'http://127.0.0.1:9000'; - // if 'auth_server' is part of the Intern arguments then using a remote server - this.useRemoteServer = !!config.AUTH_SERVER_URL; - this.mailServerUrl = this.authServerUrl.match(/^http:\/\/127/) - ? 'http://127.0.0.1:9001' - : 'http://restmail.net'; - - if (this.useRemoteServer) { - this.xhr = XHR.XMLHttpRequest; - // respond is a noop because we are using real XHR in this case - this.respond = noop; - } else { - this.requests = []; - this.responses = []; - // switch to the fake XHR - this.xhr = Sinon.useFakeXMLHttpRequest(); - this.xhr.onCreate = function(xhr) { - if (self.requests.length < self.responses.length) { - var mock = self.responses[self.requests.length]; - setTimeout(function() { - xhr.respond(mock.status, mock.headers, mock.body); - }, 0); - } - self.requests.push(xhr); - }; - // respond calls a fake XHR response using SinonJS - this.respond = function(returnValue, mock) { - if (arguments.length < 2) { - mock = returnValue; - returnValue = null; - } - if (typeof mock === 'undefined') { - console.log('Mock does not exist!'); - } - // this has to be here to work in IE + if (this.useRemoteServer) { + this.xhr = XHR.XMLHttpRequest; + // respond is a noop because we are using real XHR in this case + this.respond = noop; + } else { + this.requests = []; + this.responses = []; + // switch to the fake XHR + this.xhr = Sinon.useFakeXMLHttpRequest(); + this.xhr.onCreate = function(xhr) { + if (self.requests.length < self.responses.length) { + var mock = self.responses[self.requests.length]; setTimeout(function() { - if (self.responses.length < self.requests.length) { + xhr.respond(mock.status, mock.headers, mock.body); + }, 0); + } + self.requests.push(xhr); + }; + // respond calls a fake XHR response using SinonJS + this.respond = function(returnValue, mock) { + if (arguments.length < 2) { + mock = returnValue; + returnValue = null; + } + if (typeof mock === 'undefined') { + console.log('Mock does not exist!'); + } + // this has to be here to work in IE + setTimeout(function() { + if (self.responses.length < self.requests.length) { + try { self.requests[self.responses.length].respond( mock.status, mock.headers, mock.body ); + } catch (e) { + // mocking responses may cause `INVALID_STATE_ERR - 0` error here via the + // FakeXHR interface in sinon. } - self.responses.push(mock); - }, 0); - return returnValue; - }; - } - // initialize a new FxA Client - this.client = new FxAccountClient(this.authServerUrl, { xhr: this.xhr }); - // setup Restmail, - this.mail = new Restmail(this.mailServerUrl, this.xhr); - // account helper takes care of new verified and unverified accounts - this.accountHelper = new AccountHelper( - this.client, - this.mail, - this.respond - ); - this.ErrorMocks = ErrorMocks; - this.RequestMocks = RequestMocks; + } + self.responses.push(mock); + }, 0); + return returnValue; + }; } + // initialize a new FxA Client + this.client = new FxAccountClient(this.authServerUrl, { xhr: this.xhr }); + // setup Restmail, + this.mail = new Restmail(this.mailServerUrl, this.xhr); + // account helper takes care of new verified and unverified accounts + this.accountHelper = new AccountHelper(this.client, this.mail, this.respond); + this.ErrorMocks = ErrorMocks; + this.RequestMocks = RequestMocks; +} - function noop(val) { - return val; - } +function noop(val) { + return val; +} - return Environment; -}); +module.exports = Environment; diff --git a/packages/fxa-js-client/tests/addons/node-client.js b/packages/fxa-js-client/tests/addons/node-client.js deleted file mode 100644 index 7f962e79c82..00000000000 --- a/packages/fxa-js-client/tests/addons/node-client.js +++ /dev/null @@ -1,9 +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/. */ - -define(['intern/node_modules/dojo/node!../../node/index'], function( - FxAccountClient -) { - return FxAccountClient; -}); diff --git a/packages/fxa-js-client/tests/addons/restmail.js b/packages/fxa-js-client/tests/addons/restmail.js index 797073a34d0..cb9b869a301 100644 --- a/packages/fxa-js-client/tests/addons/restmail.js +++ b/packages/fxa-js-client/tests/addons/restmail.js @@ -2,32 +2,37 @@ * 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/. */ -define(['client/lib/request'], function(Request) { - 'use strict'; +'use strict'; - function Restmail(server, xhr) { - this.request = new Request(server, xhr); - } +const Request = require('../../client/lib/request'); + +function Restmail(server, xhr) { + this.request = new Request(server, xhr); +} - // utility function that waits for a restmail email to arrive - Restmail.prototype.wait = function(user, number) { - var self = this; +// utility function that waits for a restmail email to arrive +Restmail.prototype.wait = function(user, number = 1, requestAttempts = 0) { + let self = this; + const path = '/mail/' + user; - if (!number) number = 1; //eslint-disable-line curly - console.log('Waiting for email...'); + if (requestAttempts > 0) { + // only log if too many attempts, probably means the service is + // not properly responding + console.log('Waiting for email at:', path); + } - return this.request.send('/mail/' + user, 'GET').then(function(result) { - if (result.length === number) { - return result; - } else { - return new Promise(function(resolve, reject) { - setTimeout(function() { - self.wait(user, number).then(resolve, reject); - }, 1000); - }); - } - }); - }; + return this.request.send(path, 'GET').then(function(result) { + requestAttempts++; + if (result.length === number) { + return result; + } else { + return new Promise(function(resolve, reject) { + setTimeout(function() { + self.wait(user, number, requestAttempts).then(resolve, reject); + }, 1000); + }); + } + }); +}; - return Restmail; -}); +module.exports = Restmail; diff --git a/packages/fxa-js-client/tests/addons/sinon.js b/packages/fxa-js-client/tests/addons/sinon.js deleted file mode 100644 index 0f618108f2a..00000000000 --- a/packages/fxa-js-client/tests/addons/sinon.js +++ /dev/null @@ -1,4899 +0,0 @@ -/** - * Sinon.JS 1.7.3, 2013/12/03 - * - * @author Christian Johansen (christian@cjohansen.no) - * @author Contributors: https://github.com/cjohansen/Sinon.JS/blob/master/AUTHORS - * - * (The BSD License) - * - * Copyright (c) 2010-2013, Christian Johansen, christian@cjohansen.no - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * * Neither the name of Christian Johansen nor the names of his contributors - * may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF - * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -this.sinon = function() { - var buster = (function(setTimeout, B) { - var isNode = typeof require == 'function' && typeof module == 'object'; - var div = typeof document != 'undefined' && document.createElement('div'); - var F = function() {}; - - var buster = { - bind: function bind(obj, methOrProp) { - var method = - typeof methOrProp == 'string' ? obj[methOrProp] : methOrProp; - var args = Array.prototype.slice.call(arguments, 2); - return function() { - var allArgs = args.concat(Array.prototype.slice.call(arguments)); - return method.apply(obj, allArgs); - }; - }, - - partial: function partial(fn) { - var args = [].slice.call(arguments, 1); - return function() { - return fn.apply(this, args.concat([].slice.call(arguments))); - }; - }, - - create: function create(object) { - F.prototype = object; - return new F(); - }, - - extend: function extend(target) { - if (!target) { - return; - } - for (var i = 1, l = arguments.length, prop; i < l; ++i) { - for (prop in arguments[i]) { - target[prop] = arguments[i][prop]; - } - } - return target; - }, - - nextTick: function nextTick(callback) { - if (typeof process != 'undefined' && process.nextTick) { - return process.nextTick(callback); - } - setTimeout(callback, 0); - }, - - functionName: function functionName(func) { - if (!func) return ''; - if (func.displayName) return func.displayName; - if (func.name) return func.name; - var matches = func.toString().match(/function\s+([^\(]+)/m); - return (matches && matches[1]) || ''; - }, - - isNode: function isNode(obj) { - if (!div) return false; - try { - obj.appendChild(div); - obj.removeChild(div); - } catch (e) { - return false; - } - return true; - }, - - isElement: function isElement(obj) { - return obj && obj.nodeType === 1 && buster.isNode(obj); - }, - - isArray: function isArray(arr) { - return Object.prototype.toString.call(arr) == '[object Array]'; - }, - - flatten: function flatten(arr) { - var result = [], - arr = arr || []; - for (var i = 0, l = arr.length; i < l; ++i) { - result = result.concat( - buster.isArray(arr[i]) ? flatten(arr[i]) : arr[i] - ); - } - return result; - }, - - each: function each(arr, callback) { - for (var i = 0, l = arr.length; i < l; ++i) { - callback(arr[i]); - } - }, - - map: function map(arr, callback) { - var results = []; - for (var i = 0, l = arr.length; i < l; ++i) { - results.push(callback(arr[i])); - } - return results; - }, - - parallel: function parallel(fns, callback) { - function cb(err, res) { - if (typeof callback == 'function') { - callback(err, res); - callback = null; - } - } - if (fns.length == 0) { - return cb(null, []); - } - var remaining = fns.length, - results = []; - function makeDone(num) { - return function done(err, result) { - if (err) { - return cb(err); - } - results[num] = result; - if (--remaining == 0) { - cb(null, results); - } - }; - } - for (var i = 0, l = fns.length; i < l; ++i) { - fns[i](makeDone(i)); - } - }, - - series: function series(fns, callback) { - function cb(err, res) { - if (typeof callback == 'function') { - callback(err, res); - } - } - var remaining = fns.slice(); - var results = []; - function callNext() { - if (remaining.length == 0) return cb(null, results); - var promise = remaining.shift()(next); - if (promise && typeof promise.then == 'function') { - promise.then(buster.partial(next, null), next); - } - } - function next(err, result) { - if (err) return cb(err); - results.push(result); - callNext(); - } - callNext(); - }, - - countdown: function countdown(num, done) { - return function() { - if (--num == 0) done(); - }; - }, - }; - - if ( - typeof process === 'object' && - typeof require === 'function' && - typeof module === 'object' - ) { - var crypto = require('crypto'); - var path = require('path'); - - buster.tmpFile = function(fileName) { - var hashed = crypto.createHash('sha1'); - hashed.update(fileName); - var tmpfileName = hashed.digest('hex'); - - if (process.platform == 'win32') { - return path.join(process.env['TEMP'], tmpfileName); - } else { - return path.join('/tmp', tmpfileName); - } - }; - } - - if (Array.prototype.some) { - buster.some = function(arr, fn, thisp) { - return arr.some(fn, thisp); - }; - } else { - // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/some - buster.some = function(arr, fun, thisp) { - if (arr == null) { - throw new TypeError(); - } - arr = Object(arr); - var len = arr.length >>> 0; - if (typeof fun !== 'function') { - throw new TypeError(); - } - - for (var i = 0; i < len; i++) { - if (arr.hasOwnProperty(i) && fun.call(thisp, arr[i], i, arr)) { - return true; - } - } - - return false; - }; - } - - if (Array.prototype.filter) { - buster.filter = function(arr, fn, thisp) { - return arr.filter(fn, thisp); - }; - } else { - // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/filter - buster.filter = function(fn, thisp) { - if (this == null) { - throw new TypeError(); - } - - var t = Object(this); - var len = t.length >>> 0; - if (typeof fn != 'function') { - throw new TypeError(); - } - - var res = []; - for (var i = 0; i < len; i++) { - if (i in t) { - var val = t[i]; // in case fun mutates this - if (fn.call(thisp, val, i, t)) { - res.push(val); - } - } - } - - return res; - }; - } - - if (isNode) { - module.exports = buster; - buster.eventEmitter = require('./buster-event-emitter'); - Object.defineProperty(buster, 'defineVersionGetter', { - get: function() { - return require('./define-version-getter'); - }, - }); - } - - return buster.extend(B || {}, buster); - })(setTimeout, buster); - if (typeof buster === 'undefined') { - var buster = {}; - } - - if (typeof module === 'object' && typeof require === 'function') { - buster = require('buster-core'); - } - - buster.format = buster.format || {}; - buster.format.excludeConstructors = ['Object', /^.$/]; - buster.format.quoteStrings = true; - - buster.format.ascii = (function() { - var hasOwn = Object.prototype.hasOwnProperty; - - var specialObjects = []; - if (typeof global != 'undefined') { - specialObjects.push({ obj: global, value: '[object global]' }); - } - if (typeof document != 'undefined') { - specialObjects.push({ obj: document, value: '[object HTMLDocument]' }); - } - if (typeof window != 'undefined') { - specialObjects.push({ obj: window, value: '[object Window]' }); - } - - function keys(object) { - var k = (Object.keys && Object.keys(object)) || []; - - if (k.length == 0) { - for (var prop in object) { - if (hasOwn.call(object, prop)) { - k.push(prop); - } - } - } - - return k.sort(); - } - - function isCircular(object, objects) { - if (typeof object != 'object') { - return false; - } - - for (var i = 0, l = objects.length; i < l; ++i) { - if (objects[i] === object) { - return true; - } - } - - return false; - } - - function ascii(object, processed, indent) { - if (typeof object == 'string') { - var quote = typeof this.quoteStrings != 'boolean' || this.quoteStrings; - return processed || quote ? '"' + object + '"' : object; - } - - if (typeof object == 'function' && !(object instanceof RegExp)) { - return ascii.func(object); - } - - processed = processed || []; - - if (isCircular(object, processed)) { - return '[Circular]'; - } - - if (Object.prototype.toString.call(object) == '[object Array]') { - return ascii.array.call(this, object, processed); - } - - if (!object) { - return '' + object; - } - - if (buster.isElement(object)) { - return ascii.element(object); - } - - if ( - typeof object.toString == 'function' && - object.toString !== Object.prototype.toString - ) { - return object.toString(); - } - - for (var i = 0, l = specialObjects.length; i < l; i++) { - if (object === specialObjects[i].obj) { - return specialObjects[i].value; - } - } - - return ascii.object.call(this, object, processed, indent); - } - - ascii.func = function(func) { - return 'function ' + buster.functionName(func) + '() {}'; - }; - - ascii.array = function(array, processed) { - processed = processed || []; - processed.push(array); - var pieces = []; - - for (var i = 0, l = array.length; i < l; ++i) { - pieces.push(ascii.call(this, array[i], processed)); - } - - return '[' + pieces.join(', ') + ']'; - }; - - ascii.object = function(object, processed, indent) { - processed = processed || []; - processed.push(object); - indent = indent || 0; - var pieces = [], - properties = keys(object), - prop, - str, - obj; - var is = ''; - var length = 3; - - for (var i = 0, l = indent; i < l; ++i) { - is += ' '; - } - - for (i = 0, l = properties.length; i < l; ++i) { - prop = properties[i]; - obj = object[prop]; - - if (isCircular(obj, processed)) { - str = '[Circular]'; - } else { - str = ascii.call(this, obj, processed, indent + 2); - } - - str = (/\s/.test(prop) ? '"' + prop + '"' : prop) + ': ' + str; - length += str.length; - pieces.push(str); - } - - var cons = ascii.constructorName.call(this, object); - var prefix = cons ? '[' + cons + '] ' : ''; - - return length + indent > 80 - ? prefix + '{\n ' + is + pieces.join(',\n ' + is) + '\n' + is + '}' - : prefix + '{ ' + pieces.join(', ') + ' }'; - }; - - ascii.element = function(element) { - var tagName = element.tagName.toLowerCase(); - var attrs = element.attributes, - attribute, - pairs = [], - attrName; - - for (var i = 0, l = attrs.length; i < l; ++i) { - attribute = attrs.item(i); - attrName = attribute.nodeName.toLowerCase().replace('html:', ''); - - if (attrName == 'contenteditable' && attribute.nodeValue == 'inherit') { - continue; - } - - if (!!attribute.nodeValue) { - pairs.push(attrName + '="' + attribute.nodeValue + '"'); - } - } - - var formatted = '<' + tagName + (pairs.length > 0 ? ' ' : ''); - var content = element.innerHTML; - - if (content.length > 20) { - content = content.substr(0, 20) + '[...]'; - } - - var res = - formatted + pairs.join(' ') + '>' + content + ''; - - return res.replace(/ contentEditable="inherit"/, ''); - }; - - ascii.constructorName = function(object) { - var name = buster.functionName(object && object.constructor); - var excludes = - this.excludeConstructors || buster.format.excludeConstructors || []; - - for (var i = 0, l = excludes.length; i < l; ++i) { - if (typeof excludes[i] == 'string' && excludes[i] == name) { - return ''; - } else if (excludes[i].test && excludes[i].test(name)) { - return ''; - } - } - - return name; - }; - - return ascii; - })(); - - if (typeof module != 'undefined') { - module.exports = buster.format; - } - /*jslint eqeqeq: false, onevar: false, forin: true, nomen: false, regexp: false, plusplus: false*/ - /*global module, require, __dirname, document*/ - /** - * Sinon core utilities. For internal use only. - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2013 Christian Johansen - */ - - var sinon = (function(buster) { - var div = typeof document != 'undefined' && document.createElement('div'); - var hasOwn = Object.prototype.hasOwnProperty; - - function isDOMNode(obj) { - var success = false; - - try { - obj.appendChild(div); - success = div.parentNode == obj; - } catch (e) { - return false; - } finally { - try { - obj.removeChild(div); - } catch (e) { - // Remove failed, not much we can do about that - } - } - - return success; - } - - function isElement(obj) { - return div && obj && obj.nodeType === 1 && isDOMNode(obj); - } - - function isFunction(obj) { - return ( - typeof obj === 'function' || - !!(obj && obj.constructor && obj.call && obj.apply) - ); - } - - function mirrorProperties(target, source) { - for (var prop in source) { - if (!hasOwn.call(target, prop)) { - target[prop] = source[prop]; - } - } - } - - function isRestorable(obj) { - return ( - typeof obj === 'function' && - typeof obj.restore === 'function' && - obj.restore.sinon - ); - } - - var sinon = { - wrapMethod: function wrapMethod(object, property, method) { - if (!object) { - throw new TypeError('Should wrap property of object'); - } - - if (typeof method != 'function') { - throw new TypeError('Method wrapper should be function'); - } - - var wrappedMethod = object[property], - error; - - if (!isFunction(wrappedMethod)) { - error = new TypeError( - 'Attempted to wrap ' + - typeof wrappedMethod + - ' property ' + - property + - ' as function' - ); - } - - if (wrappedMethod.restore && wrappedMethod.restore.sinon) { - error = new TypeError( - 'Attempted to wrap ' + property + ' which is already wrapped' - ); - } - - if (wrappedMethod.calledBefore) { - var verb = !!wrappedMethod.returns ? 'stubbed' : 'spied on'; - error = new TypeError( - 'Attempted to wrap ' + property + ' which is already ' + verb - ); - } - - if (error) { - if (wrappedMethod._stack) { - error.stack += '\n--------------\n' + wrappedMethod._stack; - } - throw error; - } - - // IE 8 does not support hasOwnProperty on the window object. - var owned = hasOwn.call(object, property); - object[property] = method; - method.displayName = property; - // Set up a stack trace which can be used later to find what line of - // code the original method was created on. - method._stack = new Error('Stack Trace for original').stack; - - method.restore = function() { - // For prototype properties try to reset by delete first. - // If this fails (ex: localStorage on mobile safari) then force a reset - // via direct assignment. - if (!owned) { - delete object[property]; - } - if (object[property] === method) { - object[property] = wrappedMethod; - } - }; - - method.restore.sinon = true; - mirrorProperties(method, wrappedMethod); - - return method; - }, - - extend: function extend(target) { - for (var i = 1, l = arguments.length; i < l; i += 1) { - for (var prop in arguments[i]) { - if (arguments[i].hasOwnProperty(prop)) { - target[prop] = arguments[i][prop]; - } - - // DONT ENUM bug, only care about toString - if ( - arguments[i].hasOwnProperty('toString') && - arguments[i].toString != target.toString - ) { - target.toString = arguments[i].toString; - } - } - } - - return target; - }, - - create: function create(proto) { - var F = function() {}; - F.prototype = proto; - return new F(); - }, - - deepEqual: function deepEqual(a, b) { - if (sinon.match && sinon.match.isMatcher(a)) { - return a.test(b); - } - if (typeof a != 'object' || typeof b != 'object') { - return a === b; - } - - if (isElement(a) || isElement(b)) { - return a === b; - } - - if (a === b) { - return true; - } - - if ((a === null && b !== null) || (a !== null && b === null)) { - return false; - } - - var aString = Object.prototype.toString.call(a); - if (aString != Object.prototype.toString.call(b)) { - return false; - } - - if (aString == '[object Date]') { - return a.valueOf() === b.valueOf(); - } - - var prop, - aLength = 0, - bLength = 0; - - if (aString == '[object Array]' && a.length !== b.length) { - return false; - } - - for (prop in a) { - aLength += 1; - - if (!deepEqual(a[prop], b[prop])) { - return false; - } - } - - for (prop in b) { - bLength += 1; - } - - return aLength == bLength; - }, - - functionName: function functionName(func) { - var name = func.displayName || func.name; - - // Use function decomposition as a last resort to get function - // name. Does not rely on function decomposition to work - if it - // doesn't debugging will be slightly less informative - // (i.e. toString will say 'spy' rather than 'myFunc'). - if (!name) { - var matches = func.toString().match(/function ([^\s\(]+)/); - name = matches && matches[1]; - } - - return name; - }, - - functionToString: function toString() { - if (this.getCall && this.callCount) { - var thisValue, - prop, - i = this.callCount; - - while (i--) { - thisValue = this.getCall(i).thisValue; - - for (prop in thisValue) { - if (thisValue[prop] === this) { - return prop; - } - } - } - } - - return this.displayName || 'sinon fake'; - }, - - getConfig: function(custom) { - var config = {}; - custom = custom || {}; - var defaults = sinon.defaultConfig; - - for (var prop in defaults) { - if (defaults.hasOwnProperty(prop)) { - config[prop] = custom.hasOwnProperty(prop) - ? custom[prop] - : defaults[prop]; - } - } - - return config; - }, - - format: function(val) { - return '' + val; - }, - - defaultConfig: { - injectIntoThis: true, - injectInto: null, - properties: ['spy', 'stub', 'mock', 'clock', 'server', 'requests'], - useFakeTimers: true, - useFakeServer: true, - }, - - timesInWords: function timesInWords(count) { - return ( - (count == 1 && 'once') || - (count == 2 && 'twice') || - (count == 3 && 'thrice') || - (count || 0) + ' times' - ); - }, - - calledInOrder: function(spies) { - for (var i = 1, l = spies.length; i < l; i++) { - if (!spies[i - 1].calledBefore(spies[i]) || !spies[i].called) { - return false; - } - } - - return true; - }, - - orderByFirstCall: function(spies) { - return spies.sort(function(a, b) { - // uuid, won't ever be equal - var aCall = a.getCall(0); - var bCall = b.getCall(0); - var aId = (aCall && aCall.callId) || -1; - var bId = (bCall && bCall.callId) || -1; - - return aId < bId ? -1 : 1; - }); - }, - - log: function() {}, - - logError: function(label, err) { - var msg = label + ' threw exception: '; - sinon.log(msg + '[' + err.name + '] ' + err.message); - if (err.stack) { - sinon.log(err.stack); - } - - setTimeout(function() { - err.message = msg + err.message; - throw err; - }, 0); - }, - - typeOf: function(value) { - if (value === null) { - return 'null'; - } else if (value === undefined) { - return 'undefined'; - } - var string = Object.prototype.toString.call(value); - return string.substring(8, string.length - 1).toLowerCase(); - }, - - createStubInstance: function(constructor) { - if (typeof constructor !== 'function') { - throw new TypeError('The constructor should be a function.'); - } - return sinon.stub(sinon.create(constructor.prototype)); - }, - - restore: function(object) { - if (object !== null && typeof object === 'object') { - for (var prop in object) { - if (isRestorable(object[prop])) { - object[prop].restore(); - } - } - } else if (isRestorable(object)) { - object.restore(); - } - }, - }; - - var isNode = typeof module !== 'undefined' && module.exports; - var isAMD = - typeof define === 'function' && - typeof define.amd === 'object' && - define.amd; - - if (isAMD) { - define(function() { - return sinon; - }); - } else if (isNode) { - try { - buster = { format: require('buster-format') }; - } catch (e) {} - module.exports = sinon; - module.exports.spy = require('./sinon/spy'); - module.exports.spyCall = require('./sinon/call'); - module.exports.behavior = require('./sinon/behavior'); - module.exports.stub = require('./sinon/stub'); - module.exports.mock = require('./sinon/mock'); - module.exports.collection = require('./sinon/collection'); - module.exports.assert = require('./sinon/assert'); - module.exports.sandbox = require('./sinon/sandbox'); - module.exports.test = require('./sinon/test'); - module.exports.testCase = require('./sinon/test_case'); - module.exports.assert = require('./sinon/assert'); - module.exports.match = require('./sinon/match'); - } - - if (buster) { - var formatter = sinon.create(buster.format); - formatter.quoteStrings = false; - sinon.format = function() { - return formatter.ascii.apply(formatter, arguments); - }; - } else if (isNode) { - try { - var util = require('util'); - sinon.format = function(value) { - return typeof value == 'object' && - value.toString === Object.prototype.toString - ? util.inspect(value) - : value; - }; - } catch (e) { - /* Node, but no util module - would be very old, but better safe than - sorry */ - } - } - - return sinon; - })(typeof buster == 'object' && buster); - - /* @depend ../sinon.js */ - /*jslint eqeqeq: false, onevar: false, plusplus: false*/ - /*global module, require, sinon*/ - /** - * Match functions - * - * @author Maximilian Antoni (mail@maxantoni.de) - * @license BSD - * - * Copyright (c) 2012 Maximilian Antoni - */ - - (function(sinon) { - var commonJSModule = typeof module !== 'undefined' && module.exports; - - if (!sinon && commonJSModule) { - sinon = require('../sinon'); - } - - if (!sinon) { - return; - } - - function assertType(value, type, name) { - var actual = sinon.typeOf(value); - if (actual !== type) { - throw new TypeError( - 'Expected type of ' + name + ' to be ' + type + ', but was ' + actual - ); - } - } - - var matcher = { - toString: function() { - return this.message; - }, - }; - - function isMatcher(object) { - return matcher.isPrototypeOf(object); - } - - function matchObject(expectation, actual) { - if (actual === null || actual === undefined) { - return false; - } - for (var key in expectation) { - if (expectation.hasOwnProperty(key)) { - var exp = expectation[key]; - var act = actual[key]; - if (match.isMatcher(exp)) { - if (!exp.test(act)) { - return false; - } - } else if (sinon.typeOf(exp) === 'object') { - if (!matchObject(exp, act)) { - return false; - } - } else if (!sinon.deepEqual(exp, act)) { - return false; - } - } - } - return true; - } - - matcher.or = function(m2) { - if (!isMatcher(m2)) { - throw new TypeError('Matcher expected'); - } - var m1 = this; - var or = sinon.create(matcher); - or.test = function(actual) { - return m1.test(actual) || m2.test(actual); - }; - or.message = m1.message + '.or(' + m2.message + ')'; - return or; - }; - - matcher.and = function(m2) { - if (!isMatcher(m2)) { - throw new TypeError('Matcher expected'); - } - var m1 = this; - var and = sinon.create(matcher); - and.test = function(actual) { - return m1.test(actual) && m2.test(actual); - }; - and.message = m1.message + '.and(' + m2.message + ')'; - return and; - }; - - var match = function(expectation, message) { - var m = sinon.create(matcher); - var type = sinon.typeOf(expectation); - switch (type) { - case 'object': - if (typeof expectation.test === 'function') { - m.test = function(actual) { - return expectation.test(actual) === true; - }; - m.message = 'match(' + sinon.functionName(expectation.test) + ')'; - return m; - } - var str = []; - for (var key in expectation) { - if (expectation.hasOwnProperty(key)) { - str.push(key + ': ' + expectation[key]); - } - } - m.test = function(actual) { - return matchObject(expectation, actual); - }; - m.message = 'match(' + str.join(', ') + ')'; - break; - case 'number': - m.test = function(actual) { - return expectation == actual; - }; - break; - case 'string': - m.test = function(actual) { - if (typeof actual !== 'string') { - return false; - } - return actual.indexOf(expectation) !== -1; - }; - m.message = 'match("' + expectation + '")'; - break; - case 'regexp': - m.test = function(actual) { - if (typeof actual !== 'string') { - return false; - } - return expectation.test(actual); - }; - break; - case 'function': - m.test = expectation; - if (message) { - m.message = message; - } else { - m.message = 'match(' + sinon.functionName(expectation) + ')'; - } - break; - default: - m.test = function(actual) { - return sinon.deepEqual(expectation, actual); - }; - } - if (!m.message) { - m.message = 'match(' + expectation + ')'; - } - return m; - }; - - match.isMatcher = isMatcher; - - match.any = match(function() { - return true; - }, 'any'); - - match.defined = match(function(actual) { - return actual !== null && actual !== undefined; - }, 'defined'); - - match.truthy = match(function(actual) { - return !!actual; - }, 'truthy'); - - match.falsy = match(function(actual) { - return !actual; - }, 'falsy'); - - match.same = function(expectation) { - return match(function(actual) { - return expectation === actual; - }, 'same(' + expectation + ')'); - }; - - match.typeOf = function(type) { - assertType(type, 'string', 'type'); - return match(function(actual) { - return sinon.typeOf(actual) === type; - }, 'typeOf("' + type + '")'); - }; - - match.instanceOf = function(type) { - assertType(type, 'function', 'type'); - return match(function(actual) { - return actual instanceof type; - }, 'instanceOf(' + sinon.functionName(type) + ')'); - }; - - function createPropertyMatcher(propertyTest, messagePrefix) { - return function(property, value) { - assertType(property, 'string', 'property'); - var onlyProperty = arguments.length === 1; - var message = messagePrefix + '("' + property + '"'; - if (!onlyProperty) { - message += ', ' + value; - } - message += ')'; - return match(function(actual) { - if ( - actual === undefined || - actual === null || - !propertyTest(actual, property) - ) { - return false; - } - return onlyProperty || sinon.deepEqual(value, actual[property]); - }, message); - }; - } - - match.has = createPropertyMatcher(function(actual, property) { - if (typeof actual === 'object') { - return property in actual; - } - return actual[property] !== undefined; - }, 'has'); - - match.hasOwn = createPropertyMatcher(function(actual, property) { - return actual.hasOwnProperty(property); - }, 'hasOwn'); - - match.bool = match.typeOf('boolean'); - match.number = match.typeOf('number'); - match.string = match.typeOf('string'); - match.object = match.typeOf('object'); - match.func = match.typeOf('function'); - match.array = match.typeOf('array'); - match.regexp = match.typeOf('regexp'); - match.date = match.typeOf('date'); - - if (commonJSModule) { - module.exports = match; - } else { - sinon.match = match; - } - })((typeof sinon == 'object' && sinon) || null); - - /** - * @depend ../sinon.js - * @depend match.js - */ - /*jslint eqeqeq: false, onevar: false, plusplus: false*/ - /*global module, require, sinon*/ - /** - * Spy calls - * - * @author Christian Johansen (christian@cjohansen.no) - * @author Maximilian Antoni (mail@maxantoni.de) - * @license BSD - * - * Copyright (c) 2010-2013 Christian Johansen - * Copyright (c) 2013 Maximilian Antoni - */ - - (function(sinon) { - var commonJSModule = typeof module !== 'undefined' && module.exports; - if (!sinon && commonJSModule) { - sinon = require('../sinon'); - } - - if (!sinon) { - return; - } - - function throwYieldError(proxy, text, args) { - var msg = sinon.functionName(proxy) + text; - if (args.length) { - msg += ' Received [' + slice.call(args).join(', ') + ']'; - } - throw new Error(msg); - } - - var slice = Array.prototype.slice; - - var callProto = { - calledOn: function calledOn(thisValue) { - if (sinon.match && sinon.match.isMatcher(thisValue)) { - return thisValue.test(this.thisValue); - } - return this.thisValue === thisValue; - }, - - calledWith: function calledWith() { - for (var i = 0, l = arguments.length; i < l; i += 1) { - if (!sinon.deepEqual(arguments[i], this.args[i])) { - return false; - } - } - - return true; - }, - - calledWithMatch: function calledWithMatch() { - for (var i = 0, l = arguments.length; i < l; i += 1) { - var actual = this.args[i]; - var expectation = arguments[i]; - if (!sinon.match || !sinon.match(expectation).test(actual)) { - return false; - } - } - return true; - }, - - calledWithExactly: function calledWithExactly() { - return ( - arguments.length == this.args.length && - this.calledWith.apply(this, arguments) - ); - }, - - notCalledWith: function notCalledWith() { - return !this.calledWith.apply(this, arguments); - }, - - notCalledWithMatch: function notCalledWithMatch() { - return !this.calledWithMatch.apply(this, arguments); - }, - - returned: function returned(value) { - return sinon.deepEqual(value, this.returnValue); - }, - - threw: function threw(error) { - if (typeof error === 'undefined' || !this.exception) { - return !!this.exception; - } - - return this.exception === error || this.exception.name === error; - }, - - calledWithNew: function calledWithNew(thisValue) { - return this.thisValue instanceof this.proxy; - }, - - calledBefore: function(other) { - return this.callId < other.callId; - }, - - calledAfter: function(other) { - return this.callId > other.callId; - }, - - callArg: function(pos) { - this.args[pos](); - }, - - callArgOn: function(pos, thisValue) { - this.args[pos].apply(thisValue); - }, - - callArgWith: function(pos) { - this.callArgOnWith.apply( - this, - [pos, null].concat(slice.call(arguments, 1)) - ); - }, - - callArgOnWith: function(pos, thisValue) { - var args = slice.call(arguments, 2); - this.args[pos].apply(thisValue, args); - }, - - yield: function() { - this.yieldOn.apply(this, [null].concat(slice.call(arguments, 0))); - }, - - yieldOn: function(thisValue) { - var args = this.args; - for (var i = 0, l = args.length; i < l; ++i) { - if (typeof args[i] === 'function') { - args[i].apply(thisValue, slice.call(arguments, 1)); - return; - } - } - throwYieldError( - this.proxy, - ' cannot yield since no callback was passed.', - args - ); - }, - - yieldTo: function(prop) { - this.yieldToOn.apply( - this, - [prop, null].concat(slice.call(arguments, 1)) - ); - }, - - yieldToOn: function(prop, thisValue) { - var args = this.args; - for (var i = 0, l = args.length; i < l; ++i) { - if (args[i] && typeof args[i][prop] === 'function') { - args[i][prop].apply(thisValue, slice.call(arguments, 2)); - return; - } - } - throwYieldError( - this.proxy, - " cannot yield to '" + prop + "' since no callback was passed.", - args - ); - }, - - toString: function() { - var callStr = this.proxy.toString() + '('; - var args = []; - - for (var i = 0, l = this.args.length; i < l; ++i) { - args.push(sinon.format(this.args[i])); - } - - callStr = callStr + args.join(', ') + ')'; - - if (typeof this.returnValue != 'undefined') { - callStr += ' => ' + sinon.format(this.returnValue); - } - - if (this.exception) { - callStr += ' !' + this.exception.name; - - if (this.exception.message) { - callStr += '(' + this.exception.message + ')'; - } - } - - return callStr; - }, - }; - - callProto.invokeCallback = callProto.yield; - - function createSpyCall(spy, thisValue, args, returnValue, exception, id) { - if (typeof id !== 'number') { - throw new TypeError('Call id is not a number'); - } - var proxyCall = sinon.create(callProto); - proxyCall.proxy = spy; - proxyCall.thisValue = thisValue; - proxyCall.args = args; - proxyCall.returnValue = returnValue; - proxyCall.exception = exception; - proxyCall.callId = id; - - return proxyCall; - } - createSpyCall.toString = callProto.toString; // used by mocks - - if (commonJSModule) { - module.exports = createSpyCall; - } else { - sinon.spyCall = createSpyCall; - } - })((typeof sinon == 'object' && sinon) || null); - - /** - * @depend ../sinon.js - * @depend call.js - */ - /*jslint eqeqeq: false, onevar: false, plusplus: false*/ - /*global module, require, sinon*/ - /** - * Spy functions - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2013 Christian Johansen - */ - - (function(sinon) { - var commonJSModule = typeof module !== 'undefined' && module.exports; - var push = Array.prototype.push; - var slice = Array.prototype.slice; - var callId = 0; - - if (!sinon && commonJSModule) { - sinon = require('../sinon'); - } - - if (!sinon) { - return; - } - - function spy(object, property) { - if (!property && typeof object == 'function') { - return spy.create(object); - } - - if (!object && !property) { - return spy.create(function() {}); - } - - var method = object[property]; - return sinon.wrapMethod(object, property, spy.create(method)); - } - - function matchingFake(fakes, args, strict) { - if (!fakes) { - return; - } - - var alen = args.length; - - for (var i = 0, l = fakes.length; i < l; i++) { - if (fakes[i].matches(args, strict)) { - return fakes[i]; - } - } - } - - function incrementCallCount() { - this.called = true; - this.callCount += 1; - this.notCalled = false; - this.calledOnce = this.callCount == 1; - this.calledTwice = this.callCount == 2; - this.calledThrice = this.callCount == 3; - } - - function createCallProperties() { - this.firstCall = this.getCall(0); - this.secondCall = this.getCall(1); - this.thirdCall = this.getCall(2); - this.lastCall = this.getCall(this.callCount - 1); - } - - var vars = 'a,b,c,d,e,f,g,h,i,j,k,l'; - function createProxy(func) { - // Retain the function length: - var p; - if (func.length) { - eval( - 'p = (function proxy(' + - vars.substring(0, func.length * 2 - 1) + - ') { return p.invoke(func, this, slice.call(arguments)); });' - ); - } else { - p = function proxy() { - return p.invoke(func, this, slice.call(arguments)); - }; - } - return p; - } - - var uuid = 0; - - // Public API - var spyApi = { - reset: function() { - this.called = false; - this.notCalled = true; - this.calledOnce = false; - this.calledTwice = false; - this.calledThrice = false; - this.callCount = 0; - this.firstCall = null; - this.secondCall = null; - this.thirdCall = null; - this.lastCall = null; - this.args = []; - this.returnValues = []; - this.thisValues = []; - this.exceptions = []; - this.callIds = []; - if (this.fakes) { - for (var i = 0; i < this.fakes.length; i++) { - this.fakes[i].reset(); - } - } - }, - - create: function create(func) { - var name; - - if (typeof func != 'function') { - func = function() {}; - } else { - name = sinon.functionName(func); - } - - var proxy = createProxy(func); - - sinon.extend(proxy, spy); - delete proxy.create; - sinon.extend(proxy, func); - - proxy.reset(); - proxy.prototype = func.prototype; - proxy.displayName = name || 'spy'; - proxy.toString = sinon.functionToString; - proxy._create = sinon.spy.create; - proxy.id = 'spy#' + uuid++; - - return proxy; - }, - - invoke: function invoke(func, thisValue, args) { - var matching = matchingFake(this.fakes, args); - var exception, returnValue; - - incrementCallCount.call(this); - push.call(this.thisValues, thisValue); - push.call(this.args, args); - push.call(this.callIds, callId++); - - try { - if (matching) { - returnValue = matching.invoke(func, thisValue, args); - } else { - returnValue = (this.func || func).apply(thisValue, args); - } - } catch (e) { - push.call(this.returnValues, undefined); - exception = e; - throw e; - } finally { - push.call(this.exceptions, exception); - } - - push.call(this.returnValues, returnValue); - - createCallProperties.call(this); - - return returnValue; - }, - - getCall: function getCall(i) { - if (i < 0 || i >= this.callCount) { - return null; - } - - return sinon.spyCall( - this, - this.thisValues[i], - this.args[i], - this.returnValues[i], - this.exceptions[i], - this.callIds[i] - ); - }, - - getCalls: function() { - var calls = []; - var i; - - for (i = 0; i < this.callCount; i++) { - calls.push(this.getCall(i)); - } - - return calls; - }, - - calledBefore: function calledBefore(spyFn) { - if (!this.called) { - return false; - } - - if (!spyFn.called) { - return true; - } - - return this.callIds[0] < spyFn.callIds[spyFn.callIds.length - 1]; - }, - - calledAfter: function calledAfter(spyFn) { - if (!this.called || !spyFn.called) { - return false; - } - - return ( - this.callIds[this.callCount - 1] > spyFn.callIds[spyFn.callCount - 1] - ); - }, - - withArgs: function() { - var args = slice.call(arguments); - - if (this.fakes) { - var match = matchingFake(this.fakes, args, true); - - if (match) { - return match; - } - } else { - this.fakes = []; - } - - var original = this; - var fake = this._create(); - fake.matchingAguments = args; - fake.parent = this; - push.call(this.fakes, fake); - - fake.withArgs = function() { - return original.withArgs.apply(original, arguments); - }; - - for (var i = 0; i < this.args.length; i++) { - if (fake.matches(this.args[i])) { - incrementCallCount.call(fake); - push.call(fake.thisValues, this.thisValues[i]); - push.call(fake.args, this.args[i]); - push.call(fake.returnValues, this.returnValues[i]); - push.call(fake.exceptions, this.exceptions[i]); - push.call(fake.callIds, this.callIds[i]); - } - } - createCallProperties.call(fake); - - return fake; - }, - - matches: function(args, strict) { - var margs = this.matchingAguments; - - if ( - margs.length <= args.length && - sinon.deepEqual(margs, args.slice(0, margs.length)) - ) { - return !strict || margs.length == args.length; - } - }, - - printf: function(format) { - var spy = this; - var args = slice.call(arguments, 1); - var formatter; - - return (format || '').replace(/%(.)/g, function(match, specifyer) { - formatter = spyApi.formatters[specifyer]; - - if (typeof formatter == 'function') { - return formatter.call(null, spy, args); - } else if (!isNaN(parseInt(specifyer, 10))) { - return sinon.format(args[specifyer - 1]); - } - - return '%' + specifyer; - }); - }, - }; - - function delegateToCalls(method, matchAny, actual, notCalled) { - spyApi[method] = function() { - if (!this.called) { - if (notCalled) { - return notCalled.apply(this, arguments); - } - return false; - } - - var currentCall; - var matches = 0; - - for (var i = 0, l = this.callCount; i < l; i += 1) { - currentCall = this.getCall(i); - - if (currentCall[actual || method].apply(currentCall, arguments)) { - matches += 1; - - if (matchAny) { - return true; - } - } - } - - return matches === this.callCount; - }; - } - - delegateToCalls('calledOn', true); - delegateToCalls('alwaysCalledOn', false, 'calledOn'); - delegateToCalls('calledWith', true); - delegateToCalls('calledWithMatch', true); - delegateToCalls('alwaysCalledWith', false, 'calledWith'); - delegateToCalls('alwaysCalledWithMatch', false, 'calledWithMatch'); - delegateToCalls('calledWithExactly', true); - delegateToCalls('alwaysCalledWithExactly', false, 'calledWithExactly'); - delegateToCalls('neverCalledWith', false, 'notCalledWith', function() { - return true; - }); - delegateToCalls( - 'neverCalledWithMatch', - false, - 'notCalledWithMatch', - function() { - return true; - } - ); - delegateToCalls('threw', true); - delegateToCalls('alwaysThrew', false, 'threw'); - delegateToCalls('returned', true); - delegateToCalls('alwaysReturned', false, 'returned'); - delegateToCalls('calledWithNew', true); - delegateToCalls('alwaysCalledWithNew', false, 'calledWithNew'); - delegateToCalls('callArg', false, 'callArgWith', function() { - throw new Error( - this.toString() + ' cannot call arg since it was not yet invoked.' - ); - }); - spyApi.callArgWith = spyApi.callArg; - delegateToCalls('callArgOn', false, 'callArgOnWith', function() { - throw new Error( - this.toString() + ' cannot call arg since it was not yet invoked.' - ); - }); - spyApi.callArgOnWith = spyApi.callArgOn; - delegateToCalls('yield', false, 'yield', function() { - throw new Error( - this.toString() + ' cannot yield since it was not yet invoked.' - ); - }); - // "invokeCallback" is an alias for "yield" since "yield" is invalid in strict mode. - spyApi.invokeCallback = spyApi.yield; - delegateToCalls('yieldOn', false, 'yieldOn', function() { - throw new Error( - this.toString() + ' cannot yield since it was not yet invoked.' - ); - }); - delegateToCalls('yieldTo', false, 'yieldTo', function(property) { - throw new Error( - this.toString() + - " cannot yield to '" + - property + - "' since it was not yet invoked." - ); - }); - delegateToCalls('yieldToOn', false, 'yieldToOn', function(property) { - throw new Error( - this.toString() + - " cannot yield to '" + - property + - "' since it was not yet invoked." - ); - }); - - spyApi.formatters = { - c: function(spy) { - return sinon.timesInWords(spy.callCount); - }, - - n: function(spy) { - return spy.toString(); - }, - - C: function(spy) { - var calls = []; - - for (var i = 0, l = spy.callCount; i < l; ++i) { - var stringifiedCall = ' ' + spy.getCall(i).toString(); - if (/\n/.test(calls[i - 1])) { - stringifiedCall = '\n' + stringifiedCall; - } - push.call(calls, stringifiedCall); - } - - return calls.length > 0 ? '\n' + calls.join('\n') : ''; - }, - - t: function(spy) { - var objects = []; - - for (var i = 0, l = spy.callCount; i < l; ++i) { - push.call(objects, sinon.format(spy.thisValues[i])); - } - - return objects.join(', '); - }, - - '*': function(spy, args) { - var formatted = []; - - for (var i = 0, l = args.length; i < l; ++i) { - push.call(formatted, sinon.format(args[i])); - } - - return formatted.join(', '); - }, - }; - - sinon.extend(spy, spyApi); - - spy.spyCall = sinon.spyCall; - - if (commonJSModule) { - module.exports = spy; - } else { - sinon.spy = spy; - } - })((typeof sinon == 'object' && sinon) || null); - - /** - * @depend ../sinon.js - */ - /*jslint eqeqeq: false, onevar: false*/ - /*global module, require, sinon, process, setImmediate, setTimeout*/ - /** - * Stub behavior - * - * @author Christian Johansen (christian@cjohansen.no) - * @author Tim Fischbach (mail@timfischbach.de) - * @license BSD - * - * Copyright (c) 2010-2013 Christian Johansen - */ - - (function(sinon) { - var commonJSModule = typeof module !== 'undefined' && module.exports; - - if (!sinon && commonJSModule) { - sinon = require('../sinon'); - } - - if (!sinon) { - return; - } - - var slice = Array.prototype.slice; - var join = Array.prototype.join; - var proto; - - var nextTick = (function() { - if ( - typeof process === 'object' && - typeof process.nextTick === 'function' - ) { - return process.nextTick; - } else if (typeof setImmediate === 'function') { - return setImmediate; - } else { - return function(callback) { - setTimeout(callback, 0); - }; - } - })(); - - function throwsException(error, message) { - if (typeof error == 'string') { - this.exception = new Error(message || ''); - this.exception.name = error; - } else if (!error) { - this.exception = new Error('Error'); - } else { - this.exception = error; - } - - return this; - } - - function getCallback(behavior, args) { - var callArgAt = behavior.callArgAt; - - if (callArgAt < 0) { - var callArgProp = behavior.callArgProp; - - for (var i = 0, l = args.length; i < l; ++i) { - if (!callArgProp && typeof args[i] == 'function') { - return args[i]; - } - - if ( - callArgProp && - args[i] && - typeof args[i][callArgProp] == 'function' - ) { - return args[i][callArgProp]; - } - } - - return null; - } - - return args[callArgAt]; - } - - function getCallbackError(behavior, func, args) { - if (behavior.callArgAt < 0) { - var msg; - - if (behavior.callArgProp) { - msg = - sinon.functionName(behavior.stub) + - " expected to yield to '" + - behavior.callArgProp + - "', but no object with such a property was passed."; - } else { - msg = - sinon.functionName(behavior.stub) + - ' expected to yield, but no callback was passed.'; - } - - if (args.length > 0) { - msg += ' Received [' + join.call(args, ', ') + ']'; - } - - return msg; - } - - return ( - 'argument at index ' + - behavior.callArgAt + - ' is not a function: ' + - func - ); - } - - function callCallback(behavior, args) { - if (typeof behavior.callArgAt == 'number') { - var func = getCallback(behavior, args); - - if (typeof func != 'function') { - throw new TypeError(getCallbackError(behavior, func, args)); - } - - if (behavior.callbackAsync) { - nextTick(function() { - func.apply(behavior.callbackContext, behavior.callbackArguments); - }); - } else { - func.apply(behavior.callbackContext, behavior.callbackArguments); - } - } - } - - proto = { - create: function(stub) { - var behavior = sinon.extend({}, sinon.behavior); - delete behavior.create; - behavior.stub = stub; - - return behavior; - }, - - isPresent: function() { - return ( - typeof this.callArgAt == 'number' || - this.exception || - typeof this.returnArgAt == 'number' || - this.returnThis || - this.returnValueDefined - ); - }, - - invoke: function(context, args) { - callCallback(this, args); - - if (this.exception) { - throw this.exception; - } else if (typeof this.returnArgAt == 'number') { - return args[this.returnArgAt]; - } else if (this.returnThis) { - return context; - } - - return this.returnValue; - }, - - onCall: function(index) { - return this.stub.onCall(index); - }, - - onFirstCall: function() { - return this.stub.onFirstCall(); - }, - - onSecondCall: function() { - return this.stub.onSecondCall(); - }, - - onThirdCall: function() { - return this.stub.onThirdCall(); - }, - - withArgs: function(/* arguments */) { - throw new Error( - 'Defining a stub by invoking "stub.onCall(...).withArgs(...)" is not supported. ' + - 'Use "stub.withArgs(...).onCall(...)" to define sequential behavior for calls with certain arguments.' - ); - }, - - callsArg: function callsArg(pos) { - if (typeof pos != 'number') { - throw new TypeError('argument index is not number'); - } - - this.callArgAt = pos; - this.callbackArguments = []; - this.callbackContext = undefined; - this.callArgProp = undefined; - this.callbackAsync = false; - - return this; - }, - - callsArgOn: function callsArgOn(pos, context) { - if (typeof pos != 'number') { - throw new TypeError('argument index is not number'); - } - if (typeof context != 'object') { - throw new TypeError('argument context is not an object'); - } - - this.callArgAt = pos; - this.callbackArguments = []; - this.callbackContext = context; - this.callArgProp = undefined; - this.callbackAsync = false; - - return this; - }, - - callsArgWith: function callsArgWith(pos) { - if (typeof pos != 'number') { - throw new TypeError('argument index is not number'); - } - - this.callArgAt = pos; - this.callbackArguments = slice.call(arguments, 1); - this.callbackContext = undefined; - this.callArgProp = undefined; - this.callbackAsync = false; - - return this; - }, - - callsArgOnWith: function callsArgWith(pos, context) { - if (typeof pos != 'number') { - throw new TypeError('argument index is not number'); - } - if (typeof context != 'object') { - throw new TypeError('argument context is not an object'); - } - - this.callArgAt = pos; - this.callbackArguments = slice.call(arguments, 2); - this.callbackContext = context; - this.callArgProp = undefined; - this.callbackAsync = false; - - return this; - }, - - yields: function() { - this.callArgAt = -1; - this.callbackArguments = slice.call(arguments, 0); - this.callbackContext = undefined; - this.callArgProp = undefined; - this.callbackAsync = false; - - return this; - }, - - yieldsOn: function(context) { - if (typeof context != 'object') { - throw new TypeError('argument context is not an object'); - } - - this.callArgAt = -1; - this.callbackArguments = slice.call(arguments, 1); - this.callbackContext = context; - this.callArgProp = undefined; - this.callbackAsync = false; - - return this; - }, - - yieldsTo: function(prop) { - this.callArgAt = -1; - this.callbackArguments = slice.call(arguments, 1); - this.callbackContext = undefined; - this.callArgProp = prop; - this.callbackAsync = false; - - return this; - }, - - yieldsToOn: function(prop, context) { - if (typeof context != 'object') { - throw new TypeError('argument context is not an object'); - } - - this.callArgAt = -1; - this.callbackArguments = slice.call(arguments, 2); - this.callbackContext = context; - this.callArgProp = prop; - this.callbackAsync = false; - - return this; - }, - - throws: throwsException, - throwsException: throwsException, - - returns: function returns(value) { - this.returnValue = value; - this.returnValueDefined = true; - - return this; - }, - - returnsArg: function returnsArg(pos) { - if (typeof pos != 'number') { - throw new TypeError('argument index is not number'); - } - - this.returnArgAt = pos; - - return this; - }, - - returnsThis: function returnsThis() { - this.returnThis = true; - - return this; - }, - }; - - // create asynchronous versions of callsArg* and yields* methods - for (var method in proto) { - // need to avoid creating anotherasync versions of the newly added async methods - if ( - proto.hasOwnProperty(method) && - method.match(/^(callsArg|yields)/) && - !method.match(/Async/) - ) { - proto[method + 'Async'] = (function(syncFnName) { - return function() { - var result = this[syncFnName].apply(this, arguments); - this.callbackAsync = true; - return result; - }; - })(method); - } - } - - if (commonJSModule) { - module.exports = proto; - } else { - sinon.behavior = proto; - } - })((typeof sinon == 'object' && sinon) || null); - /** - * @depend ../sinon.js - * @depend spy.js - * @depend behavior.js - */ - /*jslint eqeqeq: false, onevar: false*/ - /*global module, require, sinon*/ - /** - * Stub functions - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2013 Christian Johansen - */ - - (function(sinon) { - var commonJSModule = typeof module !== 'undefined' && module.exports; - - if (!sinon && commonJSModule) { - sinon = require('../sinon'); - } - - if (!sinon) { - return; - } - - function stub(object, property, func) { - if (!!func && typeof func != 'function') { - throw new TypeError('Custom stub should be function'); - } - - var wrapper; - - if (func) { - wrapper = sinon.spy && sinon.spy.create ? sinon.spy.create(func) : func; - } else { - wrapper = stub.create(); - } - - if (!object && !property) { - return sinon.stub.create(); - } - - if (!property && !!object && typeof object == 'object') { - for (var prop in object) { - if (typeof object[prop] === 'function') { - stub(object, prop); - } - } - - return object; - } - - return sinon.wrapMethod(object, property, wrapper); - } - - function getDefaultBehavior(stub) { - return ( - stub.defaultBehavior || - getParentBehaviour(stub) || - sinon.behavior.create(stub) - ); - } - - function getParentBehaviour(stub) { - return stub.parent && getCurrentBehavior(stub.parent); - } - - function getCurrentBehavior(stub) { - var behavior = stub.behaviors[stub.callCount - 1]; - return behavior && behavior.isPresent() - ? behavior - : getDefaultBehavior(stub); - } - - var uuid = 0; - - sinon.extend( - stub, - (function() { - var slice = Array.prototype.slice, - proto; - - proto = { - create: function create() { - var functionStub = function() { - return getCurrentBehavior(functionStub).invoke(this, arguments); - }; - - functionStub.id = 'stub#' + uuid++; - var orig = functionStub; - functionStub = sinon.spy.create(functionStub); - functionStub.func = orig; - - sinon.extend(functionStub, stub); - functionStub._create = sinon.stub.create; - functionStub.displayName = 'stub'; - functionStub.toString = sinon.functionToString; - - functionStub.defaultBehavior = null; - functionStub.behaviors = []; - - return functionStub; - }, - - resetBehavior: function() { - var i; - - this.defaultBehavior = null; - this.behaviors = []; - - delete this.returnValue; - delete this.returnArgAt; - this.returnThis = false; - - if (this.fakes) { - for (i = 0; i < this.fakes.length; i++) { - this.fakes[i].resetBehavior(); - } - } - }, - - onCall: function(index) { - if (!this.behaviors[index]) { - this.behaviors[index] = sinon.behavior.create(this); - } - - return this.behaviors[index]; - }, - - onFirstCall: function() { - return this.onCall(0); - }, - - onSecondCall: function() { - return this.onCall(1); - }, - - onThirdCall: function() { - return this.onCall(2); - }, - }; - - for (var method in sinon.behavior) { - if ( - sinon.behavior.hasOwnProperty(method) && - !proto.hasOwnProperty(method) && - method != 'create' && - method != 'withArgs' && - method != 'invoke' - ) { - proto[method] = (function(behaviorMethod) { - return function() { - this.defaultBehavior = - this.defaultBehavior || sinon.behavior.create(this); - this.defaultBehavior[behaviorMethod].apply( - this.defaultBehavior, - arguments - ); - return this; - }; - })(method); - } - } - - return proto; - })() - ); - - if (commonJSModule) { - module.exports = stub; - } else { - sinon.stub = stub; - } - })((typeof sinon == 'object' && sinon) || null); - - /** - * @depend ../sinon.js - * @depend stub.js - */ - /*jslint eqeqeq: false, onevar: false, nomen: false*/ - /*global module, require, sinon*/ - /** - * Mock functions. - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2013 Christian Johansen - */ - - (function(sinon) { - var commonJSModule = typeof module !== 'undefined' && module.exports; - var push = [].push; - var match; - - if (!sinon && commonJSModule) { - sinon = require('../sinon'); - } - - if (!sinon) { - return; - } - - match = sinon.match; - - if (!match && commonJSModule) { - match = require('./match'); - } - - function mock(object) { - if (!object) { - return sinon.expectation.create('Anonymous mock'); - } - - return mock.create(object); - } - - sinon.mock = mock; - - sinon.extend( - mock, - (function() { - function each(collection, callback) { - if (!collection) { - return; - } - - for (var i = 0, l = collection.length; i < l; i += 1) { - callback(collection[i]); - } - } - - return { - create: function create(object) { - if (!object) { - throw new TypeError('object is null'); - } - - var mockObject = sinon.extend({}, mock); - mockObject.object = object; - delete mockObject.create; - - return mockObject; - }, - - expects: function expects(method) { - if (!method) { - throw new TypeError('method is falsy'); - } - - if (!this.expectations) { - this.expectations = {}; - this.proxies = []; - } - - if (!this.expectations[method]) { - this.expectations[method] = []; - var mockObject = this; - - sinon.wrapMethod(this.object, method, function() { - return mockObject.invokeMethod(method, this, arguments); - }); - - push.call(this.proxies, method); - } - - var expectation = sinon.expectation.create(method); - push.call(this.expectations[method], expectation); - - return expectation; - }, - - restore: function restore() { - var object = this.object; - - each(this.proxies, function(proxy) { - if (typeof object[proxy].restore == 'function') { - object[proxy].restore(); - } - }); - }, - - verify: function verify() { - var expectations = this.expectations || {}; - var messages = [], - met = []; - - each(this.proxies, function(proxy) { - each(expectations[proxy], function(expectation) { - if (!expectation.met()) { - push.call(messages, expectation.toString()); - } else { - push.call(met, expectation.toString()); - } - }); - }); - - this.restore(); - - if (messages.length > 0) { - sinon.expectation.fail(messages.concat(met).join('\n')); - } else { - sinon.expectation.pass(messages.concat(met).join('\n')); - } - - return true; - }, - - invokeMethod: function invokeMethod(method, thisValue, args) { - var expectations = this.expectations && this.expectations[method]; - var length = (expectations && expectations.length) || 0, - i; - - for (i = 0; i < length; i += 1) { - if ( - !expectations[i].met() && - expectations[i].allowsCall(thisValue, args) - ) { - return expectations[i].apply(thisValue, args); - } - } - - var messages = [], - available, - exhausted = 0; - - for (i = 0; i < length; i += 1) { - if (expectations[i].allowsCall(thisValue, args)) { - available = available || expectations[i]; - } else { - exhausted += 1; - } - push.call(messages, ' ' + expectations[i].toString()); - } - - if (exhausted === 0) { - return available.apply(thisValue, args); - } - - messages.unshift( - 'Unexpected call: ' + - sinon.spyCall.toString.call({ - proxy: method, - args: args, - }) - ); - - sinon.expectation.fail(messages.join('\n')); - }, - }; - })() - ); - - var times = sinon.timesInWords; - - sinon.expectation = (function() { - var slice = Array.prototype.slice; - var _invoke = sinon.spy.invoke; - - function callCountInWords(callCount) { - if (callCount == 0) { - return 'never called'; - } else { - return 'called ' + times(callCount); - } - } - - function expectedCallCountInWords(expectation) { - var min = expectation.minCalls; - var max = expectation.maxCalls; - - if (typeof min == 'number' && typeof max == 'number') { - var str = times(min); - - if (min != max) { - str = 'at least ' + str + ' and at most ' + times(max); - } - - return str; - } - - if (typeof min == 'number') { - return 'at least ' + times(min); - } - - return 'at most ' + times(max); - } - - function receivedMinCalls(expectation) { - var hasMinLimit = typeof expectation.minCalls == 'number'; - return !hasMinLimit || expectation.callCount >= expectation.minCalls; - } - - function receivedMaxCalls(expectation) { - if (typeof expectation.maxCalls != 'number') { - return false; - } - - return expectation.callCount == expectation.maxCalls; - } - - function verifyMatcher(possibleMatcher, arg) { - if (match && match.isMatcher(possibleMatcher)) { - return possibleMatcher.test(arg); - } else { - return true; - } - } - - return { - minCalls: 1, - maxCalls: 1, - - create: function create(methodName) { - var expectation = sinon.extend( - sinon.stub.create(), - sinon.expectation - ); - delete expectation.create; - expectation.method = methodName; - - return expectation; - }, - - invoke: function invoke(func, thisValue, args) { - this.verifyCallAllowed(thisValue, args); - - return _invoke.apply(this, arguments); - }, - - atLeast: function atLeast(num) { - if (typeof num != 'number') { - throw new TypeError("'" + num + "' is not number"); - } - - if (!this.limitsSet) { - this.maxCalls = null; - this.limitsSet = true; - } - - this.minCalls = num; - - return this; - }, - - atMost: function atMost(num) { - if (typeof num != 'number') { - throw new TypeError("'" + num + "' is not number"); - } - - if (!this.limitsSet) { - this.minCalls = null; - this.limitsSet = true; - } - - this.maxCalls = num; - - return this; - }, - - never: function never() { - return this.exactly(0); - }, - - once: function once() { - return this.exactly(1); - }, - - twice: function twice() { - return this.exactly(2); - }, - - thrice: function thrice() { - return this.exactly(3); - }, - - exactly: function exactly(num) { - if (typeof num != 'number') { - throw new TypeError("'" + num + "' is not a number"); - } - - this.atLeast(num); - return this.atMost(num); - }, - - met: function met() { - return !this.failed && receivedMinCalls(this); - }, - - verifyCallAllowed: function verifyCallAllowed(thisValue, args) { - if (receivedMaxCalls(this)) { - this.failed = true; - sinon.expectation.fail( - this.method + ' already called ' + times(this.maxCalls) - ); - } - - if ('expectedThis' in this && this.expectedThis !== thisValue) { - sinon.expectation.fail( - this.method + - ' called with ' + - thisValue + - ' as thisValue, expected ' + - this.expectedThis - ); - } - - if (!('expectedArguments' in this)) { - return; - } - - if (!args) { - sinon.expectation.fail( - this.method + - ' received no arguments, expected ' + - sinon.format(this.expectedArguments) - ); - } - - if (args.length < this.expectedArguments.length) { - sinon.expectation.fail( - this.method + - ' received too few arguments (' + - sinon.format(args) + - '), expected ' + - sinon.format(this.expectedArguments) - ); - } - - if ( - this.expectsExactArgCount && - args.length != this.expectedArguments.length - ) { - sinon.expectation.fail( - this.method + - ' received too many arguments (' + - sinon.format(args) + - '), expected ' + - sinon.format(this.expectedArguments) - ); - } - - for (var i = 0, l = this.expectedArguments.length; i < l; i += 1) { - if (!verifyMatcher(this.expectedArguments[i], args[i])) { - sinon.expectation.fail( - this.method + - ' received wrong arguments ' + - sinon.format(args) + - ", didn't match " + - this.expectedArguments.toString() - ); - } - - if (!sinon.deepEqual(this.expectedArguments[i], args[i])) { - sinon.expectation.fail( - this.method + - ' received wrong arguments ' + - sinon.format(args) + - ', expected ' + - sinon.format(this.expectedArguments) - ); - } - } - }, - - allowsCall: function allowsCall(thisValue, args) { - if (this.met() && receivedMaxCalls(this)) { - return false; - } - - if ('expectedThis' in this && this.expectedThis !== thisValue) { - return false; - } - - if (!('expectedArguments' in this)) { - return true; - } - - args = args || []; - - if (args.length < this.expectedArguments.length) { - return false; - } - - if ( - this.expectsExactArgCount && - args.length != this.expectedArguments.length - ) { - return false; - } - - for (var i = 0, l = this.expectedArguments.length; i < l; i += 1) { - if (!verifyMatcher(this.expectedArguments[i], args[i])) { - return false; - } - - if (!sinon.deepEqual(this.expectedArguments[i], args[i])) { - return false; - } - } - - return true; - }, - - withArgs: function withArgs() { - this.expectedArguments = slice.call(arguments); - return this; - }, - - withExactArgs: function withExactArgs() { - this.withArgs.apply(this, arguments); - this.expectsExactArgCount = true; - return this; - }, - - on: function on(thisValue) { - this.expectedThis = thisValue; - return this; - }, - - toString: function() { - var args = (this.expectedArguments || []).slice(); - - if (!this.expectsExactArgCount) { - push.call(args, '[...]'); - } - - var callStr = sinon.spyCall.toString.call({ - proxy: this.method || 'anonymous mock expectation', - args: args, - }); - - var message = - callStr.replace(', [...', '[, ...') + - ' ' + - expectedCallCountInWords(this); - - if (this.met()) { - return 'Expectation met: ' + message; - } - - return ( - 'Expected ' + - message + - ' (' + - callCountInWords(this.callCount) + - ')' - ); - }, - - verify: function verify() { - if (!this.met()) { - sinon.expectation.fail(this.toString()); - } else { - sinon.expectation.pass(this.toString()); - } - - return true; - }, - - pass: function(message) { - sinon.assert.pass(message); - }, - fail: function(message) { - var exception = new Error(message); - exception.name = 'ExpectationError'; - - throw exception; - }, - }; - })(); - - if (commonJSModule) { - module.exports = mock; - } else { - sinon.mock = mock; - } - })((typeof sinon == 'object' && sinon) || null); - - /** - * @depend ../sinon.js - * @depend stub.js - * @depend mock.js - */ - /*jslint eqeqeq: false, onevar: false, forin: true*/ - /*global module, require, sinon*/ - /** - * Collections of stubs, spies and mocks. - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2013 Christian Johansen - */ - - (function(sinon) { - var commonJSModule = typeof module !== 'undefined' && module.exports; - var push = [].push; - var hasOwnProperty = Object.prototype.hasOwnProperty; - - if (!sinon && commonJSModule) { - sinon = require('../sinon'); - } - - if (!sinon) { - return; - } - - function getFakes(fakeCollection) { - if (!fakeCollection.fakes) { - fakeCollection.fakes = []; - } - - return fakeCollection.fakes; - } - - function each(fakeCollection, method) { - var fakes = getFakes(fakeCollection); - - for (var i = 0, l = fakes.length; i < l; i += 1) { - if (typeof fakes[i][method] == 'function') { - fakes[i][method](); - } - } - } - - function compact(fakeCollection) { - var fakes = getFakes(fakeCollection); - var i = 0; - while (i < fakes.length) { - fakes.splice(i, 1); - } - } - - var collection = { - verify: function resolve() { - each(this, 'verify'); - }, - - restore: function restore() { - each(this, 'restore'); - compact(this); - }, - - verifyAndRestore: function verifyAndRestore() { - var exception; - - try { - this.verify(); - } catch (e) { - exception = e; - } - - this.restore(); - - if (exception) { - throw exception; - } - }, - - add: function add(fake) { - push.call(getFakes(this), fake); - return fake; - }, - - spy: function spy() { - return this.add(sinon.spy.apply(sinon, arguments)); - }, - - stub: function stub(object, property, value) { - if (property) { - var original = object[property]; - - if (typeof original != 'function') { - if (!hasOwnProperty.call(object, property)) { - throw new TypeError( - 'Cannot stub non-existent own property ' + property - ); - } - - object[property] = value; - - return this.add({ - restore: function() { - object[property] = original; - }, - }); - } - } - if (!property && !!object && typeof object == 'object') { - var stubbedObj = sinon.stub.apply(sinon, arguments); - - for (var prop in stubbedObj) { - if (typeof stubbedObj[prop] === 'function') { - this.add(stubbedObj[prop]); - } - } - - return stubbedObj; - } - - return this.add(sinon.stub.apply(sinon, arguments)); - }, - - mock: function mock() { - return this.add(sinon.mock.apply(sinon, arguments)); - }, - - inject: function inject(obj) { - var col = this; - - obj.spy = function() { - return col.spy.apply(col, arguments); - }; - - obj.stub = function() { - return col.stub.apply(col, arguments); - }; - - obj.mock = function() { - return col.mock.apply(col, arguments); - }; - - return obj; - }, - }; - - if (commonJSModule) { - module.exports = collection; - } else { - sinon.collection = collection; - } - })((typeof sinon == 'object' && sinon) || null); - - /*jslint eqeqeq: false, plusplus: false, evil: true, onevar: false, browser: true, forin: false*/ - /*global module, require, window*/ - /** - * Fake timer API - * setTimeout - * setInterval - * clearTimeout - * clearInterval - * tick - * reset - * Date - * - * Inspired by jsUnitMockTimeOut from JsUnit - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2013 Christian Johansen - */ - - if (typeof sinon == 'undefined') { - var sinon = {}; - } - - (function(global) { - var id = 1; - - function addTimer(args, recurring) { - if (args.length === 0) { - throw new Error('Function requires at least 1 parameter'); - } - - var toId = id++; - var delay = args[1] || 0; - - if (!this.timeouts) { - this.timeouts = {}; - } - - this.timeouts[toId] = { - id: toId, - func: args[0], - callAt: this.now + delay, - invokeArgs: Array.prototype.slice.call(args, 2), - }; - - if (recurring === true) { - this.timeouts[toId].interval = delay; - } - - return toId; - } - - function parseTime(str) { - if (!str) { - return 0; - } - - var strings = str.split(':'); - var l = strings.length, - i = l; - var ms = 0, - parsed; - - if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) { - throw new Error("tick only understands numbers and 'h:m:s'"); - } - - while (i--) { - parsed = parseInt(strings[i], 10); - - if (parsed >= 60) { - throw new Error('Invalid time ' + str); - } - - ms += parsed * Math.pow(60, l - i - 1); - } - - return ms * 1000; - } - - function createObject(object) { - var newObject; - - if (Object.create) { - newObject = Object.create(object); - } else { - var F = function() {}; - F.prototype = object; - newObject = new F(); - } - - newObject.Date.clock = newObject; - return newObject; - } - - sinon.clock = { - now: 0, - - create: function create(now) { - var clock = createObject(this); - - if (typeof now == 'number') { - clock.now = now; - } - - if (!!now && typeof now == 'object') { - throw new TypeError('now should be milliseconds since UNIX epoch'); - } - - return clock; - }, - - setTimeout: function setTimeout(callback, timeout) { - return addTimer.call(this, arguments, false); - }, - - clearTimeout: function clearTimeout(timerId) { - if (!this.timeouts) { - this.timeouts = []; - } - - if (timerId in this.timeouts) { - delete this.timeouts[timerId]; - } - }, - - setInterval: function setInterval(callback, timeout) { - return addTimer.call(this, arguments, true); - }, - - clearInterval: function clearInterval(timerId) { - this.clearTimeout(timerId); - }, - - tick: function tick(ms) { - ms = typeof ms == 'number' ? ms : parseTime(ms); - var tickFrom = this.now, - tickTo = this.now + ms, - previous = this.now; - var timer = this.firstTimerInRange(tickFrom, tickTo); - - var firstException; - while (timer && tickFrom <= tickTo) { - if (this.timeouts[timer.id]) { - tickFrom = this.now = timer.callAt; - try { - this.callTimer(timer); - } catch (e) { - firstException = firstException || e; - } - } - - timer = this.firstTimerInRange(previous, tickTo); - previous = tickFrom; - } - - this.now = tickTo; - - if (firstException) { - throw firstException; - } - - return this.now; - }, - - firstTimerInRange: function(from, to) { - var timer, - smallest = null, - originalTimer; - - for (var id in this.timeouts) { - if (this.timeouts.hasOwnProperty(id)) { - if ( - this.timeouts[id].callAt < from || - this.timeouts[id].callAt > to - ) { - continue; - } - - if (smallest === null || this.timeouts[id].callAt < smallest) { - originalTimer = this.timeouts[id]; - smallest = this.timeouts[id].callAt; - - timer = { - func: this.timeouts[id].func, - callAt: this.timeouts[id].callAt, - interval: this.timeouts[id].interval, - id: this.timeouts[id].id, - invokeArgs: this.timeouts[id].invokeArgs, - }; - } - } - } - - return timer || null; - }, - - callTimer: function(timer) { - if (typeof timer.interval == 'number') { - this.timeouts[timer.id].callAt += timer.interval; - } else { - delete this.timeouts[timer.id]; - } - - try { - if (typeof timer.func == 'function') { - timer.func.apply(null, timer.invokeArgs); - } else { - eval(timer.func); - } - } catch (e) { - var exception = e; - } - - if (!this.timeouts[timer.id]) { - if (exception) { - throw exception; - } - return; - } - - if (exception) { - throw exception; - } - }, - - reset: function reset() { - this.timeouts = {}; - }, - - Date: (function() { - var NativeDate = Date; - - function ClockDate(year, month, date, hour, minute, second, ms) { - // Defensive and verbose to avoid potential harm in passing - // explicit undefined when user does not pass argument - switch (arguments.length) { - case 0: - return new NativeDate(ClockDate.clock.now); - case 1: - return new NativeDate(year); - case 2: - return new NativeDate(year, month); - case 3: - return new NativeDate(year, month, date); - case 4: - return new NativeDate(year, month, date, hour); - case 5: - return new NativeDate(year, month, date, hour, minute); - case 6: - return new NativeDate(year, month, date, hour, minute, second); - default: - return new NativeDate( - year, - month, - date, - hour, - minute, - second, - ms - ); - } - } - - return mirrorDateProperties(ClockDate, NativeDate); - })(), - }; - - function mirrorDateProperties(target, source) { - if (source.now) { - target.now = function now() { - return target.clock.now; - }; - } else { - delete target.now; - } - - if (source.toSource) { - target.toSource = function toSource() { - return source.toSource(); - }; - } else { - delete target.toSource; - } - - target.toString = function toString() { - return source.toString(); - }; - - target.prototype = source.prototype; - target.parse = source.parse; - target.UTC = source.UTC; - target.prototype.toUTCString = source.prototype.toUTCString; - return target; - } - - var methods = [ - 'Date', - 'setTimeout', - 'setInterval', - 'clearTimeout', - 'clearInterval', - ]; - - function restore() { - var method; - - for (var i = 0, l = this.methods.length; i < l; i++) { - method = this.methods[i]; - if (global[method].hadOwnProperty) { - global[method] = this['_' + method]; - } else { - delete global[method]; - } - } - - // Prevent multiple executions which will completely remove these props - this.methods = []; - } - - function stubGlobal(method, clock) { - clock[method].hadOwnProperty = Object.prototype.hasOwnProperty.call( - global, - method - ); - clock['_' + method] = global[method]; - - if (method == 'Date') { - var date = mirrorDateProperties(clock[method], global[method]); - global[method] = date; - } else { - global[method] = function() { - return clock[method].apply(clock, arguments); - }; - - for (var prop in clock[method]) { - if (clock[method].hasOwnProperty(prop)) { - global[method][prop] = clock[method][prop]; - } - } - } - - global[method].clock = clock; - } - - sinon.useFakeTimers = function useFakeTimers(now) { - var clock = sinon.clock.create(now); - clock.restore = restore; - clock.methods = Array.prototype.slice.call( - arguments, - typeof now == 'number' ? 1 : 0 - ); - - if (clock.methods.length === 0) { - clock.methods = methods; - } - - for (var i = 0, l = clock.methods.length; i < l; i++) { - stubGlobal(clock.methods[i], clock); - } - - return clock; - }; - })( - typeof global != 'undefined' && typeof global !== 'function' ? global : this - ); - - sinon.timers = { - setTimeout: setTimeout, - clearTimeout: clearTimeout, - setInterval: setInterval, - clearInterval: clearInterval, - Date: Date, - }; - - if (typeof module !== 'undefined' && module.exports) { - module.exports = sinon; - } - - /*jslint eqeqeq: false, onevar: false*/ - /*global sinon, module, require, ActiveXObject, XMLHttpRequest, DOMParser*/ - /** - * Minimal Event interface implementation - * - * Original implementation by Sven Fuchs: https://gist.github.com/995028 - * Modifications and tests by Christian Johansen. - * - * @author Sven Fuchs (svenfuchs@artweb-design.de) - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2011 Sven Fuchs, Christian Johansen - */ - - if (typeof sinon == 'undefined') { - this.sinon = {}; - } - - (function() { - var push = [].push; - - sinon.Event = function Event(type, bubbles, cancelable, target) { - this.initEvent(type, bubbles, cancelable, target); - }; - - sinon.Event.prototype = { - initEvent: function(type, bubbles, cancelable, target) { - this.type = type; - this.bubbles = bubbles; - this.cancelable = cancelable; - this.target = target; - }, - - stopPropagation: function() {}, - - preventDefault: function() { - this.defaultPrevented = true; - }, - }; - - sinon.EventTarget = { - addEventListener: function addEventListener(event, listener, useCapture) { - this.eventListeners = this.eventListeners || {}; - this.eventListeners[event] = this.eventListeners[event] || []; - push.call(this.eventListeners[event], listener); - }, - - removeEventListener: function removeEventListener( - event, - listener, - useCapture - ) { - var listeners = - (this.eventListeners && this.eventListeners[event]) || []; - - for (var i = 0, l = listeners.length; i < l; ++i) { - if (listeners[i] == listener) { - return listeners.splice(i, 1); - } - } - }, - - dispatchEvent: function dispatchEvent(event) { - var type = event.type; - var listeners = - (this.eventListeners && this.eventListeners[type]) || []; - - for (var i = 0; i < listeners.length; i++) { - if (typeof listeners[i] == 'function') { - listeners[i].call(this, event); - } else { - listeners[i].handleEvent(event); - } - } - - return !!event.defaultPrevented; - }, - }; - })(); - - /** - * @depend ../../sinon.js - * @depend event.js - */ - /*jslint eqeqeq: false, onevar: false*/ - /*global sinon, module, require, ActiveXObject, XMLHttpRequest, DOMParser*/ - /** - * Fake XMLHttpRequest object - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2013 Christian Johansen - */ - - // wrapper for global - (function(global) { - if (typeof sinon === 'undefined') { - global.sinon = {}; - } - sinon.xhr = { XMLHttpRequest: global.XMLHttpRequest }; - - var xhr = sinon.xhr; - xhr.GlobalXMLHttpRequest = global.XMLHttpRequest; - xhr.GlobalActiveXObject = global.ActiveXObject; - xhr.supportsActiveX = typeof xhr.GlobalActiveXObject != 'undefined'; - xhr.supportsXHR = typeof xhr.GlobalXMLHttpRequest != 'undefined'; - xhr.workingXHR = xhr.supportsXHR - ? xhr.GlobalXMLHttpRequest - : xhr.supportsActiveX - ? function() { - return new xhr.GlobalActiveXObject('MSXML2.XMLHTTP.3.0'); - } - : false; - - /*jsl:ignore*/ - var unsafeHeaders = { - 'Accept-Charset': true, - 'Accept-Encoding': true, - Connection: true, - 'Content-Length': true, - Cookie: true, - Cookie2: true, - 'Content-Transfer-Encoding': true, - Date: true, - Expect: true, - Host: true, - 'Keep-Alive': true, - Referer: true, - TE: true, - Trailer: true, - 'Transfer-Encoding': true, - Upgrade: true, - 'User-Agent': true, - Via: true, - }; - /*jsl:end*/ - - function FakeXMLHttpRequest() { - this.readyState = FakeXMLHttpRequest.UNSENT; - this.requestHeaders = {}; - this.requestBody = null; - this.status = 0; - this.statusText = ''; - this.upload = new UploadProgress(); - - var xhr = this; - var events = ['loadstart', 'load', 'abort', 'loadend']; - - function addEventListener(eventName) { - xhr.addEventListener(eventName, function(event) { - var listener = xhr['on' + eventName]; - - if (listener && typeof listener == 'function') { - listener(event); - } - }); - } - - for (var i = events.length - 1; i >= 0; i--) { - addEventListener(events[i]); - } - - if (typeof FakeXMLHttpRequest.onCreate == 'function') { - FakeXMLHttpRequest.onCreate(this); - } - } - - // An upload object is created for each - // FakeXMLHttpRequest and allows upload - // events to be simulated using uploadProgress - // and uploadError. - function UploadProgress() { - this.eventListeners = { - progress: [], - load: [], - abort: [], - error: [], - }; - } - - UploadProgress.prototype.addEventListener = function( - listenerName, - callback - ) { - this.eventListeners[listenerName].push(callback); - }; - - UploadProgress.prototype.dispatchEvent = function(event) { - var listeners = this.eventListeners[event.type] || []; - - for (var i = 0, listener; (listener = listeners[i]) != null; i++) { - listener(event); - } - }; - - function verifyState(xhr) { - if (xhr.readyState !== FakeXMLHttpRequest.OPENED) { - throw new Error('INVALID_STATE_ERR'); - } - - if (xhr.sendFlag) { - throw new Error('INVALID_STATE_ERR'); - } - } - - // filtering to enable a white-list version of Sinon FakeXhr, - // where whitelisted requests are passed through to real XHR - function each(collection, callback) { - if (!collection) return; - for (var i = 0, l = collection.length; i < l; i += 1) { - callback(collection[i]); - } - } - function some(collection, callback) { - for (var index = 0; index < collection.length; index++) { - if (callback(collection[index]) === true) return true; - } - return false; - } - // largest arity in XHR is 5 - XHR#open - var apply = function(obj, method, args) { - switch (args.length) { - case 0: - return obj[method](); - case 1: - return obj[method](args[0]); - case 2: - return obj[method](args[0], args[1]); - case 3: - return obj[method](args[0], args[1], args[2]); - case 4: - return obj[method](args[0], args[1], args[2], args[3]); - case 5: - return obj[method](args[0], args[1], args[2], args[3], args[4]); - } - }; - - FakeXMLHttpRequest.filters = []; - FakeXMLHttpRequest.addFilter = function(fn) { - this.filters.push(fn); - }; - var IE6Re = /MSIE 6/; - FakeXMLHttpRequest.defake = function(fakeXhr, xhrArgs) { - var xhr = new sinon.xhr.workingXHR(); - each( - [ - 'open', - 'setRequestHeader', - 'send', - 'abort', - 'getResponseHeader', - 'getAllResponseHeaders', - 'addEventListener', - 'overrideMimeType', - 'removeEventListener', - ], - function(method) { - fakeXhr[method] = function() { - return apply(xhr, method, arguments); - }; - } - ); - - var copyAttrs = function(args) { - each(args, function(attr) { - try { - fakeXhr[attr] = xhr[attr]; - } catch (e) { - if (!IE6Re.test(navigator.userAgent)) throw e; - } - }); - }; - - var stateChange = function() { - fakeXhr.readyState = xhr.readyState; - if (xhr.readyState >= FakeXMLHttpRequest.HEADERS_RECEIVED) { - copyAttrs(['status', 'statusText']); - } - if (xhr.readyState >= FakeXMLHttpRequest.LOADING) { - copyAttrs(['responseText']); - } - if (xhr.readyState === FakeXMLHttpRequest.DONE) { - copyAttrs(['responseXML']); - } - if (fakeXhr.onreadystatechange) - fakeXhr.onreadystatechange.call(fakeXhr); - }; - if (xhr.addEventListener) { - for (var event in fakeXhr.eventListeners) { - if (fakeXhr.eventListeners.hasOwnProperty(event)) { - each(fakeXhr.eventListeners[event], function(handler) { - xhr.addEventListener(event, handler); - }); - } - } - xhr.addEventListener('readystatechange', stateChange); - } else { - xhr.onreadystatechange = stateChange; - } - apply(xhr, 'open', xhrArgs); - }; - FakeXMLHttpRequest.useFilters = false; - - function verifyRequestSent(xhr) { - if (xhr.readyState == FakeXMLHttpRequest.DONE) { - throw new Error('Request done'); - } - } - - function verifyHeadersReceived(xhr) { - if (xhr.async && xhr.readyState != FakeXMLHttpRequest.HEADERS_RECEIVED) { - throw new Error('No headers received'); - } - } - - function verifyResponseBodyType(body) { - if (typeof body != 'string') { - var error = new Error( - 'Attempted to respond to fake XMLHttpRequest with ' + - body + - ', which is not a string.' - ); - error.name = 'InvalidBodyException'; - throw error; - } - } - - sinon.extend(FakeXMLHttpRequest.prototype, sinon.EventTarget, { - async: true, - - open: function open(method, url, async, username, password) { - this.method = method; - this.url = url; - this.async = typeof async == 'boolean' ? async : true; - this.username = username; - this.password = password; - this.responseText = null; - this.responseXML = null; - this.requestHeaders = {}; - this.sendFlag = false; - if (sinon.FakeXMLHttpRequest.useFilters === true) { - var xhrArgs = arguments; - var defake = some(FakeXMLHttpRequest.filters, function(filter) { - return filter.apply(this, xhrArgs); - }); - if (defake) { - return sinon.FakeXMLHttpRequest.defake(this, arguments); - } - } - this.readyStateChange(FakeXMLHttpRequest.OPENED); - }, - - readyStateChange: function readyStateChange(state) { - this.readyState = state; - - if (typeof this.onreadystatechange == 'function') { - try { - this.onreadystatechange(); - } catch (e) { - sinon.logError('Fake XHR onreadystatechange handler', e); - } - } - - this.dispatchEvent(new sinon.Event('readystatechange')); - - switch (this.readyState) { - case FakeXMLHttpRequest.DONE: - this.dispatchEvent(new sinon.Event('load', false, false, this)); - this.dispatchEvent(new sinon.Event('loadend', false, false, this)); - this.upload.dispatchEvent( - new sinon.Event('load', false, false, this) - ); - // TODO: PR this, this does not work in node.js or IE. - /* - if (typeof ProgressEvent !== 'undefined') { - this.upload.dispatchEvent(new ProgressEvent("progress", {loaded: 100, total: 100})); - } - */ - break; - } - }, - - setRequestHeader: function setRequestHeader(header, value) { - verifyState(this); - - if (unsafeHeaders[header] || /^(Sec-|Proxy-)/.test(header)) { - throw new Error('Refused to set unsafe header "' + header + '"'); - } - - if (this.requestHeaders[header]) { - this.requestHeaders[header] += ',' + value; - } else { - this.requestHeaders[header] = value; - } - }, - - // Helps testing - setResponseHeaders: function setResponseHeaders(headers) { - this.responseHeaders = {}; - - for (var header in headers) { - if (headers.hasOwnProperty(header)) { - this.responseHeaders[header] = headers[header]; - } - } - - if (this.async) { - this.readyStateChange(FakeXMLHttpRequest.HEADERS_RECEIVED); - } else { - this.readyState = FakeXMLHttpRequest.HEADERS_RECEIVED; - } - }, - - // Currently treats ALL data as a DOMString (i.e. no Document) - send: function send(data) { - verifyState(this); - - if (!/^(get|head)$/i.test(this.method)) { - if (this.requestHeaders['Content-Type']) { - var value = this.requestHeaders['Content-Type'].split(';'); - this.requestHeaders['Content-Type'] = value[0] + ';charset=utf-8'; - } else { - this.requestHeaders['Content-Type'] = 'text/plain;charset=utf-8'; - } - - this.requestBody = data; - } - - this.errorFlag = false; - this.sendFlag = this.async; - this.readyStateChange(FakeXMLHttpRequest.OPENED); - - if (typeof this.onSend == 'function') { - this.onSend(this); - } - - this.dispatchEvent(new sinon.Event('loadstart', false, false, this)); - }, - - abort: function abort() { - this.aborted = true; - this.responseText = null; - this.errorFlag = true; - this.requestHeaders = {}; - - if ( - this.readyState > sinon.FakeXMLHttpRequest.UNSENT && - this.sendFlag - ) { - this.readyStateChange(sinon.FakeXMLHttpRequest.DONE); - this.sendFlag = false; - } - - this.readyState = sinon.FakeXMLHttpRequest.UNSENT; - - this.dispatchEvent(new sinon.Event('abort', false, false, this)); - - this.upload.dispatchEvent(new sinon.Event('abort', false, false, this)); - - if (typeof this.onerror === 'function') { - this.onerror(); - } - }, - - getResponseHeader: function getResponseHeader(header) { - if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) { - return null; - } - - if (/^Set-Cookie2?$/i.test(header)) { - return null; - } - - header = header.toLowerCase(); - - for (var h in this.responseHeaders) { - if (h.toLowerCase() == header) { - return this.responseHeaders[h]; - } - } - - return null; - }, - - getAllResponseHeaders: function getAllResponseHeaders() { - if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) { - return ''; - } - - var headers = ''; - - for (var header in this.responseHeaders) { - if ( - this.responseHeaders.hasOwnProperty(header) && - !/^Set-Cookie2?$/i.test(header) - ) { - headers += header + ': ' + this.responseHeaders[header] + '\r\n'; - } - } - - return headers; - }, - - setResponseBody: function setResponseBody(body) { - verifyRequestSent(this); - verifyHeadersReceived(this); - verifyResponseBodyType(body); - - var chunkSize = this.chunkSize || 10; - var index = 0; - this.responseText = ''; - - do { - if (this.async) { - this.readyStateChange(FakeXMLHttpRequest.LOADING); - } - - this.responseText += body.substring(index, index + chunkSize); - index += chunkSize; - } while (index < body.length); - - var type = this.getResponseHeader('Content-Type'); - - if ( - this.responseText && - (!type || /(text\/xml)|(application\/xml)|(\+xml)/.test(type)) - ) { - try { - this.responseXML = FakeXMLHttpRequest.parseXML(this.responseText); - } catch (e) { - // Unable to parse XML - no biggie - } - } - - if (this.async) { - this.readyStateChange(FakeXMLHttpRequest.DONE); - } else { - this.readyState = FakeXMLHttpRequest.DONE; - } - }, - - respond: function respond(status, headers, body) { - this.setResponseHeaders(headers || {}); - this.status = typeof status == 'number' ? status : 200; - this.statusText = FakeXMLHttpRequest.statusCodes[this.status]; - this.setResponseBody(body || ''); - }, - - uploadProgress: function uploadProgress(progressEventRaw) { - this.upload.dispatchEvent( - new ProgressEvent('progress', progressEventRaw) - ); - }, - - uploadError: function uploadError(error) { - this.upload.dispatchEvent(new CustomEvent('error', { detail: error })); - }, - }); - - sinon.extend(FakeXMLHttpRequest, { - UNSENT: 0, - OPENED: 1, - HEADERS_RECEIVED: 2, - LOADING: 3, - DONE: 4, - }); - - // Borrowed from JSpec - FakeXMLHttpRequest.parseXML = function parseXML(text) { - var xmlDoc; - - if (typeof DOMParser != 'undefined') { - var parser = new DOMParser(); - xmlDoc = parser.parseFromString(text, 'text/xml'); - } else { - xmlDoc = new ActiveXObject('Microsoft.XMLDOM'); - xmlDoc.async = 'false'; - xmlDoc.loadXML(text); - } - - return xmlDoc; - }; - - FakeXMLHttpRequest.statusCodes = { - 100: 'Continue', - 101: 'Switching Protocols', - 200: 'OK', - 201: 'Created', - 202: 'Accepted', - 203: 'Non-Authoritative Information', - 204: 'No Content', - 205: 'Reset Content', - 206: 'Partial Content', - 300: 'Multiple Choice', - 301: 'Moved Permanently', - 302: 'Found', - 303: 'See Other', - 304: 'Not Modified', - 305: 'Use Proxy', - 307: 'Temporary Redirect', - 400: 'Bad Request', - 401: 'Unauthorized', - 402: 'Payment Required', - 403: 'Forbidden', - 404: 'Not Found', - 405: 'Method Not Allowed', - 406: 'Not Acceptable', - 407: 'Proxy Authentication Required', - 408: 'Request Timeout', - 409: 'Conflict', - 410: 'Gone', - 411: 'Length Required', - 412: 'Precondition Failed', - 413: 'Request Entity Too Large', - 414: 'Request-URI Too Long', - 415: 'Unsupported Media Type', - 416: 'Requested Range Not Satisfiable', - 417: 'Expectation Failed', - 422: 'Unprocessable Entity', - 500: 'Internal Server Error', - 501: 'Not Implemented', - 502: 'Bad Gateway', - 503: 'Service Unavailable', - 504: 'Gateway Timeout', - 505: 'HTTP Version Not Supported', - }; - - sinon.useFakeXMLHttpRequest = function() { - sinon.FakeXMLHttpRequest.restore = function restore(keepOnCreate) { - if (xhr.supportsXHR) { - global.XMLHttpRequest = xhr.GlobalXMLHttpRequest; - } - - if (xhr.supportsActiveX) { - global.ActiveXObject = xhr.GlobalActiveXObject; - } - - delete sinon.FakeXMLHttpRequest.restore; - - if (keepOnCreate !== true) { - delete sinon.FakeXMLHttpRequest.onCreate; - } - }; - // TODO: this is bad below, breaks things - /* - if (xhr.supportsXHR) { - global.XMLHttpRequest = sinon.FakeXMLHttpRequest; - } - */ - if (xhr.supportsActiveX) { - global.ActiveXObject = function ActiveXObject(objId) { - if (objId == 'Microsoft.XMLHTTP' || /^Msxml2\.XMLHTTP/i.test(objId)) { - return new sinon.FakeXMLHttpRequest(); - } - - return new xhr.GlobalActiveXObject(objId); - }; - } - - return sinon.FakeXMLHttpRequest; - }; - - sinon.FakeXMLHttpRequest = FakeXMLHttpRequest; - })(typeof global === 'object' ? global : this); - - if (typeof module !== 'undefined' && module.exports) { - module.exports = sinon; - } - - /** - * @depend fake_xml_http_request.js - */ - /*jslint eqeqeq: false, onevar: false, regexp: false, plusplus: false*/ - /*global module, require, window*/ - /** - * The Sinon "server" mimics a web server that receives requests from - * sinon.FakeXMLHttpRequest and provides an API to respond to those requests, - * both synchronously and asynchronously. To respond synchronuously, canned - * answers have to be provided upfront. - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2013 Christian Johansen - */ - - if (typeof sinon == 'undefined') { - var sinon = {}; - } - - sinon.fakeServer = (function() { - var push = [].push; - function F() {} - - function create(proto) { - F.prototype = proto; - return new F(); - } - - function responseArray(handler) { - var response = handler; - - if (Object.prototype.toString.call(handler) != '[object Array]') { - response = [200, {}, handler]; - } - - if (typeof response[2] != 'string') { - throw new TypeError( - 'Fake server response body should be string, but was ' + - typeof response[2] - ); - } - - return response; - } - - var wloc = typeof window !== 'undefined' ? window.location : {}; - var rCurrLoc = new RegExp('^' + wloc.protocol + '//' + wloc.host); - - function matchOne(response, reqMethod, reqUrl) { - var rmeth = response.method; - var matchMethod = - !rmeth || rmeth.toLowerCase() == reqMethod.toLowerCase(); - var url = response.url; - var matchUrl = - !url || - url == reqUrl || - (typeof url.test == 'function' && url.test(reqUrl)); - - return matchMethod && matchUrl; - } - - function match(response, request) { - var requestMethod = this.getHTTPMethod(request); - var requestUrl = request.url; - - if (!/^https?:\/\//.test(requestUrl) || rCurrLoc.test(requestUrl)) { - requestUrl = requestUrl.replace(rCurrLoc, ''); - } - - if (matchOne(response, this.getHTTPMethod(request), requestUrl)) { - if (typeof response.response == 'function') { - var ru = response.url; - var args = [request].concat( - ru && typeof ru.exec == 'function' - ? ru.exec(requestUrl).slice(1) - : [] - ); - return response.response.apply(response, args); - } - - return true; - } - - return false; - } - - function log(response, request) { - var str; - - str = 'Request:\n' + sinon.format(request) + '\n\n'; - str += 'Response:\n' + sinon.format(response) + '\n\n'; - - sinon.log(str); - } - - return { - create: function() { - var server = create(this); - this.xhr = sinon.useFakeXMLHttpRequest(); - server.requests = []; - - this.xhr.onCreate = function(xhrObj) { - server.addRequest(xhrObj); - }; - - return server; - }, - - addRequest: function addRequest(xhrObj) { - var server = this; - push.call(this.requests, xhrObj); - - xhrObj.onSend = function() { - server.handleRequest(this); - - if (server.autoRespond && !server.responding) { - setTimeout(function() { - server.responding = false; - server.respond(); - }, server.autoRespondAfter || 10); - - server.responding = true; - } - }; - }, - - getHTTPMethod: function getHTTPMethod(request) { - if (this.fakeHTTPMethods && /post/i.test(request.method)) { - var matches = (request.requestBody || '').match(/_method=([^\b;]+)/); - return !!matches ? matches[1] : request.method; - } - - return request.method; - }, - - handleRequest: function handleRequest(xhr) { - if (xhr.async) { - if (!this.queue) { - this.queue = []; - } - - push.call(this.queue, xhr); - } else { - this.processRequest(xhr); - } - }, - - respondWith: function respondWith(method, url, body) { - if (arguments.length == 1 && typeof method != 'function') { - this.response = responseArray(method); - return; - } - - if (!this.responses) { - this.responses = []; - } - - if (arguments.length == 1) { - body = method; - url = method = null; - } - - if (arguments.length == 2) { - body = url; - url = method; - method = null; - } - - push.call(this.responses, { - method: method, - url: url, - response: typeof body == 'function' ? body : responseArray(body), - }); - }, - - respond: function respond() { - if (arguments.length > 0) this.respondWith.apply(this, arguments); - var queue = this.queue || []; - var request; - - while ((request = queue.shift())) { - this.processRequest(request); - } - }, - - processRequest: function processRequest(request) { - try { - if (request.aborted) { - return; - } - - var response = this.response || [404, {}, '']; - - if (this.responses) { - for (var i = 0, l = this.responses.length; i < l; i++) { - if (match.call(this, this.responses[i], request)) { - response = this.responses[i].response; - break; - } - } - } - - if (request.readyState != 4) { - log(response, request); - - request.respond(response[0], response[1], response[2]); - } - } catch (e) { - sinon.logError('Fake server request processing', e); - } - }, - - restore: function restore() { - return this.xhr.restore && this.xhr.restore.apply(this.xhr, arguments); - }, - }; - })(); - - if (typeof module !== 'undefined' && module.exports) { - module.exports = sinon; - } - - /** - * @depend fake_server.js - * @depend fake_timers.js - */ - /*jslint browser: true, eqeqeq: false, onevar: false*/ - /*global sinon*/ - /** - * Add-on for sinon.fakeServer that automatically handles a fake timer along with - * the FakeXMLHttpRequest. The direct inspiration for this add-on is jQuery - * 1.3.x, which does not use xhr object's onreadystatehandler at all - instead, - * it polls the object for completion with setInterval. Dispite the direct - * motivation, there is nothing jQuery-specific in this file, so it can be used - * in any environment where the ajax implementation depends on setInterval or - * setTimeout. - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2013 Christian Johansen - */ - - (function() { - function Server() {} - Server.prototype = sinon.fakeServer; - - sinon.fakeServerWithClock = new Server(); - - sinon.fakeServerWithClock.addRequest = function addRequest(xhr) { - if (xhr.async) { - if (typeof setTimeout.clock == 'object') { - this.clock = setTimeout.clock; - } else { - this.clock = sinon.useFakeTimers(); - this.resetClock = true; - } - - if (!this.longestTimeout) { - var clockSetTimeout = this.clock.setTimeout; - var clockSetInterval = this.clock.setInterval; - var server = this; - - this.clock.setTimeout = function(fn, timeout) { - server.longestTimeout = Math.max( - timeout, - server.longestTimeout || 0 - ); - - return clockSetTimeout.apply(this, arguments); - }; - - this.clock.setInterval = function(fn, timeout) { - server.longestTimeout = Math.max( - timeout, - server.longestTimeout || 0 - ); - - return clockSetInterval.apply(this, arguments); - }; - } - } - - return sinon.fakeServer.addRequest.call(this, xhr); - }; - - sinon.fakeServerWithClock.respond = function respond() { - var returnVal = sinon.fakeServer.respond.apply(this, arguments); - - if (this.clock) { - this.clock.tick(this.longestTimeout || 0); - this.longestTimeout = 0; - - if (this.resetClock) { - this.clock.restore(); - this.resetClock = false; - } - } - - return returnVal; - }; - - sinon.fakeServerWithClock.restore = function restore() { - if (this.clock) { - this.clock.restore(); - } - - return sinon.fakeServer.restore.apply(this, arguments); - }; - })(); - - /** - * @depend ../sinon.js - * @depend collection.js - * @depend util/fake_timers.js - * @depend util/fake_server_with_clock.js - */ - /*jslint eqeqeq: false, onevar: false, plusplus: false*/ - /*global require, module*/ - /** - * Manages fake collections as well as fake utilities such as Sinon's - * timers and fake XHR implementation in one convenient object. - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2013 Christian Johansen - */ - - if (typeof module !== 'undefined' && module.exports) { - var sinon = require('../sinon'); - sinon.extend(sinon, require('./util/fake_timers')); - } - - (function() { - var push = [].push; - - function exposeValue(sandbox, config, key, value) { - if (!value) { - return; - } - - if (config.injectInto) { - config.injectInto[key] = value; - } else { - push.call(sandbox.args, value); - } - } - - function prepareSandboxFromConfig(config) { - var sandbox = sinon.create(sinon.sandbox); - - if (config.useFakeServer) { - if (typeof config.useFakeServer == 'object') { - sandbox.serverPrototype = config.useFakeServer; - } - - sandbox.useFakeServer(); - } - - if (config.useFakeTimers) { - if (typeof config.useFakeTimers == 'object') { - sandbox.useFakeTimers.apply(sandbox, config.useFakeTimers); - } else { - sandbox.useFakeTimers(); - } - } - - return sandbox; - } - - sinon.sandbox = sinon.extend(sinon.create(sinon.collection), { - useFakeTimers: function useFakeTimers() { - this.clock = sinon.useFakeTimers.apply(sinon, arguments); - - return this.add(this.clock); - }, - - serverPrototype: sinon.fakeServer, - - useFakeServer: function useFakeServer() { - var proto = this.serverPrototype || sinon.fakeServer; - - if (!proto || !proto.create) { - return null; - } - - this.server = proto.create(); - return this.add(this.server); - }, - - inject: function(obj) { - sinon.collection.inject.call(this, obj); - - if (this.clock) { - obj.clock = this.clock; - } - - if (this.server) { - obj.server = this.server; - obj.requests = this.server.requests; - } - - return obj; - }, - - create: function(config) { - if (!config) { - return sinon.create(sinon.sandbox); - } - - var sandbox = prepareSandboxFromConfig(config); - sandbox.args = sandbox.args || []; - var prop, - value, - exposed = sandbox.inject({}); - - if (config.properties) { - for (var i = 0, l = config.properties.length; i < l; i++) { - prop = config.properties[i]; - value = exposed[prop] || (prop == 'sandbox' && sandbox); - exposeValue(sandbox, config, prop, value); - } - } else { - exposeValue(sandbox, config, 'sandbox', value); - } - - return sandbox; - }, - }); - - sinon.sandbox.useFakeXMLHttpRequest = sinon.sandbox.useFakeServer; - - if (typeof module !== 'undefined' && module.exports) { - module.exports = sinon.sandbox; - } - })(); - - /** - * @depend ../sinon.js - * @depend stub.js - * @depend mock.js - * @depend sandbox.js - */ - /*jslint eqeqeq: false, onevar: false, forin: true, plusplus: false*/ - /*global module, require, sinon*/ - /** - * Test function, sandboxes fakes - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2013 Christian Johansen - */ - - (function(sinon) { - var commonJSModule = typeof module !== 'undefined' && module.exports; - - if (!sinon && commonJSModule) { - sinon = require('../sinon'); - } - - if (!sinon) { - return; - } - - function test(callback) { - var type = typeof callback; - - if (type != 'function') { - throw new TypeError( - 'sinon.test needs to wrap a test function, got ' + type - ); - } - - return function() { - var config = sinon.getConfig(sinon.config); - config.injectInto = - (config.injectIntoThis && this) || config.injectInto; - var sandbox = sinon.sandbox.create(config); - var exception, result; - var args = Array.prototype.slice.call(arguments).concat(sandbox.args); - - try { - result = callback.apply(this, args); - } catch (e) { - exception = e; - } - - if (typeof exception !== 'undefined') { - sandbox.restore(); - throw exception; - } else { - sandbox.verifyAndRestore(); - } - - return result; - }; - } - - test.config = { - injectIntoThis: true, - injectInto: null, - properties: ['spy', 'stub', 'mock', 'clock', 'server', 'requests'], - useFakeTimers: true, - useFakeServer: true, - }; - - if (commonJSModule) { - module.exports = test; - } else { - sinon.test = test; - } - })((typeof sinon == 'object' && sinon) || null); - - /** - * @depend ../sinon.js - * @depend test.js - */ - /*jslint eqeqeq: false, onevar: false, eqeqeq: false*/ - /*global module, require, sinon*/ - /** - * Test case, sandboxes all test functions - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2013 Christian Johansen - */ - - (function(sinon) { - var commonJSModule = typeof module !== 'undefined' && module.exports; - - if (!sinon && commonJSModule) { - sinon = require('../sinon'); - } - - if (!sinon || !Object.prototype.hasOwnProperty) { - return; - } - - function createTest(property, setUp, tearDown) { - return function() { - if (setUp) { - setUp.apply(this, arguments); - } - - var exception, result; - - try { - result = property.apply(this, arguments); - } catch (e) { - exception = e; - } - - if (tearDown) { - tearDown.apply(this, arguments); - } - - if (exception) { - throw exception; - } - - return result; - }; - } - - function testCase(tests, prefix) { - /*jsl:ignore*/ - if (!tests || typeof tests != 'object') { - throw new TypeError( - 'sinon.testCase needs an object with test functions' - ); - } - /*jsl:end*/ - - prefix = prefix || 'test'; - var rPrefix = new RegExp('^' + prefix); - var methods = {}, - testName, - property, - method; - var setUp = tests.setUp; - var tearDown = tests.tearDown; - - for (testName in tests) { - if (tests.hasOwnProperty(testName)) { - property = tests[testName]; - - if (/^(setUp|tearDown)$/.test(testName)) { - continue; - } - - if (typeof property == 'function' && rPrefix.test(testName)) { - method = property; - - if (setUp || tearDown) { - method = createTest(property, setUp, tearDown); - } - - methods[testName] = sinon.test(method); - } else { - methods[testName] = tests[testName]; - } - } - } - - return methods; - } - - if (commonJSModule) { - module.exports = testCase; - } else { - sinon.testCase = testCase; - } - })((typeof sinon == 'object' && sinon) || null); - - /** - * @depend ../sinon.js - * @depend stub.js - */ - /*jslint eqeqeq: false, onevar: false, nomen: false, plusplus: false*/ - /*global module, require, sinon*/ - /** - * Assertions matching the test spy retrieval interface. - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2013 Christian Johansen - */ - - (function(sinon, global) { - var commonJSModule = typeof module !== 'undefined' && module.exports; - var slice = Array.prototype.slice; - var assert; - - if (!sinon && commonJSModule) { - sinon = require('../sinon'); - } - - if (!sinon) { - return; - } - - function verifyIsStub() { - var method; - - for (var i = 0, l = arguments.length; i < l; ++i) { - method = arguments[i]; - - if (!method) { - assert.fail('fake is not a spy'); - } - - if (typeof method != 'function') { - assert.fail(method + ' is not a function'); - } - - if (typeof method.getCall != 'function') { - assert.fail(method + ' is not stubbed'); - } - } - } - - function failAssertion(object, msg) { - object = object || global; - var failMethod = object.fail || assert.fail; - failMethod.call(object, msg); - } - - function mirrorPropAsAssertion(name, method, message) { - if (arguments.length == 2) { - message = method; - method = name; - } - - assert[name] = function(fake) { - verifyIsStub(fake); - - var args = slice.call(arguments, 1); - var failed = false; - - if (typeof method == 'function') { - failed = !method(fake); - } else { - failed = - typeof fake[method] == 'function' - ? !fake[method].apply(fake, args) - : !fake[method]; - } - - if (failed) { - failAssertion(this, fake.printf.apply(fake, [message].concat(args))); - } else { - assert.pass(name); - } - }; - } - - function exposedName(prefix, prop) { - return !prefix || /^fail/.test(prop) - ? prop - : prefix + prop.slice(0, 1).toUpperCase() + prop.slice(1); - } - - assert = { - failException: 'AssertError', - - fail: function fail(message) { - var error = new Error(message); - error.name = this.failException || assert.failException; - - throw error; - }, - - pass: function pass(assertion) {}, - - callOrder: function assertCallOrder() { - verifyIsStub.apply(null, arguments); - var expected = '', - actual = ''; - - if (!sinon.calledInOrder(arguments)) { - try { - expected = [].join.call(arguments, ', '); - var calls = slice.call(arguments); - var i = calls.length; - while (i) { - if (!calls[--i].called) { - calls.splice(i, 1); - } - } - actual = sinon.orderByFirstCall(calls).join(', '); - } catch (e) { - // If this fails, we'll just fall back to the blank string - } - - failAssertion( - this, - 'expected ' + - expected + - ' to be ' + - 'called in order but were called as ' + - actual - ); - } else { - assert.pass('callOrder'); - } - }, - - callCount: function assertCallCount(method, count) { - verifyIsStub(method); - - if (method.callCount != count) { - var msg = - 'expected %n to be called ' + - sinon.timesInWords(count) + - ' but was called %c%C'; - failAssertion(this, method.printf(msg)); - } else { - assert.pass('callCount'); - } - }, - - expose: function expose(target, options) { - if (!target) { - throw new TypeError('target is null or undefined'); - } - - var o = options || {}; - var prefix = (typeof o.prefix == 'undefined' && 'assert') || o.prefix; - var includeFail = - typeof o.includeFail == 'undefined' || !!o.includeFail; - - for (var method in this) { - if (method != 'export' && (includeFail || !/^(fail)/.test(method))) { - target[exposedName(prefix, method)] = this[method]; - } - } - - return target; - }, - }; - - mirrorPropAsAssertion( - 'called', - 'expected %n to have been called at least once but was never called' - ); - mirrorPropAsAssertion( - 'notCalled', - function(spy) { - return !spy.called; - }, - 'expected %n to not have been called but was called %c%C' - ); - mirrorPropAsAssertion( - 'calledOnce', - 'expected %n to be called once but was called %c%C' - ); - mirrorPropAsAssertion( - 'calledTwice', - 'expected %n to be called twice but was called %c%C' - ); - mirrorPropAsAssertion( - 'calledThrice', - 'expected %n to be called thrice but was called %c%C' - ); - mirrorPropAsAssertion( - 'calledOn', - 'expected %n to be called with %1 as this but was called with %t' - ); - mirrorPropAsAssertion( - 'alwaysCalledOn', - 'expected %n to always be called with %1 as this but was called with %t' - ); - mirrorPropAsAssertion('calledWithNew', 'expected %n to be called with new'); - mirrorPropAsAssertion( - 'alwaysCalledWithNew', - 'expected %n to always be called with new' - ); - mirrorPropAsAssertion( - 'calledWith', - 'expected %n to be called with arguments %*%C' - ); - mirrorPropAsAssertion( - 'calledWithMatch', - 'expected %n to be called with match %*%C' - ); - mirrorPropAsAssertion( - 'alwaysCalledWith', - 'expected %n to always be called with arguments %*%C' - ); - mirrorPropAsAssertion( - 'alwaysCalledWithMatch', - 'expected %n to always be called with match %*%C' - ); - mirrorPropAsAssertion( - 'calledWithExactly', - 'expected %n to be called with exact arguments %*%C' - ); - mirrorPropAsAssertion( - 'alwaysCalledWithExactly', - 'expected %n to always be called with exact arguments %*%C' - ); - mirrorPropAsAssertion( - 'neverCalledWith', - 'expected %n to never be called with arguments %*%C' - ); - mirrorPropAsAssertion( - 'neverCalledWithMatch', - 'expected %n to never be called with match %*%C' - ); - mirrorPropAsAssertion('threw', '%n did not throw exception%C'); - mirrorPropAsAssertion('alwaysThrew', '%n did not always throw exception%C'); - - if (commonJSModule) { - module.exports = assert; - } else { - sinon.assert = assert; - } - })( - (typeof sinon == 'object' && sinon) || null, - typeof window != 'undefined' - ? window - : typeof self != 'undefined' - ? self - : global - ); - - return sinon; -}.call((typeof window != 'undefined' && window) || {}); diff --git a/packages/fxa-js-client/tests/all.js b/packages/fxa-js-client/tests/all.js deleted file mode 100644 index 04ce2b52148..00000000000 --- a/packages/fxa-js-client/tests/all.js +++ /dev/null @@ -1,36 +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/. */ - -define([ - 'tests/lib/account', - 'tests/lib/certificateSign', - 'tests/lib/credentials', - 'tests/lib/device', - 'tests/lib/emails', - 'tests/lib/errors', - 'tests/lib/hawkCredentials', - 'tests/lib/headerLang', - 'tests/lib/hkdf', - 'tests/lib/init', - 'tests/lib/metricsContext', - 'tests/lib/misc', - 'tests/lib/oauth', - 'tests/lib/passwordChange', - 'tests/lib/recoveryCodes', - 'tests/lib/recoveryKeys', - 'tests/lib/recoveryEmail', - 'tests/lib/request', - 'tests/lib/securityEvents', - 'tests/lib/session', - 'tests/lib/signIn', - 'tests/lib/signinCodes', - 'tests/lib/signUp', - 'tests/lib/subscriptions', - 'tests/lib/totp', - 'tests/lib/tokenCodes', - 'tests/lib/sms', - 'tests/lib/unbundle', - 'tests/lib/uriVersion', - 'tests/lib/verifyCode', -], function() {}); diff --git a/packages/fxa-js-client/tests/ci/install-tunnel.sh b/packages/fxa-js-client/tests/ci/install-tunnel.sh deleted file mode 100755 index df4ed5afc6d..00000000000 --- a/packages/fxa-js-client/tests/ci/install-tunnel.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -wget https://saucelabs.com/downloads/sc-4.4.5-linux.tar.gz -O /tmp/sc-4.4.5-linux.tar.gz -cd /tmp -tar xvf /tmp/sc-4.4.5-linux.tar.gz - - - diff --git a/packages/fxa-js-client/tests/ci/travis-auth-server-test.sh b/packages/fxa-js-client/tests/ci/travis-auth-server-test.sh deleted file mode 100755 index 6074914a0df..00000000000 --- a/packages/fxa-js-client/tests/ci/travis-auth-server-test.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -ex - -# Install and start the auth server -git clone https://github.com/mozilla/fxa-auth-server.git -cd fxa-auth-server && npm i -SECONDARY_EMAIL_ENABLED=true SIGNIN_CONFIRMATION_ENABLED=true SIGNIN_CONFIRMATION_FORCE_EMAIL_REGEX="^confirm.*@restmail\\.net$" SIGNIN_UNBLOCK_ALLOWED_EMAILS="^block.*@restmail\\.net$" SIGNIN_UNBLOCK_FORCED_EMAILS="^block.*@restmail\\.net$" npm start & -cd .. -sleep 10 - -# Run the tests against the local auth server -npm run test-local diff --git a/packages/fxa-js-client/tests/intern.js b/packages/fxa-js-client/tests/intern.js deleted file mode 100644 index c5400976d2d..00000000000 --- a/packages/fxa-js-client/tests/intern.js +++ /dev/null @@ -1,50 +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/. */ - -// Learn more about configuring this file at . -// These default settings work OK for most people. The options that *must* be changed below are the -// packages, suites, excludeInstrumentation, and (if you want functional tests) functionalSuites. -define(['intern/lib/args'], function(args) { - // define a server to run against - var server; - - // if 'auth_server' in the Intern args - if (args.auth_server) { - server = args.auth_server; - if (server === 'LOCAL') { - server = 'http://127.0.0.1:9000'; - } - - if (server === 'LATEST') { - server = 'https://latest.dev.lcip.org/auth'; - } - - if (server === 'STABLE') { - server = 'https://stable.dev.lcip.org/auth'; - } - - console.log('Running against ' + server); - } else { - console.log('Running with mocks...'); - } - - return { - loader: { - // Packages that should be registered with the loader in each testing environment - packages: [{ name: 'fxa-js-client', location: 'client' }], - map: { - '*': { - 'es6-promise': 'node_modules/es6-promise/dist/es6-promise', - sjcl: 'node_modules/sjcl/sjcl', - }, - }, - }, - - suites: ['tests/all'], - functionalSuites: [], - AUTH_SERVER_URL: server, - - excludeInstrumentation: /./, - }; -}); diff --git a/packages/fxa-js-client/tests/intern_browser.js b/packages/fxa-js-client/tests/intern_browser.js deleted file mode 100755 index 8779557c0ad..00000000000 --- a/packages/fxa-js-client/tests/intern_browser.js +++ /dev/null @@ -1,23 +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/. */ - -define(['./intern'], function(intern) { - intern.proxyPort = 9090; - intern.proxyUrl = 'http://localhost:9090/'; - - intern.useSauceConnect = false; - - intern.webdriver = { - host: 'localhost', - port: 4444, - }; - - intern.capabilities = { - 'selenium-version': '2.39.0', - }; - - intern.environments = [{ browserName: 'firefox', version: '25' }]; - - return intern; -}); diff --git a/packages/fxa-js-client/tests/intern_native_node.js b/packages/fxa-js-client/tests/intern_native_node.js deleted file mode 100644 index 8d7f6ccd9b5..00000000000 --- a/packages/fxa-js-client/tests/intern_native_node.js +++ /dev/null @@ -1,10 +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/. */ - -define(['./intern'], function(intern, FxAccountClient) { - var map = intern.loader.map['*']; - map['client/FxAccountClient'] = 'tests/addons/node-client'; - - return intern; -}); diff --git a/packages/fxa-js-client/tests/intern_sauce.js b/packages/fxa-js-client/tests/intern_sauce.js deleted file mode 100644 index 2c26c0f31cd..00000000000 --- a/packages/fxa-js-client/tests/intern_sauce.js +++ /dev/null @@ -1,45 +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/. */ - -define(['./intern'], function(intern) { - intern.proxyPort = 9090; - intern.proxyUrl = 'http://localhost:9090/'; - - intern.useSauceConnect = true; - intern.maxConcurrency = 3; - - intern.tunnel = 'SauceLabsTunnel'; - intern.tunnelOptions = { - directory: '/tmp/sc-4.4.5-linux/bin', - executable: './sc', - }; - - intern.webdriver = { - host: 'localhost', - port: 4445, - }; - - intern.capabilities = { - build: '1', - }; - - intern.environments = [ - { - browserName: 'firefox', - version: ['45'], - platform: ['Windows 7', 'Linux'], - }, - { browserName: 'firefox', version: ['56'], platform: ['Windows 7'] }, // Sauce only supports Fx 56 on Windows/Mac - { - browserName: 'internet explorer', - version: ['10', '11'], - platform: ['Windows 7'], - }, - { browserName: 'chrome' }, - ]; - - console.log('SAUCE', intern.proxyUrl); - - return intern; -}); diff --git a/packages/fxa-js-client/tests/lib/account.js b/packages/fxa-js-client/tests/lib/account.js index 481c19f73c7..f3028bba423 100644 --- a/packages/fxa-js-client/tests/lib/account.js +++ b/packages/fxa-js-client/tests/lib/account.js @@ -2,613 +2,607 @@ * 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/. */ -define([ - 'intern!tdd', - 'intern/chai!assert', - 'tests/addons/environment', -], function(tdd, assert, Environment) { - with (tdd) { - suite('account', function() { - var accountHelper; - var respond; - var mail; - var client; - var RequestMocks; - var ErrorMocks; - - beforeEach(function() { - var env = new Environment(); - accountHelper = env.accountHelper; - respond = env.respond; - mail = env.mail; - client = env.client; - RequestMocks = env.RequestMocks; - ErrorMocks = env.ErrorMocks; - }); +const assert = require('chai').assert; +const Environment = require('../addons/environment'); + +describe('account', function() { + var accountHelper; + var respond; + var mail; + var client; + var RequestMocks; + var ErrorMocks; + let env; + + beforeEach(function() { + env = new Environment(); + accountHelper = env.accountHelper; + respond = env.respond; + mail = env.mail; + client = env.client; + RequestMocks = env.RequestMocks; + ErrorMocks = env.ErrorMocks; + }); + + it('#destroy', function() { + var email; + var password; + + return accountHelper + .newVerifiedAccount() + .then(function(account) { + email = account.input.email; + password = account.input.password; - test('#destroy', function() { - var email; - var password; - - return accountHelper - .newVerifiedAccount() - .then(function(account) { - email = account.input.email; - password = account.input.password; - - return respond( - client.accountDestroy(email, password), - RequestMocks.accountDestroy - ); - }) - .then(function(res) { - assert.ok(res, 'got response'); - - return respond( - client.signIn(email, password), - ErrorMocks.accountDoesNotExist - ); - }) - .then( - function() { - assert.fail(); - }, - function(error) { - assert.equal(error.errno, 102, 'Account is gone'); - assert.equal(error.code, 400, 'Correct status code'); - } - ); - }); + return respond( + client.accountDestroy(email, password), + RequestMocks.accountDestroy + ); + }) + .then(function(res) { + assert.ok(res, 'got response'); - test('#destroy with sessionToken', function() { - var email; - var password; - - return accountHelper - .newVerifiedAccount() - .then(function(account) { - email = account.input.email; - password = account.input.password; - - return respond( - client.accountDestroy( - email, - password, - {}, - account.signIn.sessionToken - ), - RequestMocks.accountDestroy - ); - }) - .then(function(res) { - assert.ok(res, 'got response'); - - return respond( - client.signIn(email, password), - ErrorMocks.accountDoesNotExist - ); - }) - .then( - function() { - assert.fail(); - }, - function(error) { - assert.equal(error.errno, 102, 'Account is gone'); - assert.equal(error.code, 400, 'Correct status code'); - } - ); - }); + return respond( + client.signIn(email, password), + ErrorMocks.accountDoesNotExist + ); + }) + .then( + function() { + assert.fail(); + }, + function(error) { + assert.equal(error.errno, 102, 'Account is gone'); + assert.equal(error.code, 400, 'Correct status code'); + } + ); + }); + + it('#destroy with sessionToken', function() { + var email; + var password; + + return accountHelper + .newVerifiedAccount() + .then(function(account) { + email = account.input.email; + password = account.input.password; - test('#destroy with sessionToken, incorrect case', function() { - var account; - - return accountHelper - .newVerifiedAccount() - .then(function(acc) { - account = acc; - var incorrectCaseEmail = - account.input.email.charAt(0).toUpperCase() + - account.input.email.slice(1); - - return respond( - client.accountDestroy( - incorrectCaseEmail, - account.input.password, - {}, - account.signIn.sessionToken - ), - RequestMocks.accountDestroy - ); - }) - .then(function(res) { - assert.ok(res); - - return respond( - client.signIn( - account.input.email, - account.input.password, - {}, - account.signIn.sessionToken - ), - ErrorMocks.accountDoesNotExist - ); - }) - .then( - function() { - assert.fail(); - }, - function(error) { - assert.ok(error); - assert.equal(error.errno, 102); - assert.equal(error.code, 400, 'Correct status code'); - } - ); - }); + return respond( + client.accountDestroy( + email, + password, + {}, + account.signIn.sessionToken + ), + RequestMocks.accountDestroy + ); + }) + .then(function(res) { + assert.ok(res, 'got response'); - test('#destroy with sessionToken, incorrect case with skipCaseError', function() { - var account; - - return accountHelper - .newVerifiedAccount() - .then(function(acc) { - account = acc; - var incorrectCaseEmail = - account.input.email.charAt(0).toUpperCase() + - account.input.email.slice(1); - - return respond( - client.accountDestroy( - incorrectCaseEmail, - account.input.password, - { skipCaseError: true }, - account.signIn.sessionToken - ), - ErrorMocks.incorrectEmailCase - ); - }) - .then( - function() { - assert.fail(); - }, - function(res) { - assert.equal(res.code, 400); - assert.equal(res.errno, 120); - } - ); - }); + return respond( + client.signIn(email, password), + ErrorMocks.accountDoesNotExist + ); + }) + .then( + function() { + assert.fail(); + }, + function(error) { + assert.equal(error.errno, 102, 'Account is gone'); + assert.equal(error.code, 400, 'Correct status code'); + } + ); + }); + + it('#destroy with sessionToken, incorrect case', function() { + var account; + + return accountHelper + .newVerifiedAccount() + .then(function(acc) { + account = acc; + var incorrectCaseEmail = + account.input.email.charAt(0).toUpperCase() + + account.input.email.slice(1); - test('#keys', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.accountKeys( - account.signIn.keyFetchToken, - account.signIn.unwrapBKey - ), - RequestMocks.accountKeys - ); - }) - .then(function(keys) { - assert.property(keys, 'kA'); - assert.property(keys, 'kB'); - }, assert.notOk); - }); + return respond( + client.accountDestroy( + incorrectCaseEmail, + account.input.password, + {}, + account.signIn.sessionToken + ), + RequestMocks.accountDestroy + ); + }) + .then(function(res) { + assert.ok(res); - test('#destroy with incorrect case', function() { - var account; - - return accountHelper - .newVerifiedAccount() - .then(function(acc) { - account = acc; - var incorrectCaseEmail = - account.input.email.charAt(0).toUpperCase() + - account.input.email.slice(1); - - return respond( - client.accountDestroy(incorrectCaseEmail, account.input.password), - RequestMocks.accountDestroy - ); - }) - .then(function(res) { - assert.ok(res); - - return respond( - client.signIn(account.input.email, account.input.password), - ErrorMocks.accountDoesNotExist - ); - }) - .then( - function() { - assert.fail(); - }, - function(error) { - assert.ok(error); - assert.equal(error.errno, 102); - assert.equal(error.code, 400, 'Correct status code'); - } - ); - }); + return respond( + client.signIn( + account.input.email, + account.input.password, + {}, + account.signIn.sessionToken + ), + ErrorMocks.accountDoesNotExist + ); + }) + .then( + function() { + assert.fail(); + }, + function(error) { + assert.ok(error); + assert.equal(error.errno, 102); + assert.equal(error.code, 400, 'Correct status code'); + } + ); + }); + + it('#destroy with sessionToken, incorrect case with skipCaseError', function() { + var account; + + return accountHelper + .newVerifiedAccount() + .then(function(acc) { + account = acc; + var incorrectCaseEmail = + account.input.email.charAt(0).toUpperCase() + + account.input.email.slice(1); - test('#destroy with incorrect case with skipCaseError', function() { - var account; - - return accountHelper - .newVerifiedAccount() - .then(function(acc) { - account = acc; - var incorrectCaseEmail = - account.input.email.charAt(0).toUpperCase() + - account.input.email.slice(1); - - return respond( - client.accountDestroy( - incorrectCaseEmail, - account.input.password, - { skipCaseError: true } - ), - ErrorMocks.incorrectEmailCase - ); - }) - .then( - function() { - assert.fail(); - }, - function(res) { - assert.equal(res.code, 400); - assert.equal(res.errno, 120); - } - ); - }); + return respond( + client.accountDestroy( + incorrectCaseEmail, + account.input.password, + { skipCaseError: true }, + account.signIn.sessionToken + ), + ErrorMocks.incorrectEmailCase + ); + }) + .then( + function() { + assert.fail(); + }, + function(res) { + assert.equal(res.code, 400); + assert.equal(res.errno, 120); + } + ); + }); + + it('#keys', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.accountKeys( + account.signIn.keyFetchToken, + account.signIn.unwrapBKey + ), + RequestMocks.accountKeys + ); + }) + .then(function(keys) { + assert.property(keys, 'kA'); + assert.property(keys, 'kB'); + }, assert.fail); + }); + + it('#destroy with incorrect case', function() { + var account; + + return accountHelper + .newVerifiedAccount() + .then(function(acc) { + account = acc; + var incorrectCaseEmail = + account.input.email.charAt(0).toUpperCase() + + account.input.email.slice(1); - /** - * Password Reset - */ - test('#reset password', function() { - var user = 'test5' + new Date().getTime(); - var email = user + '@restmail.net'; - var password = 'iliketurtles'; - var uid; - var passwordForgotToken; - var accountResetToken; - - return respond(client.signUp(email, password), RequestMocks.signUp) - .then(function(result) { - uid = result.uid; - assert.ok(uid, 'uid is returned'); - - return respond( - client.passwordForgotSendCode(email), - RequestMocks.passwordForgotSendCode - ); - }) - .then(function(result) { - passwordForgotToken = result.passwordForgotToken; - assert.ok(passwordForgotToken, 'passwordForgotToken is returned'); - - return respond( - mail.wait(user, 2), - RequestMocks.resetMailpasswordForgotresetMail - ); - }) - .then(function(emails) { - var code = emails[1].html.match(/code=([A-Za-z0-9]+)/)[1]; - assert.ok(code, 'code is returned: ' + code); - - return respond( - client.passwordForgotVerifyCode(code, passwordForgotToken), - RequestMocks.passwordForgotVerifyCode - ); - }) - .then(function(result) { - accountResetToken = result.accountResetToken; - var newPassword = 'newturles'; - assert.ok(accountResetToken, 'accountResetToken is returned'); - - return respond( - client.accountReset(email, newPassword, accountResetToken, { - keys: true, - metricsContext: { - deviceId: '0123456789abcdef0123456789abcdef', - entrypoint: 'mock-entrypoint', - entrypointExperiment: 'mock-entrypoint-experiment', - entrypointVariation: 'mock-entrypoint-variation', - flowBeginTime: 1480615985437, - flowId: - '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', - utmCampaign: 'mock-campaign', - utmContent: 'mock-content', - utmMedium: 'mock-medium', - utmSource: 'mock-source', - utmTerm: 'mock-term', - }, - sessionToken: true, - }), - RequestMocks.accountReset - ); - }) - .then(function(result) { - assert.ok(result.keyFetchToken); - assert.ok(result.sessionToken); - assert.ok(result.unwrapBKey); - assert.ok(result.uid); - }, assert.notOk); - }); + return respond( + client.accountDestroy(incorrectCaseEmail, account.input.password), + RequestMocks.accountDestroy + ); + }) + .then(function(res) { + assert.ok(res); - test('#passwordForgotSendCode with service, redirectTo, and resume', function() { - var account; - var opts = { - service: 'sync', - redirectTo: 'https://sync.127.0.0.1/after_reset', - resume: 'resumejwt', - }; - - return accountHelper - .newVerifiedAccount() - .then(function(acc) { - account = acc; - - return respond( - client.passwordForgotSendCode(account.input.email, opts), - RequestMocks.passwordForgotSendCode - ); - }) - .then(function(result) { - assert.ok(result.passwordForgotToken); - - return respond( - mail.wait(account.input.user, 3), - RequestMocks.resetMailWithServiceAndRedirect - ); - }) - .then(function(emails) { - var code = emails[2].html.match(/code=([A-Za-z0-9]+)/); - assert.ok(code, 'code found'); - var service = emails[2].html.match(/service=([A-Za-z0-9]+)/); - assert.ok(service, 'service found'); - var redirectTo = emails[2].html.match(/redirectTo=([A-Za-z0-9]+)/); - assert.ok(redirectTo, 'redirectTo found'); - var resume = emails[2].html.match(/resume=([A-Za-z0-9]+)/); - assert.ok(resume, 'resume found'); - - assert.ok(code[1], 'code is returned'); - assert.equal(service[1], 'sync', 'service is returned'); - assert.equal(redirectTo[1], 'https', 'redirectTo is returned'); - assert.equal(resume[1], 'resumejwt', 'resume is returned'); - }); - }); + return respond( + client.signIn(account.input.email, account.input.password), + ErrorMocks.accountDoesNotExist + ); + }) + .then( + function() { + assert.fail(); + }, + function(error) { + assert.ok(error); + assert.equal(error.errno, 102); + assert.equal(error.code, 400, 'Correct status code'); + } + ); + }); + + it('#destroy with incorrect case with skipCaseError', function() { + var account; + + return accountHelper + .newVerifiedAccount() + .then(function(acc) { + account = acc; + var incorrectCaseEmail = + account.input.email.charAt(0).toUpperCase() + + account.input.email.slice(1); - test('#passwordForgotStatus', function() { - return accountHelper - .newVerifiedAccount() - .then(function(result) { - return respond( - client.passwordForgotSendCode(result.input.email), - RequestMocks.passwordForgotSendCode - ); - }) - .then(function(result) { - return respond( - client.passwordForgotStatus(result.passwordForgotToken), - RequestMocks.passwordForgotStatus - ); - }) - .then(function(result) { - assert.equal(result.tries, 3); - assert.property(result, 'ttl'); - }, assert.notOk); - }); + return respond( + client.accountDestroy(incorrectCaseEmail, account.input.password, { + skipCaseError: true, + }), + ErrorMocks.incorrectEmailCase + ); + }) + .then( + function() { + assert.fail(); + }, + function(res) { + assert.equal(res.code, 400); + assert.equal(res.errno, 120); + } + ); + }); + + /** + * Password Reset + */ + it('#reset password', function() { + var user = 'test5' + new Date().getTime(); + var email = user + '@restmail.net'; + var password = 'iliketurtles'; + var uid; + var passwordForgotToken; + var accountResetToken; + + return respond(client.signUp(email, password), RequestMocks.signUp) + .then(function(result) { + uid = result.uid; + assert.ok(uid, 'uid is returned'); - test('#passwordForgotStatus error with a false token', function() { - return accountHelper - .newVerifiedAccount() - .then(function(result) { - return respond( - client.passwordForgotSendCode(result.input.email), - RequestMocks.passwordForgotSendCode - ); - }) - .then(function() { - var fakeToken = - 'e838790265a45f6ee1130070d57d67d9bb20953706f73af0e34b0d4d92f10000'; - - return respond( - client.passwordForgotStatus(fakeToken), - ErrorMocks.invalidAuthToken - ); - }) - .then(assert.notOk, function(err) { - assert.equal(err.code, 401); - assert.equal(err.errno, 110); - }); - }); + return respond( + client.passwordForgotSendCode(email), + RequestMocks.passwordForgotSendCode + ); + }) + .then(function(result) { + passwordForgotToken = result.passwordForgotToken; + assert.ok(passwordForgotToken, 'passwordForgotToken is returned'); - test('#accountStatus', function() { - return accountHelper - .newVerifiedAccount() - .then(function(result) { - return respond( - client.accountStatus(result.signIn.uid), - RequestMocks.accountStatus - ); - }) - .then(function(res) { - assert.equal(res.exists, true); - }, assert.notOk); - }); + return respond( + mail.wait(user, 2), + RequestMocks.resetMailpasswordForgotresetMail + ); + }) + .then(function(emails) { + var code = emails[1].html.match(/code=([A-Za-z0-9]+)/)[1]; + assert.ok(code, 'code is returned: ' + code); - test('#accountProfile', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.accountProfile(account.signIn.sessionToken), - RequestMocks.accountProfile - ); - }) - .then(function(res) { - assert.isNotNull(res); - }, assert.notOk); - }); + return respond( + client.passwordForgotVerifyCode(code, passwordForgotToken), + RequestMocks.passwordForgotVerifyCode + ); + }) + .then(function(result) { + accountResetToken = result.accountResetToken; + var newPassword = 'newturles'; + assert.ok(accountResetToken, 'accountResetToken is returned'); - test('#accountStatus with wrong uid', function() { return respond( - client.accountStatus('00047f01e387498e8ccc7fede1a74000'), - RequestMocks.accountStatusFalse - ).then(function(res) { - assert.equal(res.exists, false); - }, assert.notOk); - }); + client.accountReset(email, newPassword, accountResetToken, { + keys: true, + metricsContext: { + deviceId: '0123456789abcdef0123456789abcdef', + entrypoint: 'mock-entrypoint', + entrypointExperiment: 'mock-entrypoint-experiment', + entrypointVariation: 'mock-entrypoint-variation', + flowBeginTime: 1480615985437, + flowId: + '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + utmCampaign: 'mock-campaign', + utmContent: 'mock-content', + utmMedium: 'mock-medium', + utmSource: 'mock-source', + utmTerm: 'mock-term', + }, + sessionToken: true, + }), + RequestMocks.accountReset + ); + }) + .then(function(result) { + assert.ok(result.keyFetchToken); + assert.ok(result.sessionToken); + assert.ok(result.unwrapBKey); + assert.ok(result.uid); + }, assert.fail); + }); + + it('#passwordForgotSendCode with service, redirectTo, and resume', function() { + var account; + var opts = { + service: 'sync', + redirectTo: 'https://sync.127.0.0.1/after_reset', + resume: 'resumejwt', + }; + + return accountHelper + .newVerifiedAccount() + .then(function(acc) { + account = acc; - test('#accountStatus with no uid', function() { - return client.accountStatus().then( - function() { - assert.fail('client.accountStatus should reject if uid is missing'); - }, - function(err) { - assert.equal(err.message, 'Missing uid'); - } + return respond( + client.passwordForgotSendCode(account.input.email, opts), + RequestMocks.passwordForgotSendCode ); - }); + }) + .then(function(result) { + assert.ok(result.passwordForgotToken); - test('#accountStatusByEmail', function() { - return accountHelper - .newVerifiedAccount() - .then(function(result) { - return respond( - client.accountStatusByEmail(result.input.email), - RequestMocks.accountStatus - ); - }) - .then(function(res) { - assert.equal(res.exists, true); - }, assert.notOk); + return respond( + mail.wait(account.input.user, 3), + RequestMocks.resetMailWithServiceAndRedirect + ); + }) + .then(function(emails) { + var code = emails[2].html.match(/code=([A-Za-z0-9]+)/); + assert.ok(code, 'code found'); + var service = emails[2].html.match(/service=([A-Za-z0-9]+)/); + assert.ok(service, 'service found'); + var redirectTo = emails[2].html.match(/redirectTo=([A-Za-z0-9]+)/); + assert.ok(redirectTo, 'redirectTo found'); + var resume = emails[2].html.match(/resume=([A-Za-z0-9]+)/); + assert.ok(resume, 'resume found'); + + assert.ok(code[1], 'code is returned'); + assert.equal(service[1], 'sync', 'service is returned'); + assert.equal(redirectTo[1], 'https', 'redirectTo is returned'); + assert.equal(resume[1], 'resumejwt', 'resume is returned'); }); + }); - test('#accountStatusByEmail with wrong email', function() { + it('#passwordForgotStatus', function() { + return accountHelper + .newVerifiedAccount() + .then(function(result) { return respond( - client.accountStatusByEmail('invalid@email.com'), - RequestMocks.accountStatusFalse - ).then(function(res) { - assert.equal(res.exists, false); - }, assert.notOk); - }); + client.passwordForgotSendCode(result.input.email), + RequestMocks.passwordForgotSendCode + ); + }) + .then(function(result) { + return respond( + client.passwordForgotStatus(result.passwordForgotToken), + RequestMocks.passwordForgotStatus + ); + }) + .then(function(result) { + assert.equal(result.tries, 3); + assert.property(result, 'ttl'); + }, assert.fail); + }); + + it('#passwordForgotStatus error with a false token', function() { + return accountHelper + .newVerifiedAccount() + .then(function(result) { + return respond( + client.passwordForgotSendCode(result.input.email), + RequestMocks.passwordForgotSendCode + ); + }) + .then(function() { + var fakeToken = + 'e838790265a45f6ee1130070d57d67d9bb20953706f73af0e34b0d4d92f10000'; - test('#accountStatusByEmail with no email', function() { - return client.accountStatusByEmail().then( - function() { - assert.fail( - 'client.accountStatusByEmail should reject if email is missing' - ); - }, - function(err) { - assert.equal(err.message, 'Missing email'); - } + return respond( + client.passwordForgotStatus(fakeToken), + ErrorMocks.invalidAuthToken ); + }) + .then(assert.fail, function(err) { + assert.equal(err.code, 401); + assert.equal(err.errno, 110); }); + }); - test('#login unblock accept', function() { - var user = 'block' + new Date().getTime(); - var email = user + '@restmail.net'; - var password = 'iliketurtles'; - - return respond(client.signUp(email, password), RequestMocks.signUp) - .then(function(result) { - return respond( - client.signIn(email, password, { context: 'fx_desktop_v3' }), - ErrorMocks.signInBlocked - ); - }) - .then(assert.fail, function(error) { - assert.equal(error.errno, 125); - assert.equal(error.verificationMethod, 'email-captcha'); - assert.equal(error.verificationReason, 'login'); - - return respond( - client.sendUnblockCode(email, { context: 'fx_desktop_v3' }), - RequestMocks.sendUnblockCode - ); - }) - .then(function() { - return respond(mail.wait(user, 2), RequestMocks.unblockEmail); - }) - .then(function(emails) { - var unblockCode = emails[1].headers['x-unblock-code']; - assert.ok(unblockCode, 'unblockCode is returned'); - - return respond( - client.signIn(email, password, { - unblockCode: unblockCode, - context: 'fx_desktop_v3', - }), - RequestMocks.signIn - ); - }) - .then(function(result) { - assert.ok(result.uid); - }, assert.notOk); - }); + it('#accountStatus', function() { + return accountHelper + .newVerifiedAccount() + .then(function(result) { + return respond( + client.accountStatus(result.signIn.uid), + RequestMocks.accountStatus + ); + }) + .then(function(res) { + assert.equal(res.exists, true); + }, assert.fail); + }); + + it('#accountProfile', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.accountProfile(account.signIn.sessionToken), + RequestMocks.accountProfile + ); + }) + .then(function(res) { + assert.isNotNull(res); + }, assert.fail); + }); + + it('#accountStatus with wrong uid', function() { + return respond( + client.accountStatus('00047f01e387498e8ccc7fede1a74000'), + RequestMocks.accountStatusFalse + ).then(function(res) { + assert.equal(res.exists, false); + }, assert.fail); + }); + + it('#accountStatus with no uid', function() { + return client.accountStatus().then( + function() { + assert.fail('client.accountStatus should reject if uid is missing'); + }, + function(err) { + assert.equal(err.message, 'Missing uid'); + } + ); + }); + + it('#accountStatusByEmail', function() { + return accountHelper + .newVerifiedAccount() + .then(function(result) { + return respond( + client.accountStatusByEmail(result.input.email), + RequestMocks.accountStatus + ); + }) + .then(function(res) { + assert.equal(res.exists, true); + }, assert.fail); + }); + + it('#accountStatusByEmail with wrong email', function() { + return respond( + client.accountStatusByEmail('invalid@email.com'), + RequestMocks.accountStatusFalse + ).then(function(res) { + assert.equal(res.exists, false); + }, assert.fail); + }); + + it('#accountStatusByEmail with no email', function() { + return client.accountStatusByEmail().then( + function() { + assert.fail( + 'client.accountStatusByEmail should reject if email is missing' + ); + }, + function(err) { + assert.equal(err.message, 'Missing email'); + } + ); + }); + + it('#login unblock accept', function() { + var user = 'block.' + new Date().getTime(); + var email = user + '@restmail.net'; + var password = 'iliketurtles'; + + return respond(client.signUp(email, password), RequestMocks.signUp) + .then(function(result) { + return respond( + client.signIn(email, password, { context: 'fx_desktop_v3' }), + ErrorMocks.signInBlocked + ); + }) + .then(assert.fail, function(error) { + assert.equal(error.errno, 125); + assert.equal(error.verificationMethod, 'email-captcha'); + assert.equal(error.verificationReason, 'login'); - test('#login unblock reject', function() { - var user = 'block' + new Date().getTime(); - var email = user + '@restmail.net'; - var password = 'iliketurtles'; - var uid; - var unblockCode; - - return respond(client.signUp(email, password), RequestMocks.signUp) - .then(function(result) { - uid = result.uid; - return respond( - client.signIn(email, password, { context: 'fx_desktop_v3' }), - ErrorMocks.signInBlocked - ); - }) - .then(assert.fail, function(error) { - assert.equal(error.errno, 125); - assert.equal(error.verificationMethod, 'email-captcha'); - assert.equal(error.verificationReason, 'login'); - - return respond( - client.sendUnblockCode(email, { context: 'fx_desktop_v3' }), - RequestMocks.sendUnblockCode - ); - }) - .then(function() { - return respond(mail.wait(user, 2), RequestMocks.unblockEmail); - }) - .then(function(emails) { - unblockCode = emails[1].headers['x-unblock-code']; - assert.ok(unblockCode, 'unblockCode is returned'); - - return respond( - client.rejectUnblockCode(uid, unblockCode), - RequestMocks.rejectUnblockCode - ); - }) - .then(function() { - return respond( - client.signIn(email, password, { - unblockCode: unblockCode, - context: 'fx_desktop_v3', - }), - ErrorMocks.signInInvalidUnblockCode - ); - }) - .then(assert.fail, function(error) { - assert.equal(error.errno, 127); - }); - }); + return respond( + client.sendUnblockCode(email, { context: 'fx_desktop_v3' }), + RequestMocks.sendUnblockCode + ); + }) + .then(function() { + return respond(mail.wait(user, 2), RequestMocks.unblockEmail); + }) + .then(function(emails) { + var unblockCode = emails[1].headers['x-unblock-code']; + assert.ok(unblockCode, 'unblockCode is returned'); - test('account()', async () => { - const account = await accountHelper.newVerifiedAccount(); - const result = await respond( - client.account(account.signIn.sessionToken), - RequestMocks.account + return respond( + client.signIn(email, password, { + unblockCode: unblockCode, + context: 'fx_desktop_v3', + }), + RequestMocks.signIn + ); + }) + .then(function(result) { + assert.ok(result.uid); + }, assert.fail); + }); + + it('#login unblock reject', function() { + var user = 'block.' + new Date().getTime(); + var email = user + '@restmail.net'; + var password = 'iliketurtles'; + var uid; + var unblockCode; + + return respond(client.signUp(email, password), RequestMocks.signUp) + .then(function(result) { + uid = result.uid; + return respond( + client.signIn(email, password, { context: 'fx_desktop_v3' }), + ErrorMocks.signInBlocked + ); + }) + .then(assert.fail, function(error) { + assert.equal(error.errno, 125); + assert.equal(error.verificationMethod, 'email-captcha'); + assert.equal(error.verificationReason, 'login'); + + return respond( + client.sendUnblockCode(email, { context: 'fx_desktop_v3' }), + RequestMocks.sendUnblockCode + ); + }) + .then(function() { + return respond(mail.wait(user, 2), RequestMocks.unblockEmail); + }) + .then(function(emails) { + unblockCode = emails[1].headers['x-unblock-code']; + assert.ok(unblockCode, 'unblockCode is returned'); + + return respond( + client.rejectUnblockCode(uid, unblockCode), + RequestMocks.rejectUnblockCode + ); + }) + .then(function() { + return respond( + client.signIn(email, password, { + unblockCode: unblockCode, + context: 'fx_desktop_v3', + }), + ErrorMocks.signInInvalidUnblockCode ); - assert.isArray(result.subscriptions); + }) + .then(assert.fail, function(error) { + assert.equal(error.errno, 127); }); - }); - } + }); + + it('account()', async () => { + const account = await accountHelper.newVerifiedAccount(); + const result = await respond( + client.account(account.signIn.sessionToken), + RequestMocks.account + ); + assert.isArray(result.subscriptions); + }); }); diff --git a/packages/fxa-js-client/tests/lib/attachedClients.js b/packages/fxa-js-client/tests/lib/attachedClients.js index 17ac5b9bbb5..104ca96deeb 100644 --- a/packages/fxa-js-client/tests/lib/attachedClients.js +++ b/packages/fxa-js-client/tests/lib/attachedClients.js @@ -2,97 +2,98 @@ * 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/. */ -define([ - 'intern!tdd', - 'intern/chai!assert', - 'tests/addons/environment', - 'tests/addons/sinon', -], function(tdd, assert, Environment, sinon) { - with (tdd) { - suite('session', function() { - var accountHelper; - var respond; - var requests; - var client; - var RequestMocks; - var ErrorMocks; - var xhr; +const assert = require('chai').assert; +const Environment = require('../addons/environment'); +const sinon = require('sinon'); - beforeEach(function() { - var env = new Environment(); - accountHelper = env.accountHelper; - respond = env.respond; - requests = env.requests; - client = env.client; - RequestMocks = env.RequestMocks; - ErrorMocks = env.ErrorMocks; - xhr = env.xhr; - sinon.spy(xhr.prototype, 'open'); - sinon.spy(xhr.prototype, 'send'); - }); +describe('attachedClients', function() { + var accountHelper; + var respond; + var requests; + var client; + var RequestMocks; + var ErrorMocks; + var xhr; + let env; - afterEach(function() { - xhr.prototype.open.restore(); - xhr.prototype.send.restore(); - }); + beforeEach(function() { + env = new Environment(); + accountHelper = env.accountHelper; + respond = env.respond; + requests = env.requests; + client = env.client; + RequestMocks = env.RequestMocks; + ErrorMocks = env.ErrorMocks; + xhr = env.xhr; + sinon.spy(xhr.prototype, 'open'); + sinon.spy(xhr.prototype, 'send'); + }); - test('#attachedClients', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.attachedClients(account.signIn.sessionToken), - RequestMocks.attachedClients - ); - }) - .then(function(res) { - assert.equal(res.length, 2); - var s = res[0]; - assert.ok(s.deviceId); - assert.ok(s.deviceType); - assert.ok(s.lastAccessTime); - assert.ok(s.lastAccessTimeFormatted); - assert.ok(s.sessionTokenId); - s = res[1]; - assert.ok(s.deviceId); - assert.ok(s.deviceType); - assert.ok(s.lastAccessTime); - assert.ok(s.lastAccessTimeFormatted); - assert.ok(s.sessionTokenId); - }, assert.notOk); - }); + afterEach(function() { + xhr.prototype.open.restore(); + xhr.prototype.send.restore(); + }); - test('#attachedClients error', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - var fakeToken = - 'e838790265a45f6ee1130070d57d67d9bb20953706f73af0e34b0d4d92f10000'; + it('#attachedClients', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.attachedClients(account.signIn.sessionToken), + RequestMocks.attachedClients + ); + }) + .then(function(res) { + assert.equal(res.length, 2); + var s = res[0]; + assert.property(s, 'clientId'); + assert.property(s, 'deviceId'); + assert.property(s, 'deviceType'); + assert.property(s, 'refreshTokenId'); + assert.ok(s.lastAccessTime); + assert.ok(s.lastAccessTimeFormatted); + assert.ok(s.sessionTokenId); + s = res[1]; + assert.property(s, 'clientId'); + assert.property(s, 'deviceId'); + assert.property(s, 'deviceType'); + assert.property(s, 'refreshTokenId'); + assert.ok(s.lastAccessTimeFormatted); + assert.ok(s.sessionTokenId); + }, assert.fail); + }); - return respond( - client.attachedClients(fakeToken), - ErrorMocks.invalidAuthToken - ); - }) - .then(assert.notOk, function(err) { - assert.equal(err.code, 401); - assert.equal(err.errno, 110); - }); - }); + it('#attachedClients error', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + var fakeToken = + 'e838790265a45f6ee1130070d57d67d9bb20953706f73af0e34b0d4d92f10000'; - test('#destroy', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.attachedClientDestroy(account.signIn.sessionToken), - RequestMocks.attachedClientDestroy - ); - }) - .then(function(res) { - assert.ok(res, 'got response'); - }, assert.notOk); + return respond( + client.attachedClients(fakeToken), + ErrorMocks.invalidAuthToken + ); + }) + .then(assert.fail, function(err) { + assert.equal(err.code, 401); + assert.equal(err.errno, 110); }); - }); - } + }); + + it('#destroy', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.attachedClientDestroy(account.signIn.sessionToken, { + clientId: 'dcdb5ae7add825d2', + }), + RequestMocks.attachedClientDestroy + ); + }) + .then(function(res) { + assert.ok(res, 'got response'); + }, assert.fail); + }); }); diff --git a/packages/fxa-js-client/tests/lib/certificateSign.js b/packages/fxa-js-client/tests/lib/certificateSign.js index d4cb6a3eda3..6fdfa084a8a 100644 --- a/packages/fxa-js-client/tests/lib/certificateSign.js +++ b/packages/fxa-js-client/tests/lib/certificateSign.js @@ -2,80 +2,76 @@ * 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/. */ -define([ - 'intern!tdd', - 'intern/chai!assert', - 'tests/addons/environment', -], function(tdd, assert, Environment) { - with (tdd) { - suite('certificateSign', function() { - var accountHelper; - var respond; - var client; - var RequestMocks; +const assert = require('chai').assert; +const Environment = require('../addons/environment'); - beforeEach(function() { - var env = new Environment(); - accountHelper = env.accountHelper; - respond = env.respond; - client = env.client; - RequestMocks = env.RequestMocks; - }); +describe('certificateSign', function() { + var accountHelper; + var respond; + var client; + var RequestMocks; + let env; - test('#basic', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - var publicKey = { - algorithm: 'RS', - n: - '4759385967235610503571494339196749614544606692567785790953934768202714280652973091341316862993582789079872007974809511698859885077002492642203267408776123', - e: '65537', - }; - var duration = 86400000; + beforeEach(function() { + env = new Environment(); + accountHelper = env.accountHelper; + respond = env.respond; + client = env.client; + RequestMocks = env.RequestMocks; + }); - return respond( - client.certificateSign( - account.signIn.sessionToken, - publicKey, - duration - ), - RequestMocks.certificateSign - ); - }) - .then(function(res) { - assert.property(res, 'cert', 'got cert'); - }, assert.notOk); - }); + it('#basic', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + var publicKey = { + algorithm: 'RS', + n: + '4759385967235610503571494339196749614544606692567785790953934768202714280652973091341316862993582789079872007974809511698859885077002492642203267408776123', + e: '65537', + }; + var duration = 86400000; - test('#with service option', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - var publicKey = { - algorithm: 'RS', - n: - '4759385967235610503571494339196749614544606692567785790953934768202714280652973091341316862993582789079872007974809511698859885077002492642203267408776123', - e: '65537', - }; - var duration = 86400000; + return respond( + client.certificateSign( + account.signIn.sessionToken, + publicKey, + duration + ), + RequestMocks.certificateSign + ); + }) + .then(function(res) { + assert.property(res, 'cert', 'got cert'); + }, assert.fail); + }); - return respond( - client.certificateSign( - account.signIn.sessionToken, - publicKey, - duration, - { - service: 'wibble', - } - ), - RequestMocks.certificateSign - ); - }) - .then(function(res) { - assert.ok(res); - }, assert.notOk); - }); - }); - } + it('#with service option', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + var publicKey = { + algorithm: 'RS', + n: + '4759385967235610503571494339196749614544606692567785790953934768202714280652973091341316862993582789079872007974809511698859885077002492642203267408776123', + e: '65537', + }; + var duration = 86400000; + + return respond( + client.certificateSign( + account.signIn.sessionToken, + publicKey, + duration, + { + service: 'wibble', + } + ), + RequestMocks.certificateSign + ); + }) + .then(function(res) { + assert.ok(res); + }, assert.fail); + }); }); diff --git a/packages/fxa-js-client/tests/lib/credentials.js b/packages/fxa-js-client/tests/lib/credentials.js index 832a419cfb7..8a878789362 100644 --- a/packages/fxa-js-client/tests/lib/credentials.js +++ b/packages/fxa-js-client/tests/lib/credentials.js @@ -2,62 +2,54 @@ * 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/. */ -define([ - 'intern!tdd', - 'intern/chai!assert', - 'node_modules/sjcl/sjcl', - 'client/lib/credentials', -], function(tdd, assert, sjcl, credentials) { - with (tdd) { - suite('credentials', function() { - test('#client stretch-KDF vectors', function() { - var email = sjcl.codec.utf8String.fromBits( - sjcl.codec.hex.toBits('616e6472c3a9406578616d706c652e6f7267') - ); - var password = sjcl.codec.utf8String.fromBits( - sjcl.codec.hex.toBits('70c3a4737377c3b67264') - ); +const assert = require('chai').assert; +const sjcl = require('sjcl'); +const credentials = require('../../client/lib/credentials'); +describe('credentials', function() { + it('#client stretch-KDF vectors', function() { + var email = sjcl.codec.utf8String.fromBits( + sjcl.codec.hex.toBits('616e6472c3a9406578616d706c652e6f7267') + ); + var password = sjcl.codec.utf8String.fromBits( + sjcl.codec.hex.toBits('70c3a4737377c3b67264') + ); - return credentials.setup(email, password).then(function(result) { - var quickStretchedPW = sjcl.codec.hex.fromBits( - result.quickStretchedPW - ); - var authPW = sjcl.codec.hex.fromBits(result.authPW); - var unwrapBKey = sjcl.codec.hex.fromBits(result.unwrapBKey); + return credentials.setup(email, password).then(function(result) { + var quickStretchedPW = sjcl.codec.hex.fromBits(result.quickStretchedPW); + var authPW = sjcl.codec.hex.fromBits(result.authPW); + var unwrapBKey = sjcl.codec.hex.fromBits(result.unwrapBKey); - assert.equal( - quickStretchedPW, - 'e4e8889bd8bd61ad6de6b95c059d56e7b50dacdaf62bd84644af7e2add84345d', - '== quickStretchedPW is equal' - ); - assert.equal( - authPW, - '247b675ffb4c46310bc87e26d712153abe5e1c90ef00a4784594f97ef54f2375', - '== authPW is equal' - ); - assert.equal( - unwrapBKey, - 'de6a2648b78284fcb9ffa81ba95803309cfba7af583c01a8a1a63e567234dd28', - '== unwrapBkey is equal' - ); - }, assert.notOk); - }); + assert.equal( + quickStretchedPW, + 'e4e8889bd8bd61ad6de6b95c059d56e7b50dacdaf62bd84644af7e2add84345d', + '== quickStretchedPW is equal' + ); + assert.equal( + authPW, + '247b675ffb4c46310bc87e26d712153abe5e1c90ef00a4784594f97ef54f2375', + '== authPW is equal' + ); + assert.equal( + unwrapBKey, + 'de6a2648b78284fcb9ffa81ba95803309cfba7af583c01a8a1a63e567234dd28', + '== unwrapBkey is equal' + ); + }, assert.fail); + }); - test('#wrap', function() { - var bit1 = sjcl.codec.hex.toBits( - 'c347de41c8a409c17b5b88e4985e1cd10585bb79b4a80d5e576eaf97cd1277fc' - ); - var bit2 = sjcl.codec.hex.toBits( - '3afd383d9bc1857318f24c5f293af62254f0476f0aaacfb929c61b534d0b5075' - ); - var result = credentials.xor(bit1, bit2); + it('#wrap', function() { + var bit1 = sjcl.codec.hex.toBits( + 'c347de41c8a409c17b5b88e4985e1cd10585bb79b4a80d5e576eaf97cd1277fc' + ); + var bit2 = sjcl.codec.hex.toBits( + '3afd383d9bc1857318f24c5f293af62254f0476f0aaacfb929c61b534d0b5075' + ); + var result = credentials.xor(bit1, bit2); - assert.equal( - sjcl.codec.hex.fromBits(result), - 'f9bae67c53658cb263a9c4bbb164eaf35175fc16be02c2e77ea8b4c480192789', - '== wrap worked correctly' - ); - }); - }); - } + assert.equal( + sjcl.codec.hex.fromBits(result), + 'f9bae67c53658cb263a9c4bbb164eaf35175fc16be02c2e77ea8b4c480192789', + '== wrap worked correctly' + ); + }); }); diff --git a/packages/fxa-js-client/tests/lib/device.js b/packages/fxa-js-client/tests/lib/device.js index 842c8267b7b..498d3c9dc04 100644 --- a/packages/fxa-js-client/tests/lib/device.js +++ b/packages/fxa-js-client/tests/lib/device.js @@ -2,154 +2,164 @@ * 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/. */ -define([ - 'intern!tdd', - 'intern/chai!assert', - 'tests/addons/environment', - 'tests/lib/push-constants', -], function(tdd, assert, Environment, PushTestConstants) { - var DEVICE_CALLBACK = PushTestConstants.DEVICE_CALLBACK; - var DEVICE_NAME = PushTestConstants.DEVICE_NAME; - var DEVICE_NAME_2 = PushTestConstants.DEVICE_NAME_2; - var DEVICE_TYPE = PushTestConstants.DEVICE_TYPE; +const assert = require('chai').assert; +const Environment = require('../addons/environment'); - with (tdd) { - suite('device', function() { - var accountHelper; - var respond; - var client; - var RequestMocks; +const PushTestConstants = require('../mocks/pushConstants'); +var DEVICE_CALLBACK = PushTestConstants.DEVICE_CALLBACK; +var DEVICE_PUBLIC_KEY = PushTestConstants.DEVICE_PUBLIC_KEY; +var DEVICE_AUTH_KEY = PushTestConstants.DEVICE_AUTH_KEY; +var DEVICE_NAME = PushTestConstants.DEVICE_NAME; +var DEVICE_NAME_2 = PushTestConstants.DEVICE_NAME_2; +var DEVICE_TYPE = PushTestConstants.DEVICE_TYPE; - beforeEach(function() { - var env = new Environment(); - accountHelper = env.accountHelper; - respond = env.respond; - client = env.client; - RequestMocks = env.RequestMocks; - }); +describe('device', function() { + var accountHelper; + var respond; + var client; + var RequestMocks; + let env; - test('#register', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.deviceRegister( - account.signIn.sessionToken, - DEVICE_NAME, - DEVICE_TYPE, - { - deviceCallback: DEVICE_CALLBACK, - } - ), - RequestMocks.deviceRegister - ); - }) - .then( - function(res) { - assert.ok(res.id); - assert.equal(res.name, DEVICE_NAME); - assert.equal(res.pushCallback, DEVICE_CALLBACK); - assert.equal(res.type, DEVICE_TYPE); - }, - function(err) { - console.log(err); - assert.notOk(); - } - ); - }); + beforeEach(function() { + env = new Environment(); + accountHelper = env.accountHelper; + respond = env.respond; + client = env.client; + RequestMocks = env.RequestMocks; + }); - test('#update', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.deviceRegister( - account.signIn.sessionToken, - DEVICE_NAME, - DEVICE_TYPE, - { - deviceCallback: DEVICE_CALLBACK, - } - ), - RequestMocks.deviceRegister - ).then(function(device) { - return respond( - client.deviceUpdate( - account.signIn.sessionToken, - device.id, - DEVICE_NAME_2, - { - deviceCallback: DEVICE_CALLBACK, - } - ), - RequestMocks.deviceUpdate - ); - }); - }) - .then(function(res) { - assert.ok(res.id); - assert.equal(res.name, DEVICE_NAME_2); - assert.equal(res.pushCallback, DEVICE_CALLBACK); - }, assert.notOk); - }); - - test('#destroy', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.deviceRegister( - account.signIn.sessionToken, - DEVICE_NAME, - DEVICE_TYPE, - { - deviceCallback: DEVICE_CALLBACK, - } - ), - RequestMocks.deviceRegister - ).then(function(device) { - return respond( - client.deviceDestroy(account.signIn.sessionToken, device.id), - RequestMocks.deviceDestroy - ); - }); - }) - .then(function(res) { - assert.equal(Object.keys(res), 0); - }, assert.notOk); - }); + it('#register', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.deviceRegister( + account.signIn.sessionToken, + DEVICE_NAME, + DEVICE_TYPE, + { + deviceCallback: DEVICE_CALLBACK, + deviceAuthKey: DEVICE_AUTH_KEY, + devicePublicKey: DEVICE_PUBLIC_KEY, + } + ), + RequestMocks.deviceRegister + ); + }) + .then( + function(res) { + assert.ok(res.id); + assert.equal(res.name, DEVICE_NAME); + assert.equal(res.pushCallback, DEVICE_CALLBACK); + assert.equal(res.pushAuthKey, DEVICE_AUTH_KEY); + assert.equal(res.pushPublicKey, DEVICE_PUBLIC_KEY); + assert.equal(res.type, DEVICE_TYPE); + }, + function(err) { + console.log(err); + assert.fail(); + } + ); + }); - test('#list', function() { - return accountHelper.newVerifiedAccount().then(function(account) { + it('#update', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.deviceRegister( + account.signIn.sessionToken, + DEVICE_NAME, + DEVICE_TYPE, + { + deviceCallback: DEVICE_CALLBACK, + deviceAuthKey: DEVICE_AUTH_KEY, + devicePublicKey: DEVICE_PUBLIC_KEY, + } + ), + RequestMocks.deviceRegister + ).then(function(device) { return respond( - client.deviceRegister( + client.deviceUpdate( account.signIn.sessionToken, - DEVICE_NAME, - DEVICE_TYPE, + device.id, + DEVICE_NAME_2, { deviceCallback: DEVICE_CALLBACK, + deviceAuthKey: DEVICE_AUTH_KEY, + devicePublicKey: DEVICE_PUBLIC_KEY, } ), - RequestMocks.deviceRegister - ) - .then(function(device) { - return respond( - client.deviceList(account.signIn.sessionToken), - RequestMocks.deviceList - ); - }) + RequestMocks.deviceUpdate + ); + }); + }) + .then(function(res) { + assert.ok(res.id); + assert.equal(res.name, DEVICE_NAME_2); + assert.equal(res.pushCallback, DEVICE_CALLBACK); + }, assert.fail); + }); + + it('#destroy', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.deviceRegister( + account.signIn.sessionToken, + DEVICE_NAME, + DEVICE_TYPE, + { + deviceCallback: DEVICE_CALLBACK, + deviceAuthKey: DEVICE_AUTH_KEY, + devicePublicKey: DEVICE_PUBLIC_KEY, + } + ), + RequestMocks.deviceRegister + ).then(function(device) { + return respond( + client.deviceDestroy(account.signIn.sessionToken, device.id), + RequestMocks.deviceDestroy + ); + }); + }) + .then(function(res) { + assert.equal(Object.keys(res), 0); + }, assert.fail); + }); + + it('#list', function() { + return accountHelper.newVerifiedAccount().then(function(account) { + return respond( + client.deviceRegister( + account.signIn.sessionToken, + DEVICE_NAME, + DEVICE_TYPE, + { + deviceCallback: DEVICE_CALLBACK, + deviceAuthKey: DEVICE_AUTH_KEY, + devicePublicKey: DEVICE_PUBLIC_KEY, + } + ), + RequestMocks.deviceRegister + ) + .then(function(device) { + return respond( + client.deviceList(account.signIn.sessionToken), + RequestMocks.deviceList + ); + }) - .then(function(devices) { - assert.equal(devices.length, 1); + .then(function(devices) { + assert.equal(devices.length, 1); - var device = devices[0]; - assert.ok(device.id); - assert.equal(device.name, DEVICE_NAME); - assert.equal(device.pushCallback, DEVICE_CALLBACK); - assert.equal(device.type, DEVICE_TYPE); - }); + var device = devices[0]; + assert.ok(device.id); + assert.equal(device.name, DEVICE_NAME); + assert.equal(device.pushCallback, DEVICE_CALLBACK); + assert.equal(device.type, DEVICE_TYPE); }); - }); }); - } + }); }); diff --git a/packages/fxa-js-client/tests/lib/emails.js b/packages/fxa-js-client/tests/lib/emails.js index 3266a0d6301..18f79ceff08 100644 --- a/packages/fxa-js-client/tests/lib/emails.js +++ b/packages/fxa-js-client/tests/lib/emails.js @@ -2,199 +2,186 @@ * 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/. */ -define([ - 'intern!tdd', - 'intern/chai!assert', - 'tests/addons/environment', -], function(tdd, assert, Environment) { - var user2; - var user2Email; - - with (tdd) { - suite('emails', function() { - var accountHelper; - var respond; - var mail; - var client; - var RequestMocks; - var account; - - beforeEach(function() { - var env = new Environment(); - accountHelper = env.accountHelper; - respond = env.respond; - mail = env.mail; - client = env.client; - RequestMocks = env.RequestMocks; - - user2 = 'anotherEmail' + new Date().getTime(); - user2Email = user2 + '@restmail.net'; - }); - - function recoveryEmailCreate() { - return accountHelper.newVerifiedAccount().then(function(res) { - account = res; - return respond( - client.recoveryEmailCreate(account.signIn.sessionToken, user2Email), - RequestMocks.recoveryEmailCreate - ); - }, handleError); - } - - function handleError(err) { - console.log(err); - assert.notOk(); - } - - test('#recoveryEmailCreate', function() { - return recoveryEmailCreate().then(function(res) { - assert.ok(res); - }, handleError); - }); - - test('#recoveryEmails', function() { - return recoveryEmailCreate() - .then(function(res) { - assert.ok(res); - return respond( - client.recoveryEmails(account.signIn.sessionToken), - RequestMocks.recoveryEmailsUnverified - ); - }, handleError) - .then(function(res) { - assert.ok(res); - assert.equal(res.length, 2, 'returned two emails'); - assert.equal(res[1].verified, false, 'returned not verified'); - }, handleError); - }); - - test('#verifyCode', function() { - return recoveryEmailCreate() - .then(function(res) { - assert.ok(res); - - return respond( - mail.wait(user2, 1), - RequestMocks.mailUnverifiedEmail - ); - }, handleError) - .then(function(emails) { - var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; - - return respond( - client.verifyCode(account.signIn.uid, code, { - type: 'secondary', - }), - RequestMocks.verifyCode - ); - }) - .then(function(res) { - assert.ok(res); - - return respond( - client.recoveryEmails(account.signIn.sessionToken), - RequestMocks.recoveryEmailsVerified - ); - }, handleError) - .then(function(res) { - assert.ok(res); - assert.equal(res.length, 2, 'returned one email'); - assert.equal(res[1].verified, true, 'returned not verified'); - }, handleError); - }); - - test('#recoveryEmailDestroy', function() { - return recoveryEmailCreate() - .then(function(res) { - assert.ok(res); - - return respond( - client.recoveryEmails(account.signIn.sessionToken), - RequestMocks.recoveryEmailsUnverified - ); - }, handleError) - .then(function(res) { - assert.ok(res); - assert.equal(res.length, 2, 'returned two email'); - assert.equal(res[1].verified, false, 'returned not verified'); - - return respond( - client.recoveryEmailDestroy( - account.signIn.sessionToken, - user2Email - ), - RequestMocks.recoveryEmailDestroy - ); - }, handleError) - .then(function(res) { - assert.ok(res); - - return respond( - client.recoveryEmails(account.signIn.sessionToken), - RequestMocks.recoveryEmails - ); - }, handleError) - .then(function(res) { - assert.ok(res); - assert.equal(res.length, 1, 'returned one email'); - }, handleError); - }); - - test('#recoveryEmailSetPrimaryEmail', function() { - return recoveryEmailCreate() - .then(function(res) { - assert.ok(res); - - return respond( - mail.wait(user2, 1), - RequestMocks.mailUnverifiedEmail - ); - }, handleError) - .then(function(emails) { - var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; - - return respond( - client.verifyCode(account.signIn.uid, code, { - type: 'secondary', - }), - RequestMocks.verifyCode - ); - }) - .then(function(res) { - assert.ok(res); - - return respond( - client.recoveryEmailSetPrimaryEmail( - account.signIn.sessionToken, - user2Email - ), - RequestMocks.recoveryEmailSetPrimaryEmail - ); - }, handleError) - .then(function(res) { - assert.ok(res); - - return respond( - client.recoveryEmails(account.signIn.sessionToken), - RequestMocks.recoveryEmailsSetPrimaryVerified - ); - }, handleError) - .then(function(res) { - assert.ok(res); - assert.equal(res.length, 2, 'returned two emails'); - - assert.equal( - true, - res[0].email.indexOf('anotherEmail') > -1, - 'returned correct primary email' - ); - assert.equal(res[0].verified, true, 'returned verified'); - assert.equal(res[0].isPrimary, true, 'returned isPrimary true'); - - assert.equal(res[1].verified, true, 'returned verified'); - assert.equal(res[1].isPrimary, false, 'returned isPrimary false'); - }, handleError); - }); - }); +const assert = require('chai').assert; + +var user2; +var user2Email; +const Environment = require('../addons/environment'); + +describe('emails', function() { + var accountHelper; + var respond; + var mail; + var client; + var RequestMocks; + var account; + let env; + + beforeEach(function() { + env = new Environment(); + accountHelper = env.accountHelper; + respond = env.respond; + mail = env.mail; + client = env.client; + RequestMocks = env.RequestMocks; + + user2 = 'anotherEmail' + new Date().getTime(); + user2Email = user2 + '@restmail.net'; + }); + + function recoveryEmailCreate() { + return accountHelper.newVerifiedAccount().then(function(res) { + account = res; + return respond( + client.recoveryEmailCreate(account.signIn.sessionToken, user2Email), + RequestMocks.recoveryEmailCreate + ); + }, handleError); } + + function handleError(err) { + console.log(err); + assert.fail(); + } + + it('#recoveryEmailCreate', function() { + return recoveryEmailCreate().then(function(res) { + assert.ok(res); + }, handleError); + }); + + it('#recoveryEmails', function() { + return recoveryEmailCreate() + .then(function(res) { + assert.ok(res); + return respond( + client.recoveryEmails(account.signIn.sessionToken), + RequestMocks.recoveryEmailsUnverified + ); + }, handleError) + .then(function(res) { + assert.ok(res); + assert.equal(res.length, 2, 'returned two emails'); + assert.equal(res[1].verified, false, 'returned not verified'); + }, handleError); + }); + + it('#verifyCode', function() { + return recoveryEmailCreate() + .then(function(res) { + assert.ok(res); + + return respond(mail.wait(user2, 1), RequestMocks.mailUnverifiedEmail); + }, handleError) + .then(function(emails) { + var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; + + return respond( + client.verifyCode(account.signIn.uid, code, { + type: 'secondary', + }), + RequestMocks.verifyCode + ); + }) + .then(function(res) { + assert.ok(res); + + return respond( + client.recoveryEmails(account.signIn.sessionToken), + RequestMocks.recoveryEmailsVerified + ); + }, handleError) + .then(function(res) { + assert.ok(res); + assert.equal(res.length, 2, 'returned one email'); + assert.equal(res[1].verified, true, 'returned not verified'); + }, handleError); + }); + + it('#recoveryEmailDestroy', function() { + return recoveryEmailCreate() + .then(function(res) { + assert.ok(res); + + return respond( + client.recoveryEmails(account.signIn.sessionToken), + RequestMocks.recoveryEmailsUnverified + ); + }, handleError) + .then(function(res) { + assert.ok(res); + assert.equal(res.length, 2, 'returned two email'); + assert.equal(res[1].verified, false, 'returned not verified'); + + return respond( + client.recoveryEmailDestroy(account.signIn.sessionToken, user2Email), + RequestMocks.recoveryEmailDestroy + ); + }, handleError) + .then(function(res) { + assert.ok(res); + + return respond( + client.recoveryEmails(account.signIn.sessionToken), + RequestMocks.recoveryEmails + ); + }, handleError) + .then(function(res) { + assert.ok(res); + assert.equal(res.length, 1, 'returned one email'); + }, handleError); + }); + + it('#recoveryEmailSetPrimaryEmail', function() { + return recoveryEmailCreate() + .then(function(res) { + assert.ok(res); + + return respond(mail.wait(user2, 1), RequestMocks.mailUnverifiedEmail); + }, handleError) + .then(function(emails) { + var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; + + return respond( + client.verifyCode(account.signIn.uid, code, { + type: 'secondary', + }), + RequestMocks.verifyCode + ); + }) + .then(function(res) { + assert.ok(res); + + return respond( + client.recoveryEmailSetPrimaryEmail( + account.signIn.sessionToken, + user2Email + ), + RequestMocks.recoveryEmailSetPrimaryEmail + ); + }, handleError) + .then(function(res) { + assert.ok(res); + + return respond( + client.recoveryEmails(account.signIn.sessionToken), + RequestMocks.recoveryEmailsSetPrimaryVerified + ); + }, handleError) + .then(function(res) { + assert.ok(res); + assert.equal(res.length, 2, 'returned two emails'); + + assert.equal( + true, + res[0].email.indexOf('anotherEmail') > -1, + 'returned correct primary email' + ); + assert.equal(res[0].verified, true, 'returned verified'); + assert.equal(res[0].isPrimary, true, 'returned isPrimary true'); + + assert.equal(res[1].verified, true, 'returned verified'); + assert.equal(res[1].isPrimary, false, 'returned isPrimary false'); + }, handleError); + }); }); diff --git a/packages/fxa-js-client/tests/lib/errors.js b/packages/fxa-js-client/tests/lib/errors.js index 0db5d218ed8..cc4a2375b1c 100644 --- a/packages/fxa-js-client/tests/lib/errors.js +++ b/packages/fxa-js-client/tests/lib/errors.js @@ -2,71 +2,67 @@ * 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/. */ -define([ - 'intern!tdd', - 'intern/chai!assert', - 'tests/addons/environment', -], function(tdd, assert, Environment) { - with (tdd) { - suite('errors', function() { - var accountHelper; - var respond; - var client; - var ErrorMocks; +const assert = require('chai').assert; +const Environment = require('../addons/environment'); - beforeEach(function() { - var env = new Environment(); - accountHelper = env.accountHelper; - respond = env.respond; - client = env.client; - ErrorMocks = env.ErrorMocks; - }); +describe('errors', function() { + var accountHelper; + var respond; + var client; + var ErrorMocks; + let env; - test('#accountUnverified', function() { - return accountHelper - .newUnverifiedAccount() - .then(function(account) { - var pk = { algorithm: 'RS', n: 'x', e: 'y' }; - var duration = 1000; + beforeEach(function() { + env = new Environment(); + accountHelper = env.accountHelper; + respond = env.respond; + client = env.client; + ErrorMocks = env.ErrorMocks; + }); - return respond( - client.certificateSign(account.signIn.sessionToken, pk, duration), - ErrorMocks.accountUnverified - ); - }) - .then( - function() { - assert.fail(); - }, - function(error) { - assert.equal(error.code, 400); - assert.equal(error.errno, 104); - } - ); - }); + it('#accountUnverified', function() { + return accountHelper + .newUnverifiedAccount() + .then(function(account) { + var pk = { algorithm: 'RS', n: 'x', e: 'y' }; + var duration = 1000; - test('#invalidVerificationCode', function() { - return accountHelper - .newUnverifiedAccount() - .then(function(account) { - return respond( - client.verifyCode( - account.signUp.uid, - 'eb531a64deb628b2baeaceaa8762abf0' - ), - ErrorMocks.invalidVerification - ); - }) - .then( - function() { - assert.fail(); - }, - function(error) { - assert.equal(error.code, 400); - assert.equal(error.errno, 105); - } - ); - }); - }); - } + return respond( + client.certificateSign(account.signIn.sessionToken, pk, duration), + ErrorMocks.accountUnverified + ); + }) + .then( + function() { + assert.fail(); + }, + function(error) { + assert.equal(error.code, 400); + assert.equal(error.errno, 104); + } + ); + }); + + it('#invalidVerificationCode', function() { + return accountHelper + .newUnverifiedAccount() + .then(function(account) { + return respond( + client.verifyCode( + account.signUp.uid, + 'eb531a64deb628b2baeaceaa8762abf0' + ), + ErrorMocks.invalidVerification + ); + }) + .then( + function() { + assert.fail(); + }, + function(error) { + assert.equal(error.code, 400); + assert.equal(error.errno, 105); + } + ); + }); }); diff --git a/packages/fxa-js-client/tests/lib/hawkCredentials.js b/packages/fxa-js-client/tests/lib/hawkCredentials.js index f81627fec93..53551fa4587 100644 --- a/packages/fxa-js-client/tests/lib/hawkCredentials.js +++ b/packages/fxa-js-client/tests/lib/hawkCredentials.js @@ -2,37 +2,31 @@ * 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/. */ -define([ - 'intern!tdd', - 'intern/chai!assert', - 'node_modules/sjcl/sjcl', - 'client/lib/hawkCredentials', -], function(tdd, assert, sjcl, hawkCredentials) { - with (tdd) { - suite('hawkCredentials', function() { - test('#client derive hawk credentials', function() { - var context = 'sessionToken'; - var sessionToken = - 'a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf'; +const assert = require('chai').assert; +const sjcl = require('sjcl'); +const hawkCredentials = require('../../client/lib/hawkCredentials'); +describe('hawkCredentials', function() { + it('#client derive hawk credentials', function() { + var context = 'sessionToken'; + var sessionToken = + 'a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf'; - return hawkCredentials(sessionToken, context, 3 * 32).then(function( - result - ) { - var hmacKey = sjcl.codec.hex.fromBits(result.key); + return hawkCredentials(sessionToken, context, 3 * 32).then(function( + result + ) { + var hmacKey = sjcl.codec.hex.fromBits(result.key); - assert.equal( - hmacKey, - '9d8f22998ee7f5798b887042466b72d53e56ab0c094388bf65831f702d2febc0', - '== hmacKey is equal' - ); - assert.equal( - result.id, - 'c0a29dcf46174973da1378696e4c82ae10f723cf4f4d9f75e39f4ae3851595ab', - '== id is equal' - ); - }, - assert.notOk); - }); - }); - } + assert.equal( + hmacKey, + '9d8f22998ee7f5798b887042466b72d53e56ab0c094388bf65831f702d2febc0', + '== hmacKey is equal' + ); + assert.equal( + result.id, + 'c0a29dcf46174973da1378696e4c82ae10f723cf4f4d9f75e39f4ae3851595ab', + '== id is equal' + ); + }, + assert.fail); + }); }); diff --git a/packages/fxa-js-client/tests/lib/headerLang.js b/packages/fxa-js-client/tests/lib/headerLang.js index bc20920bfcc..09916c6cee7 100644 --- a/packages/fxa-js-client/tests/lib/headerLang.js +++ b/packages/fxa-js-client/tests/lib/headerLang.js @@ -2,109 +2,102 @@ * 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/. */ -define([ - 'intern!tdd', - 'intern/chai!assert', - 'tests/addons/environment', -], function(tdd, assert, Environment) { - with (tdd) { - suite('headerLanguage', function() { - var accountHelper; - var respond; - var client; - var mail; - var RequestMocks; +const assert = require('chai').assert; +const Environment = require('../addons/environment'); - beforeEach(function() { - var env = new Environment(); - accountHelper = env.accountHelper; - respond = env.respond; - RequestMocks = env.RequestMocks; - client = env.client; - mail = env.mail; - }); +describe('headerLanguage', function() { + var accountHelper; + var respond; + var client; + var mail; + var RequestMocks; + let env; - test('#signUp', function() { - var user = 'test' + new Date().getTime(); - var email = user + '@restmail.net'; - var password = 'iliketurtles'; - var opts = { - lang: 'zh-cn;', - }; + beforeEach(function() { + env = new Environment(); + accountHelper = env.accountHelper; + respond = env.respond; + RequestMocks = env.RequestMocks; + client = env.client; + mail = env.mail; + }); - return respond( - client.signUp(email, password, opts), - RequestMocks.signUp - ) - .then(function(res) { - assert.ok(res.uid); - return respond(mail.wait(user), RequestMocks.mailSignUpLang); - }) - .then(function(emails) { - assert.property(emails[0], 'headers'); - assert.equal(emails[0].headers['content-language'], 'zh-CN'); - }, assert.notOk); - }); + it('#signUp', function() { + var user = 'test' + new Date().getTime(); + var email = user + '@restmail.net'; + var password = 'iliketurtles'; + var opts = { + lang: 'zh-cn;', + }; - test('#passwordForgotSendCode', function() { - var account; - var passwordForgotToken; - var opts = { - lang: 'zh-CN', - service: 'sync', - }; + return respond(client.signUp(email, password, opts), RequestMocks.signUp) + .then(function(res) { + assert.ok(res.uid); + return respond(mail.wait(user), RequestMocks.mailSignUpLang); + }) + .then(function(emails) { + assert.property(emails[0], 'headers'); + assert.equal(emails[0].headers['content-language'], 'zh-CN'); + }, assert.fail); + }); - return accountHelper - .newUnverifiedAccount() - .then(function(acc) { - account = acc; + it('#passwordForgotSendCode', function() { + var account; + var passwordForgotToken; + var opts = { + lang: 'zh-CN', + service: 'sync', + }; - return respond( - client.passwordForgotSendCode(account.input.email, opts), - RequestMocks.passwordForgotSendCode - ); - }) - .then(function(result) { - passwordForgotToken = result.passwordForgotToken; - assert.ok(passwordForgotToken, 'passwordForgotToken is returned'); + return accountHelper + .newUnverifiedAccount() + .then(function(acc) { + account = acc; - return respond( - mail.wait(account.input.user, 3), - RequestMocks.resetMailLang - ); - }) - .then(function(emails) { - assert.property(emails[2], 'headers'); - assert.equal(emails[2].headers['content-language'], 'zh-CN'); - }, assert.notOk); - }); + return respond( + client.passwordForgotSendCode(account.input.email, opts), + RequestMocks.passwordForgotSendCode + ); + }) + .then(function(result) { + passwordForgotToken = result.passwordForgotToken; + assert.ok(passwordForgotToken, 'passwordForgotToken is returned'); - test('#recoveryEmailResendCode', function() { - var user; - var opts = { - lang: 'zh-CN', - }; + return respond( + mail.wait(account.input.user, 3), + RequestMocks.resetMailLang + ); + }) + .then(function(emails) { + assert.property(emails[2], 'headers'); + assert.equal(emails[2].headers['content-language'], 'zh-CN'); + }, assert.fail); + }); - return accountHelper - .newUnverifiedAccount() - .then(function(account) { - user = account.input.user; + it('#recoveryEmailResendCode', function() { + var user; + var opts = { + lang: 'zh-CN', + }; - return respond( - client.recoveryEmailResendCode(account.signIn.sessionToken, opts), - RequestMocks.recoveryEmailResendCode - ); - }) - .then(function(res) { - assert.ok(res); + return accountHelper + .newUnverifiedAccount() + .then(function(account) { + user = account.input.user; + + return respond( + client.recoveryEmailResendCode(account.signIn.sessionToken, opts), + RequestMocks.recoveryEmailResendCode + ); + }) + .then(function(res) { + assert.ok(res); - return respond(mail.wait(user, 3), RequestMocks.resetMailLang); - }) - .then(function(emails) { - assert.property(emails[2], 'headers'); - assert.equal(emails[2].headers['content-language'], 'zh-CN'); - }, assert.notOk); - }); - }); - } + return respond(mail.wait(user, 3), RequestMocks.resetMailLang); + }) + .then(function(emails) { + assert.property(emails[2], 'headers'); + assert.equal(emails[2].headers['content-language'], 'zh-CN'); + }, assert.fail); + }); }); diff --git a/packages/fxa-js-client/tests/lib/hkdf.js b/packages/fxa-js-client/tests/lib/hkdf.js index 068e137d001..230274777e4 100644 --- a/packages/fxa-js-client/tests/lib/hkdf.js +++ b/packages/fxa-js-client/tests/lib/hkdf.js @@ -2,80 +2,74 @@ * 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/. */ -define([ - 'intern!tdd', - 'intern/chai!assert', - 'node_modules/sjcl/sjcl', - 'client/lib/hkdf', -], function(tdd, assert, sjcl, hkdf) { - with (tdd) { - // test vectors from RFC5869 - suite('hkdf', function() { - test('#vector 1', function() { - var ikm = sjcl.codec.hex.toBits( - '0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b' - ); - var salt = sjcl.codec.hex.toBits('000102030405060708090a0b0c'); - var info = sjcl.codec.hex.toBits('f0f1f2f3f4f5f6f7f8f9'); +const assert = require('chai').assert; +const sjcl = require('sjcl'); +const hkdf = require('../../client/lib/hkdf'); +// test vectors from RFC5869 +describe('hkdf', function() { + it('#vector 1', function() { + var ikm = sjcl.codec.hex.toBits( + '0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b' + ); + var salt = sjcl.codec.hex.toBits('000102030405060708090a0b0c'); + var info = sjcl.codec.hex.toBits('f0f1f2f3f4f5f6f7f8f9'); - return hkdf(ikm, info, salt, 42).then(function(result) { - assert.equal(sjcl.codec.hex.fromBits(result).length, 84); - assert.equal( - sjcl.codec.hex.fromBits(result), - '3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865' - ); - }, assert.notOk); - }); + return hkdf(ikm, info, salt, 42).then(function(result) { + assert.equal(sjcl.codec.hex.fromBits(result).length, 84); + assert.equal( + sjcl.codec.hex.fromBits(result), + '3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865' + ); + }, assert.fail); + }); - test('#vector 2', function() { - var ikm = sjcl.codec.hex.toBits( - '0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b' - ); - var salt = sjcl.codec.hex.toBits(''); - var info = sjcl.codec.hex.toBits(''); + it('#vector 2', function() { + var ikm = sjcl.codec.hex.toBits( + '0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b' + ); + var salt = sjcl.codec.hex.toBits(''); + var info = sjcl.codec.hex.toBits(''); - return hkdf(ikm, info, salt, 42).then(function(result) { - assert.equal(sjcl.codec.hex.fromBits(result).length, 84); - assert.equal( - sjcl.codec.hex.fromBits(result), - '8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8' - ); - }, assert.notOk); - }); + return hkdf(ikm, info, salt, 42).then(function(result) { + assert.equal(sjcl.codec.hex.fromBits(result).length, 84); + assert.equal( + sjcl.codec.hex.fromBits(result), + '8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8' + ); + }, assert.fail); + }); - test('#vector 3', function() { - var ikm = sjcl.codec.hex.toBits( - '4a9cbe5ae7190a7bb7cc54d5d84f5e4ba743904f8a764933b72f10260067375a' - ); - var salt = sjcl.codec.hex.toBits(''); - var info = sjcl.codec.utf8String.toBits( - 'identity.mozilla.com/picl/v1/keyFetchToken' - ); + it('#vector 3', function() { + var ikm = sjcl.codec.hex.toBits( + '4a9cbe5ae7190a7bb7cc54d5d84f5e4ba743904f8a764933b72f10260067375a' + ); + var salt = sjcl.codec.hex.toBits(''); + var info = sjcl.codec.utf8String.toBits( + 'identity.mozilla.com/picl/v1/keyFetchToken' + ); - return hkdf(ikm, info, salt, 3 * 32).then(function(result) { - assert.equal( - sjcl.codec.hex.fromBits(result), - 'f4df04ffb79db35e94e4881719a6f145f9206e8efea17fc9f02a5ce09cbfac1e829a935f34111d75e0d16b7aa178e2766759eedb6f623c0babd2abcfea82bc12af75f6aa543a8ba7e0a029f87c785c4af0ad03889f7437f735b5256a88fc73fd' - ); - }, assert.notOk); - }); + return hkdf(ikm, info, salt, 3 * 32).then(function(result) { + assert.equal( + sjcl.codec.hex.fromBits(result), + 'f4df04ffb79db35e94e4881719a6f145f9206e8efea17fc9f02a5ce09cbfac1e829a935f34111d75e0d16b7aa178e2766759eedb6f623c0babd2abcfea82bc12af75f6aa543a8ba7e0a029f87c785c4af0ad03889f7437f735b5256a88fc73fd' + ); + }, assert.fail); + }); - test('#vector 4', function() { - var ikm = sjcl.codec.hex.toBits( - 'ba0a107dab60f3b065ff7a642d14fe824fbd71bc5c99087e9e172a1abd1634f1' - ); - var salt = sjcl.codec.hex.toBits(''); - var info = sjcl.codec.utf8String.toBits( - 'identity.mozilla.com/picl/v1/account/keys' - ); + it('#vector 4', function() { + var ikm = sjcl.codec.hex.toBits( + 'ba0a107dab60f3b065ff7a642d14fe824fbd71bc5c99087e9e172a1abd1634f1' + ); + var salt = sjcl.codec.hex.toBits(''); + var info = sjcl.codec.utf8String.toBits( + 'identity.mozilla.com/picl/v1/account/keys' + ); - return hkdf(ikm, info, salt, 3 * 32).then(function(result) { - assert.equal( - sjcl.codec.hex.fromBits(result), - '17ab463653a94c9a6419b48781930edefe500395e3b4e7879a2be1599975702285de16c3218a126404668bf9b7acfb6ce2b7e03c8889047ba48b8b854c6d8beb3ae100e145ca6d69cb519a872a83af788771954455716143bc08225ea8644d85' - ); - }, assert.notOk); - }); - }); - } + return hkdf(ikm, info, salt, 3 * 32).then(function(result) { + assert.equal( + sjcl.codec.hex.fromBits(result), + '17ab463653a94c9a6419b48781930edefe500395e3b4e7879a2be1599975702285de16c3218a126404668bf9b7acfb6ce2b7e03c8889047ba48b8b854c6d8beb3ae100e145ca6d69cb519a872a83af788771954455716143bc08225ea8644d85' + ); + }, assert.fail); + }); }); diff --git a/packages/fxa-js-client/tests/lib/init.js b/packages/fxa-js-client/tests/lib/init.js index e9079d4d6b9..d56b9c22c86 100644 --- a/packages/fxa-js-client/tests/lib/init.js +++ b/packages/fxa-js-client/tests/lib/init.js @@ -2,28 +2,22 @@ * 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/. */ -define(['intern!tdd', 'intern/chai!assert', 'client/FxAccountClient'], function( - tdd, - assert, - FxAccountClient -) { - with (tdd) { - suite('init', function() { - test('#should error if no options set', function() { - try { - void new FxAccountClient(); - } catch (e) { - assert.isDefined(e.message); - } - }); +const assert = require('chai').assert; +const FxAccountClient = require('../../client/FxAccountClient'); +describe('init', function() { + it('#should error if no options set', function() { + try { + void new FxAccountClient(); + } catch (e) { + assert.isDefined(e.message); + } + }); - test('#should catch undefined parameters for the url', function() { - try { - void new FxAccountClient(undefined, {}); - } catch (e) { - assert.isDefined(e.message); - } - }); - }); - } + it('#should catch undefined parameters for the url', function() { + try { + void new FxAccountClient(undefined, {}); + } catch (e) { + assert.isDefined(e.message); + } + }); }); diff --git a/packages/fxa-js-client/tests/lib/metricsContext.js b/packages/fxa-js-client/tests/lib/metricsContext.js index 157ab3268f7..8bb0a94e016 100644 --- a/packages/fxa-js-client/tests/lib/metricsContext.js +++ b/packages/fxa-js-client/tests/lib/metricsContext.js @@ -2,52 +2,49 @@ * 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/. */ -define([ - 'intern!tdd', - 'intern/chai!assert', - 'client/lib/metricsContext', -], function(t, assert, metricsContext) { - 'use strict'; +'use strict'; - t.suite('metricsContext', function() { - t.test('interface is correct', function() { - assert.isObject(metricsContext); - assert.lengthOf(Object.keys(metricsContext), 1); - assert.isFunction(metricsContext.marshall); - }); +const assert = require('chai').assert; +const metricsContext = require('../../client/lib/metricsContext'); + +describe('metricsContext', function() { + it('interface is correct', function() { + assert.isObject(metricsContext); + assert.lengthOf(Object.keys(metricsContext), 1); + assert.isFunction(metricsContext.marshall); + }); - t.test('marshall returns correct data', function() { - var input = { - context: 'fx_desktop_v3', - deviceId: '0123456789abcdef0123456789abcdef', - entrypoint: 'menupanel', - entrypointExperiment: 'wibble', - entrypointVariation: 'blee', - flowBeginTime: 1479815991573, - flowId: - '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', - migration: 'sync11', - service: 'sync', - utmCampaign: 'foo', - utmContent: 'bar', - utmMedium: 'baz', - utmSource: 'qux', - utmTerm: 'wibble', - }; + it('marshall returns correct data', function() { + var input = { + context: 'fx_desktop_v3', + deviceId: '0123456789abcdef0123456789abcdef', + entrypoint: 'menupanel', + entrypointExperiment: 'wibble', + entrypointVariation: 'blee', + flowBeginTime: 1479815991573, + flowId: + '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + migration: 'sync11', + service: 'sync', + utmCampaign: 'foo', + utmContent: 'bar', + utmMedium: 'baz', + utmSource: 'qux', + utmTerm: 'wibble', + }; - assert.deepEqual(metricsContext.marshall(input), { - deviceId: input.deviceId, - entrypoint: 'menupanel', - entrypointExperiment: 'wibble', - entrypointVariation: 'blee', - flowBeginTime: input.flowBeginTime, - flowId: input.flowId, - utmCampaign: 'foo', - utmContent: 'bar', - utmMedium: 'baz', - utmSource: 'qux', - utmTerm: 'wibble', - }); + assert.deepEqual(metricsContext.marshall(input), { + deviceId: input.deviceId, + entrypoint: 'menupanel', + entrypointExperiment: 'wibble', + entrypointVariation: 'blee', + flowBeginTime: input.flowBeginTime, + flowId: input.flowId, + utmCampaign: 'foo', + utmContent: 'bar', + utmMedium: 'baz', + utmSource: 'qux', + utmTerm: 'wibble', }); }); }); diff --git a/packages/fxa-js-client/tests/lib/misc.js b/packages/fxa-js-client/tests/lib/misc.js index ea66af7be57..be7991b7657 100644 --- a/packages/fxa-js-client/tests/lib/misc.js +++ b/packages/fxa-js-client/tests/lib/misc.js @@ -2,55 +2,51 @@ * 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/. */ -define([ - 'intern!tdd', - 'intern/chai!assert', - 'tests/addons/environment', -], function(tdd, assert, Environment) { - with (tdd) { - suite('misc', function() { - var respond; - var client; - var RequestMocks; - - beforeEach(function() { - var env = new Environment(); - respond = env.respond; - client = env.client; - RequestMocks = env.RequestMocks; - }); - - test('#getRandomBytes', function() { - return respond( - client.getRandomBytes(), - RequestMocks.getRandomBytes - ).then(function(res) { - assert.property(res, 'data'); - }, assert.notOk); - }); - - test('_required', function() { - assert.doesNotThrow(function() { - client._required(true, 'true_boolean'); - client._required(false, 'false_boolean'); - client._required('string', 'string'); - client._required({ hasValue: true }, 'object_with_value'); - client._required(1, 'number'); - client._required(0, 'zero'); - }); - - assert.throws(function() { - client._required('', 'empty_string'); - }); - - assert.throws(function() { - client._required({}, 'empty_object'); - }); - - assert.throws(function() { - client._required(null, 'null'); - }); - }); +const assert = require('chai').assert; +const Environment = require('../addons/environment'); + +describe('misc', function() { + var respond; + var client; + var RequestMocks; + let env; + + beforeEach(function() { + env = new Environment(); + respond = env.respond; + client = env.client; + RequestMocks = env.RequestMocks; + }); + + it('#getRandomBytes', function() { + return respond(client.getRandomBytes(), RequestMocks.getRandomBytes).then( + function(res) { + assert.property(res, 'data'); + }, + assert.fail + ); + }); + + it('_required', function() { + assert.doesNotThrow(function() { + client._required(true, 'true_boolean'); + client._required(false, 'false_boolean'); + client._required('string', 'string'); + client._required({ hasValue: true }, 'object_with_value'); + client._required(1, 'number'); + client._required(0, 'zero'); + }); + + assert.throws(function() { + client._required('', 'empty_string'); + }); + + assert.throws(function() { + client._required({}, 'empty_object'); + }); + + assert.throws(function() { + client._required(null, 'null'); }); - } + }); }); diff --git a/packages/fxa-js-client/tests/lib/oauth.js b/packages/fxa-js-client/tests/lib/oauth.js index 6cbf46cfc97..2f4a4c96cab 100644 --- a/packages/fxa-js-client/tests/lib/oauth.js +++ b/packages/fxa-js-client/tests/lib/oauth.js @@ -2,211 +2,197 @@ * 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/. */ -define([ - 'intern!tdd', - 'intern/chai!assert', - 'tests/addons/environment', -], function(tdd, assert, Environment) { - // These tests are intended to run against a mock auth-server. To test - // against a local auth-server, you will need to have it correctly - // configured to send sms and specify a real phone number here. - var env = new Environment(); - if (env.useRemoteServer) { - return; - } +const assert = require('chai').assert; +const Environment = require('../addons/environment'); - with (tdd) { - suite('oauth', function() { - var accountHelper; - var respond; - var client; - var RequestMocks; +const CLIENT_ID = 'dcdb5ae7add825d2'; +const PUBLIC_CLIENT_ID = 'a2270f727f45f648'; - beforeEach(function() { - env = new Environment(); - accountHelper = env.accountHelper; - respond = env.respond; - client = env.client; - RequestMocks = env.RequestMocks; - }); +describe('oauth', function() { + var accountHelper; + var respond; + var client; + var RequestMocks; + let env; - test('#createOAuthCode - missing sessionToken', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.createOAuthCode(null, 'client_id', 'state'), - RequestMocks.createOAuthCode - ); - }) - .then(assert.notOk, function(error) { - assert.include(error.message, 'Missing sessionToken'); - }); - }); + beforeEach(function() { + env = new Environment(); + accountHelper = env.accountHelper; + respond = env.respond; + client = env.client; + RequestMocks = env.RequestMocks; + }); - test('#createOAuthCode - missing clientId', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.createOAuthCode( - account.signIn.sessionToken, - null, - 'state' - ), - RequestMocks.createOAuthCode - ); - }) - .then(assert.notOk, function(error) { - assert.include(error.message, 'Missing clientId'); - }); + it('#createOAuthCode - missing sessionToken', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.createOAuthCode(null, CLIENT_ID, 'state'), + RequestMocks.createOAuthCode + ); + }) + .then(assert.fail, function(error) { + assert.include(error.message, 'Missing sessionToken'); }); + }); - test('#createOAuthCode - missing state', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.createOAuthCode( - account.signIn.sessionToken, - 'client_id', - null - ), - RequestMocks.createOAuthCode - ); - }) - .then(assert.notOk, function(error) { - assert.include(error.message, 'Missing state'); - }); + it('#createOAuthCode - missing clientId', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.createOAuthCode(account.signIn.sessionToken, null, 'state'), + RequestMocks.createOAuthCode + ); + }) + .then(assert.fail, function(error) { + assert.include(error.message, 'Missing clientId'); }); + }); - test('#createOAuthCode', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.createOAuthCode( - account.signIn.sessionToken, - 'client_id', - 'state' - ), - RequestMocks.createOAuthCode - ); - }) - .then(function(resp) { - assert.ok(resp); - }, assert.notOk); + it('#createOAuthCode - missing state', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.createOAuthCode(account.signIn.sessionToken, CLIENT_ID, null), + RequestMocks.createOAuthCode + ); + }) + .then(assert.fail, function(error) { + assert.include(error.message, 'Missing state'); }); + }); - test('#createOAuthToken - missing sessionToken', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.createOAuthToken(null, 'client_id'), - RequestMocks.createOAuthToken - ); - }) - .then(assert.notOk, function(error) { - assert.include(error.message, 'Missing sessionToken'); - }); - }); + it('#createOAuthCode', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.createOAuthCode( + account.signIn.sessionToken, + CLIENT_ID, + 'state' + ), + RequestMocks.createOAuthCode + ); + }) + .then(function(resp) { + assert.ok(resp); + }, assert.fail); + }); - test('#createOAuthToken - missing clientId', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.createOAuthToken(account.signIn.sessionToken, null), - RequestMocks.createOAuthToken - ); - }) - .then(assert.notOk, function(error) { - assert.include(error.message, 'Missing clientId'); - }); + it('#createOAuthToken - missing sessionToken', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.createOAuthToken(null, CLIENT_ID), + RequestMocks.createOAuthToken + ); + }) + .then(assert.fail, function(error) { + assert.include(error.message, 'Missing sessionToken'); }); + }); - test('#createOAuthToken', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.createOAuthToken(account.signIn.sessionToken, 'client_id'), - RequestMocks.createOAuthToken - ); - }) - .then(function(resp) { - assert.ok(resp); - }, assert.notOk); + it('#createOAuthToken - missing clientId', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.createOAuthToken(account.signIn.sessionToken, null), + RequestMocks.createOAuthToken + ); + }) + .then(assert.fail, function(error) { + assert.include(error.message, 'Missing clientId'); }); + }); - test('#getOAuthScopedKeyData - missing sessionToken', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.getOAuthScopedKeyData(null, 'client_id', 'profile'), - RequestMocks.getOAuthScopedKeyData - ); - }) - .then(assert.notOk, function(error) { - assert.include(error.message, 'Missing sessionToken'); - }); - }); + it('#createOAuthToken', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.createOAuthToken( + account.signIn.sessionToken, + PUBLIC_CLIENT_ID + ), + RequestMocks.createOAuthToken + ); + }) + .then(function(resp) { + assert.ok(resp); + }, assert.fail); + }); - test('#getOAuthScopedKeyData - missing clientId', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.getOAuthScopedKeyData( - account.signIn.sessionToken, - null, - 'profile' - ), - RequestMocks.getOAuthScopedKeyData - ); - }) - .then(assert.notOk, function(error) { - assert.include(error.message, 'Missing clientId'); - }); + it('#getOAuthScopedKeyData - missing sessionToken', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.getOAuthScopedKeyData(null, CLIENT_ID, 'profile'), + RequestMocks.getOAuthScopedKeyData + ); + }) + .then(assert.fail, function(error) { + assert.include(error.message, 'Missing sessionToken'); }); + }); - test('#getOAuthScopedKeyData - missing scope', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.getOAuthScopedKeyData( - account.signIn.sessionToken, - 'client_id', - null - ), - RequestMocks.getOAuthScopedKeyData - ); - }) - .then(assert.notOk, function(error) { - assert.include(error.message, 'Missing scope'); - }); + it('#getOAuthScopedKeyData - missing clientId', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.getOAuthScopedKeyData( + account.signIn.sessionToken, + null, + 'profile' + ), + RequestMocks.getOAuthScopedKeyData + ); + }) + .then(assert.fail, function(error) { + assert.include(error.message, 'Missing clientId'); }); + }); - test('#getOAuthScopedKeyData', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.getOAuthScopedKeyData( - account.signIn.sessionToken, - 'client_id', - 'profile' - ), - RequestMocks.getOAuthScopedKeyData - ); - }) - .then(function(resp) { - assert.ok(resp); - }, assert.notOk); + it('#getOAuthScopedKeyData - missing scope', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.getOAuthScopedKeyData( + account.signIn.sessionToken, + CLIENT_ID, + null + ), + RequestMocks.getOAuthScopedKeyData + ); + }) + .then(assert.fail, function(error) { + assert.include(error.message, 'Missing scope'); }); - }); - } + }); + + it('#getOAuthScopedKeyData', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.getOAuthScopedKeyData( + account.signIn.sessionToken, + CLIENT_ID, + 'profile' + ), + RequestMocks.getOAuthScopedKeyData + ); + }) + .then(function(resp) { + assert.ok(resp); + }, assert.fail); + }); }); diff --git a/packages/fxa-js-client/tests/lib/passwordChange.js b/packages/fxa-js-client/tests/lib/passwordChange.js index 38094abc452..aabb6eb256d 100644 --- a/packages/fxa-js-client/tests/lib/passwordChange.js +++ b/packages/fxa-js-client/tests/lib/passwordChange.js @@ -2,414 +2,395 @@ * 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/. */ -define([ - 'intern!tdd', - 'intern/chai!assert', - 'sjcl', - 'client/lib/credentials', - 'tests/addons/environment', -], function(tdd, assert, sjcl, credentials, Environment) { - with (tdd) { - suite('passwordChange', function() { - var accountHelper; - var respond; - var mail; - var client; - var RequestMocks; - var ErrorMocks; - var requests; +const assert = require('chai').assert; +const Environment = require('../addons/environment'); +const sjcl = require('sjcl'); +const credentials = require('../../client/lib/credentials'); - beforeEach(function() { - var env = new Environment(); - accountHelper = env.accountHelper; - respond = env.respond; - mail = env.mail; - client = env.client; - RequestMocks = env.RequestMocks; - ErrorMocks = env.ErrorMocks; - requests = env.requests; - }); +describe('passwordChange', function() { + var accountHelper; + var respond; + var mail; + var client; + var RequestMocks; + var ErrorMocks; + var requests; + let env; - test('#basic', function() { - var user = 'test7' + new Date().getTime(); - var email = user + '@restmail.net'; - var password = 'iliketurtles'; - var newPassword = 'ilikefoxes'; - var kB; - var newUnwrapBKey; - var oldCreds; - var uid; - var account; + beforeEach(function() { + env = new Environment(); + accountHelper = env.accountHelper; + respond = env.respond; + mail = env.mail; + client = env.client; + RequestMocks = env.RequestMocks; + ErrorMocks = env.ErrorMocks; + requests = env.requests; + }); - // newUnwrapBKey from email+newpassword. The submitted newWrapKB - // should equal (kB XOR newUnwrapBKey). This way we don't need to - // know what the server will return for wrapKB: handy, since - // sometimes we're using a mock (with a fixed response), but - // sometimes we're using a real server (which randomly creates - // wrapKB) + it('#basic', function() { + var user = 'test7' + new Date().getTime(); + var email = user + '@restmail.net'; + var password = 'iliketurtles'; + var newPassword = 'ilikefoxes'; + var kB; + var newUnwrapBKey; + var oldCreds; + var uid; + var account; - return credentials - .setup(email, newPassword) - .then(function(newCreds) { - newUnwrapBKey = sjcl.codec.hex.fromBits(newCreds.unwrapBKey); - return respond(client.signUp(email, password), RequestMocks.signUp); - }) - .then(function(result) { - uid = result.uid; + // newUnwrapBKey from email+newpassword. The submitted newWrapKB + // should equal (kB XOR newUnwrapBKey). This way we don't need to + // know what the server will return for wrapKB: handy, since + // sometimes we're using a mock (with a fixed response), but + // sometimes we're using a real server (which randomly creates + // wrapKB) - return respond(mail.wait(user), RequestMocks.mail); - }) - .then(function(emails) { - var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; + return credentials + .setup(email, newPassword) + .then(function(newCreds) { + newUnwrapBKey = sjcl.codec.hex.fromBits(newCreds.unwrapBKey); + return respond(client.signUp(email, password), RequestMocks.signUp); + }) + .then(function(result) { + uid = result.uid; - return respond( - client.verifyCode(uid, code), - RequestMocks.verifyCode - ); - }) - .then(function() { - return respond( - client.signIn(email, password, { keys: true }), - RequestMocks.signInWithKeys - ); - }) - .then(function(result) { - account = result; - }) - .then(function() { - return respond( - client.accountKeys(account.keyFetchToken, account.unwrapBKey), - RequestMocks.accountKeys - ); - }) - .then(function(keys) { - kB = keys.kB; - }) - .then(function() { - return respond( - client._passwordChangeStart(email, password), - RequestMocks.passwordChangeStart - ); - }) - .then(function(credentials) { - oldCreds = credentials; - assert.equal(credentials.emailToHashWith, email); + return respond(mail.wait(user), RequestMocks.mail); + }) + .then(function(emails) { + var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; - return respond( - client._passwordChangeKeys(oldCreds), - RequestMocks.accountKeys - ); - }) - .then(function(keys) { - return respond( - client._passwordChangeFinish(email, newPassword, oldCreds, keys, { - keys: false, - }), - RequestMocks.passwordChangeFinish - ); - }) - .then(function(result) { - // currently only available for mocked requests (issue #103) - if (requests) { - var req = requests[requests.length - 1]; - var args = JSON.parse(req.requestBody); - var expectedNewWrapKB = sjcl.codec.hex.fromBits( - credentials.xor( - sjcl.codec.hex.toBits(kB), - sjcl.codec.hex.toBits(newUnwrapBKey) - ) - ); - assert.equal(args.wrapKb, expectedNewWrapKB); - } - assert.notProperty(result, 'keyFetchToken'); + return respond(client.verifyCode(uid, code), RequestMocks.verifyCode); + }) + .then(function() { + return respond( + client.signIn(email, password, { keys: true }), + RequestMocks.signInWithKeys + ); + }) + .then(function(result) { + account = result; + }) + .then(function() { + return respond( + client.accountKeys(account.keyFetchToken, account.unwrapBKey), + RequestMocks.accountKeys + ); + }) + .then(function(keys) { + kB = keys.kB; + }) + .then(function() { + return respond( + client._passwordChangeStart(email, password), + RequestMocks.passwordChangeStart + ); + }) + .then(function(credentials) { + oldCreds = credentials; + assert.equal(credentials.emailToHashWith, email); - return respond( - client.signIn(email, newPassword), - RequestMocks.signIn - ); - }) - .then( - function(res) { - assert.property(res, 'sessionToken'); - }, - function(err) { - throw err; - } + return respond( + client._passwordChangeKeys(oldCreds), + RequestMocks.accountKeys + ); + }) + .then(function(keys) { + return respond( + client._passwordChangeFinish(email, newPassword, oldCreds, keys, { + keys: false, + }), + RequestMocks.passwordChangeFinish + ); + }) + .then(function(result) { + // currently only available for mocked requests (issue #103) + if (requests) { + var req = requests[requests.length - 1]; + var args = JSON.parse(req.requestBody); + var expectedNewWrapKB = sjcl.codec.hex.fromBits( + credentials.xor( + sjcl.codec.hex.toBits(kB), + sjcl.codec.hex.toBits(newUnwrapBKey) + ) ); - }); + assert.equal(args.wrapKb, expectedNewWrapKB); + } + assert.notProperty(result, 'keyFetchToken'); - test('#keys', function() { - var user = 'test7' + new Date().getTime(); - var email = user + '@restmail.net'; - var password = 'iliketurtles'; - var newPassword = 'ilikefoxes'; - var kB; - var newUnwrapBKey; - var oldCreds; - var sessionToken; - var uid; - var account; + return respond(client.signIn(email, newPassword), RequestMocks.signIn); + }) + .then( + function(res) { + assert.property(res, 'sessionToken'); + }, + function(err) { + throw err; + } + ); + }); - // newUnwrapBKey from email+newpassword. The submitted newWrapKB - // should equal (kB XOR newUnwrapBKey). This way we don't need to - // know what the server will return for wrapKB: handy, since - // sometimes we're using a mock (with a fixed response), but - // sometimes we're using a real server (which randomly creates - // wrapKB) + it('#keys', function() { + var user = 'test7' + new Date().getTime(); + var email = user + '@restmail.net'; + var password = 'iliketurtles'; + var newPassword = 'ilikefoxes'; + var kB; + var newUnwrapBKey; + var oldCreds; + var sessionToken; + var uid; + var account; - return credentials - .setup(email, newPassword) - .then(function(newCreds) { - newUnwrapBKey = sjcl.codec.hex.fromBits(newCreds.unwrapBKey); - return respond(client.signUp(email, password), RequestMocks.signUp); - }) - .then(function(result) { - uid = result.uid; + // newUnwrapBKey from email+newpassword. The submitted newWrapKB + // should equal (kB XOR newUnwrapBKey). This way we don't need to + // know what the server will return for wrapKB: handy, since + // sometimes we're using a mock (with a fixed response), but + // sometimes we're using a real server (which randomly creates + // wrapKB) - return respond(mail.wait(user), RequestMocks.mail); - }) - .then(function(emails) { - var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; + return credentials + .setup(email, newPassword) + .then(function(newCreds) { + newUnwrapBKey = sjcl.codec.hex.fromBits(newCreds.unwrapBKey); + return respond(client.signUp(email, password), RequestMocks.signUp); + }) + .then(function(result) { + uid = result.uid; - return respond( - client.verifyCode(uid, code), - RequestMocks.verifyCode - ); - }) - .then(function() { - return respond( - client.signIn(email, password, { keys: true }), - RequestMocks.signInWithKeys - ); - }) - .then(function(result) { - sessionToken = result.sessionToken; - account = result; - }) - .then(function() { - return respond( - client.accountKeys(account.keyFetchToken, account.unwrapBKey), - RequestMocks.accountKeys - ); - }) - .then(function(keys) { - kB = keys.kB; - }) - .then(function() { - return respond( - client._passwordChangeStart(email, password), - RequestMocks.passwordChangeStart - ); - }) - .then(function(credentials) { - oldCreds = credentials; - assert.equal(credentials.emailToHashWith, email); + return respond(mail.wait(user), RequestMocks.mail); + }) + .then(function(emails) { + var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; - return respond( - client._passwordChangeKeys(oldCreds), - RequestMocks.accountKeys - ); - }) - .then(function(keys) { - return respond( - client._passwordChangeFinish(email, newPassword, oldCreds, keys, { - keys: true, - sessionToken: sessionToken, - }), - RequestMocks.passwordChangeFinishKeys - ); - }) - .then(function(result) { - // currently only available for mocked requests (issue #103) - if (requests) { - var req = requests[requests.length - 1]; - var args = JSON.parse(req.requestBody); - var expectedNewWrapKB = sjcl.codec.hex.fromBits( - credentials.xor( - sjcl.codec.hex.toBits(kB), - sjcl.codec.hex.toBits(newUnwrapBKey) - ) - ); - assert.equal(args.wrapKb, expectedNewWrapKB); - } - assert.property(result, 'sessionToken'); - assert.property(result, 'keyFetchToken'); - assert.property(result, 'unwrapBKey'); - assert.isTrue(result.verified); + return respond(client.verifyCode(uid, code), RequestMocks.verifyCode); + }) + .then(function() { + return respond( + client.signIn(email, password, { keys: true }), + RequestMocks.signInWithKeys + ); + }) + .then(function(result) { + sessionToken = result.sessionToken; + account = result; + }) + .then(function() { + return respond( + client.accountKeys(account.keyFetchToken, account.unwrapBKey), + RequestMocks.accountKeys + ); + }) + .then(function(keys) { + kB = keys.kB; + }) + .then(function() { + return respond( + client._passwordChangeStart(email, password), + RequestMocks.passwordChangeStart + ); + }) + .then(function(credentials) { + oldCreds = credentials; + assert.equal(credentials.emailToHashWith, email); - return respond( - client.signIn(email, newPassword), - RequestMocks.signIn - ); - }) - .then( - function(res) { - assert.property(res, 'sessionToken'); - }, - function(err) { - throw err; - } + return respond( + client._passwordChangeKeys(oldCreds), + RequestMocks.accountKeys + ); + }) + .then(function(keys) { + return respond( + client._passwordChangeFinish(email, newPassword, oldCreds, keys, { + keys: true, + sessionToken: sessionToken, + }), + RequestMocks.passwordChangeFinishKeys + ); + }) + .then(function(result) { + // currently only available for mocked requests (issue #103) + if (requests) { + var req = requests[requests.length - 1]; + var args = JSON.parse(req.requestBody); + var expectedNewWrapKB = sjcl.codec.hex.fromBits( + credentials.xor( + sjcl.codec.hex.toBits(kB), + sjcl.codec.hex.toBits(newUnwrapBKey) + ) ); - }); + assert.equal(args.wrapKb, expectedNewWrapKB); + } + assert.property(result, 'sessionToken'); + assert.property(result, 'keyFetchToken'); + assert.property(result, 'unwrapBKey'); + assert.isTrue(result.verified); - test('#with incorrect case', function() { - var newPassword = 'ilikefoxes'; - var account; - var oldCreds; + return respond(client.signIn(email, newPassword), RequestMocks.signIn); + }) + .then( + function(res) { + assert.property(res, 'sessionToken'); + }, + function(err) { + throw err; + } + ); + }); - return accountHelper - .newVerifiedAccount() - .then(function(acc) { - account = acc; - var incorrectCaseEmail = - account.input.email.charAt(0).toUpperCase() + - account.input.email.slice(1); + it('#with incorrect case', function() { + var newPassword = 'ilikefoxes'; + var account; + var oldCreds; - return respond( - client._passwordChangeStart( - incorrectCaseEmail, - account.input.password - ), - RequestMocks.passwordChangeStart - ); - }) - .then(function(credentials) { - oldCreds = credentials; + return accountHelper + .newVerifiedAccount() + .then(function(acc) { + account = acc; + var incorrectCaseEmail = + account.input.email.charAt(0).toUpperCase() + + account.input.email.slice(1); - return respond( - client._passwordChangeKeys(oldCreds), - RequestMocks.accountKeys - ); - }) - .then(function(keys) { - return respond( - client._passwordChangeFinish( - account.input.email, - newPassword, - oldCreds, - keys - ), - RequestMocks.passwordChangeFinish - ); - }) - .then(function(result) { - assert.ok(result, '{}'); + return respond( + client._passwordChangeStart( + incorrectCaseEmail, + account.input.password + ), + RequestMocks.passwordChangeStart + ); + }) + .then(function(credentials) { + oldCreds = credentials; - return respond( - client.signIn(account.input.email, newPassword), - RequestMocks.signIn - ); - }) - .then( - function(res) { - assert.property(res, 'sessionToken'); - }, - function(err) { - throw err; - } - ); - }); + return respond( + client._passwordChangeKeys(oldCreds), + RequestMocks.accountKeys + ); + }) + .then(function(keys) { + return respond( + client._passwordChangeFinish( + account.input.email, + newPassword, + oldCreds, + keys + ), + RequestMocks.passwordChangeFinish + ); + }) + .then(function(result) { + assert.ok(result, '{}'); - test('#with incorrect case with skipCaseError', function() { - var account; + return respond( + client.signIn(account.input.email, newPassword), + RequestMocks.signIn + ); + }) + .then( + function(res) { + assert.property(res, 'sessionToken'); + }, + function(err) { + throw err; + } + ); + }); - return accountHelper - .newVerifiedAccount() - .then(function(acc) { - account = acc; - var incorrectCaseEmail = - account.input.email.charAt(0).toUpperCase() + - account.input.email.slice(1); + it('#with incorrect case with skipCaseError', function() { + var account; - return respond( - client._passwordChangeStart( - incorrectCaseEmail, - account.input.password, - { skipCaseError: true } - ), - ErrorMocks.incorrectEmailCase - ); - }) - .then( - function() { - assert.fail(); - }, - function(res) { - assert.equal(res.code, 400); - assert.equal(res.errno, 120); - } - ); - }); + return accountHelper + .newVerifiedAccount() + .then(function(acc) { + account = acc; + var incorrectCaseEmail = + account.input.email.charAt(0).toUpperCase() + + account.input.email.slice(1); + + return respond( + client._passwordChangeStart( + incorrectCaseEmail, + account.input.password, + { skipCaseError: true } + ), + ErrorMocks.incorrectEmailCase + ); + }) + .then( + function() { + assert.fail(); + }, + function(res) { + assert.equal(res.code, 400); + assert.equal(res.errno, 120); + } + ); + }); - /** - * Changing the Password failure - */ - test('#changeFailure', function() { - var user = 'test8' + new Date().getTime(); - var email = user + '@restmail.net'; - var password = 'iliketurtles'; - var newPassword = 'ilikefoxes'; - var wrongPassword = '12345678'; - var uid; - var oldCreds; + /** + * Changing the Password failure + */ + it('#changeFailure', function() { + var user = 'test8' + new Date().getTime(); + var email = user + '@restmail.net'; + var password = 'iliketurtles'; + var newPassword = 'ilikefoxes'; + var wrongPassword = '12345678'; + var uid; + var oldCreds; - return respond(client.signUp(email, password), RequestMocks.signUp) - .then(function(result) { - uid = result.uid; + return respond(client.signUp(email, password), RequestMocks.signUp) + .then(function(result) { + uid = result.uid; - return respond(mail.wait(user), RequestMocks.mail); - }) - .then(function(emails) { - var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; + return respond(mail.wait(user), RequestMocks.mail); + }) + .then(function(emails) { + var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; - return respond( - client.verifyCode(uid, code), - RequestMocks.verifyCode - ); - }) - .then(function() { - return respond( - client._passwordChangeStart(email, password), - RequestMocks.passwordChangeStart - ); - }) - .then(function(credentials) { - oldCreds = credentials; - assert.equal(credentials.emailToHashWith, email); - return respond( - client._passwordChangeKeys(oldCreds), - RequestMocks.accountKeys - ); - }) - .then(function(keys) { - return respond( - client._passwordChangeFinish(email, newPassword, oldCreds, keys), - RequestMocks.passwordChangeFinish - ); - }) - .then(function(result) { - assert.ok(result); + return respond(client.verifyCode(uid, code), RequestMocks.verifyCode); + }) + .then(function() { + return respond( + client._passwordChangeStart(email, password), + RequestMocks.passwordChangeStart + ); + }) + .then(function(credentials) { + oldCreds = credentials; + assert.equal(credentials.emailToHashWith, email); + return respond( + client._passwordChangeKeys(oldCreds), + RequestMocks.accountKeys + ); + }) + .then(function(keys) { + return respond( + client._passwordChangeFinish(email, newPassword, oldCreds, keys), + RequestMocks.passwordChangeFinish + ); + }) + .then(function(result) { + assert.ok(result); - return respond( - client.signIn(email, wrongPassword), - ErrorMocks.accountIncorrectPassword - ); - }) - .then( - function() { - assert.fail(); - }, - function(error) { - assert.ok(error); - assert.equal( - error.message, - 'Incorrect password', - '== Password is incorrect' - ); - assert.equal(error.code, 400, '== Correct status code'); - } + return respond( + client.signIn(email, wrongPassword), + ErrorMocks.accountIncorrectPassword + ); + }) + .then( + function() { + assert.fail(); + }, + function(error) { + assert.ok(error); + assert.equal( + error.message, + 'Incorrect password', + '== Password is incorrect' ); - }); - }); - } + assert.equal(error.code, 400, '== Correct status code'); + } + ); + }); }); diff --git a/packages/fxa-js-client/tests/lib/push-constants.js b/packages/fxa-js-client/tests/lib/push-constants.js deleted file mode 100644 index d8952c0a3f0..00000000000 --- a/packages/fxa-js-client/tests/lib/push-constants.js +++ /dev/null @@ -1,17 +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/. */ - -define([], function() { - return { - DEVICE_CALLBACK: - 'https://updates.push.services.mozilla.com/update/abcdef01234567890abcdefabcdef01234567890abcdef', - DEVICE_ID: '0f7aa00356e5416e82b3bef7bc409eef', - DEVICE_NAME: 'My Phone', - DEVICE_NAME_2: 'My Android Phone', - DEVICE_PUBLIC_KEY: - 'BBXOKjUb84pzws1wionFpfCBjDuCh4-s_1b52WA46K5wYL2gCWEOmFKWn_NkS5nmJwTBuO8qxxdjAIDtNeklvQc', - DEVICE_AUTH_KEY: 'GSsIiaD2Mr83iPqwFNK4rw', - DEVICE_TYPE: 'mobile', - }; -}); diff --git a/packages/fxa-js-client/tests/lib/recoveryCodes.js b/packages/fxa-js-client/tests/lib/recoveryCodes.js index 54b1d732f92..2ba1ae947e3 100644 --- a/packages/fxa-js-client/tests/lib/recoveryCodes.js +++ b/packages/fxa-js-client/tests/lib/recoveryCodes.js @@ -2,144 +2,128 @@ * 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/. */ -define([ - 'intern!tdd', - 'intern/chai!assert', - 'tests/addons/environment', - 'tests/addons/sinon', - 'node_modules/otplib/otplib-browser', -], function(tdd, assert, Environment, sinon, otplib) { - with (tdd) { - suite('recovery codes', function() { - var account; - var accountHelper; - var respond; - var client; - var RequestMocks; - var env; - var xhr; - var xhrOpen; - var xhrSend; - var recoveryCodes; - var metricsContext; +const assert = require('chai').assert; +const Environment = require('../addons/environment'); - beforeEach(function() { - env = new Environment(); - accountHelper = env.accountHelper; - respond = env.respond; - client = env.client; - RequestMocks = env.RequestMocks; - metricsContext = { - flowBeginTime: Date.now(), - flowId: - '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', - }; +const sinon = require('sinon'); +const otplib = require('otplib'); +describe('recovery codes', function() { + var account; + var accountHelper; + var respond; + var client; + var RequestMocks; + var env; + var xhr; + var xhrOpen; + var xhrSend; + var recoveryCodes; + var metricsContext; - return accountHelper - .newVerifiedAccount() - .then(function(newAccount) { - account = newAccount; - return respond( - client.createTotpToken(account.signIn.sessionToken), - RequestMocks.createTotpToken - ); - }) - .then(function(res) { - assert.ok(res.qrCodeUrl, 'should return QR code data encoded url'); - assert.ok( - res.secret, - 'should return secret that is encoded in url' - ); + beforeEach(function() { + env = new Environment(); + accountHelper = env.accountHelper; + respond = env.respond; + client = env.client; + RequestMocks = env.RequestMocks; + metricsContext = { + flowBeginTime: Date.now(), + flowId: + '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + }; - var authenticator = new otplib.authenticator.Authenticator(); - authenticator.options = otplib.authenticator.options; + return accountHelper + .newVerifiedAccount() + .then(function(newAccount) { + account = newAccount; + return respond( + client.createTotpToken(account.signIn.sessionToken), + RequestMocks.createTotpToken + ); + }) + .then(function(res) { + assert.ok(res.qrCodeUrl, 'should return QR code data encoded url'); + assert.ok(res.secret, 'should return secret that is encoded in url'); - var code = authenticator.generate(res.secret); - return respond( - client.verifyTotpCode(account.signIn.sessionToken, code), - RequestMocks.verifyTotpCodeTrueEnableToken - ); - }) - .then(function(res) { - assert.equal( - res.recoveryCodes.length, - 8, - 'should return recovery codes' - ); - recoveryCodes = res.recoveryCodes; + var authenticator = new otplib.authenticator.Authenticator(); + authenticator.options = otplib.authenticator.options; - xhr = env.xhr; - xhrOpen = sinon.spy(xhr.prototype, 'open'); - xhrSend = sinon.spy(xhr.prototype, 'send'); - }); - }); + var code = authenticator.generate(res.secret); + return respond( + client.verifyTotpCode(account.signIn.sessionToken, code), + RequestMocks.verifyTotpCodeTrueEnableToken + ); + }) + .then(function(res) { + assert.equal( + res.recoveryCodes.length, + 3, + 'should return recovery codes' + ); + recoveryCodes = res.recoveryCodes; - afterEach(function() { - xhrOpen.restore(); - xhrSend.restore(); + xhr = env.xhr; + xhrOpen = sinon.spy(xhr.prototype, 'open'); + xhrSend = sinon.spy(xhr.prototype, 'send'); }); + }); - test('#consumeRecoveryCode - fails for invalid code', function() { - return respond( - client.consumeRecoveryCode(account.signIn.sessionToken, '00000000'), - RequestMocks.consumeRecoveryCodeInvalidCode - ).then(assert.fail, function(err) { - assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); - assert.include( - xhrOpen.args[0][1], - '/session/verify/recoveryCode', - 'path is correct' - ); - assert.equal(err.errno, 156, 'invalid recovery code errno'); - }); - }); + afterEach(function() { + xhrOpen.restore(); + xhrSend.restore(); + }); - test('#consumeRecoveryCode - consumes valid code', function() { - var code = recoveryCodes[0]; - return respond( - client.consumeRecoveryCode(account.signIn.sessionToken, code, { - metricsContext: metricsContext, - }), - RequestMocks.consumeRecoveryCodeSuccess - ).then(function(res) { - assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); - assert.include( - xhrOpen.args[0][1], - '/session/verify/recoveryCode', - 'path is correct' - ); - var sentData = JSON.parse(xhrSend.args[0][0]); - assert.lengthOf(Object.keys(sentData), 1); - assert.equal(sentData.code, code, 'code is correct'); + it('#consumeRecoveryCode - fails for invalid code', function() { + return respond( + client.consumeRecoveryCode(account.signIn.sessionToken, '00000000'), + RequestMocks.consumeRecoveryCodeInvalidCode + ).then(assert.fail, function(err) { + assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); + assert.include( + xhrOpen.args[0][1], + '/session/verify/recoveryCode', + 'path is correct' + ); + assert.equal(err.errno, 156, 'invalid recovery code errno'); + }); + }); - assert.equal(res.remaining, 7, 'correct remaining recovery codes'); - }); - }); + it('#consumeRecoveryCode - consumes valid code', function() { + var code = recoveryCodes[0]; + return respond( + client.consumeRecoveryCode(account.signIn.sessionToken, code, { + metricsContext: metricsContext, + }), + RequestMocks.consumeRecoveryCodeSuccess + ).then(function(res) { + assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); + assert.include( + xhrOpen.args[0][1], + '/session/verify/recoveryCode', + 'path is correct' + ); + var sentData = JSON.parse(xhrSend.args[0][0]); + assert.lengthOf(Object.keys(sentData), 1); + assert.equal(sentData.code, code, 'code is correct'); - test('#replaceRecoveryCodes - replaces current recovery codes', function() { - return respond( - client.replaceRecoveryCodes(account.signIn.sessionToken), - RequestMocks.replaceRecoveryCodesSuccessNew - ).then(function(res) { - assert.equal(xhrOpen.args[0][0], 'GET', 'method is correct'); - assert.include( - xhrOpen.args[0][1], - '/recoveryCodes', - 'path is correct' - ); + assert.equal(res.remaining, 2, 'correct remaining recovery codes'); + }); + }); - assert.equal( - res.recoveryCodes.length, - 8, - 'should return recovery codes' - ); - assert.notDeepEqual( - res.recoveryCodes, - recoveryCodes, - 'should not be the same codes' - ); - }); - }); + it('#replaceRecoveryCodes - replaces current recovery codes', function() { + return respond( + client.replaceRecoveryCodes(account.signIn.sessionToken), + RequestMocks.replaceRecoveryCodesSuccessNew + ).then(function(res) { + assert.equal(xhrOpen.args[0][0], 'GET', 'method is correct'); + assert.include(xhrOpen.args[0][1], '/recoveryCodes', 'path is correct'); + + assert.equal(res.recoveryCodes.length, 3, 'should return recovery codes'); + assert.notDeepEqual( + res.recoveryCodes, + recoveryCodes, + 'should not be the same codes' + ); }); - } + }); }); diff --git a/packages/fxa-js-client/tests/lib/recoveryEmail.js b/packages/fxa-js-client/tests/lib/recoveryEmail.js index 2a8184d6dcc..905b2c4891d 100644 --- a/packages/fxa-js-client/tests/lib/recoveryEmail.js +++ b/packages/fxa-js-client/tests/lib/recoveryEmail.js @@ -2,104 +2,96 @@ * 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/. */ -define([ - 'intern!tdd', - 'intern/chai!assert', - 'tests/addons/environment', -], function(tdd, assert, Environment) { - with (tdd) { - suite('recoveryEmail', function() { - var accountHelper; - var respond; - var mail; - var client; - var RequestMocks; +const assert = require('chai').assert; +const Environment = require('../addons/environment'); - beforeEach(function() { - var env = new Environment(); - accountHelper = env.accountHelper; - respond = env.respond; - mail = env.mail; - client = env.client; - RequestMocks = env.RequestMocks; - }); +describe('recoveryEmail', function() { + var accountHelper; + var respond; + var mail; + var client; + var RequestMocks; + let env; - test('#recoveryEmail - recoveryEmailResendCode', function() { - var user; + beforeEach(function() { + env = new Environment(); + accountHelper = env.accountHelper; + respond = env.respond; + mail = env.mail; + client = env.client; + RequestMocks = env.RequestMocks; + }); - return accountHelper - .newUnverifiedAccount() - .then(function(account) { - user = account.input.user; + it('#recoveryEmail - recoveryEmailResendCode', function() { + var user; - return respond( - client.recoveryEmailResendCode(account.signIn.sessionToken), - RequestMocks.recoveryEmailResendCode - ); - }) - .then(function(res) { - assert.ok(res); + return accountHelper + .newUnverifiedAccount() + .then(function(account) { + user = account.input.user; - return respond( - mail.wait(user, 3), - RequestMocks.resetMailrecoveryEmailResendCode - ); - }) - .then(function(emails) { - // second email, the code is resent. - var code = emails[2].html.match(/code=([A-Za-z0-9]+)/)[1]; - assert.ok(code, 'code is returned'); - }, assert.notOk); - }); + return respond( + client.recoveryEmailResendCode(account.signIn.sessionToken), + RequestMocks.recoveryEmailResendCode + ); + }) + .then(function(res) { + assert.ok(res); - test('#recoveryEmailResendCode with service, redirectTo, type, style and resume', function() { - var user; - var opts = { - service: 'sync', - redirectTo: 'https://sync.127.0.0.1/after_reset', - resume: 'resumejwt', - style: 'trailhead', - type: 'upgradeSession', - }; + return respond( + mail.wait(user, 3), + RequestMocks.resetMailrecoveryEmailResendCode + ); + }) + .then(function(emails) { + // second email, the code is resent. + var code = emails[2].html.match(/code=([A-Za-z0-9]+)/)[1]; + assert.ok(code, 'code is returned'); + }, assert.fail); + }); - return accountHelper - .newUnverifiedAccount() - .then(function(account) { - user = account.input.user; + it('#recoveryEmailResendCode with service, redirectTo, type and resume', function() { + var user; + var opts = { + service: 'sync', + redirectTo: 'https://sync.127.0.0.1/after_reset', + resume: 'resumejwt', + type: 'upgradeSession', + }; - return respond( - client.recoveryEmailResendCode(account.signIn.sessionToken, opts), - RequestMocks.recoveryEmailResendCode - ); - }) - .then(function(res) { - assert.ok(res); + return accountHelper + .newUnverifiedAccount() + .then(function(account) { + user = account.input.user; - return respond( - mail.wait(user, 3), - RequestMocks.resetMailWithServiceAndRedirectNoSignup - ); - }) - .then(function(emails) { - // second email, the code is resent. - var code = emails[2].html.match(/code=([A-Za-z0-9]+)/); - assert.ok(code, 'code found'); - var service = emails[2].html.match(/service=([A-Za-z0-9]+)/); - assert.ok(service, 'service found'); - var redirectTo = emails[2].html.match(/redirectTo=([A-Za-z0-9]+)/); - assert.ok(redirectTo, 'redirectTo found'); - var resume = emails[2].html.match(/resume=([A-Za-z0-9]+)/); - assert.ok(resume, 'resume found'); - var style = emails[2].html.match(/style=trailhead/)[0]; - assert.ok(style, 'style found'); + return respond( + client.recoveryEmailResendCode(account.signIn.sessionToken, opts), + RequestMocks.recoveryEmailResendCode + ); + }) + .then(function(res) { + assert.ok(res); - assert.ok(code[1], 'code is returned'); - assert.equal(service[1], 'sync', 'service is returned'); - assert.equal(redirectTo[1], 'https', 'redirectTo is returned'); - assert.equal(resume[1], 'resumejwt', 'resume is returned'); - assert.ok(style, 'style is returned'); - }, assert.notOk); - }); - }); - } + return respond( + mail.wait(user, 3), + RequestMocks.resetMailWithServiceAndRedirectNoSignup + ); + }) + .then(function(emails) { + // second email, the code is resent. + var code = emails[2].html.match(/code=([A-Za-z0-9]+)/); + assert.ok(code, 'code found'); + var service = emails[2].html.match(/service=([A-Za-z0-9]+)/); + assert.ok(service, 'service found'); + var redirectTo = emails[2].html.match(/redirectTo=([A-Za-z0-9]+)/); + assert.ok(redirectTo, 'redirectTo found'); + var resume = emails[2].html.match(/resume=([A-Za-z0-9]+)/); + assert.ok(resume, 'resume found'); + + assert.ok(code[1], 'code is returned'); + assert.equal(service[1], 'sync', 'service is returned'); + assert.equal(redirectTo[1], 'https', 'redirectTo is returned'); + assert.equal(resume[1], 'resumejwt', 'resume is returned'); + }, assert.fail); + }); }); diff --git a/packages/fxa-js-client/tests/lib/recoveryKeys.js b/packages/fxa-js-client/tests/lib/recoveryKeys.js index 91a3aed3213..42ac2b51f08 100644 --- a/packages/fxa-js-client/tests/lib/recoveryKeys.js +++ b/packages/fxa-js-client/tests/lib/recoveryKeys.js @@ -2,192 +2,220 @@ * 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/. */ -define([ - 'intern!tdd', - 'intern/chai!assert', - 'tests/addons/environment', - 'tests/addons/sinon', -], function(tdd, assert, Environment, sinon) { - with (tdd) { - suite('recovery key', function() { - var account; - var accountHelper; - var respond; - var client; - var email; - var RequestMocks; - var env; - var xhr; - var xhrOpen; - var xhrSend; - var keys; - var passwordForgotToken; - var accountResetToken; - var mail; - var newPassword = '~(_8^(I)'; - var recoveryKeyId = 'edc243a821582ee9e979583be9989ee7'; - var bundle = - 'eyJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiZGlyIiwia2lkIjoiODE4NDIwZjBkYTU4ZDIwZjZhZTR' + - 'kMmM5YmVhYjkyNTEifQ..D29EXHp8ubLvftaZ.xHJd2Nl2Uco2RyywYPLkUU7fHpgO2FztY12Zjpq1ffiyLRIUcQVfmiNC6aMiHB' + - 'l7Hp-lXEbb5mR1uXHrTH9iRXEBVaAfyf9KEAWOukWGVSH8EaOkr7cfu2Yr0K93Ec8glsssjiKp8NGB8VKTUJ-lmBv2cIrG68V4eTUVDo' + - 'DhMbXhrF-Mv4JNeh338pPeatTnyg.Ow2bhEYWxzxfSPMxVwKmSA'; +const assert = require('chai').assert; +const Environment = require('../addons/environment'); - beforeEach(function() { - env = new Environment(); - accountHelper = env.accountHelper; - respond = env.respond; - client = env.client; - RequestMocks = env.RequestMocks; - mail = env.mail; +const sinon = require('sinon'); +describe('recovery key', function() { + var account; + var accountHelper; + var respond; + var client; + var email; + var RequestMocks; + var env; + var xhr; + var xhrOpen; + var xhrSend; + var keys; + var passwordForgotToken; + var accountResetToken; + var mail; + var newPassword = '~(_8^(I)'; + var recoveryKeyId = 'edc243a821582ee9e979583be9989ee7'; + var bundle = + 'eyJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiZGlyIiwia2lkIjoiODE4NDIwZjBkYTU4ZDIwZjZhZTR' + + 'kMmM5YmVhYjkyNTEifQ..D29EXHp8ubLvftaZ.xHJd2Nl2Uco2RyywYPLkUU7fHpgO2FztY12Zjpq1ffiyLRIUcQVfmiNC6aMiHB' + + 'l7Hp-lXEbb5mR1uXHrTH9iRXEBVaAfyf9KEAWOukWGVSH8EaOkr7cfu2Yr0K93Ec8glsssjiKp8NGB8VKTUJ-lmBv2cIrG68V4eTUVDo' + + 'DhMbXhrF-Mv4JNeh338pPeatTnyg.Ow2bhEYWxzxfSPMxVwKmSA'; - return accountHelper - .newVerifiedAccount() - .then(function(newAccount) { - account = newAccount; - email = account.input.email; - return respond( - client.accountKeys( - account.signIn.keyFetchToken, - account.signIn.unwrapBKey - ), - RequestMocks.accountKeys - ); - }) - .then(function(result) { - keys = result; - xhr = env.xhr; - xhrOpen = sinon.spy(xhr.prototype, 'open'); - xhrSend = sinon.spy(xhr.prototype, 'send'); - }); - }); + beforeEach(function() { + env = new Environment(); + accountHelper = env.accountHelper; + respond = env.respond; + client = env.client; + RequestMocks = env.RequestMocks; + mail = env.mail; - afterEach(function() { - xhrOpen.restore(); - xhrSend.restore(); + return accountHelper + .newVerifiedAccount() + .then(function(newAccount) { + account = newAccount; + email = account.input.email; + return respond( + client.accountKeys( + account.signIn.keyFetchToken, + account.signIn.unwrapBKey + ), + RequestMocks.accountKeys + ); + }) + .then(function(result) { + keys = result; + xhr = env.xhr; + xhrOpen = sinon.spy(xhr.prototype, 'open'); + xhrSend = sinon.spy(xhr.prototype, 'send'); }); + }); + + afterEach(function() { + xhrOpen.restore(); + xhrSend.restore(); + }); - test('#can create and get a recovery key that can be used to reset an account', function() { + it('#can create and get a recovery key that can be used to reset an account', function() { + return respond( + client.createRecoveryKey( + account.signIn.sessionToken, + recoveryKeyId, + bundle + ), + RequestMocks.createRecoveryKey + ) + .then(function(res) { + assert.ok(res); return respond( - client.createRecoveryKey( - account.signIn.sessionToken, - recoveryKeyId, - bundle - ), - RequestMocks.createRecoveryKey - ) - .then(function(res) { - assert.ok(res); - return respond( - client.passwordForgotSendCode(email), - RequestMocks.passwordForgotSendCode - ); - }) - .then(function(result) { - passwordForgotToken = result.passwordForgotToken; - assert.ok(passwordForgotToken, 'passwordForgotToken is returned'); + client.passwordForgotSendCode(email), + RequestMocks.passwordForgotSendCode + ); + }) + .then(function(result) { + passwordForgotToken = result.passwordForgotToken; + assert.ok(passwordForgotToken, 'passwordForgotToken is returned'); + + return respond( + mail.wait(account.input.user, 4), + RequestMocks.resetMailpasswordForgotRecoveryKey + ); + }) + .then(function(emails) { + var code = emails[3].html.match(/code=([A-Za-z0-9]+)/)[1]; + assert.ok(code, 'code is returned: ' + code); - return respond( - mail.wait(account.input.user, 4), - RequestMocks.resetMailpasswordForgotRecoveryKey - ); - }) - .then(function(emails) { - var code = emails[3].html.match(/code=([A-Za-z0-9]+)/)[1]; - assert.ok(code, 'code is returned: ' + code); + return respond( + client.passwordForgotVerifyCode(code, passwordForgotToken, { + accountResetWithRecoveryKey: true, + }), + RequestMocks.passwordForgotVerifyCode + ); + }) + .then(function(result) { + accountResetToken = result.accountResetToken; + assert.ok(accountResetToken, 'accountResetToken is returned'); - return respond( - client.passwordForgotVerifyCode(code, passwordForgotToken, { - accountResetWithRecoveryKey: true, - }), - RequestMocks.passwordForgotVerifyCode - ); - }) - .then(function(result) { - accountResetToken = result.accountResetToken; - assert.ok(accountResetToken, 'accountResetToken is returned'); + assert.equal(xhrOpen.args[3][0], 'POST', 'method is correct'); + assert.include( + xhrOpen.args[3][1], + '/password/forgot/verify_code', + 'path is correct' + ); + var sentData = JSON.parse(xhrSend.args[3][0]); + assert.equal(Object.keys(sentData).length, 2); + assert.equal(sentData.accountResetWithRecoveryKey, true, 'param set'); + return respond( + client.getRecoveryKey(accountResetToken, recoveryKeyId), + RequestMocks.getRecoveryKey + ); + }) + .then(function(res) { + assert.equal(xhrOpen.args[4][0], 'GET', 'method is correct'); + assert.include( + xhrOpen.args[4][1], + '/recoveryKey/' + recoveryKeyId, + 'path is correct' + ); + assert.ok(res.recoveryData, 'contains recovery data'); - assert.equal(xhrOpen.args[3][0], 'POST', 'method is correct'); - assert.include( - xhrOpen.args[3][1], - '/password/forgot/verify_code', - 'path is correct' - ); - var sentData = JSON.parse(xhrSend.args[3][0]); - assert.equal(Object.keys(sentData).length, 2); - assert.equal( - sentData.accountResetWithRecoveryKey, - true, - 'param set' - ); - return respond( - client.getRecoveryKey(accountResetToken, recoveryKeyId), - RequestMocks.getRecoveryKey - ); - }) - .then(function(res) { - assert.equal(xhrOpen.args[4][0], 'GET', 'method is correct'); - assert.include( - xhrOpen.args[4][1], - '/recoveryKey/' + recoveryKeyId, - 'path is correct' - ); - assert.ok(res.recoveryData, 'contains recovery data'); + var options = { + keys: true, + metricsContext: { + deviceId: '0123456789abcdef0123456789abcdef', + flowBeginTime: 1480615985437, + flowId: + '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + utmCampaign: 'mock-campaign', + utmContent: 'mock-content', + utmMedium: 'mock-medium', + utmSource: 'mock-source', + utmTerm: 'mock-term', + }, + sessionToken: true, + }; + return respond( + client.resetPasswordWithRecoveryKey( + accountResetToken, + email, + newPassword, + recoveryKeyId, + keys, + options + ), + RequestMocks.accountReset + ); + }) + .then(function(res) { + assert.ok(res.keyFetchToken); + assert.ok(res.sessionToken); + assert.ok(res.unwrapBKey); + assert.ok(res.uid); - var options = { - keys: true, - metricsContext: { - deviceId: '0123456789abcdef0123456789abcdef', - flowBeginTime: 1480615985437, - flowId: - '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', - utmCampaign: 'mock-campaign', - utmContent: 'mock-content', - utmMedium: 'mock-medium', - utmSource: 'mock-source', - utmTerm: 'mock-term', - }, - sessionToken: true, - }; - return respond( - client.resetPasswordWithRecoveryKey( - accountResetToken, - email, - newPassword, - recoveryKeyId, - keys, - options - ), - RequestMocks.accountReset - ); - }) - .then(function(res) { - assert.ok(res.keyFetchToken); - assert.ok(res.sessionToken); - assert.ok(res.unwrapBKey); - assert.ok(res.uid); + // Attempt to login with new password and retrieve keys + return respond( + client.signIn(email, newPassword, { keys: true }), + RequestMocks.signInWithKeys + ); + }) + .then(function(res) { + return respond( + client.accountKeys(res.keyFetchToken, res.unwrapBKey), + RequestMocks.accountKeys + ); + }) + .then(function(res) { + if (!env.useRemoteServer) { + assert.ok(res.kB, 'kB exists'); + } else { + assert.equal(res.kB, keys.kB, 'kB is equal to original kB'); + } + }); + }); - // Attempt to login with new password and retrieve keys - return respond( - client.signIn(email, newPassword, { keys: true }), - RequestMocks.signInWithKeys - ); - }) - .then(function(res) { - return respond( - client.accountKeys(res.keyFetchToken, res.unwrapBKey), - RequestMocks.accountKeys - ); - }) - .then(function(res) { - if (!env.useRemoteServer) { - assert.ok(res.kB, 'kB exists'); - } else { - assert.equal(res.kB, keys.kB, 'kB is equal to original kB'); - } - }); + it('#can create and delete recovery key', function() { + return respond( + client.createRecoveryKey( + account.signIn.sessionToken, + recoveryKeyId, + bundle + ), + RequestMocks.createRecoveryKey + ) + .then(function(res) { + assert.ok(res); + return respond( + client.deleteRecoveryKey(account.signIn.sessionToken), + RequestMocks.deleteRecoveryKey + ); + }) + .then(function(res) { + assert.ok(res); + assert.equal(xhrOpen.args[1][0], 'DELETE', 'method is correct'); + assert.include(xhrOpen.args[1][1], '/recoveryKey', 'path is correct'); }); + }); - test('#can create and delete recovery key', function() { + it('#can check if recovery exist using sessionToken', function() { + return respond( + client.recoveryKeyExists(account.signIn.sessionToken), + RequestMocks.recoveryKeyExistsFalse + ) + .then(function(res) { + assert.equal(res.exists, false, 'recovery key does not exist'); + assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); + assert.include( + xhrOpen.args[0][1], + '/recoveryKey/exists', + 'path is correct' + ); return respond( client.createRecoveryKey( account.signIn.sessionToken, @@ -195,105 +223,64 @@ define([ bundle ), RequestMocks.createRecoveryKey - ) - .then(function(res) { - assert.ok(res); - return respond( - client.deleteRecoveryKey(account.signIn.sessionToken), - RequestMocks.deleteRecoveryKey - ); - }) - .then(function(res) { - assert.ok(res); - assert.equal(xhrOpen.args[1][0], 'DELETE', 'method is correct'); - assert.include( - xhrOpen.args[1][1], - '/recoveryKey', - 'path is correct' - ); - }); - }); - - test('#can check if recovery exist using sessionToken', function() { + ); + }) + .then(function(res) { + assert.ok(res); return respond( client.recoveryKeyExists(account.signIn.sessionToken), - RequestMocks.recoveryKeyExistsFalse - ) - .then(function(res) { - assert.equal(res.exists, false, 'recovery key does not exist'); - assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); - assert.include( - xhrOpen.args[0][1], - '/recoveryKey/exists', - 'path is correct' - ); - return respond( - client.createRecoveryKey( - account.signIn.sessionToken, - recoveryKeyId, - bundle - ), - RequestMocks.createRecoveryKey - ); - }) - .then(function(res) { - assert.ok(res); - return respond( - client.recoveryKeyExists(account.signIn.sessionToken), - RequestMocks.recoveryKeyExistsTrue - ); - }) - .then(function(res) { - assert.equal(res.exists, true, 'recovery key exists'); - assert.equal(xhrOpen.args[2][0], 'POST', 'method is correct'); - assert.include( - xhrOpen.args[2][1], - '/recoveryKey/exists', - 'path is correct' - ); - }); + RequestMocks.recoveryKeyExistsTrue + ); + }) + .then(function(res) { + assert.equal(res.exists, true, 'recovery key exists'); + assert.equal(xhrOpen.args[2][0], 'POST', 'method is correct'); + assert.include( + xhrOpen.args[2][1], + '/recoveryKey/exists', + 'path is correct' + ); }); + }); + + it('#can check if recovery exist using email', function() { + return respond( + client.recoveryKeyExists(undefined, account.input.email), + RequestMocks.recoveryKeyExistsFalse + ) + .then(function(res) { + assert.equal(res.exists, false, 'recovery key does not exist'); + assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); + assert.include( + xhrOpen.args[0][1], + '/recoveryKey/exists', + 'path is correct' + ); - test('#can check if recovery exist using email', function() { + return respond( + client.createRecoveryKey( + account.signIn.sessionToken, + recoveryKeyId, + bundle + ), + RequestMocks.createRecoveryKey + ); + }) + .then(function(res) { + assert.ok(res); return respond( client.recoveryKeyExists(undefined, account.input.email), - RequestMocks.recoveryKeyExistsFalse - ) - .then(function(res) { - assert.equal(res.exists, false, 'recovery key does not exist'); - assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); - assert.include( - xhrOpen.args[0][1], - '/recoveryKey/exists', - 'path is correct' - ); - - return respond( - client.createRecoveryKey( - account.signIn.sessionToken, - recoveryKeyId, - bundle - ), - RequestMocks.createRecoveryKey - ); - }) - .then(function(res) { - assert.ok(res); - return respond( - client.recoveryKeyExists(undefined, account.input.email), - RequestMocks.recoveryKeyExistsTrue - ); - }) - .then(function(res) { - assert.equal(res.exists, true, 'recovery key exists'); - assert.equal(xhrOpen.args[2][0], 'POST', 'method is correct'); - assert.include( - xhrOpen.args[2][1], - '/recoveryKey/exists', - 'path is correct' - ); - }); + RequestMocks.recoveryKeyExistsTrue + ); + }) + .then(function(res) { + assert.equal(res.exists, true, 'recovery key exists'); + assert.equal(xhrOpen.args[2][0], 'POST', 'method is correct'); + assert.include( + xhrOpen.args[2][1], + '/recoveryKey/exists', + 'path is correct' + ); }); - }); - } + }); }); diff --git a/packages/fxa-js-client/tests/lib/request.js b/packages/fxa-js-client/tests/lib/request.js index 58dee0250e9..c58eaf03164 100644 --- a/packages/fxa-js-client/tests/lib/request.js +++ b/packages/fxa-js-client/tests/lib/request.js @@ -1,93 +1,82 @@ /* 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/. */ +const sinon = require('sinon'); -define([ - 'tests/addons/sinon', - 'intern!tdd', - 'intern/chai!assert', - 'tests/addons/environment', - 'client/lib/request', - 'tests/mocks/errors', -], function(sinon, tdd, assert, Environment, Request, ErrorMocks) { - with (tdd) { - suite('request module', function() { - var RequestMocks; - var request; - var env; +const assert = require('chai').assert; +const Environment = require('../addons/environment'); - beforeEach(function() { - env = new Environment(); - RequestMocks = env.RequestMocks; - request = new Request(env.authServerUrl, env.xhr); - }); +const Request = require('../../client/lib/request'); +const ErrorMocks = require('../mocks/errors'); +describe('request module', function() { + var RequestMocks; + var request; + var env; - test('#heartbeat', function() { - var heartbeatRequest = env - .respond( - request.send('/__heartbeat__', 'GET'), - RequestMocks.heartbeat - ) - .then(function(res) { - assert.ok(res); - }, assert.notOk); + beforeEach(function() { + env = new Environment(); + RequestMocks = env.RequestMocks; + request = new Request(env.authServerUrl, env.xhr); + }); - return heartbeatRequest; - }); + it('#heartbeat', function() { + var heartbeatRequest = env + .respond(request.send('/__heartbeat__', 'GET'), RequestMocks.heartbeat) + .then(function(res) { + assert.ok(res); + }, assert.fail); - test('#error', function() { - request = new Request('http://', env.xhr); + return heartbeatRequest; + }); - request.send('/', 'GET').then(assert.notOk, function() { - assert.ok(true); - }); - }); + it('#error', function() { + request = new Request('http://', env.xhr); - test('#timeout', function() { - request = new Request('http://google.com:81', env.xhr, { - timeout: 200, - }); + request.send('/', 'GET').then(assert.fail, function() { + assert.ok(true); + }); + }); - var timeoutRequest = env.respond( - request.send('/', 'GET'), - ErrorMocks.timeout - ); + it('#timeout', function() { + request = new Request('http://192.168.1.999', env.xhr, { + timeout: 1, + }); - return timeoutRequest.then(assert.notOk, function(err) { - assert.equal(err.error, 'Timeout error'); - }); - }); + var timeoutRequest = env.respond( + request.send('/', 'GET'), + ErrorMocks.timeout + ); - test('#bad response format error', function() { - request = new Request('http://example.com/', env.xhr); + return timeoutRequest.then(assert.fail, function(err) { + assert.equal(err.error, 'Timeout error'); + }); + }); - // Trigger an error response that's in HTML - var response = env.respond( - request.send('/nonexistent', 'GET'), - ErrorMocks.badResponseFormat - ); + it('#bad response format error', function() { + request = new Request('http://example.com/', env.xhr); - return response.then(assert.notOk, function(err) { - assert.equal(err.error, 'Unknown error'); - }); - }); + // Trigger an error response that's in HTML + var response = env.respond( + request.send('/nonexistent', 'GET'), + ErrorMocks.badResponseFormat + ); + + return response.then(assert.fail, function(err) { + assert.equal(err.error, 'Unknown error'); + }); + }); - test('#ensure is usable', function() { - request = new Request('http://google.com:81', env.xhr, { - timeout: 200, - }); - sinon.stub(env.xhr.prototype, 'open').throws(); + it('#ensure is usable', function() { + request = new Request('http://google.com:81', env.xhr, { + timeout: 200, + }); + sinon.stub(env.xhr.prototype, 'open').throws(); - return env - .respond( - request.send('/__heartbeat__', 'GET'), - RequestMocks.heartbeat - ) - .then(null, function(err) { - assert.ok(err); - env.xhr.prototype.open.restore(); - }); + return env + .respond(request.send('/__heartbeat__', 'GET'), RequestMocks.heartbeat) + .then(null, function(err) { + assert.ok(err); + env.xhr.prototype.open.restore(); }); - }); - } + }); }); diff --git a/packages/fxa-js-client/tests/lib/securityEvents.js b/packages/fxa-js-client/tests/lib/securityEvents.js index 6ef08a45aab..228b4188b5b 100644 --- a/packages/fxa-js-client/tests/lib/securityEvents.js +++ b/packages/fxa-js-client/tests/lib/securityEvents.js @@ -2,99 +2,86 @@ * 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/. */ -define([ - 'intern!tdd', - 'intern/chai!assert', - 'tests/addons/environment', - 'tests/addons/sinon', -], function(tdd, assert, Environment, sinon) { - with (tdd) { - suite('securityEvents', function() { - let account; - let accountHelper; - let env; - let respond; - let requests; - let client; - let RequestMocks; - let xhr; - let xhrOpen; - let xhrSend; +const assert = require('chai').assert; +const Environment = require('../addons/environment'); - beforeEach(() => { - env = new Environment(); - accountHelper = env.accountHelper; - respond = env.respond; - requests = env.requests; - client = env.client; - RequestMocks = env.RequestMocks; +const sinon = require('sinon'); +describe('securityEvents', function() { + let account; + let accountHelper; + let env; + let respond; + let requests; + let client; + let RequestMocks; + let xhr; + let xhrOpen; + let xhrSend; - return accountHelper.newVerifiedAccount().then(newAccount => { - account = newAccount; - xhr = env.xhr; - xhrOpen = sinon.spy(xhr.prototype, 'open'); - xhrSend = sinon.spy(xhr.prototype, 'send'); - }); - }); + beforeEach(() => { + env = new Environment(); + accountHelper = env.accountHelper; + respond = env.respond; + requests = env.requests; + client = env.client; + RequestMocks = env.RequestMocks; - afterEach(() => { - xhrOpen.restore(); - xhrSend.restore(); - }); + return accountHelper.newVerifiedAccount().then(newAccount => { + account = newAccount; + xhr = env.xhr; + xhrOpen = sinon.spy(xhr.prototype, 'open'); + xhrSend = sinon.spy(xhr.prototype, 'send'); + }); + }); - test('#securityEvents', function() { - return respond( - client.securityEvents(account.signIn.sessionToken), - RequestMocks.securityEvents - ).then(res => { - assert.equal(xhrOpen.args[0][0], 'GET', 'method is correct'); - assert.include( - xhrOpen.args[0][1], - '/securityEvents', - 'path is correct' - ); - assert.ok(res, 'got response'); - assert.equal(res.length, 2); + afterEach(() => { + xhrOpen.restore(); + xhrSend.restore(); + }); - assert.equal(res[0].name, 'account.login'); - assert.equal(res[0].verified, true); - assert.equal(res[0].createdAt < new Date().getTime(), true); + it('#securityEvents', function() { + return respond( + client.securityEvents(account.signIn.sessionToken), + RequestMocks.securityEvents + ).then(res => { + assert.equal(xhrOpen.args[0][0], 'GET', 'method is correct'); + assert.include(xhrOpen.args[0][1], '/securityEvents', 'path is correct'); + assert.ok(res, 'got response'); + assert.equal(res.length, 2); - assert.equal(res[1].name, 'account.create'); - assert.equal(res[1].verified, true); - assert.equal(res[1].createdAt < new Date().getTime(), true); - }, assert.notOk); - }); + assert.equal(res[0].name, 'account.login'); + assert.equal(res[0].verified, true); + assert.equal(res[0].createdAt < new Date().getTime(), true); + + assert.equal(res[1].name, 'account.create'); + assert.equal(res[1].verified, true); + assert.equal(res[1].createdAt < new Date().getTime(), true); + }, assert.fail); + }); - test('#deleteSecurityEvents', function() { - return respond( - client.deleteSecurityEvents(account.signIn.sessionToken), - RequestMocks.deleteSecurityEvents - ).then(res => { - assert.equal(xhrOpen.args[0][0], 'DELETE', 'method is correct'); - assert.include( - xhrOpen.args[0][1], - '/securityEvents', - 'path is correct' - ); - assert.ok(res, 'got response'); - assert.deepEqual(res, {}); + it('#deleteSecurityEvents', function() { + return respond( + client.deleteSecurityEvents(account.signIn.sessionToken), + RequestMocks.deleteSecurityEvents + ).then(res => { + assert.equal(xhrOpen.args[0][0], 'DELETE', 'method is correct'); + assert.include(xhrOpen.args[0][1], '/securityEvents', 'path is correct'); + assert.ok(res, 'got response'); + assert.deepEqual(res, {}); - return respond( - client.securityEvents(account.signIn.sessionToken), - RequestMocks.securityEventsEmptyResponse - ).then(res => { - assert.equal(xhrOpen.args[1][0], 'GET', 'method is correct'); - assert.include( - xhrOpen.args[1][1], - '/securityEvents', - 'path is correct' - ); - assert.ok(res, 'got response'); - assert.equal(res.length, 0); - }); - }, assert.notOk); + return respond( + client.securityEvents(account.signIn.sessionToken), + RequestMocks.securityEventsEmptyResponse + ).then(res => { + assert.equal(xhrOpen.args[1][0], 'GET', 'method is correct'); + assert.include( + xhrOpen.args[1][1], + '/securityEvents', + 'path is correct' + ); + assert.ok(res, 'got response'); + assert.equal(res.length, 0); }); - }); - } + }, assert.fail); + }); }); diff --git a/packages/fxa-js-client/tests/lib/session.js b/packages/fxa-js-client/tests/lib/session.js index 70a559f6562..327ab9cd90e 100644 --- a/packages/fxa-js-client/tests/lib/session.js +++ b/packages/fxa-js-client/tests/lib/session.js @@ -2,359 +2,342 @@ * 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/. */ -define([ - 'intern!tdd', - 'intern/chai!assert', - 'tests/addons/environment', - 'tests/addons/sinon', -], function(tdd, assert, Environment, sinon) { - with (tdd) { - suite('session', function() { - var accountHelper; - var respond; - var requests; - var client; - var RequestMocks; - var ErrorMocks; - var xhr; - - beforeEach(function() { - var env = new Environment(); - accountHelper = env.accountHelper; - respond = env.respond; - requests = env.requests; - client = env.client; - RequestMocks = env.RequestMocks; - ErrorMocks = env.ErrorMocks; - xhr = env.xhr; - sinon.spy(xhr.prototype, 'open'); - sinon.spy(xhr.prototype, 'send'); +const assert = require('chai').assert; +const Environment = require('../addons/environment'); + +const sinon = require('sinon'); +describe('session', function() { + var accountHelper; + var respond; + var requests; + var client; + var RequestMocks; + var ErrorMocks; + var xhr; + let env; + + beforeEach(function() { + env = new Environment(); + accountHelper = env.accountHelper; + respond = env.respond; + requests = env.requests; + client = env.client; + RequestMocks = env.RequestMocks; + ErrorMocks = env.ErrorMocks; + xhr = env.xhr; + sinon.spy(xhr.prototype, 'open'); + sinon.spy(xhr.prototype, 'send'); + }); + + afterEach(function() { + xhr.prototype.open.restore(); + xhr.prototype.send.restore(); + }); + + it('#destroy', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.sessionDestroy(account.signIn.sessionToken), + RequestMocks.sessionDestroy + ); + }) + .then(function(res) { + assert.ok(res, 'got response'); + }, assert.fail); + }); + + it('#status', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.sessionStatus(account.signIn.sessionToken), + RequestMocks.sessionStatus + ); + }) + .then(function(res) { + assert.isNotNull(res); + }, assert.fail); + }); + + it('#status error with a false token', function() { + return accountHelper + .newVerifiedAccount() + .then(function() { + var fakeToken = + 'e838790265a45f6ee1130070d57d67d9bb20953706f73af0e34b0d4d92f10000'; + + return respond( + client.passwordForgotStatus(fakeToken), + ErrorMocks.invalidAuthToken + ); + }) + .then(assert.fail, function(err) { + assert.equal(err.code, 401); + assert.equal(err.errno, 110); }); - - afterEach(function() { - xhr.prototype.open.restore(); - xhr.prototype.send.restore(); - }); - - test('#destroy', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.sessionDestroy(account.signIn.sessionToken), - RequestMocks.sessionDestroy - ); - }) - .then(function(res) { - assert.ok(res, 'got response'); - }, assert.notOk); - }); - - test('#status', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.sessionStatus(account.signIn.sessionToken), - RequestMocks.sessionStatus - ); - }) - .then(function(res) { - assert.isNotNull(res); - }, assert.notOk); - }); - - test('#status error with a false token', function() { - return accountHelper - .newVerifiedAccount() - .then(function() { - var fakeToken = - 'e838790265a45f6ee1130070d57d67d9bb20953706f73af0e34b0d4d92f10000'; - - return respond( - client.passwordForgotStatus(fakeToken), - ErrorMocks.invalidAuthToken - ); - }) - .then(assert.notOk, function(err) { - assert.equal(err.code, 401); - assert.equal(err.errno, 110); - }); - }); - - test('#sessions', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.sessions(account.signIn.sessionToken), - RequestMocks.sessions - ); - }) - .then(function(res) { - assert.equal(res.length, 2); - var s = res[0]; - assert.ok(s.id); - assert.ok(s.deviceType); - assert.equal(s.isDevice, false); - assert.ok(s.lastAccessTime); - assert.ok(s.lastAccessTimeFormatted); - }, assert.notOk); - }); - - test('#sessions error', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - var fakeToken = - 'e838790265a45f6ee1130070d57d67d9bb20953706f73af0e34b0d4d92f10000'; - - return respond( - client.sessions(fakeToken), - ErrorMocks.invalidAuthToken - ); - }) - .then(assert.notOk, function(err) { - assert.equal(err.code, 401); - assert.equal(err.errno, 110); - }); - }); - - test('#reauth', function() { - return accountHelper.newVerifiedAccount().then(function(account) { - var email = account.input.email; - var password = account.input.password; - - return respond( - client.sessionReauth(account.signIn.sessionToken, email, password), - RequestMocks.sessionReauth - ).then(function(res) { - assert.ok(res.uid); - assert.ok(res.verified); - assert.ok(res.authAt); - assert.notOk(res.keyFetchToken); - assert.notOk(res.unwrapBKey); - - var args = - xhr.prototype.open.args[xhr.prototype.open.args.length - 1]; - assert.equal(args[0], 'POST'); - assert.include(args[1], '/session/reauth'); - - var payload = JSON.parse( - xhr.prototype.send.args[xhr.prototype.send.args.length - 1][0] - ); - assert.equal(Object.keys(payload).length, 2); - assert.equal(payload.email, email); - assert.equal(payload.authPW.length, 64); - }, assert.notOk); - }); - }); - - test('#reauth with keys', function() { - return accountHelper.newVerifiedAccount().then(function(account) { - var email = account.input.email; - var password = account.input.password; - - return respond( - client.sessionReauth(account.signIn.sessionToken, email, password, { - keys: true, - }), - RequestMocks.sessionReauthWithKeys - ).then(function(res) { - assert.ok(res.uid); - assert.ok(res.verified); - assert.ok(res.authAt); - assert.ok(res.keyFetchToken); - assert.ok(res.unwrapBKey); - - var args = - xhr.prototype.open.args[xhr.prototype.open.args.length - 1]; - assert.equal(args[0], 'POST'); - assert.include(args[1], '/session/reauth?keys=true'); - - var payload = JSON.parse( - xhr.prototype.send.args[xhr.prototype.send.args.length - 1][0] - ); - assert.equal(Object.keys(payload).length, 2); - assert.equal(payload.email, email); - assert.equal(payload.authPW.length, 64); - }, assert.notOk); - }); - }); - - test('#reauth with incorrect password', function() { - return accountHelper.newVerifiedAccount().then(function(account) { - var email = account.input.email; - var password = 'incorrect password'; - - return respond( - client.sessionReauth(account.signIn.sessionToken, email, password), - ErrorMocks.accountIncorrectPassword - ).then( - function() { - assert.fail(); - }, - function(res) { - assert.equal(res.code, 400); - assert.equal(res.errno, 103); - } - ); - }); + }); + + it('#sessions', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.sessions(account.signIn.sessionToken), + RequestMocks.sessions + ); + }) + .then(function(res) { + assert.equal(res.length, 2); + var s = res[0]; + assert.ok(s.id); + assert.ok(s.deviceType); + assert.equal(s.isDevice, false); + assert.ok(s.lastAccessTime); + assert.ok(s.lastAccessTimeFormatted); + }, assert.fail); + }); + + it('#sessions error', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + var fakeToken = + 'e838790265a45f6ee1130070d57d67d9bb20953706f73af0e34b0d4d92f10000'; + + return respond(client.sessions(fakeToken), ErrorMocks.invalidAuthToken); + }) + .then(assert.fail, function(err) { + assert.equal(err.code, 401); + assert.equal(err.errno, 110); }); - - test('#reauth with incorrect email case', function() { - return accountHelper.newVerifiedAccount().then(function(account) { - var numSetupRequests = requests ? requests.length : null; - var sessionToken = account.signIn.sessionToken; - var incorrectCaseEmail = - account.input.email.charAt(0).toUpperCase() + - account.input.email.slice(1); - var password = account.input.password; - - respond(ErrorMocks.incorrectEmailCase); - return respond( - client.sessionReauth(sessionToken, incorrectCaseEmail, password), - RequestMocks.sessionReauth - ).then(function(res) { - assert.property(res, 'uid'); - assert.property(res, 'verified'); - assert.property(res, 'authAt'); - - if (requests) { - assert.equal(requests.length - numSetupRequests, 2); - } - - var args = - xhr.prototype.open.args[xhr.prototype.open.args.length - 2]; - assert.equal(args[0], 'POST'); - assert.include(args[1], '/session/reauth'); - - var payload = JSON.parse( - xhr.prototype.send.args[xhr.prototype.send.args.length - 2][0] - ); - assert.equal(Object.keys(payload).length, 2); - assert.equal(payload.email, incorrectCaseEmail); - assert.equal(payload.authPW.length, 64); - - args = xhr.prototype.open.args[xhr.prototype.open.args.length - 1]; - assert.equal(args[0], 'POST'); - assert.include(args[1], '/session/reauth'); - - payload = JSON.parse( - xhr.prototype.send.args[xhr.prototype.send.args.length - 1][0] - ); - assert.equal(Object.keys(payload).length, 3); - assert.notEqual(payload.email, incorrectCaseEmail); - assert.equal(payload.originalLoginEmail, incorrectCaseEmail); - assert.equal(payload.authPW.length, 64); - }, assert.notOk); - }); - }); - - test('#reauth with incorrect email case with skipCaseError', function() { - return accountHelper.newVerifiedAccount().then(function(account) { - var numSetupRequests = requests ? requests.length : null; - var sessionToken = account.signIn.sessionToken; - var incorrectCaseEmail = - account.input.email.charAt(0).toUpperCase() + - account.input.email.slice(1); - var password = account.input.password; - - return respond( - client.sessionReauth(sessionToken, incorrectCaseEmail, password, { - skipCaseError: true, - }), - ErrorMocks.incorrectEmailCase - ).then( - function() { - assert.fail(); - }, - function(res) { - assert.equal(res.code, 400); - assert.equal(res.errno, 120); - - if (requests) { - assert.equal(requests.length - numSetupRequests, 1); - } - - var args = - xhr.prototype.open.args[xhr.prototype.open.args.length - 1]; - assert.equal(args[0], 'POST'); - assert.include(args[1], '/session/reauth'); - - var payload = JSON.parse( - xhr.prototype.send.args[xhr.prototype.send.args.length - 1][0] - ); - assert.equal(Object.keys(payload).length, 2); - assert.equal(payload.email, incorrectCaseEmail); - assert.equal(payload.authPW.length, 64); - } + }); + + it('#reauth', function() { + return accountHelper.newVerifiedAccount().then(function(account) { + var email = account.input.email; + var password = account.input.password; + + return respond( + client.sessionReauth(account.signIn.sessionToken, email, password), + RequestMocks.sessionReauth + ).then(function(res) { + assert.ok(res.uid); + assert.ok(res.verified); + assert.ok(res.authAt); + assert.notOk(res.keyFetchToken); + assert.notOk(res.unwrapBKey); + + var args = xhr.prototype.open.args[xhr.prototype.open.args.length - 1]; + assert.equal(args[0], 'POST'); + assert.include(args[1], '/session/reauth'); + + var payload = JSON.parse( + xhr.prototype.send.args[xhr.prototype.send.args.length - 1][0] + ); + assert.equal(Object.keys(payload).length, 2); + assert.equal(payload.email, email); + assert.equal(payload.authPW.length, 64); + }, assert.fail); + }); + }); + + it('#reauth with keys', function() { + return accountHelper.newVerifiedAccount().then(function(account) { + var email = account.input.email; + var password = account.input.password; + + return respond( + client.sessionReauth(account.signIn.sessionToken, email, password, { + keys: true, + }), + RequestMocks.sessionReauthWithKeys + ).then(function(res) { + assert.ok(res.uid); + assert.ok(res.verified); + assert.ok(res.authAt); + assert.ok(res.keyFetchToken); + assert.ok(res.unwrapBKey); + + var args = xhr.prototype.open.args[xhr.prototype.open.args.length - 1]; + assert.equal(args[0], 'POST'); + assert.include(args[1], '/session/reauth?keys=true'); + + var payload = JSON.parse( + xhr.prototype.send.args[xhr.prototype.send.args.length - 1][0] + ); + assert.equal(Object.keys(payload).length, 2); + assert.equal(payload.email, email); + assert.equal(payload.authPW.length, 64); + }, assert.fail); + }); + }); + + it('#reauth with incorrect password', function() { + return accountHelper.newVerifiedAccount().then(function(account) { + var email = account.input.email; + var password = 'incorrect password'; + + return respond( + client.sessionReauth(account.signIn.sessionToken, email, password), + ErrorMocks.accountIncorrectPassword + ).then( + function() { + assert.fail(); + }, + function(res) { + assert.equal(res.code, 400); + assert.equal(res.errno, 103); + } + ); + }); + }); + + it('#reauth with incorrect email case', function() { + return accountHelper.newVerifiedAccount().then(function(account) { + var numSetupRequests = requests ? requests.length : null; + var sessionToken = account.signIn.sessionToken; + var incorrectCaseEmail = + account.input.email.charAt(0).toUpperCase() + + account.input.email.slice(1); + var password = account.input.password; + + respond(ErrorMocks.incorrectEmailCase); + return respond( + client.sessionReauth(sessionToken, incorrectCaseEmail, password), + RequestMocks.sessionReauth + ).then(function(res) { + assert.property(res, 'uid'); + assert.property(res, 'verified'); + assert.property(res, 'authAt'); + + if (requests) { + assert.equal(requests.length - numSetupRequests, 2); + } + + var args = xhr.prototype.open.args[xhr.prototype.open.args.length - 2]; + assert.equal(args[0], 'POST'); + assert.include(args[1], '/session/reauth'); + + var payload = JSON.parse( + xhr.prototype.send.args[xhr.prototype.send.args.length - 2][0] + ); + assert.equal(Object.keys(payload).length, 2); + assert.equal(payload.email, incorrectCaseEmail); + assert.equal(payload.authPW.length, 64); + + args = xhr.prototype.open.args[xhr.prototype.open.args.length - 1]; + assert.equal(args[0], 'POST'); + assert.include(args[1], '/session/reauth'); + + payload = JSON.parse( + xhr.prototype.send.args[xhr.prototype.send.args.length - 1][0] + ); + assert.equal(Object.keys(payload).length, 3); + assert.notEqual(payload.email, incorrectCaseEmail); + assert.equal(payload.originalLoginEmail, incorrectCaseEmail); + assert.equal(payload.authPW.length, 64); + }, assert.fail); + }); + }); + + it('#reauth with incorrect email case with skipCaseError', function() { + return accountHelper.newVerifiedAccount().then(function(account) { + var numSetupRequests = requests ? requests.length : null; + var sessionToken = account.signIn.sessionToken; + var incorrectCaseEmail = + account.input.email.charAt(0).toUpperCase() + + account.input.email.slice(1); + var password = account.input.password; + + return respond( + client.sessionReauth(sessionToken, incorrectCaseEmail, password, { + skipCaseError: true, + }), + ErrorMocks.incorrectEmailCase + ).then( + function() { + assert.fail(); + }, + function(res) { + assert.equal(res.code, 400); + assert.equal(res.errno, 120); + + if (requests) { + assert.equal(requests.length - numSetupRequests, 1); + } + + var args = + xhr.prototype.open.args[xhr.prototype.open.args.length - 1]; + assert.equal(args[0], 'POST'); + assert.include(args[1], '/session/reauth'); + + var payload = JSON.parse( + xhr.prototype.send.args[xhr.prototype.send.args.length - 1][0] ); - }); - }); - - test('#reauth with all the options', function() { - return accountHelper.newVerifiedAccount().then(function(account) { - var sessionToken = account.signIn.sessionToken; - var email = account.input.email; - var password = account.input.password; - var options = { - keys: true, - metricsContext: { - entrypoint: 'mock-entrypoint', - entrypointExperiment: 'mock-entrypoint-experiment', - entrypointVariation: 'mock-entrypoint-variation', - utmCampaign: 'mock-utm-campaign', - utmContent: 'mock-utm-content', - utmMedium: 'mock-utm-medium', - utmSource: 'mock-utm-source', - utmTerm: 'mock-utm-term', - }, - originalLoginEmail: email.toUpperCase(), - reason: 'password_change', - redirectTo: 'http://127.0.0.1', - resume: 'RESUME_TOKEN', - service: 'sync', - verificationMethod: 'email-2fa', - }; - - return respond( - client.sessionReauth(sessionToken, email, password, options), - RequestMocks.sessionReauthWithKeys - ).then(function(res) { - assert.ok(res.uid); - assert.ok(res.verified); - assert.ok(res.authAt); - assert.ok(res.keyFetchToken); - assert.ok(res.unwrapBKey); - - var args = - xhr.prototype.open.args[xhr.prototype.open.args.length - 1]; - assert.equal(args[0], 'POST'); - assert.include(args[1], '/session/reauth?keys=true'); - - var payload = JSON.parse( - xhr.prototype.send.args[xhr.prototype.send.args.length - 1][0] - ); - assert.equal(Object.keys(payload).length, 9); - assert.equal(payload.email, email); - assert.equal(payload.authPW.length, 64); - assert.deepEqual(payload.metricsContext, options.metricsContext); - assert.equal( - payload.originalLoginEmail, - options.originalLoginEmail - ); - assert.equal(payload.reason, options.reason); - assert.equal(payload.redirectTo, options.redirectTo); - assert.equal(payload.resume, options.resume); - assert.equal(payload.service, options.service); - assert.equal( - payload.verificationMethod, - options.verificationMethod - ); - }); - }); + assert.equal(Object.keys(payload).length, 2); + assert.equal(payload.email, incorrectCaseEmail); + assert.equal(payload.authPW.length, 64); + } + ); + }); + }); + + it('#reauth with all the options', function() { + return accountHelper.newVerifiedAccount().then(function(account) { + var sessionToken = account.signIn.sessionToken; + var email = account.input.email; + var password = account.input.password; + var options = { + keys: true, + metricsContext: { + entrypoint: 'mock-entrypoint', + entrypointExperiment: 'mock-entrypoint-experiment', + entrypointVariation: 'mock-entrypoint-variation', + utmCampaign: 'mock-utm-campaign', + utmContent: 'mock-utm-content', + utmMedium: 'mock-utm-medium', + utmSource: 'mock-utm-source', + utmTerm: 'mock-utm-term', + }, + originalLoginEmail: email.toUpperCase(), + reason: 'password_change', + redirectTo: 'http://127.0.0.1', + resume: 'RESUME_TOKEN', + service: 'sync', + verificationMethod: 'email-2fa', + }; + + return respond( + client.sessionReauth(sessionToken, email, password, options), + RequestMocks.sessionReauthWithKeys + ).then(function(res) { + assert.ok(res.uid); + assert.ok(res.verified); + assert.ok(res.authAt); + assert.ok(res.keyFetchToken); + assert.ok(res.unwrapBKey); + + var args = xhr.prototype.open.args[xhr.prototype.open.args.length - 1]; + assert.equal(args[0], 'POST'); + assert.include(args[1], '/session/reauth?keys=true'); + + var payload = JSON.parse( + xhr.prototype.send.args[xhr.prototype.send.args.length - 1][0] + ); + assert.equal(Object.keys(payload).length, 9); + assert.equal(payload.email, email); + assert.equal(payload.authPW.length, 64); + assert.deepEqual(payload.metricsContext, options.metricsContext); + assert.equal(payload.originalLoginEmail, options.originalLoginEmail); + assert.equal(payload.reason, options.reason); + assert.equal(payload.redirectTo, options.redirectTo); + assert.equal(payload.resume, options.resume); + assert.equal(payload.service, options.service); + assert.equal(payload.verificationMethod, options.verificationMethod); }); }); - } + }); }); diff --git a/packages/fxa-js-client/tests/lib/signIn.js b/packages/fxa-js-client/tests/lib/signIn.js index 0e0fb5842b5..0a8ef889c16 100644 --- a/packages/fxa-js-client/tests/lib/signIn.js +++ b/packages/fxa-js-client/tests/lib/signIn.js @@ -2,260 +2,246 @@ * 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/. */ -define([ - 'intern!tdd', - 'intern/chai!assert', - 'tests/addons/environment', - 'tests/lib/push-constants', -], function(tdd, assert, Environment, PushTestConstants) { - with (tdd) { - suite('signIn', function() { - var ErrorMocks; - var RequestMocks; - var accountHelper; - var client; - var mail; - var respond; - var requests; - - beforeEach(function() { - var env = new Environment(); - ErrorMocks = env.ErrorMocks; - RequestMocks = env.RequestMocks; - accountHelper = env.accountHelper; - client = env.client; - mail = env.mail; - respond = env.respond; - requests = env.requests; - }); - - test('#basic', function() { - var email = 'test' + new Date().getTime() + '@restmail.net'; - var password = 'iliketurtles'; - - return respond(client.signUp(email, password), RequestMocks.signUp) - .then(function() { - return respond(client.signIn(email, password), RequestMocks.signIn); - }) - .then(function(res) { - assert.ok(res.sessionToken); - }, assert.notOk); - }); - - test('#with keys', function() { - var email = 'test' + new Date().getTime() + '@restmail.net'; - var password = 'iliketurtles'; - - return respond(client.signUp(email, password), RequestMocks.signUp) - .then(function(res) { - return respond( - client.signIn(email, password, { keys: true }), - RequestMocks.signInWithKeys - ); - }) - .then(function(res) { - assert.ok(res.sessionToken); - assert.ok(res.keyFetchToken); - assert.ok(res.unwrapBKey); - }, assert.notOk); - }); - - test('#with service', function() { - var email = 'test' + new Date().getTime() + '@restmail.net'; - var password = 'iliketurtles'; - +const assert = require('chai').assert; +const Environment = require('../addons/environment'); + +const PushTestConstants = require('../mocks/pushConstants'); +describe('signIn', function() { + var ErrorMocks; + var RequestMocks; + var accountHelper; + var client; + var mail; + var respond; + var requests; + let env; + + beforeEach(function() { + env = new Environment(); + ErrorMocks = env.ErrorMocks; + RequestMocks = env.RequestMocks; + accountHelper = env.accountHelper; + client = env.client; + mail = env.mail; + respond = env.respond; + requests = env.requests; + }); + + it('#basic', function() { + var email = 'test' + new Date().getTime() + '@restmail.net'; + var password = 'iliketurtles'; + + return respond(client.signUp(email, password), RequestMocks.signUp) + .then(function() { + return respond(client.signIn(email, password), RequestMocks.signIn); + }) + .then(function(res) { + assert.ok(res.sessionToken); + }, assert.fail); + }); + + it('#with keys', function() { + var email = 'test' + new Date().getTime() + '@restmail.net'; + var password = 'iliketurtles'; + + return respond(client.signUp(email, password), RequestMocks.signUp) + .then(function(res) { return respond( - client.signUp(email, password), - RequestMocks.signUp - ).then(function() { - return respond( - client.signIn(email, password, { service: 'sync' }), - RequestMocks.signIn - ); - }); - }); - - test('#with reason', function() { - var email = 'test' + new Date().getTime() + '@restmail.net'; - var password = 'iliketurtles'; - + client.signIn(email, password, { keys: true }), + RequestMocks.signInWithKeys + ); + }) + .then(function(res) { + assert.ok(res.sessionToken); + assert.ok(res.keyFetchToken); + assert.ok(res.unwrapBKey); + }, assert.fail); + }); + + it('#with service', function() { + var email = 'test' + new Date().getTime() + '@restmail.net'; + var password = 'iliketurtles'; + + return respond(client.signUp(email, password), RequestMocks.signUp).then( + function() { return respond( - client.signUp(email, password), - RequestMocks.signUp - ).then(function() { - return respond( - client.signIn(email, password, { reason: 'password_change' }), - RequestMocks.signIn - ); - }); - }); - - test('#with Sync/redirectTo', function() { - var user = 'confirm' + new Date().getTime(); - var email = user + '@restmail.net'; - var password = 'iliketurtles'; - var opts = { - keys: true, - metricsContext: { - context: 'fx_desktop_v2', - }, - redirectTo: 'http://sync.127.0.0.1/after_reset', - service: 'sync', - }; - + client.signIn(email, password, { service: 'sync' }), + RequestMocks.signIn + ); + } + ); + }); + + it('#with reason', function() { + var email = 'test' + new Date().getTime() + '@restmail.net'; + var password = 'iliketurtles'; + + return respond(client.signUp(email, password), RequestMocks.signUp).then( + function() { return respond( - client.signUp(email, password, { preVerified: true }), - RequestMocks.signUp - ) - .then(function() { - return respond( - client.signIn(email, password, opts), - RequestMocks.signIn - ); - }) - .then(function(res) { - assert.ok(res.uid); - return respond( - mail.wait(user), - RequestMocks.mailServiceAndRedirect - ); - }) - .then(function(emails) { - var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; - var redirectTo = emails[0].html.match( - /redirectTo=([A-Za-z0-9]+)/ - )[1]; - - assert.ok(code, 'code is returned'); - assert.ok(redirectTo, 'redirectTo is returned'); - }, assert.notOk); - }); - - test('#with Sync/resume', function() { - var user = 'confirm' + new Date().getTime(); - var email = user + '@restmail.net'; - var password = 'iliketurtles'; - var opts = { - keys: true, - metricsContext: { - context: 'fx_desktop_v2', - }, - redirectTo: 'http://sync.127.0.0.1/after_reset', - resume: 'resumejwt', - service: 'sync', - }; - + client.signIn(email, password, { reason: 'password_change' }), + RequestMocks.signIn + ); + } + ); + }); + + it('#with Sync/redirectTo', function() { + var user = 'sync.' + new Date().getTime(); + var email = user + '@restmail.net'; + var password = 'iliketurtles'; + var opts = { + keys: true, + metricsContext: { + context: 'fx_desktop_v2', + }, + redirectTo: 'http://sync.127.0.0.1/after_reset', + service: 'sync', + }; + + return respond( + client.signUp(email, password, { preVerified: true }), + RequestMocks.signUp + ) + .then(function() { return respond( - client.signUp(email, password, { preVerified: true }), - RequestMocks.signUp - ) - .then(function() { - return respond( - client.signIn(email, password, opts), - RequestMocks.signIn - ); - }) - .then(function(res) { - assert.ok(res.uid); - return respond( - mail.wait(user), - RequestMocks.mailServiceAndRedirect - ); - }) - .then(function(emails) { - var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; - var resume = emails[0].html.match(/resume=([A-Za-z0-9]+)/)[1]; - - assert.ok(code, 'code is returned'); - assert.ok(resume, 'resume is returned'); - }, assert.notOk); - }); - - test('#incorrect email case', function() { - return accountHelper.newVerifiedAccount().then(function(account) { - var numSetupRequests = requests ? requests.length : null; - var incorrectCaseEmail = - account.input.email.charAt(0).toUpperCase() + - account.input.email.slice(1); - - respond(ErrorMocks.incorrectEmailCase); - return respond( - client.signIn(incorrectCaseEmail, account.input.password), - RequestMocks.signIn - ).then(function(res) { - assert.property(res, 'sessionToken'); - if (requests) { - assert.equal(requests.length - numSetupRequests, 2); - } - }, assert.notOk); - }); - }); - - test('#incorrect email case with skipCaseError', function() { - return accountHelper.newVerifiedAccount().then(function(account) { - var numSetupRequests = requests ? requests.length : null; - var incorrectCaseEmail = - account.input.email.charAt(0).toUpperCase() + - account.input.email.slice(1); - - return respond( - client.signIn(incorrectCaseEmail, account.input.password, { - skipCaseError: true, - }), - ErrorMocks.incorrectEmailCase - ).then( - function() { - assert.fail(); - }, - function(res) { - assert.equal(res.code, 400); - assert.equal(res.errno, 120); - if (requests) { - assert.equal(requests.length - numSetupRequests, 1); - } - } - ); - }); - }); - - test('#incorrectPassword', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.signIn(account.input.email, 'wrong password'), - ErrorMocks.accountIncorrectPassword - ); - }) - .then( - function() { - assert.fail(); - }, - function(res) { - assert.equal(res.code, 400); - assert.equal(res.errno, 103); - } - ); - }); - - test('#with metricsContext metadata', function() { - var email = 'test' + new Date().getTime() + '@restmail.net'; - var password = 'iliketurtles'; - - return respond(client.signUp(email, password), RequestMocks.signUp) - .then(function() { - return respond( - client.signIn(email, password, { - metricsContext: {}, - reason: 'signin', - }), - RequestMocks.signIn - ); - }) - .then(function(resp) { - assert.ok(resp); - }, assert.notOk); - }); + client.signIn(email, password, opts), + RequestMocks.signIn + ); + }) + .then(function(res) { + assert.ok(res.uid); + return respond(mail.wait(user), RequestMocks.mailServiceAndRedirect); + }) + .then(function(emails) { + var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; + var redirectTo = emails[0].html.match(/redirectTo=([A-Za-z0-9]+)/)[1]; + + assert.ok(code, 'code is returned'); + assert.ok(redirectTo, 'redirectTo is returned'); + }, assert.fail); + }); + + it('#with Sync/resume', function() { + var user = 'sync.' + new Date().getTime(); + var email = user + '@restmail.net'; + var password = 'iliketurtles'; + var opts = { + keys: true, + metricsContext: { + context: 'fx_desktop_v2', + }, + redirectTo: 'http://sync.127.0.0.1/after_reset', + resume: 'resumejwt', + service: 'sync', + }; + + return respond( + client.signUp(email, password, { preVerified: true }), + RequestMocks.signUp + ) + .then(function() { + return respond( + client.signIn(email, password, opts), + RequestMocks.signIn + ); + }) + .then(function(res) { + assert.ok(res.uid); + return respond(mail.wait(user), RequestMocks.mailServiceAndRedirect); + }) + .then(function(emails) { + var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; + var resume = emails[0].html.match(/resume=([A-Za-z0-9]+)/)[1]; + + assert.ok(code, 'code is returned'); + assert.ok(resume, 'resume is returned'); + }, assert.fail); + }); + + it('#incorrect email case', function() { + return accountHelper.newVerifiedAccount().then(function(account) { + var numSetupRequests = requests ? requests.length : null; + var incorrectCaseEmail = + account.input.email.charAt(0).toUpperCase() + + account.input.email.slice(1); + + respond(ErrorMocks.incorrectEmailCase); + return respond( + client.signIn(incorrectCaseEmail, account.input.password), + RequestMocks.signIn + ).then(function(res) { + assert.property(res, 'sessionToken'); + if (requests) { + assert.equal(requests.length - numSetupRequests, 2); + } + }, assert.fail); + }); + }); + + it('#incorrect email case with skipCaseError', function() { + return accountHelper.newVerifiedAccount().then(function(account) { + var numSetupRequests = requests ? requests.length : null; + var incorrectCaseEmail = + account.input.email.charAt(0).toUpperCase() + + account.input.email.slice(1); + + return respond( + client.signIn(incorrectCaseEmail, account.input.password, { + skipCaseError: true, + }), + ErrorMocks.incorrectEmailCase + ).then( + function() { + assert.fail(); + }, + function(res) { + assert.equal(res.code, 400); + assert.equal(res.errno, 120); + if (requests) { + assert.equal(requests.length - numSetupRequests, 1); + } + } + ); }); - } + }); + + it('#incorrectPassword', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.signIn(account.input.email, 'wrong password'), + ErrorMocks.accountIncorrectPassword + ); + }) + .then( + function() { + assert.fail(); + }, + function(res) { + assert.equal(res.code, 400); + assert.equal(res.errno, 103); + } + ); + }); + + it('#with metricsContext metadata', function() { + var email = 'test' + new Date().getTime() + '@restmail.net'; + var password = 'iliketurtles'; + + return respond(client.signUp(email, password), RequestMocks.signUp) + .then(function() { + return respond( + client.signIn(email, password, { + metricsContext: {}, + reason: 'signin', + }), + RequestMocks.signIn + ); + }) + .then(function(resp) { + assert.ok(resp); + }, assert.fail); + }); }); diff --git a/packages/fxa-js-client/tests/lib/signUp.js b/packages/fxa-js-client/tests/lib/signUp.js index ea7dc6195d6..d89a56ba1a9 100644 --- a/packages/fxa-js-client/tests/lib/signUp.js +++ b/packages/fxa-js-client/tests/lib/signUp.js @@ -2,341 +2,300 @@ * 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/. */ -define([ - 'intern!tdd', - 'intern/chai!assert', - 'tests/addons/environment', - 'tests/addons/sinon', -], function(tdd, assert, Environment, sinon) { - with (tdd) { - suite('signUp', function() { - var accountHelper; - var respond; - var mail; - var client; - var RequestMocks; - var ErrorMocks; - var xhr; - var xhrOpen; - var xhrSend; +const assert = require('chai').assert; +const Environment = require('../addons/environment'); - beforeEach(function() { - var env = new Environment(); - accountHelper = env.accountHelper; - respond = env.respond; - mail = env.mail; - client = env.client; - RequestMocks = env.RequestMocks; - ErrorMocks = env.ErrorMocks; - xhr = env.xhr; - xhrOpen = sinon.spy(xhr.prototype, 'open'); - xhrSend = sinon.spy(xhr.prototype, 'send'); - }); +const sinon = require('sinon'); +describe('signUp', function() { + var accountHelper; + var respond; + var mail; + var client; + var RequestMocks; + var ErrorMocks; + var xhr; + var xhrOpen; + var xhrSend; + let env; - afterEach(function() { - xhrOpen.restore(); - xhrSend.restore(); - }); + beforeEach(function() { + env = new Environment(); + accountHelper = env.accountHelper; + respond = env.respond; + mail = env.mail; + client = env.client; + RequestMocks = env.RequestMocks; + ErrorMocks = env.ErrorMocks; + xhr = env.xhr; + xhrOpen = sinon.spy(xhr.prototype, 'open'); + xhrSend = sinon.spy(xhr.prototype, 'send'); + }); - test('#basic', function() { - var email = 'test' + new Date().getTime() + '@restmail.net'; - var password = 'iliketurtles'; + afterEach(function() { + xhrOpen.restore(); + xhrSend.restore(); + }); - return respond( - client.signUp(email, password), - RequestMocks.signUp - ).then(function(res) { - assert.property(res, 'uid', 'uid should be returned on signUp'); - assert.property( - res, - 'sessionToken', - 'sessionToken should be returned on signUp' - ); - assert.notProperty( - res, - 'keyFetchToken', - 'keyFetchToken should not be returned on signUp' - ); + it('#basic', function() { + var email = 'test' + new Date().getTime() + '@restmail.net'; + var password = 'iliketurtles'; - assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); - assert.include( - xhrOpen.args[0][1], - '/account/create', - 'path is correct' - ); - var sentData = JSON.parse(xhrSend.args[0][0]); - assert.equal(Object.keys(sentData).length, 2); - assert.equal(sentData.email, email, 'email is correct'); - assert.equal(sentData.authPW.length, 64, 'length of authPW'); - }, assert.notOk); - }); + return respond(client.signUp(email, password), RequestMocks.signUp).then( + function(res) { + assert.property(res, 'uid', 'uid should be returned on signUp'); + assert.property( + res, + 'sessionToken', + 'sessionToken should be returned on signUp' + ); + assert.notProperty( + res, + 'keyFetchToken', + 'keyFetchToken should not be returned on signUp' + ); - test('#withKeys', function() { - var email = 'test' + new Date().getTime() + '@restmail.net'; - var password = 'iliketurtles'; - var opts = { - keys: true, - }; + assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); + assert.include( + xhrOpen.args[0][1], + '/account/create', + 'path is correct' + ); + var sentData = JSON.parse(xhrSend.args[0][0]); + assert.equal(Object.keys(sentData).length, 2); + assert.equal(sentData.email, email, 'email is correct'); + assert.equal(sentData.authPW.length, 64, 'length of authPW'); + }, + assert.fail + ); + }); - return respond( - client.signUp(email, password, opts), - RequestMocks.signUpKeys - ).then(function(res) { - assert.property(res, 'uid', 'uid should be returned on signUp'); - assert.property( - res, - 'sessionToken', - 'sessionToken should be returned on signUp' - ); - assert.property( - res, - 'keyFetchToken', - 'keyFetchToken should be returned on signUp' - ); - assert.property( - res, - 'unwrapBKey', - 'unwrapBKey should be returned on signUp' - ); + it('#withKeys', function() { + var email = 'test' + new Date().getTime() + '@restmail.net'; + var password = 'iliketurtles'; + var opts = { + keys: true, + }; - assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); - assert.include( - xhrOpen.args[0][1], - '/account/create?keys=true', - 'path is correct' - ); - }, assert.notOk); - }); + return respond( + client.signUp(email, password, opts), + RequestMocks.signUpKeys + ).then(function(res) { + assert.property(res, 'uid', 'uid should be returned on signUp'); + assert.property( + res, + 'sessionToken', + 'sessionToken should be returned on signUp' + ); + assert.property( + res, + 'keyFetchToken', + 'keyFetchToken should be returned on signUp' + ); + assert.property( + res, + 'unwrapBKey', + 'unwrapBKey should be returned on signUp' + ); - test('#create account with service, redirectTo, style, and resume', function() { - var user = 'test' + new Date().getTime(); - var email = user + '@restmail.net'; - var password = 'iliketurtles'; - var opts = { - service: 'sync', - redirectTo: 'https://sync.127.0.0.1/after_reset', - resume: 'resumejwt', - style: 'trailhead', - }; + assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); + assert.include( + xhrOpen.args[0][1], + '/account/create?keys=true', + 'path is correct' + ); + }, assert.fail); + }); - return respond( - client.signUp(email, password, opts), - RequestMocks.signUp - ) - .then(function(res) { - assert.ok(res.uid); - return respond( - mail.wait(user), - RequestMocks.mailServiceAndRedirect - ); - }) - .then(function(emails) { - var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; - var service = emails[0].html.match(/service=([A-Za-z0-9]+)/)[1]; - var redirectTo = emails[0].html.match( - /redirectTo=([A-Za-z0-9]+)/ - )[1]; - var resume = emails[0].html.match(/resume=([A-Za-z0-9]+)/)[1]; - var style = emails[0].html.match(/style=trailhead/)[0]; + it('#create account with service, redirectTo, style, and resume', function() { + var user = 'test' + new Date().getTime(); + var email = user + '@restmail.net'; + var password = 'iliketurtles'; + var opts = { + service: 'sync', + redirectTo: 'https://sync.127.0.0.1/after_reset', + resume: 'resumejwt', + style: 'trailhead', + }; - assert.ok(code, 'code is returned'); - assert.ok(service, 'service is returned'); - assert.ok(redirectTo, 'redirectTo is returned'); - assert.ok(resume, 'resume is returned'); - assert.ok(style, 'style is returned'); + return respond(client.signUp(email, password, opts), RequestMocks.signUp) + .then(function(res) { + assert.ok(res.uid); + return respond(mail.wait(user), RequestMocks.mailServiceAndRedirect); + }) + .then(function(emails) { + var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; + var service = emails[0].html.match(/service=([A-Za-z0-9]+)/)[1]; + var redirectTo = emails[0].html.match(/redirectTo=([A-Za-z0-9]+)/)[1]; + var resume = emails[0].html.match(/resume=([A-Za-z0-9]+)/)[1]; + var style = emails[0].html.match(/style=trailhead/)[0]; - assert.include( - xhrOpen.args[0][1], - '/account/create', - 'path is correct' - ); - var sentData = JSON.parse(xhrSend.args[0][0]); - assert.equal(Object.keys(sentData).length, 6); - assert.equal(sentData.email, email, 'email is correct'); - assert.equal(sentData.authPW.length, 64, 'length of authPW'); - assert.equal(sentData.service, opts.service); - assert.equal(sentData.resume, opts.resume); - assert.equal(sentData.redirectTo, opts.redirectTo); - assert.equal(sentData.style, opts.style); - }, assert.notOk); - }); + assert.ok(code, 'code is returned'); + assert.ok(service, 'service is returned'); + assert.ok(redirectTo, 'redirectTo is returned'); + assert.ok(resume, 'resume is returned'); + assert.ok(style, 'style is returned'); - test('#withService', function() { - var user = 'test' + new Date().getTime(); - var email = user + '@restmail.net'; - var password = 'iliketurtles'; - var opts = { - service: 'sync', - }; + assert.include( + xhrOpen.args[0][1], + '/account/create', + 'path is correct' + ); + var sentData = JSON.parse(xhrSend.args[0][0]); + assert.equal(Object.keys(sentData).length, 6); + assert.equal(sentData.email, email, 'email is correct'); + assert.equal(sentData.authPW.length, 64, 'length of authPW'); + assert.equal(sentData.service, opts.service); + assert.equal(sentData.resume, opts.resume); + assert.equal(sentData.redirectTo, opts.redirectTo); + assert.equal(sentData.style, opts.style); + }, assert.fail); + }); - return respond( - client.signUp(email, password, opts), - RequestMocks.signUp - ) - .then(function(res) { - assert.ok(res.uid); - return respond( - mail.wait(user), - RequestMocks.mailServiceAndRedirect - ); - }) - .then(function(emails) { - var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; - var service = emails[0].html.match(/service=([A-Za-z0-9]+)/)[1]; - - assert.ok(code, 'code is returned'); - assert.ok(service, 'service is returned'); - }, assert.notOk); - }); + it('#withService', function() { + var user = 'test' + new Date().getTime(); + var email = user + '@restmail.net'; + var password = 'iliketurtles'; + var opts = { + service: 'sync', + }; - test('#withRedirectTo', function() { - var user = 'test' + new Date().getTime(); - var email = user + '@restmail.net'; - var password = 'iliketurtles'; - var opts = { - redirectTo: 'http://sync.127.0.0.1/after_reset', - }; + return respond(client.signUp(email, password, opts), RequestMocks.signUp) + .then(function(res) { + assert.ok(res.uid); + return respond(mail.wait(user), RequestMocks.mailServiceAndRedirect); + }) + .then(function(emails) { + var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; + var service = emails[0].html.match(/service=([A-Za-z0-9]+)/)[1]; - return respond( - client.signUp(email, password, opts), - RequestMocks.signUp - ) - .then(function(res) { - assert.ok(res.uid); - return respond( - mail.wait(user), - RequestMocks.mailServiceAndRedirect - ); - }) - .then(function(emails) { - var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; - var redirectTo = emails[0].html.match( - /redirectTo=([A-Za-z0-9]+)/ - )[1]; + assert.ok(code, 'code is returned'); + assert.ok(service, 'service is returned'); + }, assert.fail); + }); - assert.ok(code, 'code is returned'); - assert.ok(redirectTo, 'redirectTo is returned'); - }, assert.notOk); - }); + it('#withRedirectTo', function() { + var user = 'test' + new Date().getTime(); + var email = user + '@restmail.net'; + var password = 'iliketurtles'; + var opts = { + redirectTo: 'http://sync.127.0.0.1/after_reset', + }; - test('#withResume', function() { - var user = 'test' + new Date().getTime(); - var email = user + '@restmail.net'; - var password = 'iliketurtles'; - var opts = { - resume: 'resumejwt', - }; + return respond(client.signUp(email, password, opts), RequestMocks.signUp) + .then(function(res) { + assert.ok(res.uid); + return respond(mail.wait(user), RequestMocks.mailServiceAndRedirect); + }) + .then(function(emails) { + var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; + var redirectTo = emails[0].html.match(/redirectTo=([A-Za-z0-9]+)/)[1]; - return respond( - client.signUp(email, password, opts), - RequestMocks.signUp - ) - .then(function(res) { - assert.ok(res.uid); - return respond( - mail.wait(user), - RequestMocks.mailServiceAndRedirect - ); - }) - .then(function(emails) { - var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; - var resume = emails[0].html.match(/resume=([A-Za-z0-9]+)/)[1]; + assert.ok(code, 'code is returned'); + assert.ok(redirectTo, 'redirectTo is returned'); + }, assert.fail); + }); - assert.ok(code, 'code is returned'); - assert.ok(resume, 'resume is returned'); - }, assert.notOk); - }); + it('#withResume', function() { + var user = 'test' + new Date().getTime(); + var email = user + '@restmail.net'; + var password = 'iliketurtles'; + var opts = { + resume: 'resumejwt', + }; - test('#preVerified', function() { - var email = 'test' + new Date().getTime() + '@restmail.net'; - var password = 'iliketurtles'; - var opts = { - preVerified: true, - }; + return respond(client.signUp(email, password, opts), RequestMocks.signUp) + .then(function(res) { + assert.ok(res.uid); + return respond(mail.wait(user), RequestMocks.mailServiceAndRedirect); + }) + .then(function(emails) { + var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; + var resume = emails[0].html.match(/resume=([A-Za-z0-9]+)/)[1]; - return respond( - client.signUp(email, password, opts), - RequestMocks.signUp - ) - .then(function(res) { - assert.ok(res.uid); + assert.ok(code, 'code is returned'); + assert.ok(resume, 'resume is returned'); + }, assert.fail); + }); - return respond(client.signIn(email, password), RequestMocks.signIn); - }) - .then(function(res) { - assert.equal(res.verified, true, '== account is verified'); - }); - }); + it('#preVerified', function() { + var email = 'test' + new Date().getTime() + '@restmail.net'; + var password = 'iliketurtles'; + var opts = { + preVerified: true, + }; - test('#withStyle', function() { - var user = 'test' + new Date().getTime(); - var email = user + '@restmail.net'; - var password = 'iliketurtles'; - var opts = { - style: 'trailhead', - }; + return respond(client.signUp(email, password, opts), RequestMocks.signUp) + .then(function(res) { + assert.ok(res.uid); - return respond( - client.signUp(email, password, opts), - RequestMocks.signUp - ) - .then(function(res) { - assert.ok(res.uid); - return respond( - mail.wait(user), - RequestMocks.mailServiceAndRedirect - ); - }) - .then(function(emails) { - var style = emails[0].html.match(/style=trailhead/)[0]; - assert.ok(style, 'style is returned'); - }, assert.notOk); + return respond(client.signIn(email, password), RequestMocks.signIn); + }) + .then(function(res) { + assert.equal(res.verified, true, '== account is verified'); }); + }); - test('#accountExists', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.signUp(account.input.email, 'somepass'), - ErrorMocks.accountExists - ); - }) - .then( - function(res) { - assert.fail(); - }, - function(err) { - assert.equal(err.code, 400); - assert.equal(err.errno, 101); - } - ); - }); + it('#withStyle', function() { + var user = 'test' + new Date().getTime(); + var email = user + '@restmail.net'; + var password = 'iliketurtles'; + var opts = { + style: 'trailhead', + }; - test('#with metricsContext metadata', function() { - var email = 'test' + new Date().getTime() + '@restmail.net'; - var password = 'iliketurtles'; + return respond(client.signUp(email, password, opts), RequestMocks.signUp) + .then(function(res) { + assert.ok(res.uid); + return respond(mail.wait(user), RequestMocks.mailServiceAndRedirect); + }) + .then(function(emails) { + var style = emails[0].html.match(/style=trailhead/)[0]; + assert.ok(style, 'style is returned'); + }, assert.fail); + }); + it('#accountExists', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { return respond( - client.signUp(email, password, { - metricsContext: { - deviceId: '0123456789abcdef0123456789abcdef', - flowId: - '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', - flowBeginTime: Date.now(), - utmCampaign: 'mock-campaign', - utmContent: 'mock-content', - utmMedium: 'mock-medium', - utmSource: 'mock-source', - utmTerm: 'mock-term', - forbiddenProperty: 666, - }, - }), - RequestMocks.signUp - ).then(function(resp) { - assert.ok(resp); - }, assert.notOk); - }); - }); - } + client.signUp(account.input.email, 'somepass'), + ErrorMocks.accountExists + ); + }) + .then( + function(res) { + assert.fail(); + }, + function(err) { + assert.equal(err.code, 400); + assert.equal(err.errno, 101); + } + ); + }); + + it('#with metricsContext metadata', function() { + var email = 'test' + new Date().getTime() + '@restmail.net'; + var password = 'iliketurtles'; + + return respond( + client.signUp(email, password, { + metricsContext: { + deviceId: '0123456789abcdef0123456789abcdef', + flowId: + '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + flowBeginTime: Date.now(), + utmCampaign: 'mock-campaign', + utmContent: 'mock-content', + utmMedium: 'mock-medium', + utmSource: 'mock-source', + utmTerm: 'mock-term', + forbiddenProperty: 666, + }, + }), + RequestMocks.signUp + ).then(function(resp) { + assert.ok(resp); + }, assert.fail); + }); }); diff --git a/packages/fxa-js-client/tests/lib/signinCodes.js b/packages/fxa-js-client/tests/lib/signinCodes.js index dffff2fc2a4..439d4e0aae8 100644 --- a/packages/fxa-js-client/tests/lib/signinCodes.js +++ b/packages/fxa-js-client/tests/lib/signinCodes.js @@ -7,107 +7,97 @@ var FLOW_ID = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; var FLOW_BEGIN_TIME = Date.now(); -define([ - 'intern!tdd', - 'intern/chai!assert', - 'tests/addons/environment', -], function(tdd, assert, Environment) { - var env = new Environment(); +const assert = require('chai').assert; +const Environment = require('../addons/environment'); - with (tdd) { - suite('signinCodes', function() { - var respond; - var client; - var RequestMocks; +describe('signinCodes', function() { + var respond; + var client; + var RequestMocks; + let env; + let remoteServer; - beforeEach(function() { - env = new Environment(); - respond = env.respond; - client = env.client; - RequestMocks = env.RequestMocks; - }); + beforeEach(function() { + env = new Environment(); + remoteServer = env.useRemoteServer; + respond = env.respond; + client = env.client; + RequestMocks = env.RequestMocks; + }); - if (env.useRemoteServer) { - // This test is intended to run against a local auth-server. To test - // against a mock auth-server would be pointless for this assertion. - test('consumeSigninCode with invalid signinCode', function() { - return client - .consumeSigninCode(SIGNIN_CODE, FLOW_ID, FLOW_BEGIN_TIME) - .then( - function() { - assert.fail( - 'client.consumeSigninCode should reject if signinCode is invalid' - ); - }, - function(err) { - assert.ok( - err, - 'client.consumeSigninCode should return an error' - ); - assert.equal( - err.code, - 400, - 'client.consumeSigninCode should return a 400 response' - ); - assert.equal( - err.errno, - 146, - 'client.consumeSigninCode should return errno 146' - ); - } - ); - }); - } else { - // This test is intended to run against a mock auth-server. To test - // against a local auth-server, we'd need to know a valid signinCode. - test('consumeSigninCode', function() { - return respond( - client.consumeSigninCode(SIGNIN_CODE, FLOW_ID, FLOW_BEGIN_TIME), - RequestMocks.consumeSigninCode - ).then(assert.ok, assert.fail); - }); + // This test is intended to run against a local auth-server. To test + // against a mock auth-server would be pointless for this assertion. + it('consumeSigninCode with invalid signinCode', function() { + if (!remoteServer) return this.skip(); + + return client.consumeSigninCode(SIGNIN_CODE, FLOW_ID, FLOW_BEGIN_TIME).then( + function() { + assert.fail( + 'client.consumeSigninCode should reject if signinCode is invalid' + ); + }, + function(err) { + assert.ok(err, 'client.consumeSigninCode should return an error'); + assert.equal( + err.code, + 400, + 'client.consumeSigninCode should return a 400 response' + ); + assert.equal( + err.errno, + 146, + 'client.consumeSigninCode should return errno 146' + ); } + ); + }); + + // This test is intended to run against a mock auth-server. To test + // against a local auth-server, we'd need to know a valid signinCode. + it('consumeSigninCode', function() { + if (remoteServer) return this.skip(); + return respond( + client.consumeSigninCode(SIGNIN_CODE, FLOW_ID, FLOW_BEGIN_TIME), + RequestMocks.consumeSigninCode + ).then(assert.ok, assert.fail); + }); - test('consumeSigninCode with missing code', function() { - return client.consumeSigninCode(null, FLOW_ID, FLOW_BEGIN_TIME).then( - function() { - assert.fail( - 'client.consumeSigninCode should reject if code is missing' - ); - }, - function(err) { - assert.equal(err.message, 'Missing code'); - } + it('consumeSigninCode with missing code', function() { + return client.consumeSigninCode(null, FLOW_ID, FLOW_BEGIN_TIME).then( + function() { + assert.fail( + 'client.consumeSigninCode should reject if code is missing' ); - }); + }, + function(err) { + assert.equal(err.message, 'Missing code'); + } + ); + }); - test('consumeSigninCode with missing flowId', function() { - return client - .consumeSigninCode(SIGNIN_CODE, null, FLOW_BEGIN_TIME) - .then( - function() { - assert.fail( - 'client.consumeSigninCode should reject if flowId is missing' - ); - }, - function(err) { - assert.equal(err.message, 'Missing flowId'); - } - ); - }); + it('consumeSigninCode with missing flowId', function() { + return client.consumeSigninCode(SIGNIN_CODE, null, FLOW_BEGIN_TIME).then( + function() { + assert.fail( + 'client.consumeSigninCode should reject if flowId is missing' + ); + }, + function(err) { + assert.equal(err.message, 'Missing flowId'); + } + ); + }); - test('consumeSigninCode with missing flowBeginTime', function() { - return client.consumeSigninCode(SIGNIN_CODE, FLOW_ID, null).then( - function() { - assert.fail( - 'client.consumeSigninCode should reject if flowBeginTime is missing' - ); - }, - function(err) { - assert.equal(err.message, 'Missing flowBeginTime'); - } + it('consumeSigninCode with missing flowBeginTime', function() { + return client.consumeSigninCode(SIGNIN_CODE, FLOW_ID, null).then( + function() { + assert.fail( + 'client.consumeSigninCode should reject if flowBeginTime is missing' ); - }); - }); - } + }, + function(err) { + assert.equal(err.message, 'Missing flowBeginTime'); + } + ); + }); }); diff --git a/packages/fxa-js-client/tests/lib/sms.js b/packages/fxa-js-client/tests/lib/sms.js index 1a2c3612136..2d597a2ad68 100644 --- a/packages/fxa-js-client/tests/lib/sms.js +++ b/packages/fxa-js-client/tests/lib/sms.js @@ -2,87 +2,69 @@ * 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/. */ -define([ - 'intern!tdd', - 'intern/chai!assert', - 'tests/addons/environment', - 'tests/lib/push-constants', -], function(tdd, assert, Environment) { - // These tests are intended to run against a mock auth-server. To test - // against a local auth-server, you will need to have it correctly - // configured to send sms and specify a real phone number here. - var env = new Environment(); - if (env.useRemoteServer) { - return; - } +const assert = require('chai').assert; +const Environment = require('../addons/environment'); - var PHONE_NUMBER = '+14071234567'; - var MESSAGE_ID = 1; +var PHONE_NUMBER = '+14168483114'; +var MESSAGE_ID = 1; - with (tdd) { - suite('sms', function() { - var accountHelper; - var respond; - var client; - var RequestMocks; +describe('sms', function() { + var accountHelper; + var respond; + var client; + var RequestMocks; + let env; - beforeEach(function() { - env = new Environment(); - accountHelper = env.accountHelper; - respond = env.respond; - client = env.client; - RequestMocks = env.RequestMocks; - }); + beforeEach(function() { + env = new Environment(); + accountHelper = env.accountHelper; + respond = env.respond; + client = env.client; + RequestMocks = env.RequestMocks; + }); - test('#send connect device', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.sendSms( - account.signIn.sessionToken, - PHONE_NUMBER, - MESSAGE_ID - ), - RequestMocks.sendSmsConnectDevice - ); - }) - .then(function(resp) { - assert.ok(resp); - }, assert.notOk); - }); + it('#send connect device', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.sendSms(account.signIn.sessionToken, PHONE_NUMBER, MESSAGE_ID), + RequestMocks.sendSmsConnectDevice + ); + }) + .then(function(resp) { + assert.ok(resp); + }, assert.fail); + }); - test('status', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.smsStatus(account.signIn.sessionToken), - RequestMocks.smsStatus - ); - }) - .then(function(resp) { - assert.ok(resp); - assert.ok(resp.ok); - assert.ok(resp.country); - }, assert.notOk); - }); + it('status', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.smsStatus(account.signIn.sessionToken), + RequestMocks.smsStatus + ); + }) + .then(function(resp) { + assert.ok(resp); + assert.ok(resp.ok); + }, assert.fail); + }); - test('status with country', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.smsStatus(account.signIn.sessionToken, { country: 'RO' }), - RequestMocks.smsStatus - ); - }) - .then(function(resp) { - assert.ok(resp); - assert.ok(resp.ok); - assert.ok(resp.country, 'RO'); - }, assert.notOk); - }); - }); - } + it('status with country', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.smsStatus(account.signIn.sessionToken, { country: 'US' }), + RequestMocks.smsStatus + ); + }) + .then(function(resp) { + assert.ok(resp); + assert.ok(resp.ok); + assert.ok(resp.country, 'US'); + }, assert.fail); + }); }); diff --git a/packages/fxa-js-client/tests/lib/subscriptions.js b/packages/fxa-js-client/tests/lib/subscriptions.js index 17d0f677f07..e82d4393447 100644 --- a/packages/fxa-js-client/tests/lib/subscriptions.js +++ b/packages/fxa-js-client/tests/lib/subscriptions.js @@ -2,101 +2,104 @@ * 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/. */ -define([ - 'intern!tdd', - 'intern/chai!assert', - 'tests/addons/environment', -], function(tdd, assert, Environment) { - var env = new Environment(); - if (env.useRemoteServer) { - return; - } +const assert = require('chai').assert; +const Environment = require('../addons/environment'); - with (tdd) { - suite('subscriptions', function() { - var accountHelper; - var respond; - var client; - var RequestMocks; +describe('subscriptions', function() { + var accountHelper; + var respond; + var client; + var RequestMocks; + let env; + let remoteServer; - beforeEach(function() { - env = new Environment(); - accountHelper = env.accountHelper; - respond = env.respond; - client = env.client; - RequestMocks = env.RequestMocks; - }); + beforeEach(function() { + env = new Environment(); + accountHelper = env.accountHelper; + respond = env.respond; + client = env.client; + RequestMocks = env.RequestMocks; + remoteServer = env.useRemoteServer; + }); - test('#getActiveSubscriptions - missing token', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.getActiveSubscriptions(), - RequestMocks.getActiveSubscriptions - ); - }) - .then(assert.notOk, function(error) { - assert.include(error.message, 'Missing token'); - }); - }); - test('#getActiveSubscriptions', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.getActiveSubscriptions('saynomore'), - RequestMocks.getActiveSubscriptions - ); - }) - .then(function(resp) { - assert.ok(resp); - }, assert.notOk); + it('#getActiveSubscriptions - missing token', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.getActiveSubscriptions(), + RequestMocks.getActiveSubscriptions + ); + }) + .then(assert.fail, function(error) { + assert.include(error.message, 'Missing token'); }); + }); + + // This test is intended to run against a mock auth-server. To test + // against a local auth-server, we'd need to know a valid subscription. + it('#getActiveSubscriptions', function() { + if (remoteServer) return this.skip(); - test('#createSupportTicket - missing token', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.createSupportTicket(), - RequestMocks.createSupportTicket - ); - }) - .then(assert.notOk, function(error) { - assert.include(error.message, 'Missing token'); - }); + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.getActiveSubscriptions('saynomore'), + RequestMocks.getActiveSubscriptions + ); + }) + .then(function(resp) { + assert.ok(resp); + }, assert.fail); + }); + + it('#createSupportTicket - missing token', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.createSupportTicket(), + RequestMocks.createSupportTicket + ); + }) + .then(assert.fail, function(error) { + assert.include(error.message, 'Missing token'); }); - test('#createSupportTicket - missing supportTicket', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.createSupportTicket('redpandas'), - RequestMocks.createSupportTicket - ); - }) - .then(assert.notOk, function(error) { - assert.include(error.message, 'Missing supportTicket'); - }); + }); + it('#createSupportTicket - missing supportTicket', function() { + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.createSupportTicket('redpandas'), + RequestMocks.createSupportTicket + ); + }) + .then(assert.fail, function(error) { + assert.include(error.message, 'Missing supportTicket'); }); - test('#createSupportTicket', function() { - return accountHelper - .newVerifiedAccount() - .then(function(account) { - return respond( - client.createSupportTicket('redpandas', { - topic: 'Species', - subject: 'Cute & Rare', - message: 'Need moar', - }), - RequestMocks.createSupportTicket - ); - }) - .then(function(resp) { - assert.ok(resp); - }, assert.notOk); + }); + + // This test is intended to run against a mock auth-server. To test + // against a local auth-server, we'd need to know a valid subscription. + it('#createSupportTicket', function() { + if (remoteServer) return this.skip(); + + return accountHelper + .newVerifiedAccount() + .then(function(account) { + return respond( + client.createSupportTicket('redpandas', { + topic: 'Species', + subject: 'Cute & Rare', + message: 'Need moar', + }), + RequestMocks.createSupportTicket + ); + }) + .then(function(resp) { + assert.ok(resp); }); - }); - } + }); }); diff --git a/packages/fxa-js-client/tests/lib/tokenCodes.js b/packages/fxa-js-client/tests/lib/tokenCodes.js index 8642529aafd..90bae24ba92 100644 --- a/packages/fxa-js-client/tests/lib/tokenCodes.js +++ b/packages/fxa-js-client/tests/lib/tokenCodes.js @@ -2,123 +2,120 @@ * 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/. */ -define([ - 'intern!tdd', - 'intern/chai!assert', - 'tests/addons/environment', -], function(tdd, assert, Environment) { - with (tdd) { - suite('tokenCodes', function() { - var account; - var accountHelper; - var respond; - var client; - var mail; - var RequestMocks; - var env = new Environment(); +const assert = require('chai').assert; +const Environment = require('../addons/environment'); - beforeEach(function() { - env = new Environment(); - accountHelper = env.accountHelper; - respond = env.respond; - client = env.client; - mail = env.mail; - RequestMocks = env.RequestMocks; - return accountHelper - .newVerifiedAccount({ username: 'confirm.' + Date.now() }) - .then(function(newAccount) { - account = newAccount; - }); +describe('tokenCodes', function() { + var account; + var accountHelper; + var respond; + var client; + var mail; + var RequestMocks; + let env; + let remoteServer; + + beforeEach(function() { + env = new Environment(); + accountHelper = env.accountHelper; + respond = env.respond; + client = env.client; + mail = env.mail; + RequestMocks = env.RequestMocks; + remoteServer = env.useRemoteServer; + return accountHelper + .newVerifiedAccount({ username: 'sync.' + Date.now() }) + .then(function(newAccount) { + account = newAccount; }); + }); + + // This test is intended to run against a local auth-server. To test + // against a mock auth-server would be pointless for this assertion. + it('verify session with invalid tokenCode', function() { + if (!remoteServer) return this.skip(); - if (env.useRemoteServer) { - // This test is intended to run against a local auth-server. To test - // against a mock auth-server would be pointless for this assertion. - test('verify session with invalid tokenCode', function() { - var opts = { verificationMethod: 'email-2fa', keys: true }; - return respond( - client.signIn(account.input.email, account.input.password, opts), - RequestMocks.signInWithVerificationMethodEmail2faResponse - ) - .then(function(res) { - assert.equal( - res.verificationMethod, - 'email-2fa', - 'should return correct verificationMethod' - ); - assert.equal( - res.verificationReason, - 'login', - 'should return correct verificationReason' - ); - return respond( - mail.wait(account.input.user, 3), - RequestMocks.signInWithVerificationMethodEmail2faCode - ); - }) - .then(function(emails) { - // should contain token code - var code = emails[2].headers['x-signin-verify-code']; - code = code === '000000' ? '000001' : '000000'; - return client.verifyTokenCode( - account.signIn.sessionToken, - account.signIn.uid, - code - ); - }) - .then( - function() { - assert.fail('should reject if tokenCode is invalid'); - }, - function(err) { - assert.ok(err, 'should return an error'); - assert.equal(err.code, 400, 'should return a 400 response'); - assert.equal(err.errno, 152, 'should return errno 152'); - } - ); - }); - } + var opts = { verificationMethod: 'email-2fa', keys: true }; + return respond( + client.signIn(account.input.email, account.input.password, opts), + RequestMocks.signInWithVerificationMethodEmail2faResponse + ) + .then(function(res) { + assert.equal( + res.verificationMethod, + 'email-2fa', + 'should return correct verificationMethod' + ); + assert.equal( + res.verificationReason, + 'login', + 'should return correct verificationReason' + ); + return respond( + mail.wait(account.input.user, 3), + RequestMocks.signInWithVerificationMethodEmail2faCode + ); + }) + .then(function(emails) { + // should contain token code + var code = emails[2].headers['x-signin-verify-code']; + code = code === '000000' ? '000001' : '000000'; + return client.verifyTokenCode( + account.signIn.sessionToken, + account.signIn.uid, + code + ); + }) + .then( + function() { + assert.fail('should reject if tokenCode is invalid'); + }, + function(err) { + assert.ok(err, 'should return an error'); + assert.equal(err.code, 400, 'should return a 400 response'); + assert.equal(err.errno, 152, 'should return errno 152'); + } + ); + }); - test('#verify session with valid tokenCode', function() { - var code; - var opts = { verificationMethod: 'email-2fa', keys: true }; + it('#verify session with valid tokenCode', function() { + var code; + var opts = { verificationMethod: 'email-2fa', keys: true }; + return respond( + client.signIn(account.input.email, account.input.password, opts), + RequestMocks.signInWithVerificationMethodEmail2faResponse + ) + .then(function(res) { + assert.equal( + res.verificationMethod, + 'email-2fa', + 'should return correct verificationMethod' + ); + assert.equal( + res.verificationReason, + 'login', + 'should return correct verificationReason' + ); + return respond( + mail.wait(account.input.user, 3), + RequestMocks.signInWithVerificationMethodEmail2faCode + ); + }) + .then(function(emails) { + // should contain token code + code = emails[2].headers['x-signin-verify-code']; + assert.ok(code, 'code is returned'); return respond( - client.signIn(account.input.email, account.input.password, opts), - RequestMocks.signInWithVerificationMethodEmail2faResponse - ) - .then(function(res) { - assert.equal( - res.verificationMethod, - 'email-2fa', - 'should return correct verificationMethod' - ); - assert.equal( - res.verificationReason, - 'login', - 'should return correct verificationReason' - ); - return respond( - mail.wait(account.input.user, 3), - RequestMocks.signInWithVerificationMethodEmail2faCode - ); - }) - .then(function(emails) { - // should contain token code - code = emails[2].headers['x-signin-verify-code']; - assert.ok(code, 'code is returned'); - return respond( - client.verifyTokenCode( - account.signIn.sessionToken, - account.signIn.uid, - code - ), - RequestMocks.sessionVerifyTokenCodeSuccess - ); - }, assert.notOk) - .then(function(res) { - assert.ok(res, 'res is ok'); - }, assert.notOk); + client.verifyTokenCode( + account.signIn.sessionToken, + account.signIn.uid, + code + ), + RequestMocks.sessionVerifyTokenCodeSuccess + ); + }, assert.fail) + .then(function(res) { + assert.ok(res, 'res is ok'); }); - }); - } + }); }); diff --git a/packages/fxa-js-client/tests/lib/totp.js b/packages/fxa-js-client/tests/lib/totp.js index f7ef0226b77..e55da9505fc 100644 --- a/packages/fxa-js-client/tests/lib/totp.js +++ b/packages/fxa-js-client/tests/lib/totp.js @@ -2,206 +2,182 @@ * 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/. */ -define([ - 'intern!tdd', - 'intern/chai!assert', - 'tests/addons/environment', - 'tests/addons/sinon', - 'node_modules/otplib/otplib-browser', -], function(tdd, assert, Environment, sinon, otplib) { - with (tdd) { - suite('totp', function() { - var authenticator; - var account; - var accountHelper; - var respond; - var client; - var RequestMocks; - var env; - var xhr; - var xhrOpen; - var xhrSend; - var secret; - var opts = { - metricsContext: { - flowBeginTime: Date.now(), - flowId: - '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', - }, - service: 'sync', - }; - - beforeEach(function() { - env = new Environment(); - accountHelper = env.accountHelper; - respond = env.respond; - client = env.client; - RequestMocks = env.RequestMocks; - - return accountHelper - .newVerifiedAccount() - .then(function(newAccount) { - account = newAccount; - return respond( - client.createTotpToken(account.signIn.sessionToken), - RequestMocks.createTotpToken - ); - }) - .then(function(res) { - assert.ok(res.qrCodeUrl, 'should return QR code data encoded url'); - assert.ok( - res.secret, - 'should return secret that is encoded in url' - ); - - // Create a new authenticator instance with shared options - authenticator = new otplib.authenticator.Authenticator(); - authenticator.options = otplib.authenticator.options; - secret = res.secret; - - xhr = env.xhr; - xhrOpen = sinon.spy(xhr.prototype, 'open'); - xhrSend = sinon.spy(xhr.prototype, 'send'); - }); - }); - - afterEach(function() { - xhrOpen.restore(); - xhrSend.restore(); - }); - - test('#createTotpToken - fails if already exists', function() { +const assert = require('chai').assert; +const Environment = require('../addons/environment'); + +const sinon = require('sinon'); +const otplib = require('otplib'); +describe('totp', function() { + var authenticator; + var account; + var accountHelper; + var respond; + var client; + var RequestMocks; + var env; + var xhr; + var xhrOpen; + var xhrSend; + var secret; + var opts = { + metricsContext: { + flowBeginTime: Date.now(), + flowId: + '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + }, + service: 'sync', + }; + + beforeEach(function() { + env = new Environment(); + accountHelper = env.accountHelper; + respond = env.respond; + client = env.client; + RequestMocks = env.RequestMocks; + + return accountHelper + .newVerifiedAccount() + .then(function(newAccount) { + account = newAccount; return respond( client.createTotpToken(account.signIn.sessionToken), - RequestMocks.createTotpTokenDuplicate - ).then(assert.fail, function(err) { - assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); - assert.include(xhrOpen.args[0][1], '/totp/create', 'path is correct'); - assert.equal( - err.errno, - 154, - 'token already exists for account errno' - ); - }); - }); - - test('#deleteTotpToken', function() { - return respond( - client.deleteTotpToken(account.signIn.sessionToken), - RequestMocks.deleteTotpToken - ).then(function(res) { - assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); - assert.include( - xhrOpen.args[0][1], - '/totp/destroy', - 'path is correct' - ); - assert.ok(res, 'should return empty response'); - }); + RequestMocks.createTotpToken + ); + }) + .then(function(res) { + assert.ok(res.qrCodeUrl, 'should return QR code data encoded url'); + assert.ok(res.secret, 'should return secret that is encoded in url'); + + // Create a new authenticator instance with shared options + authenticator = new otplib.authenticator.Authenticator(); + authenticator.options = otplib.authenticator.options; + secret = res.secret; + + xhr = env.xhr; + xhrOpen = sinon.spy(xhr.prototype, 'open'); + xhrSend = sinon.spy(xhr.prototype, 'send'); }); - - test('#checkTotpTokenExists - does not exist returns false', function() { - return accountHelper.newVerifiedAccount().then(function(newAccount) { - return respond( - client.checkTotpTokenExists(newAccount.signIn.sessionToken), - RequestMocks.checkTotpTokenExistsFalse - ).then(function(res) { - assert.equal(xhrOpen.args[4][0], 'GET', 'method is correct'); - assert.include( - xhrOpen.args[4][1], - '/totp/exists', - 'path is correct' - ); - assert.equal(res.exists, false); - }); - }); - }); - - test('#checkTotpTokenExists - created token but not verified returns false', function() { - return respond( - client.checkTotpTokenExists(account.signIn.sessionToken), - RequestMocks.checkTotpTokenExistsFalse - ).then(function(res) { - assert.equal(xhrOpen.args[0][0], 'GET', 'method is correct'); - assert.include(xhrOpen.args[0][1], '/totp/exists', 'path is correct'); - assert.equal(res.exists, false); - }); - }); - - test('#checkTotpTokenExists - verified token returns true', function() { - var code = authenticator.generate(secret); - return respond( - client.verifyTotpCode(account.signIn.sessionToken, code), - RequestMocks.verifyTotpCodeTrue - ).then(function(res) { - assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); - assert.include( - xhrOpen.args[0][1], - '/session/verify/totp', - 'path is correct' - ); - var sentData = JSON.parse(xhrSend.args[0][0]); - assert.equal(Object.keys(sentData).length, 1); - assert.equal(sentData.code, code, 'code is correct'); - - assert.equal(res.success, true); - return respond( - client.checkTotpTokenExists(account.signIn.sessionToken), - RequestMocks.checkTotpTokenExistsTrue - ).then(function(res) { - assert.equal(xhrOpen.args[1][0], 'GET', 'method is correct'); - assert.include( - xhrOpen.args[1][1], - '/totp/exists', - 'path is correct' - ); - assert.equal(res.exists, true); - }); - }); - }); - - test('#verifyTotpCode - succeeds for valid code', function() { - var code = authenticator.generate(secret); - return respond( - client.verifyTotpCode(account.signIn.sessionToken, code, opts), - RequestMocks.verifyTotpCodeTrue - ).then(function(res) { - assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); - assert.include( - xhrOpen.args[0][1], - '/session/verify/totp', - 'path is correct' - ); - var sentData = JSON.parse(xhrSend.args[0][0]); - assert.lengthOf(Object.keys(sentData), 2); - assert.equal(sentData.code, code, 'code is correct'); - assert.equal(sentData.service, opts.service, 'service is correct'); - - assert.equal(res.success, true); - }); + }); + + afterEach(function() { + xhrOpen.restore(); + xhrSend.restore(); + }); + + it('#createTotpToken - fails if already exists', function() { + return respond( + client.createTotpToken(account.signIn.sessionToken), + RequestMocks.createTotpTokenDuplicate + ).then(assert.fail, function(err) { + assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); + assert.include(xhrOpen.args[0][1], '/totp/create', 'path is correct'); + assert.equal(err.errno, 154, 'token already exists for account errno'); + }); + }); + + it('#deleteTotpToken', function() { + return respond( + client.deleteTotpToken(account.signIn.sessionToken), + RequestMocks.deleteTotpToken + ).then(function(res) { + assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); + assert.include(xhrOpen.args[0][1], '/totp/destroy', 'path is correct'); + assert.ok(res, 'should return empty response'); + }); + }); + + it('#checkTotpTokenExists - does not exist returns false', function() { + return accountHelper.newVerifiedAccount().then(function(newAccount) { + return respond( + client.checkTotpTokenExists(newAccount.signIn.sessionToken), + RequestMocks.checkTotpTokenExistsFalse + ).then(function(res) { + assert.equal(xhrOpen.args[4][0], 'GET', 'method is correct'); + assert.include(xhrOpen.args[4][1], '/totp/exists', 'path is correct'); + assert.equal(res.exists, false); }); - - test('#verifyTotpCode - fails for invalid code', function() { - var code = - authenticator.generate(secret) === '000000' ? '000001' : '000000'; - return respond( - client.verifyTotpCode(account.signIn.sessionToken, code, opts), - RequestMocks.verifyTotpCodeFalse - ).then(function(res) { - assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); - assert.include( - xhrOpen.args[0][1], - '/session/verify/totp', - 'path is correct' - ); - var sentData = JSON.parse(xhrSend.args[0][0]); - assert.lengthOf(Object.keys(sentData), 2); - assert.equal(sentData.code, code, 'code is correct'); - assert.equal(sentData.service, opts.service, 'service is correct'); - - assert.equal(res.success, false); - }); + }); + }); + + it('#checkTotpTokenExists - created token but not verified returns false', function() { + return respond( + client.checkTotpTokenExists(account.signIn.sessionToken), + RequestMocks.checkTotpTokenExistsFalse + ).then(function(res) { + assert.equal(xhrOpen.args[0][0], 'GET', 'method is correct'); + assert.include(xhrOpen.args[0][1], '/totp/exists', 'path is correct'); + assert.equal(res.exists, false); + }); + }); + + it('#checkTotpTokenExists - verified token returns true', function() { + var code = authenticator.generate(secret); + return respond( + client.verifyTotpCode(account.signIn.sessionToken, code), + RequestMocks.verifyTotpCodeTrue + ).then(function(res) { + assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); + assert.include( + xhrOpen.args[0][1], + '/session/verify/totp', + 'path is correct' + ); + var sentData = JSON.parse(xhrSend.args[0][0]); + assert.equal(Object.keys(sentData).length, 1); + assert.equal(sentData.code, code, 'code is correct'); + + assert.equal(res.success, true); + return respond( + client.checkTotpTokenExists(account.signIn.sessionToken), + RequestMocks.checkTotpTokenExistsTrue + ).then(function(res) { + assert.equal(xhrOpen.args[1][0], 'GET', 'method is correct'); + assert.include(xhrOpen.args[1][1], '/totp/exists', 'path is correct'); + assert.equal(res.exists, true); }); }); - } + }); + + it('#verifyTotpCode - succeeds for valid code', function() { + var code = authenticator.generate(secret); + return respond( + client.verifyTotpCode(account.signIn.sessionToken, code, opts), + RequestMocks.verifyTotpCodeTrue + ).then(function(res) { + assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); + assert.include( + xhrOpen.args[0][1], + '/session/verify/totp', + 'path is correct' + ); + var sentData = JSON.parse(xhrSend.args[0][0]); + assert.lengthOf(Object.keys(sentData), 2); + assert.equal(sentData.code, code, 'code is correct'); + assert.equal(sentData.service, opts.service, 'service is correct'); + + assert.equal(res.success, true); + }); + }); + + it('#verifyTotpCode - fails for invalid code', function() { + var code = + authenticator.generate(secret) === '000000' ? '000001' : '000000'; + return respond( + client.verifyTotpCode(account.signIn.sessionToken, code, opts), + RequestMocks.verifyTotpCodeFalse + ).then(function(res) { + assert.equal(xhrOpen.args[0][0], 'POST', 'method is correct'); + assert.include( + xhrOpen.args[0][1], + '/session/verify/totp', + 'path is correct' + ); + var sentData = JSON.parse(xhrSend.args[0][0]); + assert.lengthOf(Object.keys(sentData), 2); + assert.equal(sentData.code, code, 'code is correct'); + assert.equal(sentData.service, opts.service, 'service is correct'); + + assert.equal(res.success, false); + }); + }); }); diff --git a/packages/fxa-js-client/tests/lib/unbundle.js b/packages/fxa-js-client/tests/lib/unbundle.js index a51f3340942..ed039abeb83 100644 --- a/packages/fxa-js-client/tests/lib/unbundle.js +++ b/packages/fxa-js-client/tests/lib/unbundle.js @@ -2,92 +2,87 @@ * 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/. */ -define([ - 'intern!tdd', - 'intern/chai!assert', - 'node_modules/sjcl/sjcl', - 'client/lib/credentials', -], function(tdd, assert, sjcl, credentials) { - with (tdd) { - suite('unbundle', function() { - test('#vector 1', function() { - // credentials.unbundleKeyFetchResponse(bundleKey, 'account/keys', payload.bundle); - // Vectors generated from fxa-auth-server - var bundleKey = - 'ba0a107dab60f3b065ff7a642d14fe824fbd71bc5c99087e9e172a1abd1634f1'; - var keyInfo = 'account/keys'; - var bundle = - 'e47eb17e487eb4495e79846d5e0c16ea51ef51ff5ef59cd8f626f95f572ec64dcc7b97fcbc0d0ece0cc93dbe6ac84974066830280ccacf5de13a8460524238cf543edfc5027aabeddc107e9fd429a25ce6f5d94917f2a6435380ee5f11353814'; - var bitBundle = sjcl.codec.hex.toBits(bundle); +const assert = require('chai').assert; +const sjcl = require('sjcl'); +const credentials = require('../../client/lib/credentials'); - return credentials - .deriveBundleKeys(bundleKey, keyInfo) - .then(function(keys) { - assert.equal( - sjcl.codec.hex.fromBits(keys.hmacKey), - '17ab463653a94c9a6419b48781930edefe500395e3b4e7879a2be15999757022', - '== hmacKey equal' - ); - assert.equal( - sjcl.codec.hex.fromBits(keys.xorKey), - '85de16c3218a126404668bf9b7acfb6ce2b7e03c8889047ba48b8b854c6d8beb3ae100e145ca6d69cb519a872a83af788771954455716143bc08225ea8644d85', - '== xorKey equal' - ); +describe('unbundle', function() { + it('#vector 1', function() { + // credentials.unbundleKeyFetchResponse(bundleKey, 'account/keys', payload.bundle); + // Vectors generated from fxa-auth-server + var bundleKey = + 'ba0a107dab60f3b065ff7a642d14fe824fbd71bc5c99087e9e172a1abd1634f1'; + var keyInfo = 'account/keys'; + var bundle = + 'e47eb17e487eb4495e79846d5e0c16ea51ef51ff5ef59cd8f626f95f572ec64dcc7b97fcbc0d0ece0cc93dbe6ac84974066830280ccacf5de13a8460524238cf543edfc5027aabeddc107e9fd429a25ce6f5d94917f2a6435380ee5f11353814'; + var bitBundle = sjcl.codec.hex.toBits(bundle); - var keyAWrapB = credentials.xor( - sjcl.bitArray.bitSlice(bitBundle, 0, 8 * 64), - keys.xorKey - ); - assert.equal( - sjcl.codec.hex.fromBits(keyAWrapB), - '61a0a7bd69f4a62d5a1f0f94e9a0ed86b358b1c3d67c98a352ad72da1b434da6f69a971df9c763a7c798a739404be60c8119a56c59bbae1e5d32a63efa26754a', - '== xorBuffers equal' - ); - var keyObj = { - kA: sjcl.codec.hex.fromBits( - sjcl.bitArray.bitSlice(keyAWrapB, 0, 8 * 32) - ), - wrapKB: sjcl.codec.hex.fromBits( - sjcl.bitArray.bitSlice(keyAWrapB, 8 * 32, 8 * 64) - ), - }; + return credentials + .deriveBundleKeys(bundleKey, keyInfo) + .then(function(keys) { + assert.equal( + sjcl.codec.hex.fromBits(keys.hmacKey), + '17ab463653a94c9a6419b48781930edefe500395e3b4e7879a2be15999757022', + '== hmacKey equal' + ); + assert.equal( + sjcl.codec.hex.fromBits(keys.xorKey), + '85de16c3218a126404668bf9b7acfb6ce2b7e03c8889047ba48b8b854c6d8beb3ae100e145ca6d69cb519a872a83af788771954455716143bc08225ea8644d85', + '== xorKey equal' + ); - return keyObj; - }) - .then(function(result) { - assert.equal( - result.kA, - '61a0a7bd69f4a62d5a1f0f94e9a0ed86b358b1c3d67c98a352ad72da1b434da6', - '== kA equal' - ); - assert.equal( - result.wrapKB, - 'f69a971df9c763a7c798a739404be60c8119a56c59bbae1e5d32a63efa26754a', - '== wrapKB equal' - ); - }); + var keyAWrapB = credentials.xor( + sjcl.bitArray.bitSlice(bitBundle, 0, 8 * 64), + keys.xorKey + ); + assert.equal( + sjcl.codec.hex.fromBits(keyAWrapB), + '61a0a7bd69f4a62d5a1f0f94e9a0ed86b358b1c3d67c98a352ad72da1b434da6f69a971df9c763a7c798a739404be60c8119a56c59bbae1e5d32a63efa26754a', + '== xorBuffers equal' + ); + var keyObj = { + kA: sjcl.codec.hex.fromBits( + sjcl.bitArray.bitSlice(keyAWrapB, 0, 8 * 32) + ), + wrapKB: sjcl.codec.hex.fromBits( + sjcl.bitArray.bitSlice(keyAWrapB, 8 * 32, 8 * 64) + ), + }; + + return keyObj; + }) + .then(function(result) { + assert.equal( + result.kA, + '61a0a7bd69f4a62d5a1f0f94e9a0ed86b358b1c3d67c98a352ad72da1b434da6', + '== kA equal' + ); + assert.equal( + result.wrapKB, + 'f69a971df9c763a7c798a739404be60c8119a56c59bbae1e5d32a63efa26754a', + '== wrapKB equal' + ); }); + }); - test('#vector 2', function() { - var bundleKey = - 'dedd009a8275a4f672bb4b41e14a117812c0b2f400c85fa058e0293f3f45726a'; - var bundle = - 'df4717238a738501bd2ad8f7114ef193ea69751a40108149bfb88a5643a8d683a1e75b705d4db135130f0896dbac0819ab7d54334e0cd4f9c945e0a7ada91899756cedf4384be404844050270310bc2b396f100eeda0c7b428cfe77c40a873ae'; - return credentials - .unbundleKeyFetchResponse(bundleKey, bundle) - .then(function(result) { - assert.equal( - result.kA, - '939282904b808c6003ea31aeb14bc766d2ab70ba7dcaa54f820efcf4762b9619', - '== kA equal' - ); - assert.equal( - result.wrapKB, - '849ac9f71643ace46dcdd384633ec1bffe565852806ee2f859c3eba7fafeafec', - '== wrapKB equal' - ); - }); + it('#vector 2', function() { + var bundleKey = + 'dedd009a8275a4f672bb4b41e14a117812c0b2f400c85fa058e0293f3f45726a'; + var bundle = + 'df4717238a738501bd2ad8f7114ef193ea69751a40108149bfb88a5643a8d683a1e75b705d4db135130f0896dbac0819ab7d54334e0cd4f9c945e0a7ada91899756cedf4384be404844050270310bc2b396f100eeda0c7b428cfe77c40a873ae'; + return credentials + .unbundleKeyFetchResponse(bundleKey, bundle) + .then(function(result) { + assert.equal( + result.kA, + '939282904b808c6003ea31aeb14bc766d2ab70ba7dcaa54f820efcf4762b9619', + '== kA equal' + ); + assert.equal( + result.wrapKB, + '849ac9f71643ace46dcdd384633ec1bffe565852806ee2f859c3eba7fafeafec', + '== wrapKB equal' + ); }); - }); - } + }); }); diff --git a/packages/fxa-js-client/tests/lib/uriVersion.js b/packages/fxa-js-client/tests/lib/uriVersion.js index 2aae53c4144..149429f2447 100644 --- a/packages/fxa-js-client/tests/lib/uriVersion.js +++ b/packages/fxa-js-client/tests/lib/uriVersion.js @@ -2,27 +2,21 @@ * 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/. */ -define(['intern!tdd', 'intern/chai!assert', 'client/FxAccountClient'], function( - tdd, - assert, - FxAccountClient -) { - var xhr = function() {}; - var serverUri = 'https://mock.server'; - var VERSION = FxAccountClient.VERSION; +const assert = require('chai').assert; +const FxAccountClient = require('../../client/FxAccountClient'); +var xhr = function() {}; +var serverUri = 'https://mock.server'; +var VERSION = FxAccountClient.VERSION; - with (tdd) { - suite('fxa client', function() { - test('#version appended to uri when not present', function() { - var client = new FxAccountClient(serverUri, { xhr: xhr }); - assert.equal(serverUri + '/' + VERSION, client.request.baseUri); - }); +describe('fxa client', function() { + it('#version appended to uri when not present', function() { + var client = new FxAccountClient(serverUri, { xhr: xhr }); + assert.equal(serverUri + '/' + VERSION, client.request.baseUri); + }); - test('#version not appended to uri when already present', function() { - var uri = serverUri + '/' + VERSION; - var client = new FxAccountClient(uri, { xhr: xhr }); - assert.equal(uri, client.request.baseUri); - }); - }); - } + it('#version not appended to uri when already present', function() { + var uri = serverUri + '/' + VERSION; + var client = new FxAccountClient(uri, { xhr: xhr }); + assert.equal(uri, client.request.baseUri); + }); }); diff --git a/packages/fxa-js-client/tests/lib/verifyCode.js b/packages/fxa-js-client/tests/lib/verifyCode.js index c1f53ecaeca..8c2f6760c52 100644 --- a/packages/fxa-js-client/tests/lib/verifyCode.js +++ b/packages/fxa-js-client/tests/lib/verifyCode.js @@ -2,278 +2,261 @@ * 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/. */ -define([ - 'intern!tdd', - 'intern/chai!assert', - 'tests/addons/environment', - 'tests/addons/sinon', -], function(tdd, assert, Environment, sinon) { - with (tdd) { - suite('verifyCode', function() { - var respond; - var mail; - var client; - var RequestMocks; - var xhr; - var xhrOpen; - var xhrSend; - - beforeEach(function() { - var env = new Environment(); - respond = env.respond; - mail = env.mail; - client = env.client; - RequestMocks = env.RequestMocks; - xhr = env.xhr; - xhrOpen = sinon.spy(xhr.prototype, 'open'); - xhrSend = sinon.spy(xhr.prototype, 'send'); - }); - - afterEach(function() { - xhrOpen.restore(); - xhrSend.restore(); - }); - - test('#verifyEmail', function() { - var user = 'test3' + new Date().getTime(); - var email = user + '@restmail.net'; - var password = 'iliketurtles'; - var uid; - - return respond(client.signUp(email, password), RequestMocks.signUp) - .then(function(result) { - uid = result.uid; - assert.ok(uid, 'uid is returned'); - - return respond(mail.wait(user), RequestMocks.mail); - }) - .then(function(emails) { - var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; - assert.ok(code, 'code is returned'); - - return respond( - client.verifyCode(uid, code), - RequestMocks.verifyCode - ); - }) - .then(function(result) { - assert.ok(result); - }, assert.notOk); - }); - - test('#verifyEmailCheckStatus', function() { - var user = 'test4' + new Date().getTime(); - var email = user + '@restmail.net'; - var password = 'iliketurtles'; - var uid; - var sessionToken; - - return respond(client.signUp(email, password), RequestMocks.signUp) - .then(function(result) { - uid = result.uid; - assert.ok(uid, 'uid is returned'); - - return respond(client.signIn(email, password), RequestMocks.signIn); - }) - .then(function(result) { - assert.ok(result.sessionToken, 'sessionToken is returned'); - sessionToken = result.sessionToken; - - return respond( - client.recoveryEmailStatus(sessionToken), - RequestMocks.recoveryEmailUnverified - ); - }) - .then(function(result) { - assert.equal( - result.verified, - false, - 'Email should not be verified.' - ); - - return respond( - mail.wait(user, 2), - RequestMocks.mailUnverifiedSignin - ); - }) - .then(function(emails) { - var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; - assert.ok(code, 'code is returned: ' + code); - - return respond( - client.verifyCode(uid, code), - RequestMocks.verifyCode - ); - }) - .then(function(result) { - return respond( - client.recoveryEmailStatus(sessionToken), - RequestMocks.recoveryEmailVerified - ); - }) - .then(function(result) { - assert.equal(result.verified, true, 'Email should be verified.'); - }, assert.notOk); - }); - - test('#verifyEmail with service param', function() { - var user = 'test5' + new Date().getTime(); - var email = user + '@restmail.net'; - var password = 'iliketurtles'; - var uid; - - return respond(client.signUp(email, password), RequestMocks.signUp) - .then(function(result) { - uid = result.uid; - assert.ok(uid, 'uid is returned'); - - return respond(mail.wait(user), RequestMocks.mail); - }) - .then(function(emails) { - var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; - assert.ok(code, 'code is returned'); - - return respond( - client.verifyCode(uid, code, { service: 'sync' }), - RequestMocks.verifyCode - ); - }) - .then(function(result) { - assert.ok(result); - }, assert.notOk); - }); - - test('#verifyEmail with reminder param', function() { - var user = 'test6' + new Date().getTime(); - var email = user + '@restmail.net'; - var password = 'iliketurtles'; - var uid; - - return respond(client.signUp(email, password), RequestMocks.signUp) - .then(function(result) { - uid = result.uid; - assert.ok(uid, 'uid is returned'); - - return respond(mail.wait(user), RequestMocks.mail); - }) - .then(function(emails) { - var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; - assert.ok(code, 'code is returned'); - - return respond( - client.verifyCode(uid, code, { reminder: 'first' }), - RequestMocks.verifyCode - ); - }) - .then(function(result) { - assert.ok(result); - }, assert.notOk); - }); - - test('#verifyEmail with style param', function() { - var user = 'test7' + new Date().getTime(); - var email = user + '@restmail.net'; - var password = 'iliketurtles'; - var uid; - - return respond(client.signUp(email, password), RequestMocks.signUp) - .then(function(result) { - uid = result.uid; - assert.ok(uid, 'uid is returned'); - - return respond(mail.wait(user), RequestMocks.mail); - }) - .then(function(emails) { - var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; - assert.ok(code, 'code is returned'); - - return respond( - client.verifyCode(uid, code, { style: 'trailhead' }), - RequestMocks.verifyCode - ); - }) - .then(function(result) { - assert.ok(result); - assert.equal(xhrOpen.args[2][0], 'POST', 'method is correct'); - assert.include( - xhrOpen.args[2][1], - '/recovery_email/verify_code', - 'path is correct' - ); - var sentData = JSON.parse(xhrSend.args[2][0]); - assert.equal(sentData.style, 'trailhead'); - }, assert.notOk); - }); - - test('#verifyEmail with marketingOptIn param', function() { - var user = 'test7' + new Date().getTime(); - var email = user + '@restmail.net'; - var password = 'iliketurtles'; - var uid; - - return respond(client.signUp(email, password), RequestMocks.signUp) - .then(function(result) { - uid = result.uid; - assert.ok(uid, 'uid is returned'); - - return respond(mail.wait(user), RequestMocks.mail); - }) - .then(function(emails) { - var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; - assert.ok(code, 'code is returned'); - - return respond( - client.verifyCode(uid, code, { marketingOptIn: true }), - RequestMocks.verifyCode - ); - }) - .then(function(result) { - assert.ok(result); - assert.equal(xhrOpen.args[2][0], 'POST', 'method is correct'); - assert.include( - xhrOpen.args[2][1], - '/recovery_email/verify_code', - 'path is correct' - ); - var sentData = JSON.parse(xhrSend.args[2][0]); - assert.equal(sentData.marketingOptIn, true); - }, assert.notOk); - }); - - test('#verifyEmail with newsletters param', function() { - var user = 'test7' + new Date().getTime(); - var email = user + '@restmail.net'; - var password = 'iliketurtles'; - var uid; - - return respond(client.signUp(email, password), RequestMocks.signUp) - .then(function(result) { - uid = result.uid; - assert.ok(uid, 'uid is returned'); - - return respond(mail.wait(user), RequestMocks.mail); - }) - .then(function(emails) { - var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; - assert.ok(code, 'code is returned'); - - return respond( - client.verifyCode(uid, code, { newsletters: ['test-pilot'] }), - RequestMocks.verifyCode - ); - }) - .then(function(result) { - assert.ok(result); - assert.equal(xhrOpen.args[2][0], 'POST', 'method is correct'); - assert.include( - xhrOpen.args[2][1], - '/recovery_email/verify_code', - 'path is correct' - ); - var sentData = JSON.parse(xhrSend.args[2][0]); - assert.deepEqual(sentData.newsletters, ['test-pilot']); - }, assert.notOk); - }); - }); - } +const assert = require('chai').assert; +const Environment = require('../addons/environment'); + +const sinon = require('sinon'); +describe('verifyCode', function() { + var respond; + let env; + var mail; + var client; + var RequestMocks; + var xhr; + var xhrOpen; + var xhrSend; + + beforeEach(function() { + env = new Environment(); + respond = env.respond; + mail = env.mail; + client = env.client; + RequestMocks = env.RequestMocks; + xhr = env.xhr; + xhrOpen = sinon.spy(xhr.prototype, 'open'); + xhrSend = sinon.spy(xhr.prototype, 'send'); + }); + + afterEach(function() { + xhrOpen.restore(); + xhrSend.restore(); + }); + + it('#verifyEmail', function() { + var user = 'test3' + new Date().getTime(); + var email = user + '@restmail.net'; + var password = 'iliketurtles'; + var uid; + + return respond(client.signUp(email, password), RequestMocks.signUp) + .then(function(result) { + uid = result.uid; + assert.ok(uid, 'uid is returned'); + + return respond(mail.wait(user), RequestMocks.mail); + }) + .then(function(emails) { + var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; + assert.ok(code, 'code is returned'); + + return respond(client.verifyCode(uid, code), RequestMocks.verifyCode); + }) + .then(function(result) { + assert.ok(result); + }, assert.fail); + }); + + it('#verifyEmailCheckStatus', function() { + var user = 'test4' + new Date().getTime(); + var email = user + '@restmail.net'; + var password = 'iliketurtles'; + var uid; + var sessionToken; + + return respond(client.signUp(email, password), RequestMocks.signUp) + .then(function(result) { + uid = result.uid; + assert.ok(uid, 'uid is returned'); + + return respond(client.signIn(email, password), RequestMocks.signIn); + }) + .then(function(result) { + assert.ok(result.sessionToken, 'sessionToken is returned'); + sessionToken = result.sessionToken; + + return respond( + client.recoveryEmailStatus(sessionToken), + RequestMocks.recoveryEmailUnverified + ); + }) + .then(function(result) { + assert.equal(result.verified, false, 'Email should not be verified.'); + + return respond(mail.wait(user, 2), RequestMocks.mailUnverifiedSignin); + }) + .then(function(emails) { + var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; + assert.ok(code, 'code is returned: ' + code); + + return respond(client.verifyCode(uid, code), RequestMocks.verifyCode); + }) + .then(function(result) { + return respond( + client.recoveryEmailStatus(sessionToken), + RequestMocks.recoveryEmailVerified + ); + }) + .then(function(result) { + assert.equal(result.verified, true, 'Email should be verified.'); + }, assert.fail); + }); + + it('#verifyEmail with service param', function() { + var user = 'test5' + new Date().getTime(); + var email = user + '@restmail.net'; + var password = 'iliketurtles'; + var uid; + + return respond(client.signUp(email, password), RequestMocks.signUp) + .then(function(result) { + uid = result.uid; + assert.ok(uid, 'uid is returned'); + + return respond(mail.wait(user), RequestMocks.mail); + }) + .then(function(emails) { + var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; + assert.ok(code, 'code is returned'); + + return respond( + client.verifyCode(uid, code, { service: 'sync' }), + RequestMocks.verifyCode + ); + }) + .then(function(result) { + assert.ok(result); + }, assert.fail); + }); + + it('#verifyEmail with reminder param', function() { + var user = 'test6' + new Date().getTime(); + var email = user + '@restmail.net'; + var password = 'iliketurtles'; + var uid; + + return respond(client.signUp(email, password), RequestMocks.signUp) + .then(function(result) { + uid = result.uid; + assert.ok(uid, 'uid is returned'); + + return respond(mail.wait(user), RequestMocks.mail); + }) + .then(function(emails) { + var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; + assert.ok(code, 'code is returned'); + + return respond( + client.verifyCode(uid, code, { reminder: 'first' }), + RequestMocks.verifyCode + ); + }) + .then(function(result) { + assert.ok(result); + }, assert.fail); + }); + + it('#verifyEmail with style param', function() { + var user = 'test7' + new Date().getTime(); + var email = user + '@restmail.net'; + var password = 'iliketurtles'; + var uid; + + return respond(client.signUp(email, password), RequestMocks.signUp) + .then(function(result) { + uid = result.uid; + assert.ok(uid, 'uid is returned'); + + return respond(mail.wait(user), RequestMocks.mail); + }) + .then(function(emails) { + var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; + assert.ok(code, 'code is returned'); + + return respond( + client.verifyCode(uid, code, { style: 'trailhead' }), + RequestMocks.verifyCode + ); + }) + .then(function(result) { + assert.ok(result); + assert.equal(xhrOpen.args[2][0], 'POST', 'method is correct'); + assert.include( + xhrOpen.args[2][1], + '/recovery_email/verify_code', + 'path is correct' + ); + var sentData = JSON.parse(xhrSend.args[2][0]); + assert.equal(sentData.style, 'trailhead'); + }, assert.fail); + }); + + it('#verifyEmail with marketingOptIn param', function() { + var user = 'test7' + new Date().getTime(); + var email = user + '@restmail.net'; + var password = 'iliketurtles'; + var uid; + + return respond(client.signUp(email, password), RequestMocks.signUp) + .then(function(result) { + uid = result.uid; + assert.ok(uid, 'uid is returned'); + + return respond(mail.wait(user), RequestMocks.mail); + }) + .then(function(emails) { + var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; + assert.ok(code, 'code is returned'); + + return respond( + client.verifyCode(uid, code, { marketingOptIn: true }), + RequestMocks.verifyCode + ); + }) + .then(function(result) { + assert.ok(result); + assert.equal(xhrOpen.args[2][0], 'POST', 'method is correct'); + assert.include( + xhrOpen.args[2][1], + '/recovery_email/verify_code', + 'path is correct' + ); + var sentData = JSON.parse(xhrSend.args[2][0]); + assert.equal(sentData.marketingOptIn, true); + }, assert.fail); + }); + + it('#verifyEmail with newsletters param', function() { + var user = 'test7' + new Date().getTime(); + var email = user + '@restmail.net'; + var password = 'iliketurtles'; + var uid; + + return respond(client.signUp(email, password), RequestMocks.signUp) + .then(function(result) { + uid = result.uid; + assert.ok(uid, 'uid is returned'); + + return respond(mail.wait(user), RequestMocks.mail); + }) + .then(function(emails) { + var code = emails[0].html.match(/code=([A-Za-z0-9]+)/)[1]; + assert.ok(code, 'code is returned'); + + return respond( + client.verifyCode(uid, code, { newsletters: ['test-pilot'] }), + RequestMocks.verifyCode + ); + }) + .then(function(result) { + assert.ok(result); + assert.equal(xhrOpen.args[2][0], 'POST', 'method is correct'); + assert.include( + xhrOpen.args[2][1], + '/recovery_email/verify_code', + 'path is correct' + ); + var sentData = JSON.parse(xhrSend.args[2][0]); + assert.deepEqual(sentData.newsletters, ['test-pilot']); + }, assert.fail); + }); }); diff --git a/packages/fxa-js-client/tests/mocks/errors.js b/packages/fxa-js-client/tests/mocks/errors.js index 2123467b073..084dc6fd722 100644 --- a/packages/fxa-js-client/tests/mocks/errors.js +++ b/packages/fxa-js-client/tests/mocks/errors.js @@ -1,164 +1,158 @@ -/* 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/. */ - -define([], function() { - return { - // status code 400, errno 101: attempt to create an account that already exists - accountExists: { - status: 400, - headers: {}, - body: '{"code":400, "errno": 101}', - }, - // status code 400, errno 102: attempt to access an account that does not exist - accountDoesNotExist: { - status: 400, - headers: {}, - body: '{"code":400, "errno": 102}', - }, - // status code 400, errno 103: incorrect password - accountIncorrectPassword: { - status: 400, - headers: {}, - body: '{"code":400, "errno": 103, "message":"Incorrect password"}', - }, - // status code 400, errno 104: attempt to operate on an unverified account - accountUnverified: { - status: 400, - headers: {}, - body: '{"code":400, "errno": 104}', - }, - // status code 400, errno 105: invalid verification code - invalidVerification: { - status: 400, - headers: {}, - body: '{"code":400, "errno": 105}', - }, - // status code 400, errno 106: request body was not valid json - invalidJson: { - status: 400, - headers: {}, - body: '{"code":400, "errno": 106}', - }, - // status code 400, errno 107: request body contains invalid parameters - requestInvalidParams: { - status: 400, - headers: {}, - body: '{"code":400, "errno": 107}', - }, - // status code 400, errno 107: request body contains invalid parameters - requestMissingParams: { - status: 400, - headers: {}, - body: '{"code":400, "errno": 108}', - }, - // status code 401, errno 109: invalid request signature - invalidRequestSignature: { - status: 401, - headers: {}, - body: '{"code":401, "errno": 109}', - }, - // status code 401, errno 110: invalid authentication token - invalidAuthToken: { - status: 401, - headers: {}, - body: '{"code":401, "errno": 110}', - }, - // status code 401, errno 111: invalid authentication timestamp - invalidAuthTimestamp: { - status: 401, - headers: {}, - body: '{"code":401, "errno": 111}', - }, - // status code 411, errno 112: content-length header was not provided - missingContentLength: { - status: 411, - headers: {}, - body: '{"code":411, "errno": 112}', - }, - // status code 413, errno 113: request body too large - requestTooLarge: { - status: 413, - headers: {}, - body: '{"code":413, "errno": 113}', - }, - // status code 429, errno 114: client has sent too many requests (see backoff protocol) - sentTooManyRequests: { - status: 429, - headers: {}, - body: '{"code":429, "errno": 114}', - }, - // status code 429, errno 115: invalid authentication nonce - invalidAuthNonce: { - status: 401, - headers: {}, - body: '{"code":401, "errno": 115}', - }, - // status code 410, errno 116: endpoint is no longer supported - endpointNotSupported: { - status: 410, - headers: {}, - body: '{"code":410, "errno": 116}', - }, - // status code 400, errno 117: incorrect login method for this account - incorrectLoginMethod: { - status: 400, - headers: {}, - body: '{"code":400, "errno": 117}', - }, - // status code 400, errno 118: incorrect key retrieval method for this account - incorrectKeyMethod: { - status: 400, - headers: {}, - body: '{"code":400, "errno": 118}', - }, - // status code 400, errno 119: incorrect API version for this account - incorrectAPIVersion: { - status: 400, - headers: {}, - body: '{"code":400, "errno": 119}', - }, - // status code 400, errno 120: incorrect email case - incorrectEmailCase: { - status: 400, - headers: {}, - body: '{"code":400, "errno": 120, "email": "a@b.com"}', - }, - // status code 503, errno 201: service temporarily unavailable to due high load (see backoff protocol) - temporarilyUnavailable: { - status: 503, - headers: {}, - body: '{"code":503, "errno": 201}', - }, - // any status code, errno 999: unknown error - unknownError: { - status: 400, - headers: {}, - body: '{"code":400, "errno": 999}', - }, - timeout: { - status: 400, - headers: {}, - body: '', - }, - badResponseFormat: { - status: 404, - headers: {}, - body: 'Something is wrong.', - }, - signInBlocked: { - status: 429, - headers: {}, - body: JSON.stringify({ - code: 429, - errno: 125, - verificationMethod: 'email-captcha', - verificationReason: 'login', - }), - }, - signInInvalidUnblockCode: { - status: 400, - body: '{"code":400, "errno": 127}', - }, - }; -}); +module.exports = { + // status code 400, errno 101: attempt to create an account that already exists + accountExists: { + status: 400, + headers: {}, + body: '{"code":400, "errno": 101}', + }, + // status code 400, errno 102: attempt to access an account that does not exist + accountDoesNotExist: { + status: 400, + headers: {}, + body: '{"code":400, "errno": 102}', + }, + // status code 400, errno 103: incorrect password + accountIncorrectPassword: { + status: 400, + headers: {}, + body: '{"code":400, "errno": 103, "message":"Incorrect password"}', + }, + // status code 400, errno 104: attempt to operate on an unverified account + accountUnverified: { + status: 400, + headers: {}, + body: '{"code":400, "errno": 104}', + }, + // status code 400, errno 105: invalid verification code + invalidVerification: { + status: 400, + headers: {}, + body: '{"code":400, "errno": 105}', + }, + // status code 400, errno 106: request body was not valid json + invalidJson: { + status: 400, + headers: {}, + body: '{"code":400, "errno": 106}', + }, + // status code 400, errno 107: request body contains invalid parameters + requestInvalidParams: { + status: 400, + headers: {}, + body: '{"code":400, "errno": 107}', + }, + // status code 400, errno 107: request body contains invalid parameters + requestMissingParams: { + status: 400, + headers: {}, + body: '{"code":400, "errno": 108}', + }, + // status code 401, errno 109: invalid request signature + invalidRequestSignature: { + status: 401, + headers: {}, + body: '{"code":401, "errno": 109}', + }, + // status code 401, errno 110: invalid authentication token + invalidAuthToken: { + status: 401, + headers: {}, + body: '{"code":401, "errno": 110}', + }, + // status code 401, errno 111: invalid authentication timestamp + invalidAuthTimestamp: { + status: 401, + headers: {}, + body: '{"code":401, "errno": 111}', + }, + // status code 411, errno 112: content-length header was not provided + missingContentLength: { + status: 411, + headers: {}, + body: '{"code":411, "errno": 112}', + }, + // status code 413, errno 113: request body too large + requestTooLarge: { + status: 413, + headers: {}, + body: '{"code":413, "errno": 113}', + }, + // status code 429, errno 114: client has sent too many requests (see backoff protocol) + sentTooManyRequests: { + status: 429, + headers: {}, + body: '{"code":429, "errno": 114}', + }, + // status code 429, errno 115: invalid authentication nonce + invalidAuthNonce: { + status: 401, + headers: {}, + body: '{"code":401, "errno": 115}', + }, + // status code 410, errno 116: endpoint is no longer supported + endpointNotSupported: { + status: 410, + headers: {}, + body: '{"code":410, "errno": 116}', + }, + // status code 400, errno 117: incorrect login method for this account + incorrectLoginMethod: { + status: 400, + headers: {}, + body: '{"code":400, "errno": 117}', + }, + // status code 400, errno 118: incorrect key retrieval method for this account + incorrectKeyMethod: { + status: 400, + headers: {}, + body: '{"code":400, "errno": 118}', + }, + // status code 400, errno 119: incorrect API version for this account + incorrectAPIVersion: { + status: 400, + headers: {}, + body: '{"code":400, "errno": 119}', + }, + // status code 400, errno 120: incorrect email case + incorrectEmailCase: { + status: 400, + headers: {}, + body: '{"code":400, "errno": 120, "email": "a@b.com"}', + }, + // status code 503, errno 201: service temporarily unavailable to due high load (see backoff protocol) + temporarilyUnavailable: { + status: 503, + headers: {}, + body: '{"code":503, "errno": 201}', + }, + // any status code, errno 999: unknown error + unknownError: { + status: 400, + headers: {}, + body: '{"code":400, "errno": 999}', + }, + timeout: { + status: 400, + headers: {}, + body: '', + }, + badResponseFormat: { + status: 404, + headers: {}, + body: 'Something is wrong.', + }, + signInBlocked: { + status: 429, + headers: {}, + body: JSON.stringify({ + code: 429, + errno: 125, + verificationMethod: 'email-captcha', + verificationReason: 'login', + }), + }, + signInInvalidUnblockCode: { + status: 400, + body: '{"code":400, "errno": 127}', + }, +}; diff --git a/packages/fxa-js-client/tests/mocks/pushConstants.js b/packages/fxa-js-client/tests/mocks/pushConstants.js new file mode 100644 index 00000000000..5e7acdcbff6 --- /dev/null +++ b/packages/fxa-js-client/tests/mocks/pushConstants.js @@ -0,0 +1,17 @@ +/* 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'; + +module.exports = { + DEVICE_CALLBACK: + 'https://updates.push.services.mozilla.com/update/abcdef01234567890abcdefabcdef01234567890abcdef', + DEVICE_ID: '0f7aa00356e5416e82b3bef7bc409eef', + DEVICE_NAME: 'My Phone', + DEVICE_NAME_2: 'My Android Phone', + DEVICE_PUBLIC_KEY: + 'BBXOKjUb84pzws1wionFpfCBjDuCh4-s_1b52WA46K5wYL2gCWEOmFKWn_NkS5nmJwTBuO8qxxdjAIDtNeklvQc', + DEVICE_AUTH_KEY: 'GSsIiaD2Mr83iPqwFNK4rw', + DEVICE_TYPE: 'mobile', +}; diff --git a/packages/fxa-js-client/tests/mocks/request.js b/packages/fxa-js-client/tests/mocks/request.js index 2caaee56971..dc2f3135ba9 100644 --- a/packages/fxa-js-client/tests/mocks/request.js +++ b/packages/fxa-js-client/tests/mocks/request.js @@ -2,547 +2,545 @@ * 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/. */ -//jscs:disable maximumLineLength -define(['client/lib/errors', 'tests/lib/push-constants'], function( - ERRORS, - PushTestConstants -) { - var DEVICE_CALLBACK = PushTestConstants.DEVICE_CALLBACK; - var DEVICE_ID = PushTestConstants.DEVICE_ID; - var DEVICE_NAME = PushTestConstants.DEVICE_NAME; - var DEVICE_NAME_2 = PushTestConstants.DEVICE_NAME_2; - var DEVICE_PUBLIC_KEY = PushTestConstants.DEVICE_PUBLIC_KEY; - var DEVICE_AUTH_KEY = PushTestConstants.DEVICE_AUTH_KEY; - var DEVICE_TYPE = PushTestConstants.DEVICE_TYPE; +const ERRORS = require('../../client/lib/errors'); +const PushTestConstants = require('./pushConstants'); +var DEVICE_CALLBACK = PushTestConstants.DEVICE_CALLBACK; +var DEVICE_ID = PushTestConstants.DEVICE_ID; +var DEVICE_NAME = PushTestConstants.DEVICE_NAME; +var DEVICE_NAME_2 = PushTestConstants.DEVICE_NAME_2; +var DEVICE_PUBLIC_KEY = PushTestConstants.DEVICE_PUBLIC_KEY; +var DEVICE_AUTH_KEY = PushTestConstants.DEVICE_AUTH_KEY; +var DEVICE_TYPE = PushTestConstants.DEVICE_TYPE; - return { - createOAuthCode: { - status: 200, - headers: {}, - body: '{}', - }, - createOAuthToken: { - status: 200, - headers: {}, - body: '{}', - }, - deleteSecurityEvents: { - status: 200, - body: '{}', - }, - getOAuthScopedKeyData: { - status: 200, - headers: {}, - body: '{}', - }, - signUp: { - status: 200, - headers: {}, - body: - '{ "uid": "0577e7a5fbf448e3bc60dacbff5dcd5c", "sessionToken": "27cd4f4a4aa03d7d186a2ec81cbf19d5c8a604713362df9ee15c4f4a4aa03d7d"}', - }, - signUpExistingDevice: { - status: 200, - headers: {}, - body: JSON.stringify({ - device: { - id: DEVICE_ID, - name: DEVICE_NAME, - type: DEVICE_TYPE, - pushCallback: DEVICE_CALLBACK, - pushPublicKey: DEVICE_PUBLIC_KEY, - pushAuthKey: DEVICE_AUTH_KEY, - }, - sessionToken: - '6544062365c5ebee16e3c5e15448139851583b5f5f7b6bd6d4a37bac41665e8a', - uid: '9c8e5cf6915949c1b063b88fa0c53d05', - verified: true, - }), - }, - signUpKeys: { - status: 200, - headers: {}, - body: - '{ "uid": "0577e7a5fbf448e3bc60dacbff5dcd5c", "sessionToken": "27cd4f4a4aa03d7d186a2ec81cbf19d5c8a604713362df9ee15c4f4a4aa03d7d","keyFetchToken": "b1f4182d7e072567a1dbe682043a16932a84b7f4ca3b95e471a34806c87e4130"}', - }, - signIn: { - status: 200, - headers: {}, - body: - '{"uid":"9c8e5cf6915949c1b063b88fa0c53d05","verified":true,"sessionToken":"6544062365c5ebee16e3c5e15448139851583b5f5f7b6bd6d4a37bac41665e8a", "emailSent": false}', - }, - signInEmailSent: { - status: 200, - headers: {}, - body: - '{"uid":"9c8e5cf6915949c1b063b88fa0c53d05","verified":true,"sessionToken":"6544062365c5ebee16e3c5e15448139851583b5f5f7b6bd6d4a37bac41665e8a","emailSent":true}', - }, - signInFailurePassword: { - status: 400, - headers: {}, - body: '{"code":400,"message":"Incorrect password"}', - }, - signInWithKeys: { - status: 200, - headers: {}, - body: - '{"uid": "5d576e2cd3604981a8c05f6ea67fce5b", "sessionToken": "9c1fe2a0643ce23aa1b44afbe30e28d33e5726558cab215314980fc85875684f","keyFetchToken": "b1f4182d7e072567a1dbe682043a16932a84b7f4ca3b95e471a34806c87e4130","verified": true, "emailSent": false}', - }, - signInForceTokenVerification: { - status: 200, - headers: {}, - body: - '{"uid": "5d576e2cd3604981a8c05f6ea67fce5b", "sessionToken": "9c1fe2a0643ce23aa1b44afbe30e28d33e5726558cab215314980fc85875684f","keyFetchToken": "b1f4182d7e072567a1dbe682043a16932a84b7f4ca3b95e471a34806c87e4130","verified": true, "emailSent": false}', - }, - heartbeat: { - status: 200, - body: '{}', - }, - verifyCode: { - status: 200, - body: '{}', - }, - mail: { - status: 200, - body: '[{"html":"Mocked code=9001"}]', - }, - mailUnverifiedSignin: { - status: 200, - body: '[{"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}]', - }, - mailUnverifiedEmail: { - status: 200, - body: '[{"html":"Mocked code=9001"}]', - }, - mailSignUpLang: { - status: 200, - body: - '[{"html":"Mocked code=9001","headers": {"content-language": "zh-CN" }}]', - }, - mailServiceAndRedirect: { - status: 200, - body: - '[{"html":"Mocked code=9001 service=sync redirectTo=https resume=resumejwt style=trailhead"}]', - }, - resetMail: { - status: 200, - body: - '[{"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}]', - }, - resetMailrecoveryEmailResendCode: { - status: 200, - body: - '[{"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}]', - }, - resetMailpasswordForgotresetMail: { - status: 200, - body: '[{"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}]', - }, - resetMailpasswordForgotRecoveryKey: { - status: 200, - body: - '[{"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}]', - }, - resetMailUnlock: { - status: 200, - body: - '[{"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}]', - }, - resetMailWithServiceAndRedirectNoSignup: { - status: 200, - body: - '[{"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}, {"html":"Mocked code=9001 service=sync redirectTo=https resume=resumejwt style=trailhead"}]', - }, - resetMailWithServiceAndRedirect: { - status: 200, - body: - '[{"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}, {"html":"Mocked code=9001 service=sync redirectTo=https resume=resumejwt"}]', - }, - resetMailResendWithServiceAndRedirect: { - status: 200, - body: - '[{"html":"Mocked code=9001"}, {"html":"Mocked code=9001 service=sync redirectTo=https"}, {"html":"Mocked code=9001 service=sync redirectTo=https resume=resumejwt"}]', - }, - resetMailLang: { - status: 200, - body: - '[{"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}, {"html":"Mocked code=9001","headers": {"content-language": "zh-CN" }}]', - }, - recoveryEmailUnverified: { - status: 200, - body: '{"verified": false}', - }, - recoveryEmailVerified: { - status: 200, - body: '{"verified": true}', - }, - recoveryEmailResendCode: { - status: 200, - body: '{}', - }, - passwordForgotSendCode: { - status: 200, - body: - '{"passwordForgotToken":"e838790265a45f6ee1130070d57d67d9bb20953706f73af0e34b0d4d92f19103","ttl":900,"tries":3}', - }, - passwordForgotResendCode: { - status: 200, - body: - '{"passwordForgotToken":"e838790265a45f6ee1130070d57d67d9bb20953706f73af0e34b0d4d92f19103","ttl":900,"tries":3}', - }, - passwordForgotStatus: { - status: 200, - body: '{ "tries": 3, "ttl": 420 }', - }, - passwordForgotVerifyCode: { - status: 200, - body: - '{"accountResetToken":"50a2052498d538a5d3918847751c8d5077294fd637dbf20d27f2f5f854cbcf4f"}', - }, - passwordChangeStart: { - status: 200, - body: - '{ "keyFetchToken": "b1f4182d7e072567a1dbe682043a16932a84b7f4ca3b95e471a34806c87e4130", "passwordChangeToken": "0208a48ca4f777688a1017e98cedcc1c36ba9c4595088d28dcde5af04ae2215b", "verified": true }', - }, - passwordChangeFinish: { - status: 200, - body: '{}', - }, - passwordChangeFinishKeys: { - status: 200, - body: - '{"uid": "5d576e2cd3604981a8c05f6ea67fce5b", "sessionToken": "9c1fe2a0643ce23aa1b44afbe30e28d33e5726558cab215314980fc85875684f","keyFetchToken": "b1f4182d7e072567a1dbe682043a16932a84b7f4ca3b95e471a34806c87e4130","verified": true}', - }, - accountReset: { - status: 200, - body: - '{"uid": "5d576e2cd3604981a8c05f6ea67fce5b", "sessionToken": "9c1fe2a0643ce23aa1b44afbe30e28d33e5726558cab215314980fc85875684f","keyFetchToken": "b1f4182d7e072567a1dbe682043a16932a84b7f4ca3b95e471a34806c87e4130","verified": true}', - }, - accountProfile: { - status: 200, - body: - '{"email": "a@a.com", "locale": "en", "authenticationMethods": ["pwd", "email"], "authenticatorAssuranceLevel": 2, "profileChangedAt": 1539002077704}', - }, - account: { - status: 200, - body: '{"subscriptions":[{"foo":"bar"}]}', - }, - securityEvents: { - status: 200, - body: JSON.stringify([ - { - name: 'account.login', - verified: true, - createdAt: new Date().getTime(), - }, - { - name: 'account.create', - verified: true, - createdAt: new Date().getTime(), - }, - ]), - }, - securityEventsEmptyResponse: { - status: 200, - body: '[]', - }, - sessionDestroy: { - status: 200, - body: '{}', - }, - sessionStatus: { - status: 200, - body: '{}', - }, - sessions: { - status: 200, - body: JSON.stringify([ - { - id: 'device1', - userAgent: 'agent1', - deviceName: 'name1', - deviceType: 'desktop', - isDevice: false, - lastAccessTime: 100, - lastAccessTimeFormatted: 'a few seconds ago', - }, - { - id: 'device2', - userAgent: 'agent2', - deviceName: 'name2', - deviceType: 'desktop', - isDevice: false, - lastAccessTime: 101, - lastAccessTimeFormatted: 'a few seconds ago', - }, - ]), - }, - attachedClients: { - status: 200, - body: JSON.stringify([ - { - deviceId: 'device1', - deviceType: 'desktop', - isDevice: false, - lastAccessTime: 100, - lastAccessTimeFormatted: 'a few seconds ago', - name: 'name1', - sessionToken: 'session1', - userAgent: 'agent1', - }, - { - deviceId: 'device2', - deviceType: 'desktop', - isDevice: false, - lastAccessTime: 101, - lastAccessTimeFormatted: 'a few seconds ago', - name: 'name2', - sessionToken: 'session2', - userAgent: 'agent2', - }, - ]), - }, - attachedClientDestroy: { - status: 200, - body: '{}', - }, - accountDestroy: { - status: 200, - body: '{}', - }, - accountKeys: { - status: 200, - body: - '{ "bundle": "7f1a9633560774251a2d317b4539e04bcb14a767ec92e3b3f4d438fdad984831f6d1e1b0d93c23d312bf0859270dc8c0e6ebcae4c499f3a604881fc57683459b01cdfd04757835b0334a80728ce40cf50dce32bb365d8a0ac868bb747bf8aca4"}', - }, - accountStatus: { - status: 200, - body: '{ "exists": true }', - }, - accountStatusFalse: { - status: 200, - body: '{ "exists": false }', - }, - certificateSign: { - status: 200, - body: - '{ "cert": "eyJhbGciOiJEUzI1NiJ9.eyJwdWJsaWMta2V5Ijp7ImFsZ29yaXRobSI6IlJTIiwibiI6IjU3NjE1NTUwOTM3NjU1NDk2MDk4MjAyMjM2MDYyOTA3Mzg5ODMyMzI0MjUyMDY2Mzc4OTA0ODUyNDgyMjUzODg1MTA3MzQzMTY5MzI2OTEyNDkxNjY5NjQxNTQ3NzQ1OTM3NzAxNzYzMTk1NzQ3NDI1NTEyNjU5NjM2MDgwMzYzNjE3MTc1MzMzNjY5MzEyNTA2OTk1MzMyNDMiLCJlIjoiNjU1MzcifSwicHJpbmNpcGFsIjp7ImVtYWlsIjoiZm9vQGV4YW1wbGUuY29tIn0sImlhdCI6MTM3MzM5MjE4OTA5MywiZXhwIjoxMzczMzkyMjM5MDkzLCJpc3MiOiIxMjcuMC4wLjE6OTAwMCJ9.l5I6WSjsDIwCKIz_9d3juwHGlzVcvI90T2lv2maDlr8bvtMglUKFFWlN_JEzNyPBcMDrvNmu5hnhyN7vtwLu3Q" }', - }, - getRandomBytes: { - status: 200, - body: - '{ "data": "ac55c0520f2edfb026761443da0ab27b1fa18c98912af6291714e9600aa34991" }', - }, - invalidTimestamp: { - status: 401, - body: - '{ "errno": ' + - ERRORS.INVALID_TIMESTAMP + - ', "error": "Invalid authentication timestamp", "serverTime": ' + - new Date().getTime() + - ' }', - }, - deviceDestroy: { - status: 200, - body: '{}', - }, - deviceList: { - status: 200, - body: JSON.stringify([ - { - id: DEVICE_ID, - name: DEVICE_NAME, - type: DEVICE_TYPE, - pushCallback: DEVICE_CALLBACK, - pushPublicKey: DEVICE_PUBLIC_KEY, - pushAuthKey: DEVICE_AUTH_KEY, - }, - ]), - }, - deviceRegister: { - status: 200, - body: JSON.stringify({ +module.exports = { + createOAuthCode: { + status: 200, + headers: {}, + body: '{}', + }, + createOAuthToken: { + status: 200, + headers: {}, + body: '{}', + }, + deleteSecurityEvents: { + status: 200, + body: '{}', + }, + getOAuthScopedKeyData: { + status: 200, + headers: {}, + body: '{}', + }, + signUp: { + status: 200, + headers: {}, + body: + '{ "uid": "0577e7a5fbf448e3bc60dacbff5dcd5c", "sessionToken": "27cd4f4a4aa03d7d186a2ec81cbf19d5c8a604713362df9ee15c4f4a4aa03d7d"}', + }, + signUpExistingDevice: { + status: 200, + headers: {}, + body: JSON.stringify({ + device: { id: DEVICE_ID, name: DEVICE_NAME, type: DEVICE_TYPE, pushCallback: DEVICE_CALLBACK, pushPublicKey: DEVICE_PUBLIC_KEY, pushAuthKey: DEVICE_AUTH_KEY, - }), - }, - deviceUpdate: { - status: 200, - body: JSON.stringify({ + }, + sessionToken: + '6544062365c5ebee16e3c5e15448139851583b5f5f7b6bd6d4a37bac41665e8a', + uid: '9c8e5cf6915949c1b063b88fa0c53d05', + verified: true, + }), + }, + signUpKeys: { + status: 200, + headers: {}, + body: + '{ "uid": "0577e7a5fbf448e3bc60dacbff5dcd5c", "sessionToken": "27cd4f4a4aa03d7d186a2ec81cbf19d5c8a604713362df9ee15c4f4a4aa03d7d","keyFetchToken": "b1f4182d7e072567a1dbe682043a16932a84b7f4ca3b95e471a34806c87e4130"}', + }, + signIn: { + status: 200, + headers: {}, + body: + '{"uid":"9c8e5cf6915949c1b063b88fa0c53d05","verified":true,"sessionToken":"6544062365c5ebee16e3c5e15448139851583b5f5f7b6bd6d4a37bac41665e8a", "emailSent": false}', + }, + signInEmailSent: { + status: 200, + headers: {}, + body: + '{"uid":"9c8e5cf6915949c1b063b88fa0c53d05","verified":true,"sessionToken":"6544062365c5ebee16e3c5e15448139851583b5f5f7b6bd6d4a37bac41665e8a","emailSent":true}', + }, + signInFailurePassword: { + status: 400, + headers: {}, + body: '{"code":400,"message":"Incorrect password"}', + }, + signInWithKeys: { + status: 200, + headers: {}, + body: + '{"uid": "5d576e2cd3604981a8c05f6ea67fce5b", "sessionToken": "9c1fe2a0643ce23aa1b44afbe30e28d33e5726558cab215314980fc85875684f","keyFetchToken": "b1f4182d7e072567a1dbe682043a16932a84b7f4ca3b95e471a34806c87e4130","verified": true, "emailSent": false}', + }, + signInForceTokenVerification: { + status: 200, + headers: {}, + body: + '{"uid": "5d576e2cd3604981a8c05f6ea67fce5b", "sessionToken": "9c1fe2a0643ce23aa1b44afbe30e28d33e5726558cab215314980fc85875684f","keyFetchToken": "b1f4182d7e072567a1dbe682043a16932a84b7f4ca3b95e471a34806c87e4130","verified": true, "emailSent": false}', + }, + heartbeat: { + status: 200, + body: '{}', + }, + verifyCode: { + status: 200, + body: '{}', + }, + mail: { + status: 200, + body: '[{"html":"Mocked code=9001"}]', + }, + mailUnverifiedSignin: { + status: 200, + body: '[{"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}]', + }, + mailUnverifiedEmail: { + status: 200, + body: '[{"html":"Mocked code=9001"}]', + }, + mailSignUpLang: { + status: 200, + body: + '[{"html":"Mocked code=9001","headers": {"content-language": "zh-CN" }}]', + }, + mailServiceAndRedirect: { + status: 200, + body: + '[{"html":"Mocked code=9001 service=sync redirectTo=https resume=resumejwt style=trailhead"}]', + }, + resetMail: { + status: 200, + body: + '[{"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}]', + }, + resetMailrecoveryEmailResendCode: { + status: 200, + body: + '[{"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}]', + }, + resetMailpasswordForgotresetMail: { + status: 200, + body: '[{"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}]', + }, + resetMailpasswordForgotRecoveryKey: { + status: 200, + body: + '[{"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}]', + }, + resetMailUnlock: { + status: 200, + body: + '[{"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}]', + }, + resetMailWithServiceAndRedirectNoSignup: { + status: 200, + body: + '[{"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}, {"html":"Mocked code=9001 service=sync redirectTo=https resume=resumejwt style=trailhead"}]', + }, + resetMailWithServiceAndRedirect: { + status: 200, + body: + '[{"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}, {"html":"Mocked code=9001 service=sync redirectTo=https resume=resumejwt"}]', + }, + resetMailResendWithServiceAndRedirect: { + status: 200, + body: + '[{"html":"Mocked code=9001"}, {"html":"Mocked code=9001 service=sync redirectTo=https"}, {"html":"Mocked code=9001 service=sync redirectTo=https resume=resumejwt"}]', + }, + resetMailLang: { + status: 200, + body: + '[{"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}, {"html":"Mocked code=9001","headers": {"content-language": "zh-CN" }}]', + }, + recoveryEmailUnverified: { + status: 200, + body: '{"verified": false}', + }, + recoveryEmailVerified: { + status: 200, + body: '{"verified": true}', + }, + recoveryEmailResendCode: { + status: 200, + body: '{}', + }, + passwordForgotSendCode: { + status: 200, + body: + '{"passwordForgotToken":"e838790265a45f6ee1130070d57d67d9bb20953706f73af0e34b0d4d92f19103","ttl":900,"tries":3}', + }, + passwordForgotResendCode: { + status: 200, + body: + '{"passwordForgotToken":"e838790265a45f6ee1130070d57d67d9bb20953706f73af0e34b0d4d92f19103","ttl":900,"tries":3}', + }, + passwordForgotStatus: { + status: 200, + body: '{ "tries": 3, "ttl": 420 }', + }, + passwordForgotVerifyCode: { + status: 200, + body: + '{"accountResetToken":"50a2052498d538a5d3918847751c8d5077294fd637dbf20d27f2f5f854cbcf4f"}', + }, + passwordChangeStart: { + status: 200, + body: + '{ "keyFetchToken": "b1f4182d7e072567a1dbe682043a16932a84b7f4ca3b95e471a34806c87e4130", "passwordChangeToken": "0208a48ca4f777688a1017e98cedcc1c36ba9c4595088d28dcde5af04ae2215b", "verified": true }', + }, + passwordChangeFinish: { + status: 200, + body: '{}', + }, + passwordChangeFinishKeys: { + status: 200, + body: + '{"uid": "5d576e2cd3604981a8c05f6ea67fce5b", "sessionToken": "9c1fe2a0643ce23aa1b44afbe30e28d33e5726558cab215314980fc85875684f","keyFetchToken": "b1f4182d7e072567a1dbe682043a16932a84b7f4ca3b95e471a34806c87e4130","verified": true}', + }, + accountReset: { + status: 200, + body: + '{"uid": "5d576e2cd3604981a8c05f6ea67fce5b", "sessionToken": "9c1fe2a0643ce23aa1b44afbe30e28d33e5726558cab215314980fc85875684f","keyFetchToken": "b1f4182d7e072567a1dbe682043a16932a84b7f4ca3b95e471a34806c87e4130","verified": true}', + }, + accountProfile: { + status: 200, + body: + '{"email": "a@a.com", "locale": "en", "authenticationMethods": ["pwd", "email"], "authenticatorAssuranceLevel": 2, "profileChangedAt": 1539002077704}', + }, + account: { + status: 200, + body: '{"subscriptions":[{"foo":"bar"}]}', + }, + securityEvents: { + status: 200, + body: JSON.stringify([ + { + name: 'account.login', + verified: true, + createdAt: new Date().getTime() + 1, + }, + { + name: 'account.create', + verified: true, + createdAt: new Date().getTime(), + }, + ]), + }, + securityEventsEmptyResponse: { + status: 200, + body: '[]', + }, + sessionDestroy: { + status: 200, + body: '{}', + }, + sessionStatus: { + status: 200, + body: '{}', + }, + sessions: { + status: 200, + body: JSON.stringify([ + { + id: 'device1', + userAgent: 'agent1', + deviceName: 'name1', + deviceType: 'desktop', + isDevice: false, + lastAccessTime: 100, + lastAccessTimeFormatted: 'a few seconds ago', + }, + { + id: 'device2', + userAgent: 'agent2', + deviceName: 'name2', + deviceType: 'desktop', + isDevice: false, + lastAccessTime: 101, + lastAccessTimeFormatted: 'a few seconds ago', + }, + ]), + }, + attachedClients: { + status: 200, + body: JSON.stringify([ + { + clientId: null, + deviceId: 'device1', + deviceType: 'desktop', + isDevice: false, + lastAccessTime: 100, + lastAccessTimeFormatted: 'a few seconds ago', + name: 'name1', + sessionTokenId: 'session1', + refreshTokenId: null, + userAgent: 'agent1', + }, + { + clientId: null, + deviceId: 'device2', + deviceType: 'desktop', + isDevice: false, + lastAccessTime: 101, + lastAccessTimeFormatted: 'a few seconds ago', + name: 'name2', + sessionTokenId: 'session2', + refreshTokenId: null, + userAgent: 'agent2', + }, + ]), + }, + attachedClientDestroy: { + status: 200, + body: '{}', + }, + accountDestroy: { + status: 200, + body: '{}', + }, + accountKeys: { + status: 200, + body: + '{ "bundle": "7f1a9633560774251a2d317b4539e04bcb14a767ec92e3b3f4d438fdad984831f6d1e1b0d93c23d312bf0859270dc8c0e6ebcae4c499f3a604881fc57683459b01cdfd04757835b0334a80728ce40cf50dce32bb365d8a0ac868bb747bf8aca4"}', + }, + accountStatus: { + status: 200, + body: '{ "exists": true }', + }, + accountStatusFalse: { + status: 200, + body: '{ "exists": false }', + }, + certificateSign: { + status: 200, + body: + '{ "cert": "eyJhbGciOiJEUzI1NiJ9.eyJwdWJsaWMta2V5Ijp7ImFsZ29yaXRobSI6IlJTIiwibiI6IjU3NjE1NTUwOTM3NjU1NDk2MDk4MjAyMjM2MDYyOTA3Mzg5ODMyMzI0MjUyMDY2Mzc4OTA0ODUyNDgyMjUzODg1MTA3MzQzMTY5MzI2OTEyNDkxNjY5NjQxNTQ3NzQ1OTM3NzAxNzYzMTk1NzQ3NDI1NTEyNjU5NjM2MDgwMzYzNjE3MTc1MzMzNjY5MzEyNTA2OTk1MzMyNDMiLCJlIjoiNjU1MzcifSwicHJpbmNpcGFsIjp7ImVtYWlsIjoiZm9vQGV4YW1wbGUuY29tIn0sImlhdCI6MTM3MzM5MjE4OTA5MywiZXhwIjoxMzczMzkyMjM5MDkzLCJpc3MiOiIxMjcuMC4wLjE6OTAwMCJ9.l5I6WSjsDIwCKIz_9d3juwHGlzVcvI90T2lv2maDlr8bvtMglUKFFWlN_JEzNyPBcMDrvNmu5hnhyN7vtwLu3Q" }', + }, + getRandomBytes: { + status: 200, + body: + '{ "data": "ac55c0520f2edfb026761443da0ab27b1fa18c98912af6291714e9600aa34991" }', + }, + invalidTimestamp: { + status: 401, + body: + '{ "errno": ' + + ERRORS.INVALID_TIMESTAMP + + ', "error": "Invalid authentication timestamp", "serverTime": ' + + new Date().getTime() + + ' }', + }, + deviceDestroy: { + status: 200, + body: '{}', + }, + deviceList: { + status: 200, + body: JSON.stringify([ + { id: DEVICE_ID, - name: DEVICE_NAME_2, + name: DEVICE_NAME, type: DEVICE_TYPE, pushCallback: DEVICE_CALLBACK, pushPublicKey: DEVICE_PUBLIC_KEY, pushAuthKey: DEVICE_AUTH_KEY, - }), - }, - sendUnblockCode: { - status: 200, - body: '{}', - }, - unblockEmail: { - status: 200, - body: - '[{"html":"Mocked code=9001"}, {"html":"Mocked code=9001", "headers": {"x-unblock-code": "ASDF1234"}}]', - }, - rejectUnblockCode: { - status: 200, - body: '{}', - }, - sendSmsConnectDevice: { - status: 200, - body: '{}', - }, - smsStatus: { - status: 200, - body: '{"country":"RO","ok":true}', - }, - consumeSigninCode: { - status: 200, - body: '{"email":"foo@example.org"}', - }, - recoveryEmails: { - status: 200, - body: '[{"email": "a@b.com", "verified": true, "isPrimary": true}]', - }, - recoveryEmailsUnverified: { - status: 200, - body: - '[{"email": "a@b.com", "verified": true, "isPrimary": true}, {"email": "another@email.com", "verified": false, "isPrimary": false}]', - }, - recoveryEmailsVerified: { - status: 200, - body: - '[{"email": "a@b.com", "verified": true, "isPrimary": true}, {"email": "another@email.com", "verified": true, "isPrimary": false}]', - }, - recoveryEmailsSetPrimaryVerified: { - status: 200, - body: - '[{"email": "anotherEmail@email.com", "verified": true, "isPrimary": true}, {"email": "a@a.com", "verified": true, "isPrimary": false}]', - }, - recoveryEmailCreate: { - status: 200, - body: '{}', - }, - recoveryEmailDestroy: { - status: 200, - body: '{}', - }, - recoveryEmailSetPrimaryEmail: { - status: 200, - body: '{}', - }, - signInWithVerificationMethodEmail2faResponse: { - status: 200, - body: - '{"uid": "5d576e2cd3604981a8c05f6ea67fce5b", "sessionToken": "9c1fe2a0643ce23aa1b44afbe30e28d33e5726558cab215314980fc85875684f","keyFetchToken": "b1f4182d7e072567a1dbe682043a16932a84b7f4ca3b95e471a34806c87e4130","verified": true, "emailSent": false, "verificationMethod": "email-2fa", "verificationReason": "login"}', - }, - signInWithVerificationMethodEmail2faCode: { - status: 200, - body: - '[{"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}, {"html":"Mocked code=9001","headers": {"x-signin-verify-code": "000111" }}]', - }, - sessionVerifyTokenCodeSuccess: { - status: 200, - body: '{}', - }, - sessionReauth: { - status: 200, - headers: {}, - body: - '{"uid":"9c8e5cf6915949c1b063b88fa0c53d05","verified":true,"authAt":123456}', - }, - sessionReauthWithKeys: { - status: 200, - headers: {}, - body: - '{"uid": "5d576e2cd3604981a8c05f6ea67fce5b","keyFetchToken":"b1f4182d7e072567a1dbe682043a16932a84b7f4ca3b95e471a34806c87e4130","verified":true,"authAt":123456}', - }, - createTotpToken: { - status: 200, - body: - '{"qrCodeUrl": "data:image/png;base64,iVBOR", "secret": "MZEE4ODKPI2UCU3DIJ3UGYSIOVWDKV3P"}', - }, - createTotpTokenDuplicate: { - status: 400, - body: '{"errno": 154}', - }, - deleteTotpToken: { - status: 200, - body: '{}', - }, - checkTotpTokenExistsFalse: { - status: 200, - body: '{"exists": false}', - }, - checkTotpTokenExistsTrue: { - status: 200, - body: '{"exists": true}', - }, - verifyTotpCodeTrueEnableToken: { - status: 200, - body: - '{"success": true, "recoveryCodes": ["01001112", "01001113", "01001114", "01001115", "01001116", "01001117", "01001118", "01001119"]}', - }, - verifyTotpCodeTrue: { - status: 200, - body: '{"success": true}', - }, - verifyTotpCodeFalse: { - status: 200, - body: '{"success": false}', - }, - consumeRecoveryCodeInvalidCode: { - status: 400, - body: '{"errno": 156}', - }, - consumeRecoveryCodeSuccess: { - status: 200, - body: '{"remaining": 7}', - }, - replaceRecoveryCodesSuccess: { - status: 200, - body: - '{"recoveryCodes": ["01001112", "01001113", "01001114", "01001115", "01001116", "01001117", "01001118", "01001119"]}', - }, - replaceRecoveryCodesSuccessNew: { - status: 200, - body: - '{"recoveryCodes": ["99999999", "01001113", "01001114", "01001115", "01001116", "01001117", "01001118", "01001119"]}', - }, - createRecoveryKey: { - status: 200, - body: '{}', - }, - getRecoveryKey: { - status: 200, - body: - '{"recoveryData": "eyJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiZGlyIiwia2lkIjoiODE4NDIwZjBkYTU4ZDIwZjZhZTRkMmM5YmVhYjkyNTEifQ..D29EXHp8ubLvftaZ.xHJd2Nl2Uco2RyywYPLkUU7fHpgO2FztY12Zjpq1ffiyLRIUcQVfmiNC6aMiHBl7Hp-lXEbb5mR1uXHrTH9iRXEBVaAfyf9KEAWOukWGVSH8EaOkr7cfu2Yr0K93Ec8glsssjiKp8NGB8VKTUJ-lmBv2cIrG68V4eTUVDoDhMbXhrF-Mv4JNeh338pPeatTnyg.Ow2bhEYWxzxfSPMxVwKmSA"}', - }, - deleteRecoveryKey: { - status: 200, - body: '{}', - }, - recoveryKeyExistsFalse: { - status: 200, - body: '{"exists": false}', - }, - recoveryKeyExistsTrue: { - status: 200, - body: '{"exists": true}', - }, - getActiveSubscriptions: { - status: 200, - body: '[{"subscriptionId": 9},{"subscriptionId": 12}]', - }, - createSupportTicket: { - status: 200, - body: '{"success": true, "ticket": "abc123xyz"}', - }, - }; -}); + }, + ]), + }, + deviceRegister: { + status: 200, + body: JSON.stringify({ + id: DEVICE_ID, + name: DEVICE_NAME, + type: DEVICE_TYPE, + pushCallback: DEVICE_CALLBACK, + pushPublicKey: DEVICE_PUBLIC_KEY, + pushAuthKey: DEVICE_AUTH_KEY, + }), + }, + deviceUpdate: { + status: 200, + body: JSON.stringify({ + id: DEVICE_ID, + name: DEVICE_NAME_2, + type: DEVICE_TYPE, + pushCallback: DEVICE_CALLBACK, + pushPublicKey: DEVICE_PUBLIC_KEY, + pushAuthKey: DEVICE_AUTH_KEY, + }), + }, + sendUnblockCode: { + status: 200, + body: '{}', + }, + unblockEmail: { + status: 200, + body: + '[{"html":"Mocked code=9001"}, {"html":"Mocked code=9001", "headers": {"x-unblock-code": "ASDF1234"}}]', + }, + rejectUnblockCode: { + status: 200, + body: '{}', + }, + sendSmsConnectDevice: { + status: 200, + body: '{}', + }, + smsStatus: { + status: 200, + body: '{"country":"US","ok":true}', + }, + consumeSigninCode: { + status: 200, + body: '{"email":"foo@example.org"}', + }, + recoveryEmails: { + status: 200, + body: '[{"email": "a@b.com", "verified": true, "isPrimary": true}]', + }, + recoveryEmailsUnverified: { + status: 200, + body: + '[{"email": "a@b.com", "verified": true, "isPrimary": true}, {"email": "another@email.com", "verified": false, "isPrimary": false}]', + }, + recoveryEmailsVerified: { + status: 200, + body: + '[{"email": "a@b.com", "verified": true, "isPrimary": true}, {"email": "another@email.com", "verified": true, "isPrimary": false}]', + }, + recoveryEmailsSetPrimaryVerified: { + status: 200, + body: + '[{"email": "anotherEmail@email.com", "verified": true, "isPrimary": true}, {"email": "a@a.com", "verified": true, "isPrimary": false}]', + }, + recoveryEmailCreate: { + status: 200, + body: '{}', + }, + recoveryEmailDestroy: { + status: 200, + body: '{}', + }, + recoveryEmailSetPrimaryEmail: { + status: 200, + body: '{}', + }, + signInWithVerificationMethodEmail2faResponse: { + status: 200, + body: + '{"uid": "5d576e2cd3604981a8c05f6ea67fce5b", "sessionToken": "9c1fe2a0643ce23aa1b44afbe30e28d33e5726558cab215314980fc85875684f","keyFetchToken": "b1f4182d7e072567a1dbe682043a16932a84b7f4ca3b95e471a34806c87e4130","verified": true, "emailSent": false, "verificationMethod": "email-2fa", "verificationReason": "login"}', + }, + signInWithVerificationMethodEmail2faCode: { + status: 200, + body: + '[{"html":"Mocked code=9001"}, {"html":"Mocked code=9001"}, {"html":"Mocked code=9001","headers": {"x-signin-verify-code": "000111" }}]', + }, + sessionVerifyTokenCodeSuccess: { + status: 200, + body: '{}', + }, + sessionReauth: { + status: 200, + headers: {}, + body: + '{"uid":"9c8e5cf6915949c1b063b88fa0c53d05","verified":true,"authAt":123456}', + }, + sessionReauthWithKeys: { + status: 200, + headers: {}, + body: + '{"uid": "5d576e2cd3604981a8c05f6ea67fce5b","keyFetchToken":"b1f4182d7e072567a1dbe682043a16932a84b7f4ca3b95e471a34806c87e4130","verified":true,"authAt":123456}', + }, + createTotpToken: { + status: 200, + body: + '{"qrCodeUrl": "data:image/png;base64,iVBOR", "secret": "MZEE4ODKPI2UCU3DIJ3UGYSIOVWDKV3P"}', + }, + createTotpTokenDuplicate: { + status: 400, + body: '{"errno": 154}', + }, + deleteTotpToken: { + status: 200, + body: '{}', + }, + checkTotpTokenExistsFalse: { + status: 200, + body: '{"exists": false}', + }, + checkTotpTokenExistsTrue: { + status: 200, + body: '{"exists": true}', + }, + verifyTotpCodeTrueEnableToken: { + status: 200, + body: + '{"success": true, "recoveryCodes": ["01001112", "01001113", "01001114"]}', + }, + verifyTotpCodeTrue: { + status: 200, + body: '{"success": true}', + }, + verifyTotpCodeFalse: { + status: 200, + body: '{"success": false}', + }, + consumeRecoveryCodeInvalidCode: { + status: 400, + body: '{"errno": 156}', + }, + consumeRecoveryCodeSuccess: { + status: 200, + body: '{"remaining": 2}', + }, + replaceRecoveryCodesSuccess: { + status: 200, + body: '{"recoveryCodes": ["01001112", "01001113", "01001114"]}', + }, + replaceRecoveryCodesSuccessNew: { + status: 200, + body: '{"recoveryCodes": ["99999999", "01001113", "01001114"]}', + }, + createRecoveryKey: { + status: 200, + body: '{}', + }, + getRecoveryKey: { + status: 200, + body: + '{"recoveryData": "eyJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiZGlyIiwia2lkIjoiODE4NDIwZjBkYTU4ZDIwZjZhZTRkMmM5YmVhYjkyNTEifQ..D29EXHp8ubLvftaZ.xHJd2Nl2Uco2RyywYPLkUU7fHpgO2FztY12Zjpq1ffiyLRIUcQVfmiNC6aMiHBl7Hp-lXEbb5mR1uXHrTH9iRXEBVaAfyf9KEAWOukWGVSH8EaOkr7cfu2Yr0K93Ec8glsssjiKp8NGB8VKTUJ-lmBv2cIrG68V4eTUVDoDhMbXhrF-Mv4JNeh338pPeatTnyg.Ow2bhEYWxzxfSPMxVwKmSA"}', + }, + deleteRecoveryKey: { + status: 200, + body: '{}', + }, + recoveryKeyExistsFalse: { + status: 200, + body: '{"exists": false}', + }, + recoveryKeyExistsTrue: { + status: 200, + body: '{"exists": true}', + }, + getActiveSubscriptions: { + status: 200, + body: '[{"subscriptionId": 9},{"subscriptionId": 12}]', + }, + createSupportTicket: { + status: 200, + body: '{"success": true, "ticket": "abc123xyz"}', + }, +};