Merge pull request #13850 from overleaf/ab-invite-enrollment

[web] Managed users - combined invite/surrender flow

GitOrigin-RevId: 70cb0d81e0019eac69a4a565377447bb6d1a1823
This commit is contained in:
June Kelly
2023-07-14 10:11:57 +01:00
committed by Copybot
parent 09b5892fca
commit 2abec5c638
11 changed files with 297 additions and 266 deletions
@@ -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,
}
@@ -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,
},
}
@@ -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
@@ -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})}
+1
View File
@@ -809,6 +809,7 @@ module.exports = {
editorLeftMenuManageTemplate: [],
oauth2Server: [],
managedGroupSubscriptionEnrollmentNotification: [],
managedGroupEnrollmentInvite: [],
},
moduleImportSequence: ['launchpad', 'server-ce-scripts', 'user-activate'],
@@ -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": "",
@@ -0,0 +1,12 @@
import { JSXElementConstructor } from 'react'
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
const [inviteManagedModule] = importOverleafModules(
'managedGroupEnrollmentInvite'
)
const InviteManaged: JSXElementConstructor<Record<string, never>> =
inviteManagedModule?.import.default
export default function InviteManagedRoot() {
return <InviteManaged />
}
@@ -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(<InvitedManagedRoot />, element)
}
@@ -25,7 +25,7 @@
.icon {
display: flex;
flex: 1 1 10%;
flex: 0 0 32px;
> span {
font-size: 16px;
}
+4
View File
@@ -313,6 +313,7 @@
"current_password": "Current Password",
"current_session": "Current Session",
"currently_seeing_only_24_hrs_history": "Youre currently seeing the last 24 hours of changes in this project.",
"currently_signed_in_as_x": "Currently signed in as <0>__userEmail__</0>.",
"currently_subscribed_to_plan": "You are currently subscribed to the <0>__planName__</0> 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 projects compilation errors and logs.",
"logged_in_with_email": "You are currently logged in to <b>__appName__</b> with the email <b>__email__</b>.",
"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": "Youre ready to join __inviterName__",
"ready_to_join_x_in_group_y": "Youre ready to join __inviterName__ in __groupName__",
"real_time_track_changes": "Real-time <0>track-changes</0>",
"realtime_collab": "Real-time collaboration",
"realtime_collab_info": "When youre working together, you can see your collaborators cursors and their changes in real time, so everyone always has the latest version.",
@@ -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,