feat(client): joi like validation of query parameters

Use [VAT](https://github.com/shane_tomlinson/vat) to import,
transform, and validate query parameters in the reliers
and auth brokers.
This commit is contained in:
Shane Tomlinson 2016-02-25 15:08:19 +00:00
Родитель aad7e50f80
Коммит d9e18eaaaa
24 изменённых файлов: 791 добавлений и 447 удалений

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

@ -642,14 +642,16 @@ define(function (require, exports, module) {
},
_getErrorPage: function (err) {
if (OAuthErrors.is(err, 'MISSING_PARAMETER') ||
if (AuthErrors.is(err, 'INVALID_PARAMETER') ||
AuthErrors.is(err, 'MISSING_PARAMETER') ||
OAuthErrors.is(err, 'INVALID_PARAMETER') ||
OAuthErrors.is(err, 'MISSING_PARAMETER') ||
OAuthErrors.is(err, 'UNKNOWN_CLIENT')) {
var queryString = Url.objToSearchString({
client_id: err.client_id, //eslint-disable-line camelcase
context: err.context,
errno: err.errno,
message: OAuthErrors.toInterpolatedMessage(err, this._translator),
message: err.errorModule.toInterpolatedMessage(err, this._translator),
namespace: err.namespace,
param: err.param
});

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

@ -260,7 +260,7 @@ define(function (require, exports, module) {
try {
if (this.is(err, 'INVALID_PARAMETER')) {
return {
param: err.validation.keys
param: err.param || err.validation.keys
};
} else if (this.is(err, 'MISSING_PARAMETER')) {
return {

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

@ -128,6 +128,34 @@ define(function (require, exports, module) {
return err;
},
/**
* Create an INVALID_PARAMETER error. The returned
* error will contain a `param` key with the parameter
* name
*
* @param {String} paramName
* @returns {Error}
*/
toInvalidParameterError: function (paramName) {
var err = this.toError('INVALID_PARAMETER');
err.param = paramName;
return err;
},
/**
* Create a MISSING_PARAMETER error. The returned
* error will contain a `param` key with the parameter
* name
*
* @param {String} paramName
* @returns {Error}
*/
toMissingParameterError: function (paramName) {
var err = this.toError('MISSING_PARAMETER');
err.param = paramName;
return err;
},
/**
* Check if an error is of the given type
*/

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

@ -0,0 +1,45 @@
/* 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/. */
/**
* Transform and validate data. Uses the [VAT
* library](https://github.com/shane-tomlinson/vat) to
* do the heavy lifting.
*
* If a required field is missing from the data, a
* `MISSING_ERROR` error is generated, with the error's
* `param` field set to the missing field's name.
*
* If a field does not pass validation, an
* `INVALID_PARAMETER` error is generated, with the error's
* `param` field set to the invalid field's name.
*/
define(function (require, exports, module) {
'use strict';
var Vat = require('lib/vat');
module.exports = {
/**
* Transform and validate `data` using `schema`.
*
* @param {object} data - data to validate
* @param {object} schema - schema that can be passed to the validator
* @param {object} Errors - Errors module used to create errors
* @returns {object} validation/transformation results
*/
transformUsingSchema: function (data, schema, Errors) {
var result = Vat.validate(data, schema);
var error = result.error;
if (error instanceof ReferenceError) {
throw Errors.toMissingParameterError(error.key);
} else if (error instanceof TypeError) {
throw Errors.toInvalidParameterError(error.key);
}
return result.value;
}
};
});

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

@ -34,6 +34,9 @@ define(function (require, exports, module) {
// * http://tools.ietf.org/html/rfc5321#section-4.5.3.1.1
var emailRegex = /^[\w.!#$%&*+/=?^`{|}~-]{1,64}@[a-z\d](?:[a-z\d-]{0,253}[a-z\d])?(?:\.[a-z\d](?:[a-z\d-]{0,253}[a-z\d])?)+$/i;
// A Base64 encoded JWT
var BASE64_JWT = /^(?:[a-zA-Z0-9-_]+[=]{0,2}\.){2}[a-zA-Z0-9-_]+[=]{0,2}$/;
var self = {
/**
* Check if an email address is valid
@ -233,7 +236,7 @@ define(function (require, exports, module) {
},
/**
* Check if the verification redirect value is valid
* Check if the verification redirect value is valid.
*
* @param value
* @returns {boolean}
@ -245,6 +248,16 @@ define(function (require, exports, module) {
];
return _.contains(valid, value);
},
/**
* Check if a JSON Web Token (JWT) is valid.
*
* @param value
* @returns {boolean}
*/
isBase64JwtValid: function isJwtValid(value) {
return BASE64_JWT.test(value);
}
};

21
app/scripts/lib/vat.js Normal file
Просмотреть файл

@ -0,0 +1,21 @@
/* 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/. */
// Do some validation.
define(function (require, exports, module) {
'use strict';
var Validate = require('lib/validate');
var Vat = require('vat');
Vat.register('base64jwt', Vat.string().test(Validate.isBase64JwtValid));
Vat.register('hex', Vat.string().test(Validate.isHexValid));
Vat.register('uri', Vat.string().test(Validate.isUriValid));
Vat.register('url', Vat.string().test(Validate.isUrlValid));
Vat.register('urn', Vat.string().test(Validate.isUrnValid));
return Vat;
});

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

@ -10,12 +10,18 @@
define(function (require, exports, module) {
'use strict';
var _ = require('underscore');
var AuthErrors = require('lib/auth-errors');
var Backbone = require('backbone');
var Cocktail = require('cocktail');
var NullBehavior = require('views/behaviors/null');
var p = require('lib/promise');
var SameBrowserVerificationModel = require('models/verification/same-browser');
var SearchParamMixin = require('models/mixins/search-param');
var Vat = require('lib/vat');
var QUERY_PARAMETER_SCHEMA = {
automatedBrowser: Vat.boolean()
};
var BaseAuthenticationBroker = Backbone.Model.extend({
type: 'base',
@ -85,7 +91,7 @@ define(function (require, exports, module) {
return p()
.then(function () {
self._isForceAuth = self._isForceAuthUrl();
self.importBooleanSearchParam('automatedBrowser');
self.importSearchParamsUsingSchema(QUERY_PARAMETER_SCHEMA, AuthErrors);
});
},
@ -409,7 +415,10 @@ define(function (require, exports, module) {
verificationInfo.clear();
}
_.extend(BaseAuthenticationBroker.prototype, SearchParamMixin);
Cocktail.mixin(
BaseAuthenticationBroker,
SearchParamMixin
);
module.exports = BaseAuthenticationBroker;
});

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

@ -11,13 +11,20 @@ define(function (require, exports, module) {
'use strict';
var _ = require('underscore');
var Cocktail = require('cocktail');
var OAuthErrors = require('lib/oauth-errors');
var ChannelMixin = require('models/auth_brokers/mixins/channel');
var OAuthAuthenticationBroker = require('models/auth_brokers/oauth');
var p = require('lib/promise');
var Vat = require('lib/vat');
var WebChannel = require('lib/channels/web');
var proto = OAuthAuthenticationBroker.prototype;
var QUERY_PARAMETER_SCHEMA = {
webChannelId: Vat.string()
};
var WebChannelAuthenticationBroker = OAuthAuthenticationBroker.extend({
type: 'web-channel',
defaults: _.extend({}, proto.defaults, {
@ -207,7 +214,7 @@ define(function (require, exports, module) {
},
_setupSigninSignupFlow: function () {
this.importSearchParam('webChannelId');
this.importSearchParamsUsingSchema(QUERY_PARAMETER_SCHEMA, OAuthErrors);
},
_setupVerificationFlow: function () {
@ -223,6 +230,10 @@ define(function (require, exports, module) {
}
});
_.extend(WebChannelAuthenticationBroker.prototype, ChannelMixin);
Cocktail.mixin(
WebChannelAuthenticationBroker,
ChannelMixin
);
module.exports = WebChannelAuthenticationBroker;
});

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

@ -9,6 +9,7 @@
define(function (require, exports, module) {
'use strict';
var Transform = require('lib/transform');
var Url = require('lib/url');
module.exports = {
@ -16,75 +17,43 @@ define(function (require, exports, module) {
* Get a value from the URL search parameter
*
* @param {String} paramName - name of the search parameter to get
* @returns {String}
*/
getSearchParam: function (paramName) {
return Url.searchParam(paramName, this.window.location.search);
},
/**
* Set a value based on a value in window.location.search. Only updates
* model if parameter exists in window.location.search.
* Get values from the URL search parameters.
*
* @param {String} paramName - name of the search parameter
* @param {String} [modelName] - name to set in model. If not specified,
* use the same value as `paramName`
* @param {array of Strings} paramNames - name of the search parameters
* to get
* @returns {Object}
*/
importSearchParam: function (paramName, modelName) {
modelName = modelName || paramName;
var value = this.getSearchParam(paramName);
if (typeof value !== 'undefined') {
this.set(modelName, value);
}
getSearchParams: function (paramNames) {
return Url.searchParams(this.window.location.search, paramNames);
},
/**
* Import a boolean search parameter. Search parameter must be `true`
* nor `false` or model item will not be set.
* Import search parameters defined in the schema. Parameters are
* transformed and validated based on the rules defined in the `schema`.
*
* @param {String} paramName - name of the search parameter
* @param {String} [modelName] - name to set in model. If not specified,
* use the same value as `paramName`
* @param {Errors} Errors - corresponding Errors object
* @throws {error}
* @param {Object} schema - schema used to define data to import
* and validate against
* @param {Object} Errors - errors object used to generate errors
* @throws
* If a required field is missing from the data, a
* `MISSING_ERROR` error is generated, with the error's
* `param` field set to the missing field's name.
*
* If a field does not pass validation, an
* `INVALID_PARAMETER` error is generated, with the error's
* `param` field set to the invalid field's name.
*/
importBooleanSearchParam: function (paramName, modelName, Errors) {
modelName = modelName || paramName;
var self = this;
var textValue = self.getSearchParam(paramName);
if (typeof textValue !== 'undefined') {
if (textValue === 'true') {
self.set(modelName, true);
} else if (textValue === 'false') {
self.set(modelName, false);
} else {
var err = Errors.toError('INVALID_PARAMETER');
err.param = paramName;
throw err;
}
}
},
/**
* Set a value based on a value in window.location.search. Throws error
* if paramName param is not in window.location.search.
*
* Throws Error mapped to `MISSING_PARAMETER` in Errors object.
*
* @param {string} paramName - name of the search parameter
* @param {string} modelName - name to set in model
* @param {Errors} Errors - corresponding Errors object
* @throws {error}
*/
importRequiredSearchParam: function (paramName, modelName, Errors) {
var self = this;
self.importSearchParam(paramName, modelName);
if (! self.has(modelName || paramName)) {
var err = Errors.toError('MISSING_PARAMETER');
err.param = paramName;
throw err;
}
importSearchParamsUsingSchema: function (schema, Errors) {
var params = this.getSearchParams(Object.keys(schema));
var result = Transform.transformUsingSchema(params, schema, Errors);
this.set(result);
}
};
});

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

@ -14,11 +14,52 @@ define(function (require, exports, module) {
var OAuthErrors = require('lib/oauth-errors');
var Relier = require('models/reliers/relier');
var RelierKeys = require('lib/relier-keys');
var Transform = require('lib/transform');
var Url = require('lib/url');
var Validate = require('lib/validate');
var Vat = require('lib/vat');
var RELIER_FIELDS_IN_RESUME_TOKEN = ['state', 'verificationRedirect'];
/*eslint-disable camelcase*/
var CLIENT_INFO_SCHEMA = {
id: Vat.hex().required().renameTo('clientId'),
image_uri: Vat.url().allow('').renameTo('imageUri'),
name: Vat.string().required().min(1).renameTo('serviceName'),
privacy_uri: Vat.url().allow('').renameTo('privacyUri'),
redirect_uri: Vat.uri().required().renameTo('redirectUri'),
terms_uri: Vat.url().allow('').renameTo('termsUri'),
trusted: Vat.boolean().required()
};
var SIGNIN_SIGNUP_QUERY_PARAM_SCHEMA = {
access_type: Vat.string().test(Validate.isAccessTypeValid).renameTo('accessType'),
client_id: Vat.hex().required().renameTo('clientId'),
keys: Vat.boolean(),
prompt: Vat.string().test(Validate.isPromptValid),
redirectTo: Vat.uri(),
redirect_uri: Vat.uri().renameTo('redirectUri'),
scope: Vat.string().required().min(1),
service: Vat.string(),
state: Vat.string(),
verification_redirect: Vat.string().test(Validate.isVerificationRedirectValid).renameTo('verificationRedirect')
};
var VERIFICATION_INFO_SCHEMA = {
access_type: Vat.string().test(Validate.isAccessTypeValid).renameTo('accessType'),
action: Vat.string().min(1),
client_id: Vat.hex().required().renameTo('clientId'),
keys: Vat.boolean(),
prompt: Vat.string().test(Validate.isPromptValid),
redirect_uri: Vat.uri().renameTo('redirectUri'),
// scopes are optional when verifying, user could be verifying in a 2nd browser
scope: Vat.string().min(1),
service: Vat.string().min(1),
state: Vat.string().min(1)
};
/*eslint-enable camelcase*/
var OAuthRelier = Relier.extend({
defaults: _.extend({}, Relier.prototype.defaults, {
accessType: null,
@ -74,16 +115,6 @@ define(function (require, exports, module) {
return self._setupOAuthRPInfo();
})
.then(function () {
if (! self.get('clientId')) {
throw toMissingParameterError('client_id');
} else if (! self.get('redirectUri')) {
// Guard against a possible empty redirect_uri
// sent by the server. redirect_uri is not required
// by the OAuth server, but if it's missing, we redirect
// to ourselves. See issue #3452
throw toMissingParameterError('redirect_uri');
}
if (self.has('scope')) {
// normalization depends on `trusted` field set in
// setupOAuthRPInfo.
@ -106,7 +137,7 @@ define(function (require, exports, module) {
}
if (! permissions.length) {
throw toInvalidParameterError('scope');
throw OAuthErrors.toInvalidParameterError('scope');
}
this.set('scope', permissions.join(' '));
@ -150,90 +181,17 @@ define(function (require, exports, module) {
};
}
this._validateOAuthValues(resumeObj);
var result = Transform.transformUsingSchema(
resumeObj, VERIFICATION_INFO_SCHEMA, OAuthErrors);
self.set({
accessType: resumeObj.access_type,
clientId: resumeObj.client_id,
keys: resumeObj.keys,
redirectUri: resumeObj.redirect_uri,
scope: resumeObj.scope,
state: resumeObj.state
});
self.set(result);
},
_setupSignInSignUpFlow: function () {
var self = this;
/*eslint-disable camelcase*/
var oauthOptions = {
access_type: self.getSearchParam('access_type'),
client_id: self.getSearchParam('client_id'),
prompt: self.getSearchParam('prompt'),
redirectTo: self.getSearchParam('redirectTo'),
redirect_uri: self.getSearchParam('redirect_uri'),
verification_redirect: self.getSearchParam('verification_redirect'),
};
/*eslint-enable camelcase*/
this._validateOAuthValues(oauthOptions);
// params listed in:
// https://github.com/mozilla/fxa-oauth-server/blob/master/docs/api.md#post-v1authorization
self.importSearchParam('access_type', 'accessType');
self.importRequiredSearchParam('client_id', 'clientId', OAuthErrors);
self.importBooleanSearchParam('keys', 'keys', OAuthErrors);
self.importSearchParam('prompt');
self.importSearchParam('redirectTo');
self.importSearchParam('redirect_uri', 'redirectUri');
self.importRequiredSearchParam('scope', 'scope', OAuthErrors);
self.importSearchParam('state');
self.importSearchParam('verification_redirect', 'verificationRedirect');
},
_validateOAuthValues: function (options) {
var accessType = options.access_type;
var clientId = options.client_id;
var prompt = options.prompt;
var redirectUri = options.redirect_uri;
var redirectTo = options.redirectTo;
var termsUri = options.terms_uri;
var privacyUri = options.privacy_uri;
var verificationRedirect = options.verification_redirect;
if (isDefined(accessType) && ! Validate.isAccessTypeValid(accessType)) {
throw toInvalidParameterError('access_type');
}
if (isDefined(clientId) && ! Validate.isHexValid(clientId)) {
throw toInvalidParameterError('client_id');
}
// privacyUri is allowed to be ``
if (privacyUri && ! Validate.isUriValid(privacyUri)) {
throw toInvalidParameterError('privacy_uri');
}
if (isDefined(prompt) && ! Validate.isPromptValid(prompt)) {
throw toInvalidParameterError('prompt');
}
if (isDefined(redirectTo) && ! Validate.isUriValid(redirectTo)) {
throw toInvalidParameterError('redirectTo');
}
if (isDefined(redirectUri) && ! Validate.isUriValid(redirectUri)) {
throw toInvalidParameterError('redirect_uri');
}
// termsUri is allowed to be ``
if (termsUri && ! Validate.isUriValid(termsUri)) {
throw toInvalidParameterError('terms_uri');
}
if (isDefined(verificationRedirect) &&
! Validate.isVerificationRedirectValid(verificationRedirect)) {
throw toInvalidParameterError('verification_redirect');
}
this.importSearchParamsUsingSchema(
SIGNIN_SIGNUP_QUERY_PARAM_SCHEMA, OAuthErrors);
},
_setupOAuthRPInfo: function () {
@ -242,15 +200,9 @@ define(function (require, exports, module) {
return self._oAuthClient.getClientInfo(clientId)
.then(function (serviceInfo) {
self.set('serviceName', serviceInfo.name);
self._validateOAuthValues(serviceInfo);
// server version always takes precedent over the search parameter
self.set('redirectUri', serviceInfo.redirect_uri);
self.set('termsUri', serviceInfo.terms_uri);
self.set('privacyUri', serviceInfo.privacy_uri);
self.set('trusted', serviceInfo.trusted);
var result = Transform.transformUsingSchema(
serviceInfo, CLIENT_INFO_SCHEMA, OAuthErrors);
self.set(result);
self.set('origin', Url.getOrigin(serviceInfo.redirect_uri));
}, function (err) {
// the server returns an invalid request parameter for an
@ -327,23 +279,5 @@ define(function (require, exports, module) {
return _.intersection(permissions, Constants.OAUTH_UNTRUSTED_ALLOWED_PERMISSIONS);
}
function toInvalidParameterError(param) {
return toOAuthParameterError('INVALID_PARAMETER', param);
}
function toMissingParameterError(param) {
return toOAuthParameterError('MISSING_PARAMETER', param);
}
function toOAuthParameterError(type, param) {
var err = OAuthErrors.toError(type);
err.param = param;
return err;
}
function isDefined(value) {
return ! _.isUndefined(value);
}
module.exports = OAuthRelier;
});

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

@ -13,31 +13,61 @@
define(function (require, exports, module) {
'use strict';
var AuthErrors = require('lib/auth-errors');
var BaseRelier = require('models/reliers/base');
var Cocktail = require('cocktail');
var Constants = require('lib/constants');
var p = require('lib/promise');
var ResumeTokenMixin = require('models/mixins/resume-token');
var SearchParamMixin = require('models/mixins/search-param');
var Vat = require('lib/vat');
var RELIER_FIELDS_IN_RESUME_TOKEN = [
'utmTerm',
'utmSource',
'utmMedium',
'utmContent',
'utmCampaign',
'campaign',
'entrypoint'
'entrypoint',
'utmCampaign',
'utmContent',
'utmMedium',
'utmSource',
'utmTerm'
];
/*eslint-disable camelcase*/
var QUERY_PARAMETER_SCHEMA = {
campaign: Vat.string(),
// `email` will be further validated by views to
// show the appropriate error message
email: Vat.string().allow(Constants.DISALLOW_CACHED_CREDENTIALS),
// FxDesktop declares both `entryPoint` (capital P) and
// `entrypoint` (lowcase p). Normalize to `entrypoint`.
entryPoint: Vat.string(),
entrypoint: Vat.string(),
migration: Vat.string().valid(Constants.AMO_MIGRATION, Constants.SYNC11_MIGRATION),
preVerifyToken: Vat.base64jwt(),
service: Vat.string(),
setting: Vat.string(),
// `uid` will be further validated by the views to
// show the appropriate error message
uid: Vat.string(),
utm_campaign: Vat.string().renameTo('utmCampaign'),
utm_content: Vat.string().renameTo('utmContent'),
utm_medium: Vat.string().renameTo('utmMedium'),
utm_source: Vat.string().renameTo('utmSource'),
utm_term: Vat.string().renameTo('utmTerm')
};
/*eslint-enable camelcase*/
var Relier = BaseRelier.extend({
defaults: {
allowCachedCredentials: true,
campaign: null,
email: null,
entrypoint: null,
migration: null,
preVerifyToken: null,
service: null,
setting: null,
uid: null,
utmCampaign: null,
utmContent: null,
utmMedium: null,
@ -75,36 +105,20 @@ define(function (require, exports, module) {
// query parameters and server provided data override
// resume provided data.
self.populateFromStringifiedResumeToken(self.getSearchParam('resume'));
// TODO - validate data coming from the resume token
self.importSearchParamsUsingSchema(QUERY_PARAMETER_SCHEMA, AuthErrors);
self.importSearchParam('service');
self.importSearchParam('preVerifyToken');
self.importSearchParam('uid');
self.importSearchParam('setting');
self.importSearchParam('entrypoint');
if (! self.has('entrypoint')) {
// FxDesktop declares both `entryPoint` (capital P) and
// `entrypoint` (lowcase p). Normalize to `entrypoint`.
self.importSearchParam('entryPoint', 'entrypoint');
if (self.has('entryPoint') && ! self.has('entrypoint')) {
self.set('entrypoint', self.get('entryPoint'));
}
self.importSearchParam('campaign');
self.importSearchParam('utm_campaign', 'utmCampaign');
self.importSearchParam('utm_content', 'utmContent');
self.importSearchParam('utm_medium', 'utmMedium');
self.importSearchParam('utm_source', 'utmSource');
self.importSearchParam('utm_term', 'utmTerm');
// A relier can indicate they do not want to allow
// cached credentials if they set email === 'blank'
var email = self.getSearchParam('email');
if (email === Constants.DISALLOW_CACHED_CREDENTIALS) {
if (self.get('email') === Constants.DISALLOW_CACHED_CREDENTIALS) {
self.unset('email');
self.set('allowCachedCredentials', false);
} else {
self.importSearchParam('email');
}
self.importSearchParam('migration');
});
},

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

@ -13,14 +13,24 @@
define(function (require, exports, module) {
'use strict';
var AuthErrors = require('lib/auth-errors');
var _ = require('underscore');
var Relier = require('models/reliers/relier');
var ServiceNameTranslator = require('lib/service-name');
var Vat = require('lib/vat');
/*eslint-disable camelcase*/
var QUERY_PARAMETER_SCHEMA = {
// context is not available when verifying.
context: Vat.string().min(1),
customizeSync: Vat.boolean()
};
/*eslint-enable camelcase*/
var SyncRelier = Relier.extend({
defaults: _.extend({}, Relier.prototype.defaults, {
context: null,
migration: null
customizeSync: false
}),
initialize: function (options) {
@ -35,19 +45,7 @@ define(function (require, exports, module) {
var self = this;
return Relier.prototype.fetch.call(self)
.then(function () {
self.importSearchParam('context');
try {
self.importBooleanSearchParam('customizeSync');
} catch (e) {
// ignore it for now.
// TODO - handle the error whenever startup error handling is
// complete - see #1982. This includes logging the error.
// Use something like:
// var err = AuthErrors.toError('INVALID_PARAMETER')
// err.param = 'customizeSync';
// throw err;
}
self.importSearchParamsUsingSchema(QUERY_PARAMETER_SCHEMA, AuthErrors);
self._setupServiceName();
});

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

@ -48,6 +48,7 @@ require.config({
text: '../bower_components/requirejs-text/text',
underscore: '../bower_components/underscore/underscore',
uuid: '../bower_components/node-uuid/uuid',
vat: '../bower_components/vat/vat',
webrtc: '../bower_components/webrtc-adapter/adapter'
},
shim: {

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

@ -89,6 +89,18 @@ define(function (require, exports, module) {
return email.split('@')[0];
}
function getValueLabel(value) {
if (_.isUndefined(value)) {
return 'not set';
} else if (value === '') {
return 'empty';
} else if (/^\s+$/.test(value)) {
return 'whitespace only';
}
return value;
}
function indexOfEvent(metrics, eventName) {
var events = metrics.getFilteredData().events;
@ -145,6 +157,7 @@ define(function (require, exports, module) {
createEmail: createEmail,
createRandomHexString: createRandomHexString,
emailToUser: emailToUser,
getValueLabel: getValueLabel,
indexOfEvent: indexOfEvent,
isErrorLogged: isErrorLogged,
isEventLogged: isEventLogged,

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

@ -536,19 +536,19 @@ define(function (require, exports, module) {
});
describe('_getErrorPage', function () {
it('returns BAD_REQUEST_PAGE for a missing OAuth parameter', function () {
var badRequestPageErrors = [
AuthErrors.toError('INVALID_PARAMETER'),
AuthErrors.toError('MISSING_PARAMETER'),
OAuthErrors.toError('INVALID_PARAMETER'),
OAuthErrors.toError('MISSING_PARAMETER'),
OAuthErrors.toError('UNKNOWN_CLIENT')
];
badRequestPageErrors.forEach(function (err) {
it('redirects to BAD_REQUEST_PAGE for ' + err.message, function () {
var errorUrl = appStart._getErrorPage(OAuthErrors.toError('MISSING_PARAMETER'));
assert.include(errorUrl, Constants.BAD_REQUEST_PAGE);
});
it('returns BAD_REQUEST_PAGE for an invalid OAuth parameter', function () {
var errorUrl = appStart._getErrorPage(OAuthErrors.toError('INVALID_PARAMETER'));
assert.include(errorUrl, Constants.BAD_REQUEST_PAGE);
});
it('returns BAD_REQUEST_PAGE for an unknown OAuth client', function () {
var errorUrl = appStart._getErrorPage(OAuthErrors.toError('UNKNOWN_CLIENT'));
assert.include(errorUrl, Constants.BAD_REQUEST_PAGE);
});
it('returns INTERNAL_ERROR_PAGE by default', function () {

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

@ -32,6 +32,33 @@ define(function (require, exports, module) {
});
});
describe('toInvalidParameterError', function () {
var err;
before(function () {
err = AuthErrors.toInvalidParameterError('param name', AuthErrors);
});
it('creates an INVALID_PARAMTER Error', function () {
assert.isTrue(AuthErrors.is(err, 'INVALID_PARAMETER'));
assert.equal(err.param, 'param name');
});
});
describe('toMissingParameterError', function () {
var err;
before(function () {
err = AuthErrors.toMissingParameterError('param name', AuthErrors);
});
it('creates an MISSING_PARAMTER Error', function () {
assert.isTrue(AuthErrors.is(err, 'MISSING_PARAMETER'));
assert.equal(err.param, 'param name');
});
});
describe('toMessage', function () {
it('converts a code to a message', function () {
assert.equal(AuthErrors.toMessage(102), 'Unknown account');

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

@ -0,0 +1,87 @@
/* 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 (require, exports, module) {
'use strict';
var AuthErrors = require('lib/auth-errors');
var chai = require('chai');
var Transform = require('lib/transform');
var Vat = require('lib/vat');
var assert = chai.assert;
describe('lib/transform', function () {
describe('transformUsingSchema', function () {
describe('with a missing parameter', function () {
var err;
before(function () {
var schema = {
optional: Vat.any(),
required: Vat.any().required()
};
try {
Transform.transformUsingSchema({}, schema, AuthErrors);
} catch (_err) {
err = _err;
}
});
it('throws a `MISSING_PARAMETER` error', function () {
assert.isTrue(AuthErrors.is(err, 'MISSING_PARAMETER'));
assert.equal(err.param, 'required');
});
});
describe('with an invalid parameter', function () {
var err;
before(function () {
var schema = {
numeric: Vat.number()
};
try {
Transform.transformUsingSchema({
numeric: 'a'
}, schema, AuthErrors);
} catch (_err) {
err = _err;
}
});
it('throws a `INVALID_PARAMETER` error', function () {
assert.isTrue(AuthErrors.is(err, 'INVALID_PARAMETER'));
assert.equal(err.param, 'numeric');
});
});
describe('valid', function () {
var result;
before(function () {
var schema = {
numeric: Vat.number(),
optional: Vat.any(),
required: Vat.any().required()
};
result = Transform.transformUsingSchema({
numeric: 123,
required: true,
}, schema, AuthErrors);
});
it('succeeds', function () {
assert.deepEqual(result, {
numeric: 123,
required: true
});
});
});
});
});
});

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

@ -447,5 +447,25 @@ define(function (require, exports, module) {
});
});
});
describe('isBase64JwtValid', function () {
it('is defined', function () {
assert.isFunction(Validate.isBase64JwtValid);
});
var invalidTypes = ['', 'word.asdf=', 'crazyType'];
invalidTypes.forEach( function (item) {
it('returns false for ' + item, function () {
assert.isFalse(Validate.isBase64JwtValid(item));
});
});
var validTypes = ['abcd-=.asdfbc==.asdf90154-_=='];
validTypes.forEach( function (item) {
it('returns true for ' + item, function () {
assert.isTrue(Validate.isBase64JwtValid(item));
});
});
});
});
});

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

@ -11,6 +11,7 @@ define(function (require, exports, module) {
var Cocktail = require('cocktail');
var SearchParamMixin = require('models/mixins/search-param');
var TestHelpers = require('../../../lib/helpers');
var Vat = require('vat');
var WindowMock = require('../../../mocks/window');
var assert = chai.assert;
@ -45,150 +46,67 @@ define(function (require, exports, module) {
});
});
describe('importSearchParam', function () {
it('imports the value of a search parameter, onto the model', function () {
windowMock.location.search = TestHelpers.toSearchString({
searchParam: 'value'
describe('importSearchParamsUsingSchema', function () {
var schema = {
optional: Vat.string().optional(),
required: Vat.string().valid('value').required()
};
describe('passes validation', function () {
beforeEach(function () {
windowMock.location.search =
TestHelpers.toSearchString({ ignored: true, required: 'value' });
model.importSearchParamsUsingSchema(schema, AuthErrors);
});
model.importSearchParam('searchParam');
assert.equal(model.get('searchParam'), 'value');
it('imports fields in the schema that have values', function () {
assert.equal(model.get('required'), 'value');
});
model.importSearchParam('notAvailable');
assert.isUndefined(model.get('notAvailable'));
it('does not import optional fields in the schema w/o values', function () {
assert.isFalse(model.has('optional'));
});
it('ignores fields not in the schema', function () {
assert.isFalse(model.has('ignored'));
});
});
describe('importBooleanSearchParam', function () {
describe('does not pass validation', function () {
var err;
function importExpectFailure(searchParams, sourceName, destName) {
windowMock.location.search = TestHelpers.toSearchString(searchParams);
describe('missing data', function () {
beforeEach(function () {
windowMock.location.search = TestHelpers.toSearchString({});
try {
model.importBooleanSearchParam(sourceName, destName, AuthErrors);
model.importSearchParamsUsingSchema(schema, AuthErrors);
} catch (e) {
err = e;
}
}
});
it('sets value to the boolean `true` if search param is `true`', function () {
it('throws a MISSING_PARAMETER error', function () {
assert.isTrue(AuthErrors.is(err, 'MISSING_PARAMETER'));
assert.equal(err.param, 'required');
});
});
describe('invalid data', function () {
beforeEach(function () {
windowMock.location.search = TestHelpers.toSearchString({
expectBoolean: true
required: 'invalid'
});
model.importBooleanSearchParam('expectBoolean');
assert.isTrue(model.get('expectBoolean'));
});
it('sets value to the boolean `false` if search param is `false`', function () {
windowMock.location.search = TestHelpers.toSearchString({
expectBoolean: false
});
model.importBooleanSearchParam('expectBoolean');
assert.isFalse(model.get('expectBoolean'));
});
describe('value is not boolean', function () {
beforeEach(function () {
importExpectFailure({ expectBoolean: 'not a boolean' }, 'expectBoolean');
});
it('errors correctly', function () {
assert.isTrue(AuthErrors.is(err, 'INVALID_PARAMETER'));
});
});
describe('value is empty', function () {
beforeEach(function () {
importExpectFailure({ expectBoolean: '' }, 'expectBoolean');
});
it('errors correctly', function () {
assert.isTrue(AuthErrors.is(err, 'INVALID_PARAMETER'));
});
});
describe('value is a space', function () {
beforeEach(function () {
importExpectFailure({ expectBoolean: ' ' }, 'expectBoolean');
});
it('errors correctly', function () {
assert.isTrue(AuthErrors.is(err, 'INVALID_PARAMETER'));
});
});
});
describe('importRequiredSearchParam', function () {
var err;
function importExpectFailure(searchParams, sourceName, destName) {
windowMock.location.search = TestHelpers.toSearchString(searchParams);
try {
model.importRequiredSearchParam(sourceName, destName, AuthErrors);
model.importSearchParamsUsingSchema(schema, AuthErrors);
} catch (e) {
err = e;
}
}
describe('missing', function () {
beforeEach(function () {
importExpectFailure({}, 'searchParam');
});
it('errors correctly', function () {
assert.isTrue(AuthErrors.is(err, 'MISSING_PARAMETER'));
assert.equal(err.param, 'searchParam');
});
});
describe('empty', function () {
beforeEach(function () {
importExpectFailure({searchParam: ''}, 'searchParam');
});
it('errors correctly', function () {
assert.isTrue(AuthErrors.is(err, 'MISSING_PARAMETER'));
assert.equal(err.param, 'searchParam');
});
});
describe('space', function () {
beforeEach(function () {
importExpectFailure({searchParam: ' '}, 'searchParam');
});
it('errors correctly', function () {
assert.isTrue(AuthErrors.is(err, 'MISSING_PARAMETER'));
assert.equal(err.param, 'searchParam');
});
});
describe('available', function () {
describe('without destName', function () {
beforeEach(function () {
windowMock.location.search = TestHelpers.toSearchString({ searchParam: 'value'});
model.importRequiredSearchParam('searchParam', 'searchParam', AuthErrors);
});
it('imports the value', function () {
assert.equal(model.get('searchParam'), 'value');
});
});
describe('with destName', function () {
beforeEach(function () {
windowMock.location.search = TestHelpers.toSearchString({ searchParam: 'value'});
model.importRequiredSearchParam('searchParam', 'key2', AuthErrors);
});
it('does not import to `sourceName`', function () {
assert.isFalse(model.has('searchParam'));
});
it('imports the value to `destName`', function () {
assert.equal(model.get('key2'), 'value');
it('throws a INVALID_PARAMETER', function () {
assert.isTrue(AuthErrors.is(err, 'INVALID_PARAMETER'));
assert.equal(err.param, 'required');
});
});
});

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

@ -22,6 +22,7 @@ define(function (require, exports, module) {
/*eslint-disable camelcase */
var assert = chai.assert;
var getValueLabel = TestHelpers.getValueLabel;
describe('models/reliers/oauth', function () {
var err;
@ -34,7 +35,8 @@ define(function (require, exports, module) {
var ACCESS_TYPE = 'offline';
var ACTION = 'signup';
var CLIENT_ID = 'dcdb5ae7add825d2';
var PREVERIFY_TOKEN = 'abigtoken';
var CLIENT_IMAGE_URI = 'https://mozorg.cdn.mozilla.net/media/img/firefox/new/header-firefox.pngx';
var PREVERIFY_TOKEN = 'a=.big==.token==';
var PROMPT = Constants.OAUTH_PROMPT_CONSENT;
var REDIRECT_URI = 'http://redirect.here';
var SCOPE = 'profile:email profile:uid';
@ -157,7 +159,7 @@ define(function (require, exports, module) {
});
});
describe('parameter validation', function () {
describe('query parameter validation', function () {
describe('access_type', function () {
var validValues = [undefined, 'offline', 'online'];
testValidQueryParams('access_type', validValues, 'accessType', validValues);
@ -253,14 +255,6 @@ define(function (require, exports, module) {
testValidQueryParams('keys', validValues, 'keys', expectedValues);
});
describe('privacy_uri', function () {
var validValues = ['', PRIVACY_URI];
testValidClientInfoValues('privacy_uri', validValues, 'privacyUri', validValues);
var invalidValues = [' ', 'not-a-url'];
testInvalidClientInfoValues('privacy_uri', invalidValues);
});
describe('prompt', function () {
var invalidValues = ['', ' ', 'invalid'];
testInvalidQueryParams('prompt', invalidValues);
@ -278,10 +272,6 @@ define(function (require, exports, module) {
});
describe('redirect_uri', function () {
describe('is missing on the server', function () {
testMissingClientInfoValue('redirect_uri');
});
var validQueryParamValues = [undefined, REDIRECT_URI];
// redirectUri will always be loaded from the server
var expectedValues = [SERVER_REDIRECT_URI, SERVER_REDIRECT_URI];
@ -289,9 +279,6 @@ define(function (require, exports, module) {
var invalidQueryParamValues = ['', ' ', 'not-a-url'];
testInvalidQueryParams('redirect_uri', invalidQueryParamValues);
var invalidClientInfoValues = ['', ' '];
testInvalidClientInfoValues('redirect_uri', invalidClientInfoValues);
});
describe('scope', function () {
@ -336,14 +323,6 @@ define(function (require, exports, module) {
});
});
describe('terms_uri', function () {
var invalidValues = [' ', 'not-a-url'];
testInvalidClientInfoValues('terms_uri', invalidValues);
var validValues = ['', TERMS_URI];
testValidClientInfoValues('terms_uri', validValues, 'termsUri', validValues);
});
describe('verification_redirect', function () {
var invalidValues = ['', ' ', 'invalid'];
testInvalidQueryParams('verification_redirect', invalidValues);
@ -352,7 +331,65 @@ define(function (require, exports, module) {
var expectedValues = ['no', 'no', 'always'];
testValidQueryParams('verification_redirect', validValues, 'verificationRedirect', expectedValues);
});
});
describe('client info validation', function () {
describe('image_uri', function () {
// leading & trailing whitespace will be trimmed
var validValues = ['', ' ', CLIENT_IMAGE_URI, ' ' + CLIENT_IMAGE_URI];
var expectedValues = ['', '', CLIENT_IMAGE_URI, CLIENT_IMAGE_URI];
testValidClientInfoValues(
'image_uri', validValues, 'imageUri', expectedValues);
var invalidValues = ['not-a-url'];
testInvalidClientInfoValues('image_uri', invalidValues);
});
describe('name', function () {
var validValues = ['client name'];
testValidClientInfoValues('name', validValues, 'serviceName', validValues);
var invalidValues = ['', ' '];
testInvalidClientInfoValues('name', invalidValues);
});
describe('privacy_uri', function () {
var validValues = ['', ' ', PRIVACY_URI, PRIVACY_URI + ' '];
var expectedValues = ['', '', PRIVACY_URI, PRIVACY_URI];
testValidClientInfoValues(
'privacy_uri', validValues, 'privacyUri', expectedValues);
var invalidValues = ['not-a-url'];
testInvalidClientInfoValues('privacy_uri', invalidValues);
});
describe('redirect_uri', function () {
describe('is missing on the server', function () {
testMissingClientInfoValue('redirect_uri');
});
var invalidClientInfoValues = ['', ' '];
testInvalidClientInfoValues('redirect_uri', invalidClientInfoValues);
});
describe('terms_uri', function () {
var invalidValues = ['not-a-url'];
testInvalidClientInfoValues('terms_uri', invalidValues);
var validValues = ['', ' ', TERMS_URI, ' ' + TERMS_URI];
var expectedValues = ['', '', TERMS_URI, TERMS_URI];
testValidClientInfoValues('terms_uri', validValues, 'termsUri', expectedValues);
});
describe('trusted', function () {
var validValues = ['true', true, 'false', false];
var expected = [true, true, false, false];
testValidClientInfoValues('trusted', validValues, 'trusted', expected);
var invalidValues = ['', 'not-a-boolean'];
testInvalidClientInfoValues('trusted', invalidValues);
});
});
});
describe('isTrusted', function () {
@ -385,7 +422,6 @@ define(function (require, exports, module) {
});
});
});
});
describe('isOAuth', function () {
it('returns `true`', function () {
@ -581,6 +617,7 @@ define(function (require, exports, module) {
sinon.stub(oAuthClient, 'getClientInfo', function () {
var clientInfo = {
id: CLIENT_ID,
name: SERVICE_NAME,
privacy_uri: PRIVACY_URI,
redirect_uri: SERVER_REDIRECT_URI,
@ -774,18 +811,6 @@ define(function (require, exports, module) {
});
});
}
function getValueLabel(value) {
if (_.isUndefined(value)) {
return 'not set';
} else if (value === '') {
return 'empty';
} else if (/^\s+$/.test(value)) {
return 'whitespace only';
}
return value;
}
});
});

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

@ -5,6 +5,8 @@
define(function (require, exports, module) {
'use strict';
var _ = require('underscore');
var AuthErrors = require('lib/auth-errors');
var chai = require('chai');
var Constants = require('lib/constants');
var Relier = require('models/reliers/relier');
@ -13,18 +15,19 @@ define(function (require, exports, module) {
var WindowMock = require('../../../mocks/window');
var assert = chai.assert;
var getValueLabel = TestHelpers.getValueLabel;
describe('models/reliers/relier', function () {
var relier;
var windowMock;
var SERVICE = 'service';
var PREVERIFY_TOKEN = 'abigtoken';
var EMAIL = 'email@moz.org';
var UID = 'uid';
var ENTRYPOINT = 'preferences';
var CAMPAIGN = 'fennec';
var EMAIL = 'email@moz.org';
var ENTRYPOINT = 'preferences';
var PREVERIFY_TOKEN = 'a=.big==.token==';
var SERVICE = 'sync';
var SETTING = 'avatar';
var UID = TestHelpers.createRandomHexString(Constants.UID_LENGTH);
var UTM_CAMPAIGN = 'utm_campaign';
var UTM_CONTENT = 'utm_content';
var UTM_MEDIUM = 'utm_medium';
@ -119,6 +122,46 @@ define(function (require, exports, module) {
});
});
describe('preVerifyToken', function () {
describe('invalid', function () {
var invalidTokens = ['', ' ', 'invalid token'];
invalidTokens.forEach(function (value) {
describe(getValueLabel(value), function () {
testInvalidQueryParam('preVerifyToken', value);
});
});
});
describe('valid', function () {
var validTokens = [undefined, PREVERIFY_TOKEN];
validTokens.forEach(function (value) {
describe(getValueLabel(value), function () {
testValidQueryParam('preVerifyToken', value, 'preVerifyToken', value);
});
});
});
});
describe('migration', function () {
describe('invalid', function () {
var invalidMigrations = ['', ' ', 'invalid migration'];
invalidMigrations.forEach(function (token) {
describe(getValueLabel(token), function () {
testInvalidQueryParam('migration', token);
});
});
});
describe('valid', function () {
var validMigrations = [undefined, Constants.AMO_MIGRATION, Constants.SYNC11_MIGRATION];
validMigrations.forEach(function (value) {
describe(getValueLabel(value), function () {
testValidQueryParam('migration', value, 'migration', value);
});
});
});
});
describe('isOAuth', function () {
it('returns `false`', function () {
assert.isFalse(relier.isOAuth());
@ -225,6 +268,56 @@ define(function (require, exports, module) {
});
});
});
function testInvalidQueryParam(paramName, value) {
var err;
beforeEach(function () {
var params = {};
if (! _.isUndefined(value)) {
params[paramName] = value;
} else {
delete params[paramName];
}
windowMock.location.search = TestHelpers.toSearchString(params);
return relier.fetch()
.then(assert.fail, function (_err) {
err = _err;
});
});
it('errors correctly', function () {
assert.isTrue(AuthErrors.is(err, 'INVALID_PARAMETER'));
assert.equal(err.param, paramName);
});
}
function testValidQueryParam(paramName, paramValue, modelName, expectedValue) {
beforeEach(function () {
var params = {};
if (! _.isUndefined(paramValue)) {
params[paramName] = paramValue;
} else {
delete params[paramName];
}
windowMock.location.search = TestHelpers.toSearchString(params);
return relier.fetch();
});
it('is successful', function () {
if (_.isUndefined(expectedValue)) {
assert.isFalse(relier.has(modelName));
} else {
assert.equal(relier.get(modelName), expectedValue);
}
});
}
});
});

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

@ -5,6 +5,7 @@
define(function (require, exports, module) {
'use strict';
var AuthErrors = require('lib/auth-errors');
var chai = require('chai');
var Relier = require('models/reliers/sync');
var TestHelpers = require('../../../lib/helpers');
@ -14,13 +15,25 @@ define(function (require, exports, module) {
var assert = chai.assert;
describe('models/reliers/sync', function () {
var windowMock;
var translator;
var err;
var relier;
var translator;
var windowMock;
var CONTEXT = 'fx_desktop_v1';
var SYNC_MIGRATION = 'sync11';
var SYNC_SERVICE = 'sync';
function fetchExpectError () {
return relier.fetch()
.then(assert.fail, function (_err) {
err = _err;
});
}
beforeEach(function () {
windowMock = new WindowMock();
translator = new Translator('en-US', ['en-US']);
windowMock = new WindowMock();
relier = new Relier({
translator: translator,
@ -31,35 +44,134 @@ define(function (require, exports, module) {
describe('fetch', function () {
it('populates model from the search parameters', function () {
windowMock.location.search = TestHelpers.toSearchString({
context: 'fx_desktop_v1',
context: CONTEXT,
customizeSync: 'true',
migration: 'sync11',
service: 'sync'
migration: SYNC_MIGRATION,
service: SYNC_SERVICE
});
return relier.fetch()
.then(function () {
assert.equal(relier.get('context'), 'fx_desktop_v1');
assert.equal(relier.get('migration'), 'sync11');
assert.equal(relier.get('service'), 'sync');
assert.equal(relier.get('context'), CONTEXT);
assert.equal(relier.get('migration'), SYNC_MIGRATION);
assert.equal(relier.get('service'), SYNC_SERVICE);
assert.isTrue(relier.get('customizeSync'));
});
});
it('does not throw if `customizeSync` is not a boolean', function () {
describe('context query parameter', function () {
describe('missing', function () {
beforeEach(function () {
windowMock.location.search = TestHelpers.toSearchString({});
return relier.fetch();
});
it('succeeds', function () {
// it's OK
assert.isFalse(relier.has('context'));
});
});
describe('emtpy', function () {
beforeEach(function () {
windowMock.location.search = TestHelpers.toSearchString({
context: ''
});
return fetchExpectError();
});
it('errors correctly', function () {
assert.isTrue(AuthErrors.is(err, 'INVALID_PARAMETER'));
assert.equal(err.param, 'context');
});
});
describe('whitepsace', function () {
beforeEach(function () {
windowMock.location.search = TestHelpers.toSearchString({
context: ' '
});
return fetchExpectError();
});
it('errors correctly', function () {
assert.isTrue(AuthErrors.is(err, 'INVALID_PARAMETER'));
assert.equal(err.param, 'context');
});
});
});
describe('customizeSync query parameter', function () {
describe('missing', function () {
beforeEach(function () {
windowMock.location.search = TestHelpers.toSearchString({
context: CONTEXT
});
return relier.fetch();
});
it('succeeds', function () {
assert.isFalse(relier.get('customizeSync'));
});
});
describe('emtpy', function () {
beforeEach(function () {
windowMock.location.search = TestHelpers.toSearchString({
context: CONTEXT,
customizeSync: ''
});
return fetchExpectError();
});
it('errors correctly', function () {
assert.isTrue(AuthErrors.is(err, 'INVALID_PARAMETER'));
assert.equal(err.param, 'customizeSync');
});
});
describe('whitepsace', function () {
beforeEach(function () {
windowMock.location.search = TestHelpers.toSearchString({
context: CONTEXT,
customizeSync: ' '
});
return fetchExpectError();
});
it('errors correctly', function () {
assert.isTrue(AuthErrors.is(err, 'INVALID_PARAMETER'));
assert.equal(err.param, 'customizeSync');
});
});
describe('not a boolean', function () {
beforeEach(function () {
windowMock.location.search = TestHelpers.toSearchString({
context: CONTEXT,
customizeSync: 'not a boolean'
});
return relier.fetch()
.then(function () {
assert.isFalse(relier.has('customizeSync'));
return fetchExpectError();
});
it('errors correctly', function () {
assert.isTrue(AuthErrors.is(err, 'INVALID_PARAMETER'));
assert.equal(err.param, 'customizeSync');
});
});
});
it('translates `service` to `serviceName`', function () {
windowMock.location.search = TestHelpers.toSearchString({
service: 'sync'
context: CONTEXT,
service: SYNC_SERVICE
});
return relier.fetch()
@ -78,6 +190,7 @@ define(function (require, exports, module) {
describe('isCustomizeSyncChecked', function () {
it('returns true if `customizeSync=true`', function () {
windowMock.location.search = TestHelpers.toSearchString({
context: CONTEXT,
customizeSync: 'true'
});
@ -89,6 +202,7 @@ define(function (require, exports, module) {
it('returns false if `customizeSync=false`', function () {
windowMock.location.search = TestHelpers.toSearchString({
context: CONTEXT,
customizeSync: 'false'
});

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

@ -61,6 +61,7 @@ function (Translator, Session) {
'../tests/spec/lib/height-observer',
'../tests/spec/lib/config-loader',
'../tests/spec/lib/require-on-demand',
'../tests/spec/lib/transform',
'../tests/spec/head/startup-styles',
'../tests/spec/views/app',
'../tests/spec/views/base',

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

@ -36,7 +36,8 @@
"speed-trap": "0.0.5",
"tos-pp": "https://github.com/mozilla/legal-docs.git",
"underscore": "1.8.3",
"webrtc-adapter": "0.2.5"
"webrtc-adapter": "0.2.5",
"vat": "0.0.5"
},
"devDependencies": {
"moment": "2.8.3"