feat(session): Upgrade user session (#5626), r=@shane-tomlinson, @vladikoff

This commit is contained in:
Vijay Budhram 2017-11-09 11:33:18 -05:00 коммит произвёл GitHub
Родитель b01a496145
Коммит 04cff4e99f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
23 изменённых файлов: 515 добавлений и 123 удалений

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

@ -420,7 +420,11 @@ define(function (require, exports, module) {
INVALID_WEB_CHANNEL: {
errno: 1051,
message: 'Browser not configured to accept WebChannel messages from this domain'
}
},
REUSED_PRIMARY_EMAIL_VERIFICATION_CODE: {
errno: 1052,
message: t('That confirmation link was already used, and can only be used once.')
},
};
/*eslint-enable sorting/sort-object-props*/

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

@ -365,6 +365,21 @@ define(function (require, exports, module) {
return client.recoveryEmailResendCode(sessionToken, clientOptions);
}),
/**
* Sends a verification code to the account's recovery email address
* that will verify the current session.
*
* @param {String} sessionToken sessionToken obtained from signIn
* @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
*/
sessionVerifyResend: withClient((client, sessionToken) => {
const clientOptions = {
type: 'upgradeSession'
};
return client.recoveryEmailResendCode(sessionToken, clientOptions);
}),
/**
* Destroy the user's current or custom session
*
@ -857,14 +872,6 @@ define(function (require, exports, module) {
deleteEmail: createClientDelegate('deleteEmail'),
/**
* Responds with whether or not secondary emails is enabled for a user.
*
* @param {String} sessionToken User session token
* @return {Promise} resolves when complete
*/
recoveryEmailSecondaryEmailEnabled: createClientDelegate('recoveryEmailSecondaryEmailEnabled'),
/**
* Set the new primary email address for a user.
*

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

@ -89,6 +89,7 @@ define(function (require, exports, module) {
'oauth/force_auth(/)': createViewHandler(ForceAuthView),
'oauth/signin(/)': createViewHandler(SignInView),
'oauth/signup(/)': createViewHandler(SignUpView),
'primary_email_verified(/)': createViewHandler(ReadyView, { type: VerificationReasons.PRIMARY_EMAIL_VERIFIED }),
'report_signin(/)': createViewHandler(ReportSignInView),
'reset_password(/)': createViewHandler(ResetPasswordView),
'reset_password_confirmed(/)': createViewHandler(ReadyView, { type: VerificationReasons.PASSWORD_RESET }),
@ -120,6 +121,7 @@ define(function (require, exports, module) {
'sms/sent(/)': createViewHandler(SmsSentView),
'sms/why(/)': createChildViewHandler(WhyConnectAnotherDeviceView, SmsSendView),
'verify_email(/)': createViewHandler(CompleteSignUpView, { type: VerificationReasons.SIGN_UP }),
'verify_primary_email(/)': createViewHandler(CompleteSignUpView, { type: VerificationReasons.PRIMARY_EMAIL_VERIFIED }),
'verify_secondary_email(/)': createViewHandler(CompleteSignUpView, { type: VerificationReasons.SECONDARY_EMAIL_VERIFIED })
},

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

@ -12,6 +12,7 @@ define(function (require, exports, module) {
return {
FORCE_AUTH: 'force_auth',
PASSWORD_RESET: 'reset_password',
PRIMARY_EMAIL_VERIFIED: 'primary_email_verified',
SECONDARY_EMAIL_VERIFIED: 'secondary_email_verified',
SIGN_IN: 'login',
SIGN_UP: 'signup'

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

@ -324,31 +324,6 @@ define(function (require, exports, module) {
});
},
/**
* Check to see if secondary emails is enabled for this user, using support email
* domain and is in a verified session.
*
* @returns {Promise} Resolve to true/false
*/
recoveryEmailSecondaryEmailEnabled () {
const sessionToken = this.get('sessionToken');
if (! sessionToken) {
return p.reject(AuthErrors.toError('INVALID_TOKEN'));
}
return this._fxaClient.recoveryEmailSecondaryEmailEnabled(sessionToken)
.then((resp) => {
return resp.ok;
}, (err) => {
if (AuthErrors.is(err, 'INVALID_TOKEN')) {
// sessionToken is no longer valid, kill it.
this.unset('sessionToken');
}
throw err;
});
},
isSignedIn () {
return this._fxaClient.isSignedIn(this.get('sessionToken'));
},
@ -552,6 +527,17 @@ define(function (require, exports, module) {
);
},
/**
* Request to verify current session.
*
* @returns {Promise} - resolves when complete
*/
requestVerifySession () {
return this._fxaClient.sessionVerifyResend(
this.get('sessionToken')
);
},
/**
* Verify the account using the verification code
*

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

@ -21,6 +21,7 @@ define(function (require, exports, module) {
const SameBrowserVerificationModel = require('../verification/same-browser');
const SearchParamMixin = require('../mixins/search-param');
const SettingsIfSignedInBehavior = require('../../views/behaviors/settings');
const t = (msg) => msg;
const Vat = require('../../lib/vat');
const QUERY_PARAMETER_SCHEMA = {
@ -64,8 +65,16 @@ define(function (require, exports, module) {
*/
defaultBehaviors: {
afterChangePassword: new NullBehavior(),
afterCompletePrimaryEmail: new SettingsIfSignedInBehavior(new NavigateBehavior('primary_email_verified'), {
// Upon verifying primary email, we want to reopen the emails panel to let user continue adding more
// emails
endpoint: 'settings/emails',
success: t('Primary email verified successfully')
}),
afterCompleteResetPassword: new NullBehavior(),
afterCompleteSecondaryEmail: new SettingsIfSignedInBehavior(new NavigateBehavior('secondary_email_verified')),
afterCompleteSecondaryEmail: new SettingsIfSignedInBehavior(new NavigateBehavior('secondary_email_verified'), {
success: t('Secondary email verified successfully')
}),
afterCompleteSignIn: new NavigateBehavior('signin_verified'),
afterCompleteSignUp: new NavigateBehavior('signup_verified'),
afterDeleteAccount: new NullBehavior(),
@ -231,6 +240,17 @@ define(function (require, exports, module) {
.then(() => this.getBehavior('afterCompleteSignIn'));
},
/**
* Called after primary email verification, in the verification tab.
*
* @param {Object} account
* @return {Promise}
*/
afterCompletePrimaryEmail (account) {
return this.unpersistVerificationData(account)
.then(() => this.getBehavior('afterCompletePrimaryEmail'));
},
/**
* Called after secondary email verification, in the verification tab.
*

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

@ -1,7 +1,10 @@
<div id="main-content" class="card">
{{#isLinkUsed}}
<header>
<h1 id="fxa-verification-link-reused-header">{{#t}}Sign-in already confirmed{{/t}}</h1>
<h1 id="fxa-verification-link-reused-header">
{{#isPrimaryEmailVerification}}{{#t}}Primary email already confirmed{{/t}}{{/isPrimaryEmailVerification}}
{{^isPrimaryEmailVerification}}{{#t}}Sign-in already confirmed{{/t}}{{/isPrimaryEmailVerification}}
</h1>
</header>
<section>
@ -14,7 +17,7 @@
</section>
{{/isLinkUsed}}
{{#isLinkExpired}}
<header>
<h1 id="fxa-verification-link-expired-header">{{#t}}Verification link expired{{/t}}</h1>

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

@ -17,12 +17,13 @@
<p class="account-ready-service">{{#t}}You are now ready to use %(serviceName)s{{/t}}</p>
{{/serviceName}}
{{^serviceName}}
{{#secondaryEmailVerified}}
<p class="account-ready-service">{{#t}}Account notifications will now also be sent to %(secondaryEmailVerified)s{{/t}}</p>
{{/secondaryEmailVerified}}
{{^secondaryEmailVerified}}
{{#emailVerified}}
<p class="account-ready-service">{{{escapedEmailReadyText}}}</p>
{{/emailVerified}}
{{^emailVerified}}
<p class="account-ready-generic">{{#t}}Your account is ready!{{/t}}</p>
{{/secondaryEmailVerified}}
{{/emailVerified}}
{{/serviceName}}
{{/isSync}}

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

@ -75,7 +75,7 @@
<div class="button-row">
{{#hasSecondaryEmail}}
<button class="settings-button email-refresh primary enabled">{{#t}}Refresh{{/t}}</button>
<button type="submit" class="settings-button email-refresh primary enabled">{{#t}}Refresh{{/t}}</button>
<button
class="settings-button {{#hasSecondaryVerifiedEmail}}cancel secondary enabled{{/hasSecondaryVerifiedEmail}}

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

@ -0,0 +1,36 @@
<div id="upgrade-session" class="settings-unit {{#isPanelOpen}}open{{/isPanelOpen}}">
<div class="settings-unit-stub">
<header class="settings-unit-summary">
<h2 class="settings-unit-title">{{ title }}</h2>
</header>
<button class="settings-button primary settings-unit-toggle" data-href="{{ gatedHref }}">
<span class="unlock-button">{{#t}}Unlock…{{/t}}</span>
</button>
</div>
<div class="settings-unit-details">
<div class="error"></div>
<form novalidate>
<p> {{ caption }} </p>
<ul class="email-list button-row">
<li class="email-address">
<div class="address">{{ email }}</div>
<div class="details">
<div class="not-verified">{{#t}}verify to unlock section{{/t}}</div>
</div>
<div class="settings-button-group">
<button class="settings-button warning send-verification-email" data-id="{{ email }}">
{{#t}}Send{{/t}}
</button>
</div>
</li>
<a class="resend" data-id="{{ email }}">{{#t}}Not in inbox or spam folder? Resend{{/t}}</a>
</ul>
<div class="button-row">
<button type="submit" class="settings-button primary refresh-verification-state">{{#t}}Refresh{{/t}}</button>
<button class="settings-button secondary cancel">{{#t}}Done{{/t}}</button>
</div>
</form>
</div>
</div>

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

@ -11,16 +11,33 @@ define(function (require, exports, module) {
'use strict';
const NavigateBehavior = require('../behaviors/navigate');
const t = (msg) => msg;
const success = t('Account verified successfully');
module.exports = function (defaultBehavior) {
/**
* Creates navigation behavior that displays a success message
* and redirects to settings.
*
* @param {Object} defaultBehavior - default behavior to invoke if not signed in
* @param {Object} [options]
* @param {String} [options.success] - success message when redirected
* @param {String} [options.endpoint] - endpoint to redirect to
* @return {Object} promise
*/
module.exports = function (defaultBehavior, options = {}) {
const behavior = function (view, account) {
return account.isSignedIn()
.then((isSignedIn) => {
if (isSignedIn) {
return new NavigateBehavior('settings', { success });
let success = t('Account verified successfully');
let endpoint = 'settings';
if (options.success) {
success = options.success;
}
if (options.endpoint) {
endpoint = options.endpoint;
}
return new NavigateBehavior(endpoint, { success });
}
return defaultBehavior;

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

@ -73,6 +73,7 @@ define(function (require, exports, module) {
const code = verificationInfo.get('code');
const options = {
primaryEmailVerified: this.getSearchParam('primary_email_verified') || null,
reminder: verificationInfo.get('reminder'),
secondaryEmailVerified: this.getSearchParam('secondary_email_verified') || null,
serverVerificationStatus: this.getSearchParam('server_verification') || null,
@ -94,7 +95,8 @@ define(function (require, exports, module) {
// If the link is invalid, print a special error message.
isLinkDamaged: ! verificationInfo.isValid(),
isLinkExpired: verificationInfo.isExpired(),
isLinkUsed: verificationInfo.isUsed()
isLinkUsed: verificationInfo.isUsed(),
isPrimaryEmailVerification: this.isPrimaryEmail()
});
},
@ -153,7 +155,9 @@ define(function (require, exports, module) {
*/
_getBrokerMethod () {
let brokerMethod;
if (this.isSecondaryEmail()) {
if (this.isPrimaryEmail()) {
brokerMethod = 'afterCompletePrimaryEmail';
} else if (this.isSecondaryEmail()) {
brokerMethod = 'afterCompleteSecondaryEmail';
} else if (this.isSignIn()) {
brokerMethod = 'afterCompleteSignIn';
@ -182,10 +186,14 @@ define(function (require, exports, module) {
AuthErrors.is(err, 'INVALID_VERIFICATION_CODE') ||
AuthErrors.is(err, 'INVALID_PARAMETER')) {
// When coming from sign-in confirmation verification, show a
// verification link expired error instead of damaged verification link.
// This error is generated because the link has already been used.
if (this.isSignIn()) {
if (this.isPrimaryEmail()) {
verificationInfo.markUsed();
err = AuthErrors.toError('REUSED_PRIMARY_EMAIL_VERIFICATION_CODE');
} else if (this.isSignIn()) {
// When coming from sign-in confirmation verification, show a
// verification link expired error instead of damaged verification link.
// This error is generated because the link has already been used.
//
// Disable resending verification, can only be triggered from new sign-in
verificationInfo.markUsed();
err = AuthErrors.toError('REUSED_SIGNIN_VERIFICATION_CODE');

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

@ -0,0 +1,113 @@
/* 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/. */
/**
* View mixin to support a user upgrading their session to
* be verified. This is useful in situations where a panel
* might contain sensitive information or security related
* features.
*
* This mix-in replaces the template loaded by the view with
* the upgrade-session template. Once the email has been
* verified, the page is re-rendered and the user can see
* the gated panel.
*
* @mixin UpgradeSessionMixin
*/
define(function (require, exports, module) {
'use strict';
const BaseView = require('../base');
const { preventDefaultThen } = BaseView;
const SettingsPanelMixin = require('../mixins/settings-panel-mixin');
const UpgradeSessionTemplate = require('stache!templates/settings/upgrade_session');
const t = BaseView.t;
const showProgressIndicator = require('../decorators/progress_indicator');
const EMAIL_REFRESH_SELECTOR = 'button.settings-button.refresh-verification-state';
const EMAIL_REFRESH_DELAYMS = 350;
/**
* The UpgradeSessionMixin can be configured to display different titles and captions
* depending on what panel is being gated.
*
* @param {Object} [options]
* @param {String} [options.caption] - caption describing what the panel is unlocking
* @param {String} [options.gatedHref] - location that is redirected after session is verified
* @param {String} [options.title] - title name of the panel
* @returns {Object} UpgradeSessionMixin
*/
module.exports = (options = {}) => {
return {
dependsOn: [SettingsPanelMixin],
events: {
'click .refresh-verification-state': preventDefaultThen('_clickRefreshVerificationState'),
'click .send-verification-email': preventDefaultThen('_clickSendVerificationEmail')
},
initialize () {
this.gatedTemplate = this.template;
},
_clickRefreshVerificationState: showProgressIndicator(function() {
this.model.set({
isPanelOpen: true
});
return this.setupSessionGateIfRequired()
.then((verified) => {
if (verified) {
this.displaySuccess(t('Primary email verified successfully'), {
closePanel: false
});
}
return this.render();
});
}, EMAIL_REFRESH_SELECTOR, EMAIL_REFRESH_DELAYMS),
_clickSendVerificationEmail () {
const account = this.getSignedInAccount();
return account.requestVerifySession(this.relier)
.then(() => {
this.displaySuccess(t('Verification email sent'), {
closePanel: false
});
});
},
setInitialContext (context) {
context.set({
caption: this.translate(options.caption),
email: this.getSignedInAccount().get('email'),
gatedHref: options.gatedHref,
isPanelOpen: this.isPanelOpen(),
title: this.translate(options.title)
});
},
/**
* Checks to see if the current session is verified. If it is,
* then it renders the original template, otherwise it renders
* the upgrade-session template. This template prompts user
* to verify their email address before they can see the original
* template.
*
* @returns {Boolean} sessionVerified
*/
setupSessionGateIfRequired () {
const account = this.getSignedInAccount();
return account.sessionVerificationStatus()
.then(({sessionVerified}) => {
if (! sessionVerified) {
this.template = UpgradeSessionTemplate;
} else {
this.template = this.gatedTemplate;
}
return sessionVerified;
});
}
};
};
});

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

@ -45,6 +45,15 @@ define(function (require, exports, module) {
return this.model.get('type') === VerificationReasons.SIGN_UP;
},
/**
* Is a primary email being verified?
*
* @returns {Boolean}
*/
isPrimaryEmail () {
return this.model.get('type') === VerificationReasons.PRIMARY_EMAIL_VERIFIED;
},
/**
* Is a secondary email being verified?
*

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

@ -40,9 +40,15 @@ define(function (require, exports, module) {
headerTitle: t('Password reset'),
readyToSyncText: t('Complete set-up by entering the new password on your other Firefox devices.')
},
SECONDARY_EMAIL_VERIFIED: {
PRIMARY_EMAIL_VERIFIED: {
emailReadyText: t('You are now ready to make changes to your Firefox Account.'),
headerId: 'fxa-sign-up-complete-header',
headerTitle: t('Email verified')
headerTitle: t('Primary email verified')
},
SECONDARY_EMAIL_VERIFIED: {
emailReadyText: t('Account notifications will now also be sent to %(secondaryEmailVerified)s.'),
headerId: 'fxa-sign-up-complete-header',
headerTitle: t('Secondary email verified')
},
// signin_confirmed and signin_verified are only shown to Sync for now.
SIGN_IN: {
@ -69,6 +75,8 @@ define(function (require, exports, module) {
setInitialContext (context) {
context.set({
emailVerified: this.getSearchParam('secondary_email_verified') || this.getSearchParam('primary_email_verified'),
escapedEmailReadyText: this._getEscapedEmailReadyText(),
escapedHeaderTitle: this._getEscapedHeaderTitle(),
escapedReadyToSyncText: this._getEscapedReadyToSyncText(),
headerId: this._getHeaderId(),
@ -102,6 +110,17 @@ define(function (require, exports, module) {
const readyToSyncText = this._templateInfo.readyToSyncText;
// translateInTemplate HTML escapes
return this.translateInTemplate(readyToSyncText);
},
/**
* Get the HTML escaped "Email Ready" text.
*
* @returns {String}
*/
_getEscapedEmailReadyText () {
const emailReadyText = this._templateInfo.emailReadyText;
// translateInTemplate HTML escapes
return this.translateInTemplate(emailReadyText);
}
});

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

@ -14,6 +14,7 @@ define(function (require, exports, module) {
const FormView = require('../form');
const preventDefaultThen = require('../base').preventDefaultThen;
const SettingsPanelMixin = require('../mixins/settings-panel-mixin');
const UpgradeSessionMixin = require('../mixins/upgrade-session-mixin');
const SearchParamMixin = require('../../lib/search-param-mixin');
const Strings = require('../../lib/strings');
const showProgressIndicator = require('../decorators/progress_indicator');
@ -37,7 +38,16 @@ define(function (require, exports, module) {
'click .set-primary': preventDefaultThen('setPrimary')
},
initialize (options) {
beforeRender () {
return this.setupSessionGateIfRequired()
.then((isEnabled) => {
if (isEnabled) {
return this._fetchEmails();
}
});
},
initialize (options = {}) {
if (options.emails) {
this._emails = options.emails;
} else {
@ -53,15 +63,10 @@ define(function (require, exports, module) {
hasSecondaryEmail: this._hasSecondaryEmail(),
hasSecondaryVerifiedEmail: this._hasSecondaryVerifiedEmail(),
isPanelOpen: this.isPanelOpen(),
newEmail: this.newEmail
newEmail: this.newEmail,
});
},
beforeRender () {
// Only show this view on verified session
return this._isSecondaryEmailEnabled();
},
afterRender () {
// Panel should remain open if there are any unverified secondary emails
if (this._hasSecondaryEmail() && ! this._hasSecondaryVerifiedEmail()) {
@ -77,25 +82,6 @@ define(function (require, exports, module) {
return false;
},
_isSecondaryEmailEnabled () {
// Only show secondary email panel if the user is in a verified session and feature is enabled.
const account = this.getSignedInAccount();
return account.recoveryEmailSecondaryEmailEnabled()
.then((isEnabled) => {
if (! isEnabled) {
return this.remove();
}
// If we fail to fetch emails, then this user does not have this feature enabled
// and we should not display this panel.
return this._fetchEmails()
.fail(() => {
return this.remove();
});
});
},
_hasSecondaryEmail () {
return this._emails.length > 1;
},
@ -177,6 +163,11 @@ define(function (require, exports, module) {
Cocktail.mixin(
View,
UpgradeSessionMixin({
caption: t('A secondary email is an additional address for receiving security notices and confirming new Sync devices'),
gatedHref: 'settings/emails',
title: t('Secondary email')
}),
AvatarMixin,
SettingsPanelMixin,
FloatingPlaceholderMixin,

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

@ -224,6 +224,17 @@ define(function (require, exports, module) {
});
});
describe('afterCompletePrimaryEmail', () => {
it('unpersists VerificationData, returns the expected behavior', function () {
sinon.spy(broker, 'unpersistVerificationData');
return broker.afterCompletePrimaryEmail(account)
.then((behavior) => {
assert.isTrue(broker.unpersistVerificationData.calledWith(account));
assert.equal(behavior.type, 'settings');
});
});
});
describe('afterCompleteSecondaryEmail', function () {
it('unpersist VerificationData, returns the expected behavior', function () {
sinon.spy(broker, 'unpersistVerificationData');

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

@ -209,7 +209,14 @@ define(function (require, exports, module) {
var args = account.verifySignUp.getCall(0).args;
assert.isTrue(account.verifySignUp.called);
assert.ok(args[0]);
assert.deepEqual(args[1], {reminder: null, secondaryEmailVerified: null, serverVerificationStatus: null, service: validService, type: null});
assert.deepEqual(args[1], {
primaryEmailVerified: null,
reminder: null,
secondaryEmailVerified: null,
serverVerificationStatus: null,
service: validService,
type: null
});
});
});
@ -229,7 +236,14 @@ define(function (require, exports, module) {
var args = account.verifySignUp.getCall(0).args;
assert.isTrue(account.verifySignUp.called);
assert.ok(args[0]);
assert.deepEqual(args[1], {reminder: validReminder, secondaryEmailVerified: null, serverVerificationStatus: null, service: null, type: null});
assert.deepEqual(args[1], {
primaryEmailVerified: null,
reminder: validReminder,
secondaryEmailVerified: null,
serverVerificationStatus: null,
service: null,
type: null
});
});
});
@ -250,7 +264,14 @@ define(function (require, exports, module) {
var args = account.verifySignUp.getCall(0).args;
assert.isTrue(account.verifySignUp.called);
assert.ok(args[0]);
assert.deepEqual(args[1], {reminder: validReminder, secondaryEmailVerified: null, serverVerificationStatus: null, service: validService, type: null});
assert.deepEqual(args[1], {
primaryEmailVerified: null,
reminder: validReminder,
secondaryEmailVerified: null,
serverVerificationStatus: null,
service: validService,
type: null
});
});
});
@ -271,7 +292,14 @@ define(function (require, exports, module) {
var args = account.verifySignUp.getCall(0).args;
assert.isTrue(account.verifySignUp.called);
assert.ok(args[0]);
assert.deepEqual(args[1], {reminder: null, secondaryEmailVerified: null, serverVerificationStatus: 'verified', service: null, type: null});
assert.deepEqual(args[1], {
primaryEmailVerified: null,
reminder: null,
secondaryEmailVerified: null,
serverVerificationStatus: 'verified',
service: null,
type: null
});
});
});
@ -292,7 +320,14 @@ define(function (require, exports, module) {
var args = account.verifySignUp.getCall(0).args;
assert.isTrue(account.verifySignUp.called);
assert.ok(args[0]);
assert.deepEqual(args[1], {reminder: null, secondaryEmailVerified: null, serverVerificationStatus: null, service: null, type: 'secondary'});
assert.deepEqual(args[1], {
primaryEmailVerified: null,
reminder: null,
secondaryEmailVerified: null,
serverVerificationStatus: null,
service: null,
type: 'secondary'
});
});
});
@ -314,7 +349,42 @@ define(function (require, exports, module) {
var args = account.verifySignUp.getCall(0).args;
assert.isTrue(account.verifySignUp.called);
assert.ok(args[0]);
assert.deepEqual(args[1], {reminder: null, secondaryEmailVerified: 'some@email.com', serverVerificationStatus: null, service: null, type: null});
assert.deepEqual(args[1], {
primaryEmailVerified: null,
reminder: null,
secondaryEmailVerified: 'some@email.com',
serverVerificationStatus: null,
service: null,
type: null
});
});
});
describe('if primary_email_verified is in the url', () => {
beforeEach(function () {
windowMock.location.search = '?code=' + validCode + '&uid=' + validUid +
'&primary_email_verified=some@email.com';
relier = new Relier({}, {
window: windowMock
});
relier.fetch();
initView(account);
sinon.stub(view, '_notifyBrokerAndComplete').callsFake(() => p());
return view.render();
});
it('attempt to pass secondary_email_verified to verifySignUp', () => {
const { args } = account.verifySignUp.getCall(0);
assert.isTrue(account.verifySignUp.called);
assert.ok(args[0]);
assert.deepEqual(args[1], {
primaryEmailVerified: 'some@email.com',
reminder: null,
secondaryEmailVerified: null,
serverVerificationStatus: null,
service: null,
type: null
});
});
});
@ -422,6 +492,34 @@ define(function (require, exports, module) {
});
});
describe('REUSED_PRIMARY_EMAIL_VERIFICATION_CODE error', () => {
beforeEach(function () {
verificationError = AuthErrors.toError('INVALID_VERIFICATION_CODE', 'this isn\'t a lottery');
windowMock.location.search = '?code=' + validCode + '&uid=' + validUid;
var model = new Backbone.Model();
model.set('type', VerificationReasons.PRIMARY_EMAIL_VERIFIED);
view = new View({
account: account,
broker: broker,
metrics: metrics,
model: model,
notifier: notifier,
relier: relier,
user: user,
window: windowMock
});
return view.render();
});
it('displays the verification link expired screen', () => {
assert.ok(view.$('#fxa-verification-link-reused-header').length);
testErrorLogged(AuthErrors.toError('REUSED_PRIMARY_EMAIL_VERIFICATION_CODE'));
});
});
describe('all other server errors', function () {
beforeEach(function () {
verificationError = AuthErrors.toError('UNEXPECTED_ERROR');
@ -485,6 +583,12 @@ define(function (require, exports, module) {
});
describe('_getBrokerMethod', () => {
it('works for primary email', () => {
sinon.stub(view, 'isPrimaryEmail').callsFake(() => true);
assert.equal(view._getBrokerMethod(), 'afterCompletePrimaryEmail');
});
it('works for secondary emails', () => {
sinon.stub(view, 'isSecondaryEmail').callsFake(() => true);

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

@ -97,37 +97,26 @@ define(function (require, exports, module) {
});
});
describe('feature disabled', () => {
describe('for user', () => {
describe('feature gated in unverified session', () => {
beforeEach(() => {
sinon.stub(account, 'sessionVerificationStatus').callsFake(() => {
return Promise.resolve({sessionVerified: false});
});
});
describe('shows upgrade session', () => {
beforeEach(() => {
sinon.stub(account, 'recoveryEmails').callsFake(() => {
return p();
});
sinon.stub(account, 'recoveryEmailSecondaryEmailEnabled').callsFake(() => {
return p(false);
});
view = new View({
broker: broker,
emails: emails,
metrics: metrics,
notifier: notifier,
parentView: parentView,
translator: translator,
user: user,
window: windowMock
});
sinon.stub(view, 'remove').callsFake(() => {
return true;
});
return view.render();
emails = [{
email: 'primary@email.com',
isPrimary: true,
verified: true
}];
return initView();
});
it('should be disabled when feature is disabled for user', () => {
assert.equal(view.remove.callCount, 1);
it('has upgrade session panel', () => {
assert.ok(view.$('.email-address .address').length, 1);
assert.equal(view.$('.email-address .address').text(), email);
});
});
});
@ -135,11 +124,11 @@ define(function (require, exports, module) {
describe('feature enabled', () => {
beforeEach(() => {
sinon.stub(account, 'recoveryEmails').callsFake(() => {
return p(emails);
return Promise.resolve(emails);
});
sinon.stub(account, 'recoveryEmailSecondaryEmailEnabled').callsFake(() => {
return p(true);
sinon.stub(account, 'sessionVerificationStatus').callsFake(() => {
return Promise.resolve({ sessionVerified: true });
});
sinon.stub(account, 'recoveryEmailDestroy').callsFake(() => {

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

@ -10,7 +10,7 @@
"cocktail": "0.5.9",
"easteregg": "https://github.com/mozilla/fxa-easter-egg.git#ab20cd517cf8ae9feee115e48745189d28e13bc3",
"fxa-checkbox": "mozilla/fxa-checkbox#7f856afffd394a144f718e28e6fb79092d6ccddd",
"fxa-js-client": "https://github.com/mozilla/fxa-js-client.git#0.1.65",
"fxa-js-client": "https://github.com/mozilla/fxa-js-client.git#0.1.66",
"html5shiv": "3.7.2",
"jquery": "3.1.0",
"jquery-modal": "https://github.com/shane-tomlinson/jquery-modal.git#0576775d1b4590314b114386019f4c7421c77503",

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

@ -23,6 +23,7 @@ module.exports = function () {
'oauth/force_auth',
'oauth/signin',
'oauth/signup',
'primary_email_verified',
'report_signin',
'reset_password',
'reset_password_confirmed',
@ -54,6 +55,7 @@ module.exports = function () {
'sms/sent',
'sms/why',
'verify_email',
'verify_primary_email',
'verify_secondary_email'
].join('|'); // prepare for use in a RegExp

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

@ -90,7 +90,10 @@ define([], function () {
SET_PRIMARY_EMAIL_BUTTON: '.email-address .set-primary',
SUCCESS: '.success',
TOOLTIP: '.tooltip',
VERIFIED_LABEL: '.verified'
UNLOCK_BUTTON: '.emails .unlock-button',
UNLOCK_REFRESH_BUTTON: '.emails .refresh-verification-state',
UNLOCK_SEND_BUTTON: '.emails .send-verification-email',
VERIFIED_LABEL: '.verified',
},
ENTER_EMAIL: {
EMAIL: 'input[type=email]',

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

@ -49,8 +49,8 @@ define([
name: 'settings secondary emails',
beforeEach: function () {
email = TestHelpers.createEmail();
secondaryEmail = TestHelpers.createEmail();
email = TestHelpers.createEmail('sync{id}');
secondaryEmail = TestHelpers.createEmail('sync{id}');
client = FunctionalHelpers.getFxaClient();
return this.remote.then(clearBrowserState({ force: true }));
@ -60,6 +60,73 @@ define([
return this.remote.then(clearBrowserState());
},
'gated in unverified session open verification same tab': function () {
return this.remote
// when an account is created, the original session is verified
// re-login to destroy original session and created an unverified one
.then(createUser(email, PASSWORD, { preVerified: true }))
.then(openPage(SIGNIN_URL, selectors.SIGNIN.HEADER))
.then(fillOutSignIn(email, PASSWORD))
.then(testElementExists(selectors.EMAIL.UNLOCK_BUTTON))
// unlock panel
.then(click(selectors.EMAIL.UNLOCK_BUTTON))
.then(testElementExists(selectors.EMAIL.UNLOCK_REFRESH_BUTTON))
// send and open verification in same tab
.then(click(selectors.EMAIL.UNLOCK_SEND_BUTTON))
.then(openVerificationLinkInSameTab(email, 0))
// panel becomes verified and opens add secondary panel
.then(visibleByQSA(selectors.EMAIL.INPUT));
},
'gated in unverified session open verification new tab': function () {
return this.remote
// when an account is created, the original session is verified
// re-login to destroy original session and created an unverified one
.then(createUser(email, PASSWORD, { preVerified: true }))
.then(openPage(SIGNIN_URL, selectors.SIGNIN.HEADER))
.then(fillOutSignIn(email, PASSWORD))
.then(testElementExists(selectors.EMAIL.UNLOCK_BUTTON))
// unlock panel
.then(click(selectors.EMAIL.UNLOCK_BUTTON))
.then(testElementExists(selectors.EMAIL.UNLOCK_REFRESH_BUTTON))
// send and open verification in same tab
.then(click(selectors.EMAIL.UNLOCK_SEND_BUTTON))
.then(openVerificationLinkInNewTab(email, 0))
.then(switchToWindow(1))
// panel becomes verified and opens add secondary panel
.then(visibleByQSA(selectors.EMAIL.INPUT))
.then(closeCurrentWindow())
.then(switchToWindow(0))
.then(click(selectors.EMAIL.UNLOCK_REFRESH_BUTTON))
.then(visibleByQSA(selectors.EMAIL.INPUT));
},
'gated in unverified session open verification different browser': function () {
return this.remote
// when an account is created, the original session is verified
// re-login to destroy original session and created an unverified one
.then(createUser(email, PASSWORD, { preVerified: true }))
.then(openPage(SIGNIN_URL, selectors.SIGNIN.HEADER))
.then(fillOutSignIn(email, PASSWORD))
.then(testElementExists(selectors.EMAIL.UNLOCK_BUTTON))
// unlock panel
.then(click(selectors.EMAIL.UNLOCK_BUTTON))
.then(testElementExists(selectors.EMAIL.UNLOCK_REFRESH_BUTTON))
// send and open verification in same tab
.then(click(selectors.EMAIL.UNLOCK_SEND_BUTTON))
.then(openVerificationLinkInDifferentBrowser(email, 0))
.then(click(selectors.EMAIL.UNLOCK_REFRESH_BUTTON))
.then(visibleByQSA(selectors.EMAIL.INPUT));
},
'add and verify secondary email': function () {
return this.remote
// sign up via the UI, we need a verified session to use secondary email
@ -204,7 +271,6 @@ define([
.then(testElementExists(selectors.SETTINGS.HEADER));
},
'signin confirmation is sent to secondary emails': function () {
const PAGE_SIGNIN_DESKTOP = `${SIGNIN_URL}?context=fx_desktop_v3&service=sync&forceAboutAccounts=true`;
const SETTINGS_URL = `${config.fxaContentRoot}settings?context=fx_desktop_v3&service=sync&forceAboutAccounts=true`;