pre-verify tokens work end to end!
* Add functional tests for the preverified flow. To test using 123done, apply the following diff to fxa-auth-server/ diff --git a/config/dev.json b/config/dev.json index 7919bd9..f08a03a 100644 --- a/config/dev.json +++ b/config/dev.json @@ -19,5 +19,6 @@ "bounces": { "region": "us-east-1" }, - "customsUrl": "none" + "customsUrl": "none", + "trustedJKUs": ["http://127.0.0.1:8080/.well-known/public-keys"] } Then visit: http://127.0.0.1:8080/api/preverify-signup?email=<your_email>
This commit is contained in:
Родитель
d30dd6d3f4
Коммит
acbb5523f6
|
@ -89,9 +89,13 @@ function (P, jwcrypto, FxaClient) {
|
|||
});
|
||||
}
|
||||
|
||||
bundle.generate = bundle;
|
||||
function Assertion() {
|
||||
}
|
||||
|
||||
return bundle;
|
||||
Assertion.prototype = {
|
||||
generate: bundle
|
||||
};
|
||||
|
||||
return Assertion;
|
||||
});
|
||||
|
||||
|
|
|
@ -98,12 +98,14 @@ define([
|
|||
}
|
||||
|
||||
return {
|
||||
setupOAuth: function (params) {
|
||||
setupOAuth: function (params, deps) {
|
||||
deps = deps || {};
|
||||
|
||||
if (! this._configLoader) {
|
||||
this._configLoader = new ConfigLoader();
|
||||
}
|
||||
|
||||
this._oAuthClient = new OAuthClient();
|
||||
this._oAuthClient = deps.oAuthClient || new OAuthClient();
|
||||
|
||||
if (! params) {
|
||||
// params listed in:
|
||||
|
@ -120,7 +122,7 @@ define([
|
|||
|
||||
// assertion library to use to generate assertions
|
||||
// can be substituted for testing
|
||||
this.assertionLibrary = Assertion;
|
||||
this.assertionLibrary = deps.assertionLibrary || new Assertion();
|
||||
|
||||
Session.set('service', this.service);
|
||||
// A hint that allows Session to determine whether the user
|
||||
|
|
|
@ -16,11 +16,16 @@ function (_, p, BaseView, SignUpView, ServiceMixin) {
|
|||
className: 'sign-up oauth-sign-up',
|
||||
|
||||
initialize: function (options) {
|
||||
options = options || {};
|
||||
|
||||
/* jshint camelcase: false */
|
||||
SignUpView.prototype.initialize.call(this, options);
|
||||
|
||||
// Set up OAuth so we can retrieve the pretty service name
|
||||
this.setupOAuth();
|
||||
this.setupOAuth(null, {
|
||||
assertionLibrary: options.assertionLibrary,
|
||||
oAuthClient: options.oAuthClient
|
||||
});
|
||||
},
|
||||
|
||||
beforeRender: function() {
|
||||
|
@ -40,7 +45,13 @@ function (_, p, BaseView, SignUpView, ServiceMixin) {
|
|||
// Store oauth state for when/if the oauth flow completes
|
||||
// in this browser
|
||||
this.persistOAuthParams();
|
||||
return SignUpView.prototype.onSignUpSuccess.call(this, accountData);
|
||||
if (accountData.verified) {
|
||||
// the account is verified using the pre-verify flow. Send the user
|
||||
// back to the RP without further interaction.
|
||||
return this.finishOAuthFlow();
|
||||
} else {
|
||||
return SignUpView.prototype.onSignUpSuccess.call(this, accountData);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -49,7 +49,8 @@ define([
|
|||
function removeFxaClientSpy(fxaClient) {
|
||||
// return the client to its original state.
|
||||
for (var key in fxaClient) {
|
||||
if (typeof fxaClient[key] === 'function') {
|
||||
if (typeof fxaClient[key] === 'function' &&
|
||||
typeof fxaClient[key].restore === 'function') {
|
||||
fxaClient[key].restore();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,10 +28,12 @@ function (chai, $, P,
|
|||
var email;
|
||||
var password = 'password';
|
||||
var client;
|
||||
var assertionLibrary;
|
||||
|
||||
describe('lib/assertion', function () {
|
||||
beforeEach(function () {
|
||||
Session.clear();
|
||||
assertionLibrary = new Assertion();
|
||||
client = new FxaClientWrapper();
|
||||
email = ' testuser' + Math.random() + '@testuser.com ';
|
||||
return client.signUp(email, password, { preVerified: true });
|
||||
|
@ -44,7 +46,7 @@ function (chai, $, P,
|
|||
describe('validate', function () {
|
||||
it('generates a valid assertion', function () {
|
||||
var assertion;
|
||||
return Assertion.generate(AUDIENCE)
|
||||
return assertionLibrary.generate(AUDIENCE)
|
||||
.then(function(ass) {
|
||||
assertion = ass;
|
||||
assert.isNotNull(ass, 'Assertion is not null');
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
define([
|
||||
'chai',
|
||||
'jquery',
|
||||
'sinon',
|
||||
'lib/promise',
|
||||
'../../mocks/channel',
|
||||
'../../lib/helpers',
|
||||
|
@ -16,7 +17,7 @@ define([
|
|||
// FxaClientWrapper is the object that is used in
|
||||
// fxa-content-server views. It wraps FxaClient to
|
||||
// take care of some app-specific housekeeping.
|
||||
function (chai, $, p, ChannelMock, testHelpers,
|
||||
function (chai, $, sinon, p, ChannelMock, testHelpers,
|
||||
Session, FxaClientWrapper, AuthErrors, Constants) {
|
||||
'use strict';
|
||||
|
||||
|
@ -168,6 +169,18 @@ function (chai, $, p, ChannelMock, testHelpers,
|
|||
it('signUp a preverified user using preVerifyToken', function () {
|
||||
var password = 'password';
|
||||
var preVerifyToken = 'somebiglongtoken';
|
||||
|
||||
// we are going to take over from here.
|
||||
testHelpers.removeFxaClientSpy(realClient);
|
||||
sinon.stub(realClient, 'signUp', function () {
|
||||
return true;
|
||||
});
|
||||
sinon.stub(realClient, 'signIn', function () {
|
||||
return {
|
||||
sessionToken: 'asessiontoken'
|
||||
};
|
||||
});
|
||||
|
||||
return client.signUp(email, password, {
|
||||
preVerifyToken: preVerifyToken
|
||||
})
|
||||
|
|
|
@ -7,25 +7,58 @@
|
|||
define([
|
||||
'chai',
|
||||
'jquery',
|
||||
'sinon',
|
||||
'views/oauth_sign_up',
|
||||
'lib/promise',
|
||||
'lib/session',
|
||||
'lib/fxa-client',
|
||||
'lib/metrics',
|
||||
'lib/auth-errors',
|
||||
'lib/oauth-client',
|
||||
'lib/assertion',
|
||||
'models/reliers/relier',
|
||||
'../../mocks/window',
|
||||
'../../mocks/router',
|
||||
'../../mocks/oauth_servers',
|
||||
'../../lib/helpers'
|
||||
],
|
||||
function (chai, $, View, Session, FxaClient, WindowMock, RouterMock, OAuthServersMock, TestHelpers) {
|
||||
function (chai, $, sinon, View, p, Session, FxaClient, Metrics, AuthErrors,
|
||||
OAuthClient, Assertion, Relier, WindowMock, RouterMock, TestHelpers) {
|
||||
var assert = chai.assert;
|
||||
|
||||
describe('views/oauth_sign_up', function () {
|
||||
var view, email, router, windowMock, CLIENT_ID, STATE, SCOPE, CLIENT_NAME, BASE_REDIRECT_URL, fxaClient, oAuthServersMock;
|
||||
function fillOutSignUp (email, password, opts) {
|
||||
opts = opts || {};
|
||||
var context = opts.context || window;
|
||||
var year = opts.year || '1960';
|
||||
|
||||
CLIENT_ID = 'dcdb5ae7add825d2';
|
||||
STATE = '123';
|
||||
SCOPE = 'profile:email';
|
||||
CLIENT_NAME = '123Done';
|
||||
BASE_REDIRECT_URL = 'http://127.0.0.1:8080/api/oauth';
|
||||
context.$('[type=email]').val(email);
|
||||
context.$('[type=password]').val(password);
|
||||
|
||||
if (!opts.ignoreYear) {
|
||||
$('#fxa-age-year').val(year);
|
||||
}
|
||||
|
||||
if (context.enableSubmitIfValid) {
|
||||
context.enableSubmitIfValid();
|
||||
}
|
||||
}
|
||||
|
||||
var CLIENT_ID = 'dcdb5ae7add825d2';
|
||||
var STATE = '123';
|
||||
var SCOPE = 'profile:email';
|
||||
var CLIENT_NAME = '123Done';
|
||||
var BASE_REDIRECT_URL = 'http://127.0.0.1:8080/api/oauth';
|
||||
|
||||
describe('views/oauth_sign_up', function () {
|
||||
var nowYear = (new Date()).getFullYear();
|
||||
var view;
|
||||
var router;
|
||||
var email;
|
||||
var metrics;
|
||||
var windowMock;
|
||||
var fxaClient;
|
||||
var oAuthClient;
|
||||
var assertionLibrary;
|
||||
var relier;
|
||||
|
||||
beforeEach(function () {
|
||||
Session.clear();
|
||||
|
@ -34,15 +67,32 @@ function (chai, $, View, Session, FxaClient, WindowMock, RouterMock, OAuthServer
|
|||
windowMock = new WindowMock();
|
||||
windowMock.location.search = '?client_id=' + CLIENT_ID + '&state=' + STATE + '&scope=' + SCOPE + '&redirect_uri=' + encodeURIComponent(BASE_REDIRECT_URL);
|
||||
|
||||
oAuthServersMock = new OAuthServersMock();
|
||||
|
||||
metrics = new Metrics();
|
||||
relier = new Relier();
|
||||
|
||||
oAuthClient = new OAuthClient();
|
||||
sinon.stub(oAuthClient, 'getClientInfo', function () {
|
||||
return p({
|
||||
name: '123Done',
|
||||
//jshint camelcase: false
|
||||
redirect_uri: BASE_REDIRECT_URL
|
||||
});
|
||||
});
|
||||
|
||||
assertionLibrary = new Assertion();
|
||||
fxaClient = new FxaClient();
|
||||
|
||||
view = new View({
|
||||
router: router,
|
||||
metrics: metrics,
|
||||
window: windowMock,
|
||||
fxaClient: fxaClient
|
||||
fxaClient: fxaClient,
|
||||
relier: relier,
|
||||
assertionLibrary: assertionLibrary,
|
||||
oAuthClient: oAuthClient
|
||||
});
|
||||
|
||||
return view.render()
|
||||
.then(function () {
|
||||
$('#container').html(view.el);
|
||||
|
@ -53,7 +103,6 @@ function (chai, $, View, Session, FxaClient, WindowMock, RouterMock, OAuthServer
|
|||
Session.clear();
|
||||
view.remove();
|
||||
view.destroy();
|
||||
oAuthServersMock.destroy();
|
||||
});
|
||||
|
||||
describe('render', function () {
|
||||
|
@ -67,27 +116,78 @@ function (chai, $, View, Session, FxaClient, WindowMock, RouterMock, OAuthServer
|
|||
});
|
||||
});
|
||||
|
||||
describe('submit', function () {
|
||||
describe('submit without a preVerifyToken', function () {
|
||||
it('sets up the user\'s ouath session on success', function () {
|
||||
var password = 'password';
|
||||
fillOutSignUp(email, 'password', { year: nowYear - 14, context: view });
|
||||
|
||||
// the screen is rendered, we can take over from here.
|
||||
oAuthServersMock.destroy();
|
||||
return view.fxaClient.signUp(email, password)
|
||||
.then(function () {
|
||||
$('.email').val(email);
|
||||
$('[type=password]').val(password);
|
||||
$('#fxa-age-year').val('1990');
|
||||
return view.submit();
|
||||
})
|
||||
sinon.stub(fxaClient, 'signUp', function () {
|
||||
return p({
|
||||
sessionToken: 'asessiontoken',
|
||||
verified: false
|
||||
});
|
||||
});
|
||||
|
||||
return view.submit()
|
||||
.then(function () {
|
||||
assert.equal(Session.oauth.state, STATE);
|
||||
assert.equal(Session.service, CLIENT_ID);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('submit with a preVerifyToken', function () {
|
||||
beforeEach(function () {
|
||||
relier.set('preVerifyToken', 'preverifytoken');
|
||||
});
|
||||
|
||||
it('redirects to the rp if pre-verification is successful', function () {
|
||||
sinon.stub(fxaClient, 'signUp', function () {
|
||||
return p({
|
||||
sessionToken: 'asessiontoken',
|
||||
verified: true
|
||||
});
|
||||
});
|
||||
|
||||
sinon.stub(oAuthClient, 'getCode', function () {
|
||||
return {
|
||||
redirect: BASE_REDIRECT_URL + '?state=fakestate&code=faketcode'
|
||||
};
|
||||
});
|
||||
|
||||
sinon.stub(assertionLibrary, 'generate', function () {
|
||||
return 'fakeassertion';
|
||||
});
|
||||
|
||||
fillOutSignUp(email, 'password', { year: nowYear - 14, context: view });
|
||||
return view.submit()
|
||||
.then(function () {
|
||||
assert.include(windowMock.location.href, BASE_REDIRECT_URL);
|
||||
});
|
||||
});
|
||||
|
||||
it('redirects to /confirm if pre-verification is not successful', function () {
|
||||
sinon.stub(fxaClient, 'signUp', function (email, password, options) {
|
||||
// force the preVerifyToken to be invalid
|
||||
if (options.preVerifyToken) {
|
||||
return p().then(function () {
|
||||
throw AuthErrors.toError('INVALID_VERIFICATION_CODE');
|
||||
});
|
||||
} else {
|
||||
return p({
|
||||
sessionToken: 'sessiontoken',
|
||||
verified: false
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
fillOutSignUp(email, 'password', { year: nowYear - 14, context: view });
|
||||
return view.submit()
|
||||
.then(function () {
|
||||
assert.equal(router.page, 'confirm');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ define([
|
|||
'chai',
|
||||
'underscore',
|
||||
'jquery',
|
||||
'sinon',
|
||||
'lib/promise',
|
||||
'views/sign_up',
|
||||
'lib/session',
|
||||
|
@ -18,13 +17,12 @@ define([
|
|||
'lib/fxa-client',
|
||||
'lib/translator',
|
||||
'lib/service-name',
|
||||
'models/reliers/relier',
|
||||
'../../mocks/router',
|
||||
'../../mocks/window',
|
||||
'../../lib/helpers'
|
||||
],
|
||||
function (chai, _, $, sinon, p, View, Session, AuthErrors, Metrics, FxaClient, Translator, ServiceName,
|
||||
Relier, RouterMock, WindowMock, TestHelpers) {
|
||||
function (chai, _, $, p, View, Session, AuthErrors, Metrics, FxaClient, Translator, ServiceName,
|
||||
RouterMock, WindowMock, TestHelpers) {
|
||||
var assert = chai.assert;
|
||||
var wrapAssertion = TestHelpers.wrapAssertion;
|
||||
var translator = new Translator('en-US', ['en-US']);
|
||||
|
@ -429,94 +427,6 @@ function (chai, _, $, sinon, p, View, Session, AuthErrors, Metrics, FxaClient, T
|
|||
});
|
||||
});
|
||||
|
||||
describe('views/sign_up with a pre-verified user', function () {
|
||||
var view, router, email, metrics, windowMock, fxaClient, fakeServer, relier;
|
||||
|
||||
beforeEach(function () {
|
||||
Session.clear();
|
||||
|
||||
email = TestHelpers.createEmail();
|
||||
document.cookie = 'tooyoung=1; expires=Thu, 01-Jan-1970 00:00:01 GMT';
|
||||
router = new RouterMock();
|
||||
windowMock = new WindowMock();
|
||||
metrics = new Metrics();
|
||||
fxaClient = new FxaClient();
|
||||
relier = new Relier();
|
||||
relier.set('preVerifyToken', 'bigscarytoken');
|
||||
|
||||
fakeServer = sinon.fakeServer.create();
|
||||
fakeServer.autoRespond = true;
|
||||
fakeServer.respondWith('GET', '/config',
|
||||
[200, { 'Content-Type': 'application/json' }, JSON.stringify({
|
||||
fxaccountUrl: 'http://127.0.0.1:9000/v1'
|
||||
})]);
|
||||
|
||||
view = new View({
|
||||
router: router,
|
||||
metrics: metrics,
|
||||
window: windowMock,
|
||||
fxaClient: fxaClient,
|
||||
relier: relier
|
||||
});
|
||||
|
||||
return view.render()
|
||||
.then(function () {
|
||||
$('#container').append(view.el);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
metrics.destroy();
|
||||
|
||||
view.remove();
|
||||
view.destroy();
|
||||
document.cookie = 'tooyoung=1; expires=Thu, 01-Jan-1970 00:00:01 GMT';
|
||||
|
||||
fakeServer.restore();
|
||||
fakeServer = view = router = metrics = null;
|
||||
});
|
||||
|
||||
describe('submit', function () {
|
||||
it('redirects to /signup_complete if pre-verification is successful', function () {
|
||||
fakeServer.respondWith('POST', 'http://127.0.0.1:9000/v1/account/create?keys=true',
|
||||
[200, { 'Content-Type': 'application/json' }, JSON.stringify({})]);
|
||||
fakeServer.respondWith('POST', 'http://127.0.0.1:9000/v1/account/login?keys=true',
|
||||
[200, { 'Content-Type': 'application/json' }, JSON.stringify({ verified: true })]);
|
||||
|
||||
var nowYear = (new Date()).getFullYear();
|
||||
fillOutSignUp(email, 'password', { year: nowYear - 14, context: view });
|
||||
return view.submit()
|
||||
.then(function () {
|
||||
assert.equal(router.page, 'signup_complete');
|
||||
});
|
||||
});
|
||||
|
||||
it('redirects to /confirm if pre-verification is not successful', function () {
|
||||
fakeServer.respondWith(/\/account\/create\?keys=true/, function (xhr) {
|
||||
// force the preVerifyToken to be invalid.
|
||||
if (xhr.requestBody.preVerifyToken) {
|
||||
xhr.respond(500, { 'Content-Type': 'application/json' }, JSON.stringify({
|
||||
errno: AuthErrors.toCode('INVALID_VERIFICATION_CODE')
|
||||
}));
|
||||
} else {
|
||||
xhr.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({}));
|
||||
}
|
||||
});
|
||||
|
||||
fakeServer.respondWith('POST', 'http://127.0.0.1:9000/v1/account/login?keys=true',
|
||||
[200, { 'Content-Type': 'application/json' }, JSON.stringify({ verfied: false })]);
|
||||
|
||||
var nowYear = (new Date()).getFullYear();
|
||||
fillOutSignUp(email, 'password', { year: nowYear - 14, context: view });
|
||||
return view.submit()
|
||||
.then(function () {
|
||||
fakeServer.restore();
|
||||
assert.equal(router.page, 'confirm');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"blanket": "1.1.5",
|
||||
"chai": "1.8.1",
|
||||
"fxa-content-server-l10n": "https://github.com/mozilla/fxa-content-server-l10n.git",
|
||||
"fxa-js-client": "https://github.com/mozilla/fxa-js-client.git#0.1.23",
|
||||
"fxa-js-client": "https://github.com/mozilla/fxa-js-client.git#0.1.24",
|
||||
"html5shiv": "3.7.2",
|
||||
"jquery": "1.11.1",
|
||||
"mocha": "1.18.2",
|
||||
|
|
|
@ -39,7 +39,6 @@ module.exports = function (config, templates, i18n) {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
return function (app) {
|
||||
// handle password reset links
|
||||
app.get('/v1/complete_reset_password', function (req, res) {
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
/* 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',
|
||||
'intern!object',
|
||||
'intern/chai!assert',
|
||||
'require',
|
||||
'intern/node_modules/dojo/node!xmlhttprequest',
|
||||
'app/bower_components/fxa-js-client/fxa-client',
|
||||
'tests/lib/restmail',
|
||||
'tests/lib/helpers',
|
||||
'tests/functional/lib/helpers'
|
||||
], function (intern, registerSuite, assert, require, nodeXMLHttpRequest, FxaClient, restmail, TestHelpers, FunctionalHelpers) {
|
||||
'use strict';
|
||||
|
||||
var config = intern.config;
|
||||
var CONTENT_SERVER = config.fxaContentRoot;
|
||||
var OAUTH_APP = config.fxaOauthApp;
|
||||
var TOO_YOUNG_YEAR = new Date().getFullYear() - 13;
|
||||
|
||||
var PASSWORD = 'password';
|
||||
var user;
|
||||
var email;
|
||||
|
||||
registerSuite({
|
||||
name: 'preverified oauth sign up',
|
||||
|
||||
setup: function () {
|
||||
email = TestHelpers.createEmail();
|
||||
user = TestHelpers.emailToUser(email);
|
||||
},
|
||||
|
||||
beforeEach: function () {
|
||||
var self = this;
|
||||
// clear localStorage to avoid polluting other tests.
|
||||
// Without the clear, /signup tests fail because of the info stored
|
||||
// in prefillEmail
|
||||
return self.get('remote')
|
||||
// always go to the content server so the browser state is cleared
|
||||
.get(require.toUrl(CONTENT_SERVER))
|
||||
.setFindTimeout(intern.config.pageLoadTimeout)
|
||||
.then(function () {
|
||||
return FunctionalHelpers.clearBrowserState(self);
|
||||
});
|
||||
},
|
||||
|
||||
'preverified sign up': function () {
|
||||
var self = this;
|
||||
|
||||
return TestHelpers.getEmailPreverifyToken(email)
|
||||
.then(function (token) {
|
||||
var SIGNUP_URL = OAUTH_APP + 'api/preverified-signup?' +
|
||||
'email=' + encodeURIComponent(email);
|
||||
|
||||
return self.get('remote')
|
||||
.get(require.toUrl(SIGNUP_URL))
|
||||
.setFindTimeout(intern.config.pageLoadTimeout)
|
||||
|
||||
.findByCssSelector('#fxa-signup-header')
|
||||
.end()
|
||||
|
||||
.findByCssSelector('form input.password')
|
||||
.click()
|
||||
.type(PASSWORD)
|
||||
.end()
|
||||
|
||||
.findByCssSelector('#fxa-age-year')
|
||||
.click()
|
||||
.end()
|
||||
|
||||
.findById('fxa-' + (TOO_YOUNG_YEAR - 1))
|
||||
.pressMouseButton()
|
||||
.releaseMouseButton()
|
||||
.click()
|
||||
.end()
|
||||
|
||||
.findByCssSelector('button[type="submit"]')
|
||||
.click()
|
||||
.end()
|
||||
|
||||
// user is pre-verified and sent directly to the RP.
|
||||
.findByCssSelector('#loggedin')
|
||||
.getVisibleText()
|
||||
.then(function (text) {
|
||||
// user is signed in as pre-verified email
|
||||
assert.equal(text, email);
|
||||
})
|
||||
.end();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
});
|
|
@ -6,7 +6,10 @@ define([
|
|||
'./functional/oauth_sign_in',
|
||||
'./functional/oauth_sign_up',
|
||||
'./functional/oauth_reset_password',
|
||||
'./functional/oauth_webchannel'
|
||||
'./functional/oauth_webchannel'/*,
|
||||
TODO - enable this whenever 123done and the oauth-server are patched to handle
|
||||
preverified emails.
|
||||
'./functional/oauth_preverified_sign_up'*/
|
||||
], function () {
|
||||
'use strict';
|
||||
});
|
||||
|
|
Загрузка…
Ссылка в новой задаче