Skip to content
This repository was archived by the owner on Apr 3, 2019. It is now read-only.

Commit 8dd33fe

Browse files
vbudhramShane Tomlinson
authored andcommitted
feat(totp): add totp as an experiment and enable for mozilla/softvision (#6141) r=@shane-tomlinson
1 parent f9743c4 commit 8dd33fe

File tree

8 files changed

+205
-8
lines changed

8 files changed

+205
-8
lines changed

app/scripts/lib/experiments/grouping-rules/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ const experimentGroupingRules = [
1818
require('./send-sms-install-link'),
1919
require('./sentry'),
2020
require('./sessions'),
21-
require('./token-code')
21+
require('./token-code'),
22+
require('./totp'),
2223
].map(ExperimentGroupingRule => new ExperimentGroupingRule());
2324

2425
class ExperimentChoiceIndex {
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
'use strict';
6+
7+
const BaseGroupingRule = require('./base');
8+
const GROUPS = ['control', 'treatment'];
9+
const ENABLED_EMAIL_REGEX = /(.+@mozilla\.(com|org)$)|(.+@softvision\.(com|ro)$)/;
10+
11+
module.exports = class TotpGroupingRule extends BaseGroupingRule {
12+
constructor() {
13+
super();
14+
this.name = 'totp';
15+
this.ROLLOUT_RATE = 0.00;
16+
}
17+
18+
choose(subject) {
19+
if (! subject || ! subject.account || ! subject.uniqueUserId) {
20+
return false;
21+
}
22+
23+
// Is feature enabled explicitly? ex. from `?showTwoStepAuthentication=true` feature flag?
24+
if (subject.showTwoStepAuthentication) {
25+
return true;
26+
}
27+
28+
// Is this a Mozilla/Softvision based email?
29+
const email = subject.account.get('email');
30+
if (ENABLED_EMAIL_REGEX.test(email)) {
31+
return true;
32+
}
33+
34+
// Are they apart of rollout?
35+
if (this.bernoulliTrial(this.ROLLOUT_RATE, subject.uniqueUserId)) {
36+
return this.uniformChoice(GROUPS, subject.uniqueUserId);
37+
}
38+
39+
// Otherwise don't show panel
40+
return false;
41+
}
42+
};
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
/**
6+
* An TotpExperimentMixin factory.
7+
*
8+
* @mixin TotpExperimentMixin
9+
*/
10+
'use strict';
11+
12+
const ExperimentMixin = require('./experiment-mixin');
13+
const EXPERIMENT_NAME = 'totp';
14+
15+
/**
16+
* Creates the mixin
17+
*
18+
* @returns {Object} mixin
19+
*/
20+
module.exports = {
21+
dependsOn: [ExperimentMixin],
22+
23+
beforeRender() {
24+
if (this.isInTotpExperiment()) {
25+
const experimentGroup = this.getTotpExperimentGroup();
26+
this.createExperiment(EXPERIMENT_NAME, experimentGroup);
27+
}
28+
},
29+
30+
/**
31+
* Get TOTP experiment group
32+
*
33+
* @returns {String}
34+
*/
35+
getTotpExperimentGroup() {
36+
return this.getExperimentGroup(EXPERIMENT_NAME, this._getTotpExperimentSubject());
37+
},
38+
39+
40+
/**
41+
* Is the user in the TOTP experiment?
42+
*
43+
* @returns {Boolean}
44+
*/
45+
isInTotpExperiment() {
46+
return this.isInExperiment(EXPERIMENT_NAME, this._getTotpExperimentSubject());
47+
},
48+
49+
/**
50+
* Get the TOTP experiment choice subject
51+
*
52+
* @returns {Object}
53+
* @private
54+
*/
55+
_getTotpExperimentSubject() {
56+
const subject = {
57+
account: this.getSignedInAccount(),
58+
showTwoStepAuthentication: this.broker.getCapability('showTwoStepAuthentication'),
59+
};
60+
return subject;
61+
}
62+
};

app/scripts/views/settings/two_step_authentication.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const UpgradeSessionMixin = require('../mixins/upgrade-session-mixin');
1515
const Template = require('templates/settings/two_step_authentication.mustache');
1616
const preventDefaultThen = require('../base').preventDefaultThen;
1717
const showProgressIndicator = require('../decorators/progress_indicator');
18+
const TotpExperimentMixin = require('../mixins/totp-experiment-mixin');
1819

1920
var t = BaseView.t;
2021

@@ -66,6 +67,11 @@ const View = FormView.extend({
6667
if (this.broker.hasCapability('showTwoStepAuthentication')) {
6768
return true;
6869
}
70+
71+
if (this.isInTotpExperiment()) {
72+
return true;
73+
}
74+
6975
return false;
7076
},
7177

@@ -175,7 +181,8 @@ Cocktail.mixin(
175181
}),
176182
AvatarMixin,
177183
SettingsPanelMixin,
178-
FloatingPlaceholderMixin
184+
FloatingPlaceholderMixin,
185+
TotpExperimentMixin
179186
);
180187

181188
module.exports = View;

app/tests/spec/lib/experiments/grouping-rules/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ define(function (require, exports, module) {
1212

1313
describe('lib/experiments/grouping-rules/index', () => {
1414
it('EXPERIMENT_NAMES is exported', () => {
15-
assert.lengthOf(ExperimentGroupingRules.EXPERIMENT_NAMES, 8);
15+
assert.lengthOf(ExperimentGroupingRules.EXPERIMENT_NAMES, 9);
1616
});
1717

1818
describe('choose', () => {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
'use strict';
6+
7+
const {assert} = require('chai');
8+
const Account = require('models/account');
9+
const Experiment = require('lib/experiments/grouping-rules/totp');
10+
const sinon = require('sinon');
11+
12+
describe('lib/experiments/grouping-rules/totp', () => {
13+
describe('choose', () => {
14+
let account;
15+
let experiment;
16+
let subject;
17+
18+
beforeEach(() => {
19+
account = new Account();
20+
experiment = new Experiment();
21+
subject = {
22+
account: account,
23+
experimentGroupingRules: {},
24+
showTwoStepAuthentication: false,
25+
uniqueUserId: 'user-id'
26+
};
27+
});
28+
29+
it('returns true experiment if broker has capability', () => {
30+
subject.showTwoStepAuthentication = true;
31+
assert.equal(experiment.choose(subject), true);
32+
});
33+
34+
['a@mozilla.org', 'a@softvision.com', 'a@softvision.ro', 'a@softvision.com'].forEach((email) => {
35+
it(`returns true experiment for ${email} email`, () => {
36+
subject.account.set('email', email);
37+
assert.equal(experiment.choose(subject), true);
38+
});
39+
});
40+
41+
it('delegates to uniformChoice if in rollout', () => {
42+
experiment.ROLLOUT_RATE = 1.0;
43+
sinon.stub(experiment, 'uniformChoice').callsFake(() => 'control');
44+
experiment.choose(subject);
45+
assert.isTrue(experiment.uniformChoice.calledOnce);
46+
assert.isTrue(experiment.uniformChoice.calledWith(['control', 'treatment'], 'user-id'));
47+
});
48+
49+
it('returns false if not in rollout', () => {
50+
experiment.ROLLOUT_RATE = 0.0;
51+
assert.equal(experiment.choose(subject), false);
52+
});
53+
});
54+
});

app/tests/spec/views/settings/two_step_authentication.js

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,40 @@
44

55
const $ = require('jquery');
66
const assert = require('chai').assert;
7+
const Broker = require('models/auth_brokers/base');
78
const Metrics = require('lib/metrics');
89
const Notifier = require('lib/channels/notifier');
10+
const SentryMetrics = require('lib/sentry');
911
const sinon = require('sinon');
1012
const TestHelpers = require('../../../lib/helpers');
1113
const User = require('models/user');
1214
const View = require('views/settings/two_step_authentication');
1315

1416
describe('views/settings/two_step_authentication', () => {
1517
let account;
18+
let broker;
1619
let email;
1720
let metrics;
1821
let notifier;
1922
let featureEnabled;
2023
let hasToken;
24+
let inTotpExperiment;
25+
let sentryMetrics;
2126
let validCode;
2227
const UID = '123';
2328
let user;
2429
let view;
2530

2631
function initView() {
2732
view = new View({
33+
broker: broker,
2834
metrics: metrics,
2935
notifier: notifier,
3036
user: user
3137
});
3238

33-
sinon.stub(view, '_isPanelEnabled').callsFake(() => featureEnabled);
3439
sinon.stub(view, 'setupSessionGateIfRequired').callsFake(() => Promise.resolve(featureEnabled));
40+
sinon.stub(view, 'isInTotpExperiment').callsFake(() => inTotpExperiment);
3541
sinon.stub(view, 'getSignedInAccount').callsFake(() => account);
3642
sinon.spy(view, 'remove');
3743

@@ -40,9 +46,11 @@ describe('views/settings/two_step_authentication', () => {
4046
}
4147

4248
beforeEach(() => {
49+
broker = new Broker();
4350
email = TestHelpers.createEmail();
4451
notifier = new Notifier();
45-
metrics = new Metrics({notifier});
52+
sentryMetrics = new SentryMetrics();
53+
metrics = new Metrics({notifier, sentryMetrics});
4654
user = new User();
4755
account = user.initAccount({
4856
email: email,
@@ -70,6 +78,7 @@ describe('views/settings/two_step_authentication', () => {
7078

7179
featureEnabled = true;
7280
hasToken = true;
81+
inTotpExperiment = true;
7382
});
7483

7584
afterEach(() => {
@@ -80,21 +89,42 @@ describe('views/settings/two_step_authentication', () => {
8089

8190
describe('feature disabled', () => {
8291
beforeEach(() => {
83-
featureEnabled = false;
92+
inTotpExperiment = false;
8493
return initView();
8594
});
8695

87-
it('should remove panel if `showTwoStepAuthentication` query is not specified', () => {
96+
it('should not display panel if `isInTotpExperiment` is false', () => {
8897
assert.equal(view.remove.callCount, 1);
8998
});
9099
});
91100

92101
describe('feature enabled', () => {
93102
beforeEach(() => {
94-
featureEnabled = true;
95103
return initView();
96104
});
97105

106+
describe('should show panel when broker capability `showTwoStepAuthentication` is true', () => {
107+
beforeEach(() => {
108+
view.broker.setCapability('showTwoStepAuthentication', true);
109+
return initView();
110+
});
111+
112+
it('should show panel when broker has capability', () => {
113+
assert.equal(view.remove.callCount, 0);
114+
});
115+
});
116+
117+
describe('should show panel when `inTotpExperiment` is true', () => {
118+
beforeEach(() => {
119+
inTotpExperiment = true;
120+
return initView();
121+
});
122+
123+
it('should show panel when in experiment', () => {
124+
assert.equal(view.remove.callCount, 0);
125+
});
126+
});
127+
98128
describe('should show token status', () => {
99129
beforeEach(() => {
100130
hasToken = true;

app/tests/test_start.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ require('./spec/lib/experiments/grouping-rules/send-sms-install-link');
4747
require('./spec/lib/experiments/grouping-rules/sentry');
4848
require('./spec/lib/experiments/grouping-rules/sessions');
4949
require('./spec/lib/experiments/grouping-rules/token-code');
50+
require('./spec/lib/experiments/grouping-rules/totp');
5051
require('./spec/lib/fxa-client');
5152
require('./spec/lib/height-observer');
5253
require('./spec/lib/image-loader');

0 commit comments

Comments
 (0)