From 90b1c4a23c4bfeb858c498975a7e866b2dfd72d7 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Tue, 23 Jul 2019 11:06:45 -0500 Subject: [PATCH] Merge pull request #1995 from overleaf/ta-conversion-metrics-query-fix Pass conversion metrics query params to v1 GitOrigin-RevId: 0f6cb0118ac92896590fbe26b24a6b2a723e1f2c --- .../launchpad/app/src/LaunchpadController.js | 278 +++++ .../launchpad/app/src/LaunchpadRouter.js | 53 + .../modules/launchpad/app/views/launchpad.pug | 267 +++++ services/web/modules/launchpad/index.js | 9 + .../main/controllers/LaunchpadController.js | 155 +++ .../launchpad/public/src/main/index.js | 3 + .../test/unit/src/LaunchpadControllerTests.js | 1005 +++++++++++++++++ 7 files changed, 1770 insertions(+) create mode 100644 services/web/modules/launchpad/app/src/LaunchpadController.js create mode 100644 services/web/modules/launchpad/app/src/LaunchpadRouter.js create mode 100644 services/web/modules/launchpad/app/views/launchpad.pug create mode 100644 services/web/modules/launchpad/index.js create mode 100644 services/web/modules/launchpad/public/src/main/controllers/LaunchpadController.js create mode 100644 services/web/modules/launchpad/public/src/main/index.js create mode 100644 services/web/modules/launchpad/test/unit/src/LaunchpadControllerTests.js diff --git a/services/web/modules/launchpad/app/src/LaunchpadController.js b/services/web/modules/launchpad/app/src/LaunchpadController.js new file mode 100644 index 0000000000..6bc58865f0 --- /dev/null +++ b/services/web/modules/launchpad/app/src/LaunchpadController.js @@ -0,0 +1,278 @@ +/* eslint-disable + handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let LaunchpadController +const Settings = require('settings-sharelatex') +const Path = require('path') +const Url = require('url') +const logger = require('logger-sharelatex') +const metrics = require('metrics-sharelatex') +const UserRegistrationHandler = require('../../../../app/src/Features/User/UserRegistrationHandler') +const EmailHandler = require('../../../../app/src/Features/Email/EmailHandler') +const _ = require('underscore') +const UserGetter = require('../../../../app/src/Features/User/UserGetter') +const { User } = require('../../../../app/src/models/User') +const AuthenticationController = require('../../../../app/src/Features/Authentication/AuthenticationController') + +module.exports = LaunchpadController = { + _getAuthMethod() { + if (Settings.ldap) { + return 'ldap' + } else if (Settings.saml) { + return 'saml' + } else { + return 'local' + } + }, + + launchpadPage(req, res, next) { + // TODO: check if we're using external auth? + // * how does all this work with ldap and saml? + const sessionUser = AuthenticationController.getSessionUser(req) + const authMethod = LaunchpadController._getAuthMethod() + return LaunchpadController._atLeastOneAdminExists(function( + err, + adminUserExists + ) { + if (err != null) { + return next(err) + } + if (!sessionUser) { + if (!adminUserExists) { + return res.render(Path.resolve(__dirname, '../views/launchpad'), { + adminUserExists, + authMethod + }) + } else { + return AuthenticationController._redirectToLoginPage(req, res) + } + } else { + return UserGetter.getUser(sessionUser._id, { isAdmin: 1 }, function( + err, + user + ) { + if (err != null) { + return next(err) + } + if (user && user.isAdmin) { + return res.render(Path.resolve(__dirname, '../views/launchpad'), { + adminUserExists, + authMethod + }) + } else { + return res.redirect('/restricted') + } + }) + } + }) + }, + + _atLeastOneAdminExists(callback) { + if (callback == null) { + callback = function(err, exists) {} + } + return UserGetter.getUser( + { isAdmin: true }, + { _id: 1, isAdmin: 1 }, + function(err, user) { + if (err != null) { + return callback(err) + } + return callback(null, user != null) + } + ) + }, + + sendTestEmail(req, res, next) { + const { email } = req.body + if (!email) { + logger.log({}, 'no email address supplied') + return res.sendStatus(400) + } + logger.log({ email }, 'sending test email') + const emailOptions = { to: email } + return EmailHandler.sendEmail('testEmail', emailOptions, function(err) { + if (err != null) { + logger.warn({ email }, 'error sending test email') + return next(err) + } + logger.log({ email }, 'sent test email') + return res.sendStatus(201) + }) + }, + + registerExternalAuthAdmin(authMethod) { + return function(req, res, next) { + if (LaunchpadController._getAuthMethod() !== authMethod) { + logger.log( + { authMethod }, + 'trying to register external admin, but that auth service is not enabled, disallow' + ) + return res.sendStatus(403) + } + const { email } = req.body + if (!email) { + logger.log({ authMethod }, 'no email supplied, disallow') + return res.sendStatus(400) + } + + logger.log({ email }, 'attempted register first admin user') + return LaunchpadController._atLeastOneAdminExists(function(err, exists) { + if (err != null) { + return next(err) + } + + if (exists) { + logger.log( + { email }, + 'already have at least one admin user, disallow' + ) + return res.sendStatus(403) + } + + const body = { + email, + password: 'password_here', + first_name: email, + last_name: '' + } + logger.log( + { body, authMethod }, + 'creating admin account for specified external-auth user' + ) + + return UserRegistrationHandler.registerNewUser(body, function( + err, + user + ) { + if (err != null) { + logger.warn( + { err, email, authMethod }, + 'error with registerNewUser' + ) + return next(err) + } + + return User.update( + { _id: user._id }, + { $set: { isAdmin: true } }, + function(err) { + if (err != null) { + logger.warn( + { user_id: user._id, err }, + 'error setting user to admin' + ) + return next(err) + } + + AuthenticationController.setRedirectInSession(req, '/launchpad') + logger.log( + { email, user_id: user._id, authMethod }, + 'created first admin account' + ) + + return res.json({ redir: '/launchpad', email }) + } + ) + }) + }) + } + }, + + registerAdmin(req, res, next) { + const { email } = req.body + const { password } = req.body + if (!email || !password) { + logger.log({}, 'must supply both email and password, disallow') + return res.sendStatus(400) + } + + logger.log({ email }, 'attempted register first admin user') + return LaunchpadController._atLeastOneAdminExists(function(err, exists) { + if (err != null) { + return next(err) + } + + if (exists) { + logger.log( + { email: req.body.email }, + 'already have at least one admin user, disallow' + ) + return res.sendStatus(403) + } + + const body = { email, password } + return UserRegistrationHandler.registerNewUser(body, function(err, user) { + if (err != null) { + return next(err) + } + + logger.log({ user_id: user._id }, 'making user an admin') + const proceed = () => + User.update({ _id: user._id }, { $set: { isAdmin: true } }, function( + err + ) { + if (err != null) { + logger.err( + { user_id: user._id, err }, + 'error setting user to admin' + ) + return next(err) + } + + AuthenticationController.setRedirectInSession(req, '/launchpad') + logger.log( + { email, user_id: user._id }, + 'created first admin account' + ) + return res.json({ + redir: '', + id: user._id.toString(), + first_name: user.first_name, + last_name: user.last_name, + email: user.email, + created: Date.now() + }) + }) + + if ( + Settings.overleaf != null && + Settings.createV1AccountOnLogin != null + ) { + logger.log( + { user_id: user._id }, + 'Creating backing account in v1 for new admin user' + ) + const SharelatexAuthController = require('../../../overleaf-integration/app/src/SharelatexAuth/SharelatexAuthController') + return UserGetter.getUser(user._id, function(err, user) { + if (err != null) { + return next(err) + } + return SharelatexAuthController._createBackingAccountIfNeeded( + user, + req, + function(err) { + if (err != null) { + return next(err) + } + return proceed() + } + ) + }) + } else { + return proceed() + } + }) + }) + } +} diff --git a/services/web/modules/launchpad/app/src/LaunchpadRouter.js b/services/web/modules/launchpad/app/src/LaunchpadRouter.js new file mode 100644 index 0000000000..1f33512c45 --- /dev/null +++ b/services/web/modules/launchpad/app/src/LaunchpadRouter.js @@ -0,0 +1,53 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const logger = require('logger-sharelatex') +const LaunchpadController = require('./LaunchpadController') +const AuthenticationController = require('../../../../app/src/Features/Authentication/AuthenticationController') +const AuthorizationMiddleware = require('../../../../app/src/Features/Authorization/AuthorizationMiddleware') + +module.exports = { + apply(webRouter, apiRouter) { + logger.log({}, 'Init launchpad router') + + webRouter.get('/launchpad', LaunchpadController.launchpadPage) + webRouter.post( + '/launchpad/register_admin', + LaunchpadController.registerAdmin + ) + webRouter.post( + '/launchpad/register_ldap_admin', + LaunchpadController.registerExternalAuthAdmin('ldap') + ) + webRouter.post( + '/launchpad/register_saml_admin', + LaunchpadController.registerExternalAuthAdmin('saml') + ) + webRouter.post( + '/launchpad/send_test_email', + AuthorizationMiddleware.ensureUserIsSiteAdmin, + LaunchpadController.sendTestEmail + ) + + if (AuthenticationController.addEndpointToLoginWhitelist != null) { + AuthenticationController.addEndpointToLoginWhitelist('/launchpad') + AuthenticationController.addEndpointToLoginWhitelist( + '/launchpad/register_admin' + ) + AuthenticationController.addEndpointToLoginWhitelist( + '/launchpad/register_ldap_admin' + ) + return AuthenticationController.addEndpointToLoginWhitelist( + '/launchpad/register_saml_admin' + ) + } + } +} diff --git a/services/web/modules/launchpad/app/views/launchpad.pug b/services/web/modules/launchpad/app/views/launchpad.pug new file mode 100644 index 0000000000..4fbf4fe2e7 --- /dev/null +++ b/services/web/modules/launchpad/app/views/launchpad.pug @@ -0,0 +1,267 @@ +extends ../../../../app/views/layout + +block content + + script(type="text/javascript"). + window.data = { + adminUserExists: !{adminUserExists == true}, + ideJsPath: "!{buildJsPath('ide.js')}", + authMethod: "!{authMethod}" + } + + script(type="text/javascript" src='/socket.io/socket.io.js') + + style. + hr { margin-bottom: 5px; } + .status-check { + margin-bottom: 15px; + } + .alert { margin-top: 15px; margin-bottom: 15px; } + a small { cursor: pointer; color: #a93529; } + .launchpad-body img { width: 150px; margin-top: 15px; } + + + .content.content-alt(ng-controller="LaunchpadController") + .container(ng-cloak) + .row + .col-md-8.col-md-offset-2 + .card.launchpad-body + .row + .col-md-12 + .text-center + h1 #{translate('welcome_to_sl')} + p + img(src=buildImgPath('/brand/lion.svg')) + + + .row + .col-md-8.col-md-offset-2 + + + + .row(ng-if="shouldShowAdminForm()") + .col-md-12 + h2 #{translate('create_first_admin_account')} + + // Local Auth Form + div(ng-if="authMethod == 'local'") + form(async-form="register", name="registerForm", on-success="onCreateAdminSuccess" + on-error="onCreateAdminError" + action="/launchpad/register_admin", method="POST", ng-cloak) + input(name='_csrf', type='hidden', value=csrfToken) + form-messages(for="registerForm") + .form-group + label(for='email') #{translate("email")} + input.form-control( + type='email', + name='email', + placeholder="email@example.com" + required, + ng-model="email", + ng-model-options="{ updateOn: 'blur' }", + focus="true" + ) + span.small.text-primary(ng-show="registerForm.email.$invalid && registerForm.email.$dirty") + | #{translate("must_be_email_address")} + .form-group + label(for='password') #{translate("password")} + input.form-control#passwordField( + type='password', + name='password', + placeholder="********", + required, + ng-model="password", + complex-password + ) + span.small.text-primary(ng-show="registerForm.password.$error.complexPassword", + ng-bind-html="complexPasswordErrorMessage") + .actions + button.btn-primary.btn( + type='submit' + ng-disabled="registerForm.inflight || registerForm.password.$error.required|| registerForm.password.$error.complexPassword || createAdminSuccess" + ) + span(ng-show="!registerForm.inflight") #{translate("register")} + span(ng-show="registerForm.inflight") #{translate("registering")}... + + // Ldap Form + div(ng-if="authMethod == 'ldap'") + h3 #{translate('ldap')} + p + | #{translate('ldap_create_admin_instructions')} + + form(async-form="register", name="registerLdapForm", on-success="onCreateAdminSuccess" + on-error="onCreateAdminError" + action="/launchpad/register_ldap_admin", method="POST", ng-cloak) + input(name='_csrf', type='hidden', value=csrfToken) + form-messages(for="registerLdapForm") + .form-group + label(for='email') #{translate("email")} + input.form-control( + type='email', + name='email', + placeholder="email@example.com" + required, + ng-model="email", + ng-model-options="{ updateOn: 'blur' }", + focus="true" + ) + span.small.text-primary(ng-show="registerLdapForm.email.$invalid && registerLdapForm.email.$dirty") + | #{translate("must_be_email_address")} + .actions + button.btn-primary.btn( + type='submit' + ng-disabled="registerLdapForm.inflight || registerLdapForm.password.$error.required|| registerLdapForm.password.$error.complexPassword || createAdminSuccess" + ) + span(ng-show="!registerLdapForm.inflight") #{translate("register")} + span(ng-show="registerLdapForm.inflight") #{translate("registering")}... + + + // Saml Form + div(ng-if="authMethod == 'saml'") + h3 #{translate('saml')} + p + | #{translate('saml_create_admin_instructions')} + + form(async-form="register", name="registerSamlForm", on-success="onCreateAdminSuccess" + on-error="onCreateAdminError" + action="/launchpad/register_saml_admin", method="POST", ng-cloak) + input(name='_csrf', type='hidden', value=csrfToken) + form-messages(for="registerSamlForm") + .form-group + label(for='email') #{translate("email")} + input.form-control( + type='email', + name='email', + placeholder="email@example.com" + required, + ng-model="email", + ng-model-options="{ updateOn: 'blur' }", + focus="true" + ) + span.small.text-primary(ng-show="registerSamlForm.email.$invalid && registerSamlForm.email.$dirty") + | #{translate("must_be_email_address")} + .actions + button.btn-primary.btn( + type='submit' + ng-disabled="registerSamlForm.inflight || registerSamlForm.password.$error.required|| registerSamlForm.password.$error.complexPassword || createAdminSuccess" + ) + span(ng-show="!registerSamlForm.inflight") #{translate("register")} + span(ng-show="registerSamlForm.inflight") #{translate("registering")}... + + + + .row(ng-if="createAdminSuccess") + .col-md-12.text-center + .alert.alert-success + | !{translate('admin_user_created_message', {link: '/login?redir=/launchpad'})} + + + .row(ng-if="createAdminError") + .col-md-12.text-center + .alert.alert-danger + | #{translate('generic_something_went_wrong')} + + br + + + div(ng-if="!shouldShowAdminForm()") + .row + .col-md-12.status-indicators + + h2 #{translate('status_checks')} + + + .row.status-check + .col-sm-5 + | #{translate('editor_resources')} + .col-sm-7 + div(ng-switch="statusChecks.ideJs.status") + + span(ng-switch-when="inflight") + i.fa.fa-fw.fa-spinner.fa-spin + span  #{translate('checking')} + + span(ng-switch-when="ok") + i.fa.fa-check + span  #{translate('ok')} + a(ng-click="tryFetchIdeJs()") + small  #{translate('retry')} + + span(ng-switch-when="error") + i.fa.fa-exclamation + span  #{translate('error')} + a(ng-click="tryFetchIdeJs()") + small  #{translate('retry')} + div.alert.alert-danger + | {{ statusChecks.ideJs.error }} + + + + .row.status-check + .col-sm-5 + | #{translate('websockets')} + .col-sm-7 + div(ng-switch="statusChecks.websocket.status") + + span(ng-switch-when="inflight") + i.fa.fa-fw.fa-spinner.fa-spin + span  #{translate('checking')} + + span(ng-switch-when="ok") + i.fa.fa-check + span  #{translate('ok')} + a(ng-click="tryOpenWebSocket()") + small  #{translate('retry')} + + span(ng-switch-when="error") + i.fa.fa-exclamation + span  #{translate('error')} + a(ng-click="tryOpenWebSocket()") + small  #{translate('retry')} + div.alert.alert-danger + | {{ statusChecks.websocket.error }} + + + + hr + + + .row + .col-md-12 + h2 #{translate('other_actions')} + + h3 #{translate('send_test_email')} + form.form + .form-group + label(for="emailInput") Email + input(type="text", name="emailInput" ng-model="testEmail.emailAddress").form-control + button(ng-click="sendTestEmail()", ng-disabled="testEmail.inflight").btn.btn-primary + span(ng-show="!testEmail.inflight") #{translate("send")} + span(ng-show="testEmail.inflight") #{translate("sending")}... + div(ng-if="testEmail.status == 'ok'") + .alert.alert-success + | #{translate('email_sent')} + div(ng-if="testEmail.status == 'error'") + .alert.alert-danger + | #{translate('generic_something_went_wrong')} + + + + + + + hr + + + + .row + .col-md-12 + .text-center + br + p + a(href="/admin").btn.btn-md + | Go To Admin Panel + |   + a(href="/project").btn.btn-md.btn-primary + | Start Using ShareLaTeX + br diff --git a/services/web/modules/launchpad/index.js b/services/web/modules/launchpad/index.js new file mode 100644 index 0000000000..274679ce88 --- /dev/null +++ b/services/web/modules/launchpad/index.js @@ -0,0 +1,9 @@ +/* eslint-disable + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +let Launchpad +const LaunchpadRouter = require('./app/src/LaunchpadRouter') + +module.exports = Launchpad = { router: LaunchpadRouter } diff --git a/services/web/modules/launchpad/public/src/main/controllers/LaunchpadController.js b/services/web/modules/launchpad/public/src/main/controllers/LaunchpadController.js new file mode 100644 index 0000000000..1032bbb170 --- /dev/null +++ b/services/web/modules/launchpad/public/src/main/controllers/LaunchpadController.js @@ -0,0 +1,155 @@ +/* eslint-disable + max-len, + no-return-assign, + no-undef, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +define(['base'], App => + App.controller('LaunchpadController', function($scope, $http, $timeout) { + $scope.adminUserExists = window.data.adminUserExists + $scope.ideJsPath = window.data.ideJsPath + $scope.authMethod = window.data.authMethod + + $scope.createAdminSuccess = null + $scope.createAdminError = null + + $scope.statusChecks = { + ideJs: { status: 'inflight', error: null }, + websocket: { status: 'inflight', error: null }, + healthCheck: { status: 'inflight', error: null } + } + + $scope.testEmail = { + emailAddress: '', + inflight: false, + status: null // | 'ok' | 'success' + } + + $scope.shouldShowAdminForm = () => !$scope.adminUserExists + + $scope.onCreateAdminSuccess = function(response) { + const { status } = response + if (status >= 200 && status < 300) { + return ($scope.createAdminSuccess = true) + } + } + + $scope.onCreateAdminError = () => ($scope.createAdminError = true) + + $scope.sendTestEmail = function() { + $scope.testEmail.inflight = true + $scope.testEmail.status = null + return $http + .post('/launchpad/send_test_email', { + email: $scope.testEmail.emailAddress, + _csrf: window.csrfToken + }) + .then(function(response) { + const { status } = response + $scope.testEmail.inflight = false + if (status >= 200 && status < 300) { + return ($scope.testEmail.status = 'ok') + } + }) + .catch(function() { + $scope.testEmail.inflight = false + return ($scope.testEmail.status = 'error') + }) + } + + $scope.tryFetchIdeJs = function() { + $scope.statusChecks.ideJs.status = 'inflight' + return $timeout( + () => + $http + .get($scope.ideJsPath) + .then(function(response) { + const { status } = response + if (status >= 200 && status < 300) { + return ($scope.statusChecks.ideJs.status = 'ok') + } + }) + .catch(function(response) { + const { status } = response + $scope.statusChecks.ideJs.status = 'error' + return ($scope.statusChecks.ideJs.error = new Error( + `Http status: ${status}` + )) + }), + + 1000 + ) + } + + $scope.tryOpenWebSocket = function() { + $scope.statusChecks.websocket.status = 'inflight' + return $timeout(function() { + if (typeof io === 'undefined' || io === null) { + $scope.statusChecks.websocket.status = 'error' + $scope.statusChecks.websocket.error = 'socket.io not loaded' + return + } + const socket = io.connect( + null, + { + reconnect: false, + 'connect timeout': 30 * 1000, + 'force new connection': true + } + ) + + socket.on('connectionAccepted', function() { + $scope.statusChecks.websocket.status = 'ok' + return $scope.$apply(function() {}) + }) + + socket.on('connectionRejected', function(err) { + $scope.statusChecks.websocket.status = 'error' + $scope.statusChecks.websocket.error = err + return $scope.$apply(function() {}) + }) + + return socket.on('connect_failed', function(err) { + $scope.statusChecks.websocket.status = 'error' + $scope.statusChecks.websocket.error = err + return $scope.$apply(function() {}) + }) + }, 1000) + } + + $scope.tryHealthCheck = function() { + $scope.statusChecks.healthCheck.status = 'inflight' + return $http + .get('/health_check') + .then(function(response) { + const { status } = response + if (status >= 200 && status < 300) { + return ($scope.statusChecks.healthCheck.status = 'ok') + } + }) + .catch(function(response) { + const { status } = response + $scope.statusChecks.healthCheck.status = 'error' + return ($scope.statusChecks.healthCheck.error = new Error( + `Http status: ${status}` + )) + }) + } + + $scope.runStatusChecks = function() { + $timeout(() => $scope.tryFetchIdeJs(), 1000) + return $timeout(() => $scope.tryOpenWebSocket(), 2000) + } + + // kick off the status checks on load + if ($scope.adminUserExists) { + return $scope.runStatusChecks() + } + })) diff --git a/services/web/modules/launchpad/public/src/main/index.js b/services/web/modules/launchpad/public/src/main/index.js new file mode 100644 index 0000000000..99f2674d49 --- /dev/null +++ b/services/web/modules/launchpad/public/src/main/index.js @@ -0,0 +1,3 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. +define(['main/launchpad/controllers/LaunchpadController'], function() {}) diff --git a/services/web/modules/launchpad/test/unit/src/LaunchpadControllerTests.js b/services/web/modules/launchpad/test/unit/src/LaunchpadControllerTests.js new file mode 100644 index 0000000000..892237d639 --- /dev/null +++ b/services/web/modules/launchpad/test/unit/src/LaunchpadControllerTests.js @@ -0,0 +1,1005 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +require('chai').should() +const { expect } = require('chai') +const sinon = require('sinon') +const { ObjectId } = require('mongojs') +const modulePath = require('path').join( + __dirname, + '../../../app/src/LaunchpadController.js' +) + +describe('LaunchpadController', function() { + beforeEach(function() { + this.user = { + _id: '323123', + first_name: 'fn', + last_name: 'ln', + save: sinon.stub().callsArgWith(0) + } + + this.User = {} + this.LaunchpadController = SandboxedModule.require(modulePath, { + globals: { + console: console + }, + requires: { + 'settings-sharelatex': (this.Settings = {}), + 'logger-sharelatex': (this.Logger = { + log() {}, + warn() {}, + err() {}, + error() {} + }), + 'metrics-sharelatex': (this.Metrics = {}), + '../../../../app/src/Features/User/UserRegistrationHandler': (this.UserRegistrationHandler = {}), + '../../../../app/src/Features/Email/EmailHandler': (this.EmailHandler = {}), + '../../../../app/src/Features/User/UserGetter': (this.UserGetter = {}), + '../../../../app/src/models/User': { User: this.User }, + '../../../../app/src/Features/Authentication/AuthenticationController': (this.AuthenticationController = {}), + '../../../overleaf-integration/app/src/SharelatexAuth/SharelatexAuthController': (this.SharelatexAuthController = {}) + } + }) + + this.email = 'bob@smith.com' + + this.req = { + query: {}, + body: {}, + session: {} + } + + this.res = { + render: sinon.stub(), + send: sinon.stub(), + sendStatus: sinon.stub() + } + + return (this.next = sinon.stub()) + }) + + describe('launchpadPage', function() { + beforeEach(function() { + this._atLeastOneAdminExists = sinon.stub( + this.LaunchpadController, + '_atLeastOneAdminExists' + ) + return (this.AuthenticationController._redirectToLoginPage = sinon.stub()) + }) + + afterEach(function() { + return this._atLeastOneAdminExists.restore() + }) + + describe('when the user is not logged in', function() { + beforeEach(function() { + this.AuthenticationController.getSessionUser = sinon + .stub() + .returns(null) + return (this.res.render = sinon.stub()) + }) + + describe('when there are no admins', function() { + beforeEach(function() { + this._atLeastOneAdminExists.callsArgWith(0, null, false) + return this.LaunchpadController.launchpadPage( + this.req, + this.res, + this.next + ) + }) + + it('should render the launchpad page', function() { + const viewPath = require('path').join( + __dirname, + '../../../app/views/launchpad' + ) + this.res.render.callCount.should.equal(1) + return this.res.render + .calledWith(viewPath, { + adminUserExists: false, + authMethod: 'local' + }) + .should.equal(true) + }) + }) + + describe('when there is at least one admin', function() { + beforeEach(function() { + this._atLeastOneAdminExists.callsArgWith(0, null, true) + return this.LaunchpadController.launchpadPage( + this.req, + this.res, + this.next + ) + }) + + it('should redirect to login page', function() { + return this.AuthenticationController._redirectToLoginPage.callCount.should.equal( + 1 + ) + }) + + it('should not render the launchpad page', function() { + return this.res.render.callCount.should.equal(0) + }) + }) + }) + + describe('when the user is logged in', function() { + beforeEach(function() { + this.user = { + _id: 'abcd', + email: 'abcd@example.com' + } + this.AuthenticationController.getSessionUser = sinon + .stub() + .returns(this.user) + this._atLeastOneAdminExists.callsArgWith(0, null, true) + this.res.render = sinon.stub() + return (this.res.redirect = sinon.stub()) + }) + + describe('when the user is an admin', function() { + beforeEach(function() { + this.UserGetter.getUser = sinon + .stub() + .callsArgWith(2, null, { isAdmin: true }) + return this.LaunchpadController.launchpadPage( + this.req, + this.res, + this.next + ) + }) + + it('should render the launchpad page', function() { + const viewPath = require('path').join( + __dirname, + '../../../app/views/launchpad' + ) + this.res.render.callCount.should.equal(1) + return this.res.render + .calledWith(viewPath, { + adminUserExists: true, + authMethod: 'local' + }) + .should.equal(true) + }) + }) + + describe('when the user is not an admin', function() { + beforeEach(function() { + this.UserGetter.getUser = sinon + .stub() + .callsArgWith(2, null, { isAdmin: false }) + return this.LaunchpadController.launchpadPage( + this.req, + this.res, + this.next + ) + }) + + it('should redirect to restricted page', function() { + this.res.redirect.callCount.should.equal(1) + return this.res.redirect.calledWith('/restricted').should.equal(true) + }) + }) + }) + }) + + describe('_atLeastOneAdminExists', function() { + describe('when there are no admins', function() { + beforeEach(function() { + return (this.UserGetter.getUser = sinon + .stub() + .callsArgWith(2, null, null)) + }) + + it('should callback with false', function(done) { + return this.LaunchpadController._atLeastOneAdminExists( + (err, exists) => { + expect(err).to.equal(null) + expect(exists).to.equal(false) + return done() + } + ) + }) + }) + + describe('when there are some admins', function() { + beforeEach(function() { + return (this.UserGetter.getUser = sinon + .stub() + .callsArgWith(2, null, { _id: 'abcd' })) + }) + + it('should callback with true', function(done) { + return this.LaunchpadController._atLeastOneAdminExists( + (err, exists) => { + expect(err).to.equal(null) + expect(exists).to.equal(true) + return done() + } + ) + }) + }) + + describe('when getUser produces an error', function() { + beforeEach(function() { + return (this.UserGetter.getUser = sinon + .stub() + .callsArgWith(2, new Error('woops'))) + }) + + it('should produce an error', function(done) { + return this.LaunchpadController._atLeastOneAdminExists( + (err, exists) => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + expect(exists).to.equal(undefined) + return done() + } + ) + }) + }) + }) + + describe('sendTestEmail', function() { + beforeEach(function() { + this.EmailHandler.sendEmail = sinon.stub().callsArgWith(2, null) + this.req.body.email = 'someone@example.com' + this.res.sendStatus = sinon.stub() + return (this.next = sinon.stub()) + }) + + it('should produce a 201 response', function() { + this.LaunchpadController.sendTestEmail(this.req, this.res, this.next) + this.res.sendStatus.callCount.should.equal(1) + return this.res.sendStatus.calledWith(201).should.equal(true) + }) + + it('should not call next with an error', function() { + this.LaunchpadController.sendTestEmail(this.req, this.res, this.next) + return this.next.callCount.should.equal(0) + }) + + it('should have called sendEmail', function() { + this.LaunchpadController.sendTestEmail(this.req, this.res, this.next) + this.EmailHandler.sendEmail.callCount.should.equal(1) + return this.EmailHandler.sendEmail + .calledWith('testEmail') + .should.equal(true) + }) + + describe('when sendEmail produces an error', function() { + beforeEach(function() { + return (this.EmailHandler.sendEmail = sinon + .stub() + .callsArgWith(2, new Error('woops'))) + }) + + it('should call next with an error', function() { + this.LaunchpadController.sendTestEmail(this.req, this.res, this.next) + this.next.callCount.should.equal(1) + return expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + }) + }) + + describe('when no email address is supplied', function() { + beforeEach(function() { + return (this.req.body.email = undefined) + }) + + it('should produce a 400 response', function() { + this.LaunchpadController.sendTestEmail(this.req, this.res, this.next) + this.res.sendStatus.callCount.should.equal(1) + return this.res.sendStatus.calledWith(400).should.equal(true) + }) + }) + }) + + describe('registerAdmin', function() { + beforeEach(function() { + return (this._atLeastOneAdminExists = sinon.stub( + this.LaunchpadController, + '_atLeastOneAdminExists' + )) + }) + + afterEach(function() { + return this._atLeastOneAdminExists.restore() + }) + + describe('when all goes well', function() { + beforeEach(function() { + this._atLeastOneAdminExists.callsArgWith(0, null, false) + this.email = 'someone@example.com' + this.password = 'a_really_bad_password' + this.req.body.email = this.email + this.req.body.password = this.password + this.user = { + _id: 'abcdef', + email: this.email + } + this.UserRegistrationHandler.registerNewUser = sinon + .stub() + .callsArgWith(1, null, this.user) + this.User.update = sinon.stub().callsArgWith(2, null) + this.AuthenticationController.setRedirectInSession = sinon.stub() + this.res.json = sinon.stub() + this.next = sinon.stub() + return this.LaunchpadController.registerAdmin( + this.req, + this.res, + this.next + ) + }) + + it('should send back a json response', function() { + this.res.json.callCount.should.equal(1) + return expect(this.res.json.lastCall.args[0].email).to.equal(this.email) + }) + + it('should have checked for existing admins', function() { + return this._atLeastOneAdminExists.callCount.should.equal(1) + }) + + it('should have called registerNewUser', function() { + this.UserRegistrationHandler.registerNewUser.callCount.should.equal(1) + return this.UserRegistrationHandler.registerNewUser + .calledWith({ email: this.email, password: this.password }) + .should.equal(true) + }) + + it('should have updated the user to make them an admin', function() { + this.User.update.callCount.should.equal(1) + return this.User.update + .calledWith({ _id: this.user._id }, { $set: { isAdmin: true } }) + .should.equal(true) + }) + + it('should have set a redirect in session', function() { + this.AuthenticationController.setRedirectInSession.callCount.should.equal( + 1 + ) + return this.AuthenticationController.setRedirectInSession + .calledWith(this.req, '/launchpad') + .should.equal(true) + }) + }) + + describe('when no email is supplied', function() { + beforeEach(function() { + this._atLeastOneAdminExists.callsArgWith(0, null, false) + this.email = undefined + this.password = 'a_really_bad_password' + this.req.body.email = this.email + this.req.body.password = this.password + this.user = { + _id: 'abcdef', + email: this.email + } + this.UserRegistrationHandler.registerNewUser = sinon.stub() + this.User.update = sinon.stub() + this.AuthenticationController.setRedirectInSession = sinon.stub() + this.res.sendStatus = sinon.stub() + this.next = sinon.stub() + return this.LaunchpadController.registerAdmin( + this.req, + this.res, + this.next + ) + }) + + it('should send a 400 response', function() { + this.res.sendStatus.callCount.should.equal(1) + return this.res.sendStatus.calledWith(400).should.equal(true) + }) + + it('should not check for existing admins', function() { + return this._atLeastOneAdminExists.callCount.should.equal(0) + }) + + it('should not call registerNewUser', function() { + return this.UserRegistrationHandler.registerNewUser.callCount.should.equal( + 0 + ) + }) + }) + + describe('when no password is supplied', function() { + beforeEach(function() { + this._atLeastOneAdminExists.callsArgWith(0, null, false) + this.email = 'someone@example.com' + this.password = undefined + this.req.body.email = this.email + this.req.body.password = this.password + this.user = { + _id: 'abcdef', + email: this.email + } + this.UserRegistrationHandler.registerNewUser = sinon.stub() + this.User.update = sinon.stub() + this.AuthenticationController.setRedirectInSession = sinon.stub() + this.res.sendStatus = sinon.stub() + this.next = sinon.stub() + return this.LaunchpadController.registerAdmin( + this.req, + this.res, + this.next + ) + }) + + it('should send a 400 response', function() { + this.res.sendStatus.callCount.should.equal(1) + return this.res.sendStatus.calledWith(400).should.equal(true) + }) + + it('should not check for existing admins', function() { + return this._atLeastOneAdminExists.callCount.should.equal(0) + }) + + it('should not call registerNewUser', function() { + return this.UserRegistrationHandler.registerNewUser.callCount.should.equal( + 0 + ) + }) + }) + + describe('when there are already existing admins', function() { + beforeEach(function() { + this._atLeastOneAdminExists.callsArgWith(0, null, true) + this.email = 'someone@example.com' + this.password = 'a_really_bad_password' + this.req.body.email = this.email + this.req.body.password = this.password + this.user = { + _id: 'abcdef', + email: this.email + } + this.UserRegistrationHandler.registerNewUser = sinon.stub() + this.User.update = sinon.stub() + this.AuthenticationController.setRedirectInSession = sinon.stub() + this.res.sendStatus = sinon.stub() + this.next = sinon.stub() + return this.LaunchpadController.registerAdmin( + this.req, + this.res, + this.next + ) + }) + + it('should send a 403 response', function() { + this.res.sendStatus.callCount.should.equal(1) + return this.res.sendStatus.calledWith(403).should.equal(true) + }) + + it('should not call registerNewUser', function() { + return this.UserRegistrationHandler.registerNewUser.callCount.should.equal( + 0 + ) + }) + }) + + describe('when checking admins produces an error', function() { + beforeEach(function() { + this._atLeastOneAdminExists.callsArgWith(0, new Error('woops')) + this.email = 'someone@example.com' + this.password = 'a_really_bad_password' + this.req.body.email = this.email + this.req.body.password = this.password + this.user = { + _id: 'abcdef', + email: this.email + } + this.UserRegistrationHandler.registerNewUser = sinon.stub() + this.User.update = sinon.stub() + this.AuthenticationController.setRedirectInSession = sinon.stub() + this.res.sendStatus = sinon.stub() + this.next = sinon.stub() + return this.LaunchpadController.registerAdmin( + this.req, + this.res, + this.next + ) + }) + + it('should call next with an error', function() { + this.next.callCount.should.equal(1) + return expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + }) + + it('should have checked for existing admins', function() { + return this._atLeastOneAdminExists.callCount.should.equal(1) + }) + + it('should not call registerNewUser', function() { + return this.UserRegistrationHandler.registerNewUser.callCount.should.equal( + 0 + ) + }) + }) + + describe('when registerNewUser produces an error', function() { + beforeEach(function() { + this._atLeastOneAdminExists.callsArgWith(0, null, false) + this.email = 'someone@example.com' + this.password = 'a_really_bad_password' + this.req.body.email = this.email + this.req.body.password = this.password + this.user = { + _id: 'abcdef', + email: this.email + } + this.UserRegistrationHandler.registerNewUser = sinon + .stub() + .callsArgWith(1, new Error('woops')) + this.User.update = sinon.stub() + this.AuthenticationController.setRedirectInSession = sinon.stub() + this.res.json = sinon.stub() + this.next = sinon.stub() + return this.LaunchpadController.registerAdmin( + this.req, + this.res, + this.next + ) + }) + + it('should call next with an error', function() { + this.next.callCount.should.equal(1) + return expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + }) + + it('should have checked for existing admins', function() { + return this._atLeastOneAdminExists.callCount.should.equal(1) + }) + + it('should have called registerNewUser', function() { + this.UserRegistrationHandler.registerNewUser.callCount.should.equal(1) + return this.UserRegistrationHandler.registerNewUser + .calledWith({ email: this.email, password: this.password }) + .should.equal(true) + }) + + it('should not call update', function() { + return this.User.update.callCount.should.equal(0) + }) + }) + + describe('when user update produces an error', function() { + beforeEach(function() { + this._atLeastOneAdminExists.callsArgWith(0, null, false) + this.email = 'someone@example.com' + this.password = 'a_really_bad_password' + this.req.body.email = this.email + this.req.body.password = this.password + this.user = { + _id: 'abcdef', + email: this.email + } + this.UserRegistrationHandler.registerNewUser = sinon + .stub() + .callsArgWith(1, null, this.user) + this.User.update = sinon.stub().callsArgWith(2, new Error('woops')) + this.AuthenticationController.setRedirectInSession = sinon.stub() + this.res.json = sinon.stub() + this.next = sinon.stub() + return this.LaunchpadController.registerAdmin( + this.req, + this.res, + this.next + ) + }) + + it('should call next with an error', function() { + this.next.callCount.should.equal(1) + return expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + }) + + it('should have checked for existing admins', function() { + return this._atLeastOneAdminExists.callCount.should.equal(1) + }) + + it('should have called registerNewUser', function() { + this.UserRegistrationHandler.registerNewUser.callCount.should.equal(1) + return this.UserRegistrationHandler.registerNewUser + .calledWith({ email: this.email, password: this.password }) + .should.equal(true) + }) + }) + + describe('when overleaf', function() { + beforeEach(function() { + this.Settings.overleaf = { one: 1 } + this.Settings.createV1AccountOnLogin = true + this._atLeastOneAdminExists.callsArgWith(0, null, false) + this.email = 'someone@example.com' + this.password = 'a_really_bad_password' + this.req.body.email = this.email + this.req.body.password = this.password + this.user = { + _id: 'abcdef', + email: this.email + } + this.UserRegistrationHandler.registerNewUser = sinon + .stub() + .callsArgWith(1, null, this.user) + this.User.update = sinon.stub().callsArgWith(2, null) + this.AuthenticationController.setRedirectInSession = sinon.stub() + this.SharelatexAuthController._createBackingAccountIfNeeded = sinon + .stub() + .callsArgWith(2, null) + this.UserGetter.getUser = sinon + .stub() + .callsArgWith(1, null, { _id: '1234' }) + this.res.json = sinon.stub() + this.next = sinon.stub() + return this.LaunchpadController.registerAdmin( + this.req, + this.res, + this.next + ) + }) + + it('should send back a json response', function() { + this.res.json.callCount.should.equal(1) + return expect(this.res.json.lastCall.args[0].email).to.equal(this.email) + }) + + it('should have checked for existing admins', function() { + return this._atLeastOneAdminExists.callCount.should.equal(1) + }) + + it('should have called registerNewUser', function() { + this.UserRegistrationHandler.registerNewUser.callCount.should.equal(1) + return this.UserRegistrationHandler.registerNewUser + .calledWith({ email: this.email, password: this.password }) + .should.equal(true) + }) + + it('should have created a backing account for the user', function() { + return this.SharelatexAuthController._createBackingAccountIfNeeded.callCount.should.equal( + 1 + ) + }) + + it('should have updated the user to make them an admin', function() { + return this.User.update + .calledWith({ _id: this.user._id }, { $set: { isAdmin: true } }) + .should.equal(true) + }) + + it('should have set a redirect in session', function() { + this.AuthenticationController.setRedirectInSession.callCount.should.equal( + 1 + ) + return this.AuthenticationController.setRedirectInSession + .calledWith(this.req, '/launchpad') + .should.equal(true) + }) + }) + }) + + describe('registerExternalAuthAdmin', function() { + beforeEach(function() { + this.Settings.ldap = { one: 1 } + return (this._atLeastOneAdminExists = sinon.stub( + this.LaunchpadController, + '_atLeastOneAdminExists' + )) + }) + + afterEach(function() { + return this._atLeastOneAdminExists.restore() + }) + + describe('when all goes well', function() { + beforeEach(function() { + this._atLeastOneAdminExists.callsArgWith(0, null, false) + this.email = 'someone@example.com' + this.req.body.email = this.email + this.user = { + _id: 'abcdef', + email: this.email + } + this.UserRegistrationHandler.registerNewUser = sinon + .stub() + .callsArgWith(1, null, this.user) + this.User.update = sinon.stub().callsArgWith(2, null) + this.AuthenticationController.setRedirectInSession = sinon.stub() + this.res.json = sinon.stub() + this.next = sinon.stub() + return this.LaunchpadController.registerExternalAuthAdmin('ldap')( + this.req, + this.res, + this.next + ) + }) + + it('should send back a json response', function() { + this.res.json.callCount.should.equal(1) + return expect(this.res.json.lastCall.args[0].email).to.equal(this.email) + }) + + it('should have checked for existing admins', function() { + return this._atLeastOneAdminExists.callCount.should.equal(1) + }) + + it('should have called registerNewUser', function() { + this.UserRegistrationHandler.registerNewUser.callCount.should.equal(1) + return this.UserRegistrationHandler.registerNewUser + .calledWith({ + email: this.email, + password: 'password_here', + first_name: this.email, + last_name: '' + }) + .should.equal(true) + }) + + it('should have updated the user to make them an admin', function() { + this.User.update.callCount.should.equal(1) + return this.User.update + .calledWith({ _id: this.user._id }, { $set: { isAdmin: true } }) + .should.equal(true) + }) + + it('should have set a redirect in session', function() { + this.AuthenticationController.setRedirectInSession.callCount.should.equal( + 1 + ) + return this.AuthenticationController.setRedirectInSession + .calledWith(this.req, '/launchpad') + .should.equal(true) + }) + }) + + describe('when the authMethod is invalid', function() { + beforeEach(function() { + this._atLeastOneAdminExists.callsArgWith(0, null, false) + this.email = undefined + this.req.body.email = this.email + this.user = { + _id: 'abcdef', + email: this.email + } + this.UserRegistrationHandler.registerNewUser = sinon.stub() + this.User.update = sinon.stub() + this.AuthenticationController.setRedirectInSession = sinon.stub() + this.res.sendStatus = sinon.stub() + this.next = sinon.stub() + return this.LaunchpadController.registerExternalAuthAdmin( + 'NOTAVALIDAUTHMETHOD' + )(this.req, this.res, this.next) + }) + + it('should send a 403 response', function() { + this.res.sendStatus.callCount.should.equal(1) + return this.res.sendStatus.calledWith(403).should.equal(true) + }) + + it('should not check for existing admins', function() { + return this._atLeastOneAdminExists.callCount.should.equal(0) + }) + + it('should not call registerNewUser', function() { + return this.UserRegistrationHandler.registerNewUser.callCount.should.equal( + 0 + ) + }) + }) + + describe('when no email is supplied', function() { + beforeEach(function() { + this._atLeastOneAdminExists.callsArgWith(0, null, false) + this.email = undefined + this.req.body.email = this.email + this.user = { + _id: 'abcdef', + email: this.email + } + this.UserRegistrationHandler.registerNewUser = sinon.stub() + this.User.update = sinon.stub() + this.AuthenticationController.setRedirectInSession = sinon.stub() + this.res.sendStatus = sinon.stub() + this.next = sinon.stub() + return this.LaunchpadController.registerExternalAuthAdmin('ldap')( + this.req, + this.res, + this.next + ) + }) + + it('should send a 400 response', function() { + this.res.sendStatus.callCount.should.equal(1) + return this.res.sendStatus.calledWith(400).should.equal(true) + }) + + it('should not check for existing admins', function() { + return this._atLeastOneAdminExists.callCount.should.equal(0) + }) + + it('should not call registerNewUser', function() { + return this.UserRegistrationHandler.registerNewUser.callCount.should.equal( + 0 + ) + }) + }) + + describe('when there are already existing admins', function() { + beforeEach(function() { + this._atLeastOneAdminExists.callsArgWith(0, null, true) + this.email = 'someone@example.com' + this.req.body.email = this.email + this.user = { + _id: 'abcdef', + email: this.email + } + this.UserRegistrationHandler.registerNewUser = sinon.stub() + this.User.update = sinon.stub() + this.AuthenticationController.setRedirectInSession = sinon.stub() + this.res.sendStatus = sinon.stub() + this.next = sinon.stub() + return this.LaunchpadController.registerExternalAuthAdmin('ldap')( + this.req, + this.res, + this.next + ) + }) + + it('should send a 403 response', function() { + this.res.sendStatus.callCount.should.equal(1) + return this.res.sendStatus.calledWith(403).should.equal(true) + }) + + it('should not call registerNewUser', function() { + return this.UserRegistrationHandler.registerNewUser.callCount.should.equal( + 0 + ) + }) + }) + + describe('when checking admins produces an error', function() { + beforeEach(function() { + this._atLeastOneAdminExists.callsArgWith(0, new Error('woops')) + this.email = 'someone@example.com' + this.req.body.email = this.email + this.user = { + _id: 'abcdef', + email: this.email + } + this.UserRegistrationHandler.registerNewUser = sinon.stub() + this.User.update = sinon.stub() + this.AuthenticationController.setRedirectInSession = sinon.stub() + this.res.sendStatus = sinon.stub() + this.next = sinon.stub() + return this.LaunchpadController.registerExternalAuthAdmin('ldap')( + this.req, + this.res, + this.next + ) + }) + + it('should call next with an error', function() { + this.next.callCount.should.equal(1) + return expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + }) + + it('should have checked for existing admins', function() { + return this._atLeastOneAdminExists.callCount.should.equal(1) + }) + + it('should not call registerNewUser', function() { + return this.UserRegistrationHandler.registerNewUser.callCount.should.equal( + 0 + ) + }) + }) + + describe('when registerNewUser produces an error', function() { + beforeEach(function() { + this._atLeastOneAdminExists.callsArgWith(0, null, false) + this.email = 'someone@example.com' + this.req.body.email = this.email + this.user = { + _id: 'abcdef', + email: this.email + } + this.UserRegistrationHandler.registerNewUser = sinon + .stub() + .callsArgWith(1, new Error('woops')) + this.User.update = sinon.stub() + this.AuthenticationController.setRedirectInSession = sinon.stub() + this.res.json = sinon.stub() + this.next = sinon.stub() + return this.LaunchpadController.registerExternalAuthAdmin('ldap')( + this.req, + this.res, + this.next + ) + }) + + it('should call next with an error', function() { + this.next.callCount.should.equal(1) + return expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + }) + + it('should have checked for existing admins', function() { + return this._atLeastOneAdminExists.callCount.should.equal(1) + }) + + it('should have called registerNewUser', function() { + this.UserRegistrationHandler.registerNewUser.callCount.should.equal(1) + return this.UserRegistrationHandler.registerNewUser + .calledWith({ + email: this.email, + password: 'password_here', + first_name: this.email, + last_name: '' + }) + .should.equal(true) + }) + + it('should not call update', function() { + return this.User.update.callCount.should.equal(0) + }) + }) + + describe('when user update produces an error', function() { + beforeEach(function() { + this._atLeastOneAdminExists.callsArgWith(0, null, false) + this.email = 'someone@example.com' + this.req.body.email = this.email + this.user = { + _id: 'abcdef', + email: this.email + } + this.UserRegistrationHandler.registerNewUser = sinon + .stub() + .callsArgWith(1, null, this.user) + this.User.update = sinon.stub().callsArgWith(2, new Error('woops')) + this.AuthenticationController.setRedirectInSession = sinon.stub() + this.res.json = sinon.stub() + this.next = sinon.stub() + return this.LaunchpadController.registerExternalAuthAdmin('ldap')( + this.req, + this.res, + this.next + ) + }) + + it('should call next with an error', function() { + this.next.callCount.should.equal(1) + return expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + }) + + it('should have checked for existing admins', function() { + return this._atLeastOneAdminExists.callCount.should.equal(1) + }) + + it('should have called registerNewUser', function() { + this.UserRegistrationHandler.registerNewUser.callCount.should.equal(1) + return this.UserRegistrationHandler.registerNewUser + .calledWith({ + email: this.email, + password: 'password_here', + first_name: this.email, + last_name: '' + }) + .should.equal(true) + }) + }) + }) +})