From 2abec5c638ce5d9683e8fe1c61aec1174f0122fe Mon Sep 17 00:00:00 2001 From: June Kelly Date: Fri, 14 Jul 2023 10:11:57 +0100 Subject: [PATCH] Merge pull request #13850 from overleaf/ab-invite-enrollment [web] Managed users - combined invite/surrender flow GitOrigin-RevId: 70cb0d81e0019eac69a4a565377447bb6d1a1823 --- .../Subscription/TeamInvitesController.js | 67 +++- .../Subscription/TeamInvitesHandler.js | 373 ++++++++---------- .../subscriptions/team/invite-managed.pug | 14 + .../app/views/subscriptions/team/invite.pug | 9 +- services/web/config/settings.defaults.js | 1 + .../web/frontend/extracted-translations.json | 5 + .../components/invite-managed-root.tsx | 12 + .../pages/user/subscription/invite-managed.js | 8 + .../stylesheets/modules/managed-users.less | 2 +- services/web/locales/en.json | 4 + .../Subscription/TeamInvitesHandlerTests.js | 68 ++-- 11 files changed, 297 insertions(+), 266 deletions(-) create mode 100644 services/web/app/views/subscriptions/team/invite-managed.pug create mode 100644 services/web/frontend/js/features/subscription/components/invite-managed-root.tsx create mode 100644 services/web/frontend/js/pages/user/subscription/invite-managed.js diff --git a/services/web/app/src/Features/Subscription/TeamInvitesController.js b/services/web/app/src/Features/Subscription/TeamInvitesController.js index 606e7e094c..341406a019 100644 --- a/services/web/app/src/Features/Subscription/TeamInvitesController.js +++ b/services/web/app/src/Features/Subscription/TeamInvitesController.js @@ -6,6 +6,8 @@ const ErrorController = require('../Errors/ErrorController') const EmailHelper = require('../Helpers/EmailHelper') const UserGetter = require('../User/UserGetter') const { expressify } = require('../../util/promises') +const HttpErrorHandler = require('../Errors/HttpErrorHandler') +const PermissionsManager = require('../Authorization/PermissionsManager') function createInvite(req, res, next) { const teamManagerId = SessionManager.getLoggedInUserId(req.session) @@ -53,11 +55,14 @@ async function viewInvite(req, res, next) { const { token } = req.params const userId = SessionManager.getLoggedInUserId(req.session) - const { invite } = await TeamInvitesHandler.promises.getInvite(token) + const { invite, subscription } = await TeamInvitesHandler.promises.getInvite( + token + ) if (!invite) { return ErrorController.notFound(req, res) } + let validationStatus = new Map() if (userId) { const personalSubscription = await SubscriptionLocator.promises.getUsersSubscription(userId) @@ -69,19 +74,51 @@ async function viewInvite(req, res, next) { personalSubscription.recurlySubscription_id && personalSubscription.recurlySubscription_id !== '' - res.render('subscriptions/team/invite', { - inviterName: invite.inviterName, - inviteToken: invite.token, - hasIndividualRecurlySubscription, - appName: settings.appName, - expired: req.query.expired, - }) + if (subscription?.groupPolicy) { + if (!subscription.populated('groupPolicy')) { + await subscription.populate('groupPolicy') + } + + const user = await UserGetter.promises.getUser(userId) + + if ( + user.enrollment?.managedBy && + user.enrollment?.managedBy.toString() !== subscription._id.toString() + ) { + return HttpErrorHandler.forbidden( + req, + res, + 'User is already managed by a different subscription' + ) + } + + validationStatus = + await PermissionsManager.promises.getUserValidationStatus( + user, + subscription.groupPolicy + ) + + return res.render('subscriptions/team/invite-managed', { + inviterName: invite.inviterName, + inviteToken: invite.token, + expired: req.query.expired, + validationStatus: Object.fromEntries(validationStatus), + }) + } else { + return res.render('subscriptions/team/invite', { + inviterName: invite.inviterName, + inviteToken: invite.token, + hasIndividualRecurlySubscription, + appName: settings.appName, + expired: req.query.expired, + }) + } } else { const userByEmail = await UserGetter.promises.getUserByMainEmail( invite.email ) - res.render('subscriptions/team/invite_logged_out', { + return res.render('subscriptions/team/invite_logged_out', { inviterName: invite.inviterName, inviteToken: invite.token, appName: settings.appName, @@ -91,16 +128,12 @@ async function viewInvite(req, res, next) { } } -function acceptInvite(req, res, next) { +async function acceptInvite(req, res, next) { const { token } = req.params const userId = SessionManager.getLoggedInUserId(req.session) - TeamInvitesHandler.acceptInvite(token, userId, function (err, results) { - if (err) { - return next(err) - } - res.sendStatus(204) - }) + await TeamInvitesHandler.promises.acceptInvite(token, userId) + res.sendStatus(204) } function revokeInvite(req, res, next) { @@ -127,6 +160,6 @@ function revokeInvite(req, res, next) { module.exports = { createInvite, viewInvite: expressify(viewInvite), - acceptInvite, + acceptInvite: expressify(acceptInvite), revokeInvite, } diff --git a/services/web/app/src/Features/Subscription/TeamInvitesHandler.js b/services/web/app/src/Features/Subscription/TeamInvitesHandler.js index eaf4fca1ea..8a5967e6dc 100644 --- a/services/web/app/src/Features/Subscription/TeamInvitesHandler.js +++ b/services/web/app/src/Features/Subscription/TeamInvitesHandler.js @@ -1,6 +1,5 @@ const logger = require('@overleaf/logger') const crypto = require('crypto') -const async = require('async') const settings = require('@overleaf/settings') const { ObjectId } = require('mongodb') @@ -11,220 +10,164 @@ const UserGetter = require('../User/UserGetter') const SubscriptionLocator = require('./SubscriptionLocator') const SubscriptionUpdater = require('./SubscriptionUpdater') const LimitationsManager = require('./LimitationsManager') +const ManagedUsersHandler = require('./ManagedUsersHandler') const EmailHandler = require('../Email/EmailHandler') const EmailHelper = require('../Helpers/EmailHelper') const Errors = require('../Errors/Errors') -const { promisifyMultiResult, promisify } = require('../../util/promises') +const { callbackify, callbackifyMultiResult } = require('../../util/promises') -function getInvite(token, callback) { - Subscription.findOne( - { 'teamInvites.token': token }, - function (err, subscription) { - if (err) { - return callback(err) - } - if (!subscription) { - return callback(new Errors.NotFoundError('team not found')) - } +async function getInvite(token) { + const subscription = await Subscription.findOne({ + 'teamInvites.token': token, + }) + if (!subscription) { + throw new Errors.NotFoundError('team not found') + } - const invite = subscription.teamInvites.find(i => i.token === token) - callback(null, invite, subscription) - } - ) + const invite = subscription.teamInvites.find(i => i.token === token) + return { invite, subscription } } -function createInvite(teamManagerId, subscription, email, callback) { +async function createInvite(teamManagerId, subscription, email) { email = EmailHelper.parseEmail(email) if (!email) { - return callback(new Error('invalid email')) + throw new Error('invalid email') } - UserGetter.getUser(teamManagerId, function (error, teamManager) { - if (error) { - return callback(error) - } + const teamManager = await UserGetter.promises.getUser(teamManagerId) - _removeLegacyInvite(subscription.id, email, function (error) { - if (error) { - return callback(error) - } - _createInvite(subscription, email, teamManager, callback) - }) - }) + await _removeLegacyInvite(subscription.id, email) + return _createInvite(subscription, email, teamManager) } -function importInvite( - subscription, - inviterName, - email, - token, - sentAt, - callback -) { - _checkIfInviteIsPossible( +async function importInvite(subscription, inviterName, email, token, sentAt) { + const { possible, reason } = await _checkIfInviteIsPossible( subscription, - email, - function (error, possible, reason) { - if (error) { - return callback(error) - } - if (!possible) { - return callback(reason) - } - - subscription.teamInvites.push({ - email, - inviterName, - token, - sentAt, - }) - - subscription.save(callback) - } + email ) -} - -function acceptInvite(token, userId, callback) { - getInvite(token, function (err, invite, subscription) { - if (err) { - return callback(err) - } - if (!invite) { - return callback(new Errors.NotFoundError('invite not found')) - } - - SubscriptionUpdater.addUserToGroup( - subscription._id, - userId, - function (err) { - if (err) { - return callback(err) - } - - _removeInviteFromTeam(subscription.id, invite.email, callback) - } - ) + if (!possible) { + throw reason + } + subscription.teamInvites.push({ + email, + inviterName, + token, + sentAt, }) + + return subscription.save() } -function revokeInvite(teamManagerId, subscription, email, callback) { +async function acceptInvite(token, userId) { + const { invite, subscription } = await getInvite(token) + if (!invite) { + throw new Errors.NotFoundError('invite not found') + } + + await SubscriptionUpdater.promises.addUserToGroup(subscription._id, userId) + + if (subscription.groupPolicy) { + await ManagedUsersHandler.promises.enrollInSubscription( + userId, + subscription + ) + } + + await _removeInviteFromTeam(subscription.id, invite.email) +} + +async function revokeInvite(teamManagerId, subscription, email) { email = EmailHelper.parseEmail(email) if (!email) { - return callback(new Error('invalid email')) + throw new Error('invalid email') } - _removeInviteFromTeam(subscription.id, email, callback) + await _removeInviteFromTeam(subscription.id, email) } // Legacy method to allow a user to receive a confirmation email if their // email is in Subscription.invited_emails when they join. We'll remove this // after a short while. -function createTeamInvitesForLegacyInvitedEmail(email, callback) { - SubscriptionLocator.getGroupsWithEmailInvite(email, function (err, teams) { - if (err) { - return callback(err) - } +async function createTeamInvitesForLegacyInvitedEmail(email) { + const teams = await SubscriptionLocator.promises.getGroupsWithEmailInvite( + email + ) - async.map( - teams, - (team, cb) => createInvite(team.admin_id, team, email, cb), - callback - ) - }) -} - -function _createInvite(subscription, email, inviter, callback) { - _checkIfInviteIsPossible( - subscription, - email, - function (error, possible, reason) { - if (error) { - return callback(error) - } - if (!possible) { - return callback(reason) - } - - // don't send invites when inviting self; add user directly to the group - const isInvitingSelf = inviter.emails.some( - emailData => emailData.email === email - ) - if (isInvitingSelf) { - return SubscriptionUpdater.addUserToGroup( - subscription._id, - inviter._id, - err => { - if (err) { - return callback(err) - } - - // legacy: remove any invite that might have been created in the past - _removeInviteFromTeam(subscription._id, email, error => { - const inviteUserData = { - email: inviter.email, - first_name: inviter.first_name, - last_name: inviter.last_name, - invite: false, - } - callback(error, inviteUserData) - }) - } - ) - } - - const inviterName = _getInviterName(inviter) - let invite = subscription.teamInvites.find( - invite => invite.email === email - ) - - if (invite) { - invite = invite.toObject() - invite.sentAt = new Date() - } else { - invite = { - email, - inviterName, - token: crypto.randomBytes(32).toString('hex'), - sentAt: new Date(), - } - subscription.teamInvites.push(invite) - } - - subscription.save(function (error) { - if (error) { - return callback(error) - } - - const opts = { - to: email, - inviter, - acceptInviteUrl: `${settings.siteUrl}/subscription/invites/${invite.token}/`, - appName: settings.appName, - } - EmailHandler.sendEmail('verifyEmailToJoinTeam', opts, error => { - Object.assign(invite, { invite: true }) - callback(error, invite) - }) - }) - } + return Promise.all( + teams.map(team => createInvite(team.admin_id, team, email)) ) } -function _removeInviteFromTeam(subscriptionId, email, callback) { +async function _createInvite(subscription, email, inviter) { + const { possible, reason } = await _checkIfInviteIsPossible( + subscription, + email + ) + + if (!possible) { + throw reason + } + + // don't send invites when inviting self; add user directly to the group + const isInvitingSelf = inviter.emails.some( + emailData => emailData.email === email + ) + if (isInvitingSelf) { + await SubscriptionUpdater.promises.addUserToGroup( + subscription._id, + inviter._id + ) + + // legacy: remove any invite that might have been created in the past + await _removeInviteFromTeam(subscription._id, email) + + return { + email: inviter.email, + first_name: inviter.first_name, + last_name: inviter.last_name, + invite: false, + } + } + + const inviterName = _getInviterName(inviter) + let invite = subscription.teamInvites.find(invite => invite.email === email) + + if (invite) { + invite = invite.toObject() + invite.sentAt = new Date() + } else { + invite = { + email, + inviterName, + token: crypto.randomBytes(32).toString('hex'), + sentAt: new Date(), + } + subscription.teamInvites.push(invite) + } + + await subscription.save() + + const opts = { + to: email, + inviter, + acceptInviteUrl: `${settings.siteUrl}/subscription/invites/${invite.token}/`, + appName: settings.appName, + } + await EmailHandler.promises.sendEmail('verifyEmailToJoinTeam', opts) + Object.assign(invite, { invite: true }) + return invite +} + +async function _removeInviteFromTeam(subscriptionId, email, callback) { const searchConditions = { _id: new ObjectId(subscriptionId.toString()) } const removeInvite = { $pull: { teamInvites: { email } } } - async.series( - [ - cb => Subscription.updateOne(searchConditions, removeInvite, cb), - cb => _removeLegacyInvite(subscriptionId, email, cb), - ], - callback - ) + await Subscription.updateOne(searchConditions, removeInvite) + await _removeLegacyInvite(subscriptionId, email) } -const _removeLegacyInvite = (subscriptionId, email, callback) => - Subscription.updateOne( +async function _removeLegacyInvite(subscriptionId, email) { + await Subscription.updateOne( { _id: new ObjectId(subscriptionId.toString()), }, @@ -232,17 +175,17 @@ const _removeLegacyInvite = (subscriptionId, email, callback) => $pull: { invited_emails: email, }, - }, - callback + } ) +} -function _checkIfInviteIsPossible(subscription, email, callback) { +async function _checkIfInviteIsPossible(subscription, email) { if (!subscription.groupPlan) { logger.debug( { subscriptionId: subscription.id }, 'can not add members to a subscription that is not in a group plan' ) - return callback(null, false, { wrongPlan: true }) + return { possible: false, reason: { wrongPlan: true } } } if (LimitationsManager.teamHasReachedMemberLimit(subscription)) { @@ -250,31 +193,27 @@ function _checkIfInviteIsPossible(subscription, email, callback) { { subscriptionId: subscription.id }, 'team has reached member limit' ) - return callback(null, false, { limitReached: true }) + return { possible: false, reason: { limitReached: true } } } - UserGetter.getUserByAnyEmail(email, function (error, existingUser) { - if (error) { - return callback(error) - } - if (!existingUser) { - return callback(null, true) - } + const existingUser = await UserGetter.promises.getUserByAnyEmail(email) + if (!existingUser) { + return { possible: true } + } - const existingMember = subscription.member_ids.find( - memberId => memberId.toString() === existingUser._id.toString() + const existingMember = subscription.member_ids.find( + memberId => memberId.toString() === existingUser._id.toString() + ) + + if (existingMember) { + logger.debug( + { subscriptionId: subscription.id, email }, + 'user already in team' ) - - if (existingMember) { - logger.debug( - { subscriptionId: subscription.id, email }, - 'user already in team' - ) - callback(null, false, { alreadyInTeam: true }) - } else { - callback(null, true) - } - }) + return { possible: false, reason: { alreadyInTeam: true } } + } else { + return { possible: true } + } } function _getInviterName(inviter) { @@ -289,20 +228,20 @@ function _getInviterName(inviter) { } module.exports = { - getInvite, - createInvite, - importInvite, - acceptInvite, - revokeInvite, - createTeamInvitesForLegacyInvitedEmail, + getInvite: callbackifyMultiResult(getInvite, ['invite', 'subscription']), + createInvite: callbackify(createInvite), + importInvite: callbackify(importInvite), + acceptInvite: callbackify(acceptInvite), + revokeInvite: callbackify(revokeInvite), + createTeamInvitesForLegacyInvitedEmail: callbackify( + createTeamInvitesForLegacyInvitedEmail + ), promises: { - getInvite: promisifyMultiResult(getInvite, ['invite', 'subscription']), - createInvite: promisify(createInvite), - importInvite: promisify(importInvite), - acceptInvite: promisify(acceptInvite), - revokeInvite: promisify(revokeInvite), - createTeamInvitesForLegacyInvitedEmail: promisify( - createTeamInvitesForLegacyInvitedEmail - ), + getInvite, + createInvite, + importInvite, + acceptInvite, + revokeInvite, + createTeamInvitesForLegacyInvitedEmail, }, } diff --git a/services/web/app/views/subscriptions/team/invite-managed.pug b/services/web/app/views/subscriptions/team/invite-managed.pug new file mode 100644 index 0000000000..29b6ec61e3 --- /dev/null +++ b/services/web/app/views/subscriptions/team/invite-managed.pug @@ -0,0 +1,14 @@ +extends ../../layout-marketing + +block entrypointVar + - entrypoint = 'pages/user/subscription/invite-managed' + +block append meta + meta(name="ol-inviteToken" content=inviteToken) + meta(name="ol-inviterName" content=inviterName) + meta(name="ol-expired" data-type="boolean" content=expired) + meta(name="ol-alreadyEnrolled" data-type="boolean" content=alreadyEnrolled) + meta(name="ol-validationStatus" data-type="json" content=validationStatus) + +block content + main.content.content-alt.team-invite#invite-managed-root diff --git a/services/web/app/views/subscriptions/team/invite.pug b/services/web/app/views/subscriptions/team/invite.pug index decc046f00..fc34a5f0f2 100644 --- a/services/web/app/views/subscriptions/team/invite.pug +++ b/services/web/app/views/subscriptions/team/invite.pug @@ -28,10 +28,11 @@ block content div(ng-show="view =='teamInvite'") p #{translate("join_team_explanation", {appName: appName})} - p - a.btn.btn-secondary(href="/project") #{translate("not_now")} - |   - a.btn.btn.btn-primary(ng-click="joinTeam()", ng-disabled="inflight") #{translate("accept_invitation")} + if (!expired) + p + a.btn.btn-secondary(href="/project") #{translate("not_now")} + |   + a.btn.btn.btn-primary(ng-click="joinTeam()", ng-disabled="inflight") #{translate("accept_invitation")} div(ng-show="view =='inviteAccepted'") p(ng-non-bindable) #{translate("joined_team", {inviterName: inviterName})} diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js index 80c39035f0..18319c6d84 100644 --- a/services/web/config/settings.defaults.js +++ b/services/web/config/settings.defaults.js @@ -809,6 +809,7 @@ module.exports = { editorLeftMenuManageTemplate: [], oauth2Server: [], managedGroupSubscriptionEnrollmentNotification: [], + managedGroupEnrollmentInvite: [], }, moduleImportSequence: ['launchpad', 'server-ce-scripts', 'user-activate'], diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 645144f71f..12cfa29ab4 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -197,6 +197,7 @@ "current_file": "", "current_password": "", "currently_seeing_only_24_hrs_history": "", + "currently_signed_in_as_x": "", "currently_subscribed_to_plan": "", "customize_your_group_subscription": "", "customizing_figures": "", @@ -280,6 +281,7 @@ "educational_percent_discount_applied": "", "email": "", "email_limit_reached": "", + "email_link_expired": "", "email_or_password_wrong_try_again": "", "emails_and_affiliations_explanation": "", "emails_and_affiliations_title": "", @@ -581,6 +583,7 @@ "log_entry_maximum_entries_title": "", "log_hint_extra_info": "", "log_in_with_primary_email_address": "", + "log_out_lowercase_dot": "", "log_viewer_error": "", "login_to_transfer_account": "", "login_with_service": "", @@ -803,6 +806,8 @@ "read_only": "", "read_only_token": "", "read_write_token": "", + "ready_to_join_x": "", + "ready_to_join_x_in_group_y": "", "realtime_track_changes": "", "reauthorize_github_account": "", "recaptcha_conditions": "", diff --git a/services/web/frontend/js/features/subscription/components/invite-managed-root.tsx b/services/web/frontend/js/features/subscription/components/invite-managed-root.tsx new file mode 100644 index 0000000000..58906ac650 --- /dev/null +++ b/services/web/frontend/js/features/subscription/components/invite-managed-root.tsx @@ -0,0 +1,12 @@ +import { JSXElementConstructor } from 'react' +import importOverleafModules from '../../../../macros/import-overleaf-module.macro' + +const [inviteManagedModule] = importOverleafModules( + 'managedGroupEnrollmentInvite' +) +const InviteManaged: JSXElementConstructor> = + inviteManagedModule?.import.default + +export default function InviteManagedRoot() { + return +} diff --git a/services/web/frontend/js/pages/user/subscription/invite-managed.js b/services/web/frontend/js/pages/user/subscription/invite-managed.js new file mode 100644 index 0000000000..4a593aea17 --- /dev/null +++ b/services/web/frontend/js/pages/user/subscription/invite-managed.js @@ -0,0 +1,8 @@ +import './base' +import ReactDOM from 'react-dom' +import InvitedManagedRoot from '../../../features/subscription/components/invite-managed-root' + +const element = document.getElementById('invite-managed-root') +if (element) { + ReactDOM.render(, element) +} diff --git a/services/web/frontend/stylesheets/modules/managed-users.less b/services/web/frontend/stylesheets/modules/managed-users.less index 3fb6ecb72c..7ba835a065 100644 --- a/services/web/frontend/stylesheets/modules/managed-users.less +++ b/services/web/frontend/stylesheets/modules/managed-users.less @@ -25,7 +25,7 @@ .icon { display: flex; - flex: 1 1 10%; + flex: 0 0 32px; > span { font-size: 16px; } diff --git a/services/web/locales/en.json b/services/web/locales/en.json index ac20f4ecb0..c3ae16f29f 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -313,6 +313,7 @@ "current_password": "Current Password", "current_session": "Current Session", "currently_seeing_only_24_hrs_history": "You’re currently seeing the last 24 hours of changes in this project.", + "currently_signed_in_as_x": "Currently signed in as <0>__userEmail__.", "currently_subscribed_to_plan": "You are currently subscribed to the <0>__planName__ plan.", "custom_resource_portal": "Custom resource portal", "custom_resource_portal_info": "You can have your own custom portal page on Overleaf. This is a great place for your users to find out more about Overleaf, access templates, FAQs and Help resources, and sign up to Overleaf.", @@ -924,6 +925,7 @@ "log_in_with_primary_email_address": "This will be the email address to use if you log in with an email address and password. Important __appName__ notifications will be sent to this email address.", "log_out": "Log Out", "log_out_from": "Log out from __email__", + "log_out_lowercase_dot": "Log out.", "log_viewer_error": "There was a problem displaying this project’s compilation errors and logs.", "logged_in_with_email": "You are currently logged in to __appName__ with the email __email__.", "logging_in": "Logging in", @@ -1266,6 +1268,8 @@ "read_only": "Read Only", "read_only_token": "Read-Only Token", "read_write_token": "Read-Write Token", + "ready_to_join_x": "You’re ready to join __inviterName__", + "ready_to_join_x_in_group_y": "You’re ready to join __inviterName__ in __groupName__", "real_time_track_changes": "Real-time <0>track-changes", "realtime_collab": "Real-time collaboration", "realtime_collab_info": "When you’re working together, you can see your collaborators’ cursors and their changes in real time, so everyone always has the latest version.", diff --git a/services/web/test/unit/src/Subscription/TeamInvitesHandlerTests.js b/services/web/test/unit/src/Subscription/TeamInvitesHandlerTests.js index 1d28f4c37c..59eea28c2d 100644 --- a/services/web/test/unit/src/Subscription/TeamInvitesHandlerTests.js +++ b/services/web/test/unit/src/Subscription/TeamInvitesHandlerTests.js @@ -33,21 +33,27 @@ describe('TeamInvitesHandler', function () { groupPlan: true, member_ids: [], teamInvites: [this.teamInvite], - save: sinon.stub().yields(null), + save: sinon.stub().resolves(), } this.SubscriptionLocator = { - getUsersSubscription: sinon.stub(), - getSubscription: sinon.stub().yields(null, this.subscription), + promises: { + getUsersSubscription: sinon.stub(), + getSubscription: sinon.stub().resolves(this.subscription), + }, } this.UserGetter = { - getUser: sinon.stub().yields(), - getUserByAnyEmail: sinon.stub().yields(), + promises: { + getUser: sinon.stub().resolves(), + getUserByAnyEmail: sinon.stub().resolves(), + }, } this.SubscriptionUpdater = { - addUserToGroup: sinon.stub().yields(), + promises: { + addUserToGroup: sinon.stub().resolves(), + }, } this.LimitationsManager = { @@ -55,12 +61,20 @@ describe('TeamInvitesHandler', function () { } this.Subscription = { - findOne: sinon.stub().yields(), - updateOne: sinon.stub().yields(), + findOne: sinon.stub().resolves(), + updateOne: sinon.stub().resolves(), } this.EmailHandler = { - sendEmail: sinon.stub().yields(null), + promises: { + sendEmail: sinon.stub().resolves(null), + }, + } + + this.ManagedUsersHandler = { + promises: { + enrollInSubscription: sinon.stub().resolves(), + }, } this.newToken = 'bbbbbbbbb' @@ -71,18 +85,17 @@ describe('TeamInvitesHandler', function () { }, } - this.UserGetter.getUser + this.UserGetter.promises.getUser .withArgs(this.manager._id) - .yields(null, this.manager) - this.UserGetter.getUserByAnyEmail + .resolves(this.manager) + this.UserGetter.promises.getUserByAnyEmail .withArgs(this.manager.email) - .yields(null, this.manager) + .resolves(this.manager) - this.SubscriptionLocator.getUsersSubscription.yields( - null, + this.SubscriptionLocator.promises.getUsersSubscription.resolves( this.subscription ) - this.Subscription.findOne.yields(null, this.subscription) + this.Subscription.findOne.resolves(this.subscription) this.TeamInvitesHandler = SandboxedModule.require(modulePath, { requires: { @@ -96,6 +109,7 @@ describe('TeamInvitesHandler', function () { './SubscriptionUpdater': this.SubscriptionUpdater, './LimitationsManager': this.LimitationsManager, '../Email/EmailHandler': this.EmailHandler, + './ManagedUsersHandler': this.ManagedUsersHandler, }, }) }) @@ -114,7 +128,7 @@ describe('TeamInvitesHandler', function () { }) it("returns teamNotFound if there's none", function (done) { - this.Subscription.findOne = sinon.stub().yields(null, null) + this.Subscription.findOne = sinon.stub().resolves(null) this.TeamInvitesHandler.getInvite( this.token, @@ -152,7 +166,7 @@ describe('TeamInvitesHandler', function () { this.subscription, 'John.Snow@example.com', (err, invite) => { - this.EmailHandler.sendEmail + this.EmailHandler.promises.sendEmail .calledWith( 'verifyEmailToJoinTeam', sinon.match({ @@ -214,7 +228,7 @@ describe('TeamInvitesHandler', function () { this.manager.email, (err, invite) => { sinon.assert.calledWith( - this.SubscriptionUpdater.addUserToGroup, + this.SubscriptionUpdater.promises.addUserToGroup, this.subscription._id, this.manager._id ) @@ -266,9 +280,9 @@ describe('TeamInvitesHandler', function () { email: 'tyrion@example.com', } - this.UserGetter.getUserByAnyEmail + this.UserGetter.promises.getUserByAnyEmail .withArgs(this.user.email) - .yields(null, this.user) + .resolves(this.user) this.subscription.teamInvites.push({ email: 'john.snow@example.com', @@ -279,7 +293,7 @@ describe('TeamInvitesHandler', function () { it('adds the user to the team', function (done) { this.TeamInvitesHandler.acceptInvite('dddddddd', this.user.id, () => { - this.SubscriptionUpdater.addUserToGroup + this.SubscriptionUpdater.promises.addUserToGroup .calledWith(this.subscription._id, this.user.id) .should.eq(true) done() @@ -331,10 +345,10 @@ describe('TeamInvitesHandler', function () { 'eddard@example.com', 'robert@example.com', ] - this.TeamInvitesHandler.createInvite = sinon.stub().yields(null) - this.SubscriptionLocator.getGroupsWithEmailInvite = sinon + this.TeamInvitesHandler.createInvite = sinon.stub().resolves(null) + this.SubscriptionLocator.promises.getGroupsWithEmailInvite = sinon .stub() - .yields(null, [this.subscription]) + .resolves([this.subscription]) }) it('sends an invitation email to addresses in the legacy invited_emails field', function (done) { @@ -396,9 +410,9 @@ describe('TeamInvitesHandler', function () { } this.subscription.member_ids = [member.id] - this.UserGetter.getUserByAnyEmail + this.UserGetter.promises.getUserByAnyEmail .withArgs(member.email) - .yields(null, member) + .resolves(member) this.TeamInvitesHandler.createInvite( this.manager._id,