diff --git a/app/scripts/lib/assertion.js b/app/scripts/lib/assertion.js index c8e722438..f8e682c35 100644 --- a/app/scripts/lib/assertion.js +++ b/app/scripts/lib/assertion.js @@ -89,9 +89,13 @@ function (P, jwcrypto, FxaClient) { }); } - bundle.generate = bundle; + function Assertion() { + } - return bundle; + Assertion.prototype = { + generate: bundle + }; + return Assertion; }); diff --git a/app/scripts/views/mixins/service-mixin.js b/app/scripts/views/mixins/service-mixin.js index ce9eb0c01..9e168132d 100644 --- a/app/scripts/views/mixins/service-mixin.js +++ b/app/scripts/views/mixins/service-mixin.js @@ -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 diff --git a/app/scripts/views/oauth_sign_up.js b/app/scripts/views/oauth_sign_up.js index 768333981..bd9e6ac90 100644 --- a/app/scripts/views/oauth_sign_up.js +++ b/app/scripts/views/oauth_sign_up.js @@ -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); + } } }); diff --git a/app/tests/lib/helpers.js b/app/tests/lib/helpers.js index 94b30dbeb..bf8ce9689 100644 --- a/app/tests/lib/helpers.js +++ b/app/tests/lib/helpers.js @@ -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(); } } diff --git a/app/tests/spec/lib/assertion.js b/app/tests/spec/lib/assertion.js index 2a2dbc395..05f3120cf 100644 --- a/app/tests/spec/lib/assertion.js +++ b/app/tests/spec/lib/assertion.js @@ -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'); diff --git a/app/tests/spec/lib/fxa-client.js b/app/tests/spec/lib/fxa-client.js index f6188c266..bc624bc7d 100644 --- a/app/tests/spec/lib/fxa-client.js +++ b/app/tests/spec/lib/fxa-client.js @@ -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 }) diff --git a/app/tests/spec/views/oauth_sign_up.js b/app/tests/spec/views/oauth_sign_up.js index 76336ff1e..9e1894b0b 100644 --- a/app/tests/spec/views/oauth_sign_up.js +++ b/app/tests/spec/views/oauth_sign_up.js @@ -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'); + }); + }); + }); + }); }); diff --git a/app/tests/spec/views/sign_up.js b/app/tests/spec/views/sign_up.js index b1397c896..e496b37dc 100644 --- a/app/tests/spec/views/sign_up.js +++ b/app/tests/spec/views/sign_up.js @@ -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'); - }); - }); - }); - - }); }); diff --git a/bower.json b/bower.json index 7e63345d1..77d22f5f7 100644 --- a/bower.json +++ b/bower.json @@ -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", diff --git a/server/lib/routes.js b/server/lib/routes.js index 818a3db46..7de16eb30 100644 --- a/server/lib/routes.js +++ b/server/lib/routes.js @@ -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) { diff --git a/tests/functional/oauth_preverified_sign_up.js b/tests/functional/oauth_preverified_sign_up.js new file mode 100644 index 000000000..b1eb5a4b1 --- /dev/null +++ b/tests/functional/oauth_preverified_sign_up.js @@ -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(); + }); + } + }); + +}); diff --git a/tests/functional_oauth.js b/tests/functional_oauth.js index 1faeedd17..2c8938906 100644 --- a/tests/functional_oauth.js +++ b/tests/functional_oauth.js @@ -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'; });