TeamInvitesHandler.test.mjs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711
  1. import { vi, expect } from 'vitest'
  2. import sinon from 'sinon'
  3. import mongodb from 'mongodb-legacy'
  4. import Errors from '../../../../app/src/Features/Errors/Errors.js'
  5. const modulePath =
  6. '../../../../app/src/Features/Subscription/TeamInvitesHandler'
  7. vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
  8. vi.importActual('../../../../app/src/Features/Errors/Errors.js')
  9. )
  10. const { ObjectId } = mongodb
  11. describe('TeamInvitesHandler', function () {
  12. beforeEach(async function (ctx) {
  13. ctx.manager = {
  14. _id: '666666',
  15. first_name: 'Daenerys',
  16. last_name: 'Targaryen',
  17. email: 'daenerys@example.com',
  18. emails: [{ email: 'daenerys@example.com' }],
  19. }
  20. ctx.token = 'aaaaaaaaaaaaaaaaaaaaaa'
  21. ctx.teamInvite = {
  22. email: 'jorah@example.com',
  23. token: ctx.token,
  24. }
  25. // ensure teamInvite can be converted from Document to Object
  26. ctx.teamInvite.toObject = () => ctx.teamInvite
  27. ctx.subscription = {
  28. id: '55153a8014829a865bbf700d',
  29. _id: new ObjectId('55153a8014829a865bbf700d'),
  30. recurlySubscription_id: '1a2b3c4d5e6f7g',
  31. admin_id: ctx.manager._id,
  32. groupPlan: true,
  33. member_ids: [],
  34. teamInvites: [ctx.teamInvite],
  35. save: sinon.stub().resolves(),
  36. }
  37. ctx.SubscriptionLocator = {
  38. promises: {
  39. getUsersSubscription: sinon.stub(),
  40. getSubscription: sinon.stub().resolves(ctx.subscription),
  41. },
  42. }
  43. ctx.UserGetter = {
  44. promises: {
  45. getUser: sinon.stub().resolves(),
  46. getUserByAnyEmail: sinon.stub().resolves(),
  47. getUserByMainEmail: sinon.stub().resolves(),
  48. },
  49. }
  50. ctx.SubscriptionUpdater = {
  51. promises: {
  52. addUserToGroup: sinon.stub().resolves(),
  53. deleteSubscription: sinon.stub().resolves(),
  54. },
  55. }
  56. ctx.LimitationsManager = {
  57. teamHasReachedMemberLimit: sinon.stub().returns(false),
  58. }
  59. ctx.Subscription = {
  60. findOne: sinon.stub().resolves(),
  61. updateOne: sinon.stub().resolves(),
  62. }
  63. ctx.SSOConfig = {
  64. findById: sinon.stub().resolves(),
  65. }
  66. ctx.EmailHandler = {
  67. promises: {
  68. sendEmail: sinon.stub().resolves(null),
  69. },
  70. }
  71. ctx.newToken = 'bbbbbbbbb'
  72. ctx.crypto = {
  73. randomBytes: () => {
  74. return { toString: sinon.stub().returns(ctx.newToken) }
  75. },
  76. }
  77. ctx.UserGetter.promises.getUser
  78. .withArgs(ctx.manager._id)
  79. .resolves(ctx.manager)
  80. ctx.UserGetter.promises.getUserByAnyEmail
  81. .withArgs(ctx.manager.email)
  82. .resolves(ctx.manager)
  83. ctx.UserGetter.promises.getUserByMainEmail
  84. .withArgs(ctx.manager.email)
  85. .resolves(ctx.manager)
  86. ctx.SubscriptionLocator.promises.getUsersSubscription.resolves(
  87. ctx.subscription
  88. )
  89. ctx.NotificationsBuilder = {
  90. promises: {
  91. groupInvitation: sinon.stub().returns({
  92. create: sinon.stub().resolves(),
  93. read: sinon.stub().resolves(),
  94. }),
  95. },
  96. }
  97. ctx.Subscription.findOne.resolves(ctx.subscription)
  98. ctx.RecurlyClient = {
  99. promises: {
  100. terminateSubscriptionByUuid: sinon.stub().resolves(),
  101. },
  102. }
  103. vi.doMock('mongodb-legacy', () => ({
  104. default: { ObjectId },
  105. }))
  106. vi.doMock('crypto', () => ({
  107. default: ctx.crypto,
  108. }))
  109. vi.doMock('@overleaf/settings', () => ({
  110. default: { siteUrl: 'http://example.com' },
  111. }))
  112. vi.doMock('../../../../app/src/models/TeamInvite', () => ({
  113. TeamInvite: (ctx.TeamInvite = {}),
  114. }))
  115. vi.doMock('../../../../app/src/models/Subscription', () => ({
  116. Subscription: ctx.Subscription,
  117. }))
  118. vi.doMock('../../../../app/src/models/SSOConfig', () => ({
  119. SSOConfig: ctx.SSOConfig,
  120. }))
  121. vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
  122. default: ctx.UserGetter,
  123. }))
  124. vi.doMock(
  125. '../../../../app/src/Features/Subscription/SubscriptionLocator',
  126. () => ({
  127. default: ctx.SubscriptionLocator,
  128. })
  129. )
  130. vi.doMock(
  131. '../../../../app/src/Features/Subscription/SubscriptionUpdater',
  132. () => ({
  133. default: ctx.SubscriptionUpdater,
  134. })
  135. )
  136. vi.doMock(
  137. '../../../../app/src/Features/Subscription/LimitationsManager',
  138. () => ({
  139. default: ctx.LimitationsManager,
  140. })
  141. )
  142. vi.doMock('../../../../app/src/Features/Email/EmailHandler', () => ({
  143. default: ctx.EmailHandler,
  144. }))
  145. vi.doMock(
  146. '../../../../app/src/Features/Notifications/NotificationsBuilder',
  147. () => ({
  148. default: ctx.NotificationsBuilder,
  149. })
  150. )
  151. vi.doMock('../../../../app/src/infrastructure/Modules', () => ({
  152. default: (ctx.Modules = {
  153. promises: { hooks: { fire: sinon.stub().resolves() } },
  154. }),
  155. }))
  156. vi.doMock(
  157. '../../../../app/src/Features/Subscription/RecurlyClient',
  158. () => ({
  159. default: ctx.RecurlyClient,
  160. })
  161. )
  162. ctx.TeamInvitesHandler = (await import(modulePath)).default
  163. })
  164. describe('getInvite', function () {
  165. it("returns the invite if there's one", async function (ctx) {
  166. const { invite, subscription } =
  167. await ctx.TeamInvitesHandler.promises.getInvite(ctx.token)
  168. expect(invite).to.deep.eq(ctx.teamInvite)
  169. expect(subscription).to.deep.eq(ctx.subscription)
  170. })
  171. it("returns teamNotFound if there's none", async function (ctx) {
  172. ctx.Subscription.findOne = sinon.stub().resolves(null)
  173. let error
  174. try {
  175. await ctx.TeamInvitesHandler.promises.getInvite(ctx.token)
  176. } catch (err) {
  177. error = err
  178. }
  179. expect(error).to.be.instanceOf(Errors.NotFoundError)
  180. })
  181. })
  182. describe('createInvite', function () {
  183. it('adds the team invite to the subscription', async function (ctx) {
  184. const invite = await ctx.TeamInvitesHandler.promises.createInvite(
  185. ctx.manager._id,
  186. ctx.subscription,
  187. 'John.Snow@example.com'
  188. )
  189. expect(invite.token).to.eq(ctx.newToken)
  190. expect(invite.email).to.eq('john.snow@example.com')
  191. expect(invite.inviterName).to.eq(
  192. 'Daenerys Targaryen (daenerys@example.com)'
  193. )
  194. expect(invite.invite).to.be.true
  195. expect(ctx.subscription.teamInvites).to.deep.include(invite)
  196. })
  197. it('sends an email', async function (ctx) {
  198. await ctx.TeamInvitesHandler.promises.createInvite(
  199. ctx.manager._id,
  200. ctx.subscription,
  201. 'John.Snow@example.com'
  202. )
  203. ctx.EmailHandler.promises.sendEmail
  204. .calledWith(
  205. 'verifyEmailToJoinTeam',
  206. sinon.match({
  207. to: 'john.snow@example.com',
  208. inviter: ctx.manager,
  209. acceptInviteUrl: `http://example.com/subscription/invites/${ctx.newToken}/`,
  210. })
  211. )
  212. .should.equal(true)
  213. })
  214. it('refreshes the existing invite if the email has already been invited', async function (ctx) {
  215. const originalInvite = Object.assign({}, ctx.teamInvite)
  216. const invite = await ctx.TeamInvitesHandler.promises.createInvite(
  217. ctx.manager._id,
  218. ctx.subscription,
  219. originalInvite.email
  220. )
  221. expect(invite).to.exist
  222. expect(ctx.subscription.teamInvites.length).to.eq(1)
  223. expect(ctx.subscription.teamInvites).to.deep.include(invite)
  224. expect(invite.email).to.eq(originalInvite.email)
  225. ctx.subscription.save.calledOnce.should.eq(true)
  226. })
  227. it('removes any legacy invite from the subscription', async function (ctx) {
  228. await ctx.TeamInvitesHandler.promises.createInvite(
  229. ctx.manager._id,
  230. ctx.subscription,
  231. 'John.Snow@example.com'
  232. )
  233. ctx.Subscription.updateOne
  234. .calledWith(
  235. { _id: new ObjectId('55153a8014829a865bbf700d') },
  236. { $pull: { invited_emails: 'john.snow@example.com' } }
  237. )
  238. .should.eq(true)
  239. })
  240. it('add user to subscription if inviting self', async function (ctx) {
  241. const invite = await ctx.TeamInvitesHandler.promises.createInvite(
  242. ctx.manager._id,
  243. ctx.subscription,
  244. ctx.manager.email
  245. )
  246. sinon.assert.calledWith(
  247. ctx.SubscriptionUpdater.promises.addUserToGroup,
  248. ctx.subscription._id,
  249. ctx.manager._id
  250. )
  251. sinon.assert.notCalled(ctx.subscription.save)
  252. expect(invite.token).to.not.exist
  253. expect(invite.email).to.eq(ctx.manager.email)
  254. expect(invite.first_name).to.eq(ctx.manager.first_name)
  255. expect(invite.last_name).to.eq(ctx.manager.last_name)
  256. expect(invite.invite).to.be.false
  257. })
  258. it('sends an SSO invite if SSO is enabled and inviting self', async function (ctx) {
  259. ctx.subscription.ssoConfig = new ObjectId('abc123abc123abc123abc123')
  260. ctx.SSOConfig.findById
  261. .withArgs(ctx.subscription.ssoConfig)
  262. .resolves({ enabled: true })
  263. await ctx.TeamInvitesHandler.promises.createInvite(
  264. ctx.manager._id,
  265. ctx.subscription,
  266. ctx.manager.email
  267. )
  268. sinon.assert.calledWith(
  269. ctx.Modules.promises.hooks.fire,
  270. 'sendGroupSSOReminder',
  271. ctx.manager._id,
  272. ctx.subscription._id
  273. )
  274. })
  275. it('does not send an SSO invite if SSO is disabled and inviting self', async function (ctx) {
  276. ctx.subscription.ssoConfig = new ObjectId('abc123abc123abc123abc123')
  277. ctx.SSOConfig.findById
  278. .withArgs(ctx.subscription.ssoConfig)
  279. .resolves({ enabled: false })
  280. await ctx.TeamInvitesHandler.promises.createInvite(
  281. ctx.manager._id,
  282. ctx.subscription,
  283. ctx.manager.email
  284. )
  285. sinon.assert.notCalled(ctx.Modules.promises.hooks.fire)
  286. })
  287. it('sends a notification if inviting registered user', async function (ctx) {
  288. const id = new ObjectId('6a6b3a8014829a865bbf700d')
  289. const managedUsersEnabled = false
  290. ctx.UserGetter.promises.getUserByMainEmail
  291. .withArgs('john.snow@example.com')
  292. .resolves({
  293. _id: id,
  294. })
  295. const invite = await ctx.TeamInvitesHandler.promises.createInvite(
  296. ctx.manager._id,
  297. ctx.subscription,
  298. 'John.Snow@example.com'
  299. )
  300. ctx.NotificationsBuilder.promises
  301. .groupInvitation(
  302. id.toString(),
  303. ctx.subscription._id,
  304. managedUsersEnabled
  305. )
  306. .create.calledWith(invite)
  307. .should.eq(true)
  308. })
  309. })
  310. describe('importInvite', function () {
  311. beforeEach(function (ctx) {
  312. ctx.sentAt = new Date()
  313. })
  314. it('can imports an invite from v1', function (ctx) {
  315. ctx.TeamInvitesHandler.importInvite(
  316. ctx.subscription,
  317. 'A-Team',
  318. 'hannibal@a-team.org',
  319. 'secret',
  320. ctx.sentAt,
  321. error => {
  322. expect(error).not.to.exist
  323. ctx.subscription.save.calledOnce.should.eq(true)
  324. const invite = ctx.subscription.teamInvites.find(
  325. i => i.email === 'hannibal@a-team.org'
  326. )
  327. expect(invite.token).to.eq('secret')
  328. expect(invite.sentAt).to.eq(ctx.sentAt)
  329. }
  330. )
  331. })
  332. })
  333. describe('acceptInvite', function () {
  334. beforeEach(function (ctx) {
  335. ctx.user = {
  336. id: '123456789',
  337. first_name: 'Tyrion',
  338. last_name: 'Lannister',
  339. email: 'tyrion@example.com',
  340. }
  341. ctx.user_subscription = {
  342. id: '66264b9125930b976cc0811e',
  343. _id: new ObjectId('66264b9125930b976cc0811e'),
  344. groupPlan: false,
  345. recurlySubscription_id: 'fa1b2cfa156gh',
  346. admin_id: '123456789',
  347. member_ids: [],
  348. teamInvites: [],
  349. save: sinon.stub().resolves(),
  350. }
  351. ctx.ipAddress = '127.0.0.1'
  352. ctx.UserGetter.promises.getUserByAnyEmail
  353. .withArgs(ctx.user.email)
  354. .resolves(ctx.user)
  355. ctx.SubscriptionLocator.promises.getUsersSubscription
  356. .withArgs(ctx.user.id)
  357. .resolves(ctx.user_subscription)
  358. ctx.subscription.teamInvites.push({
  359. email: 'john.snow@example.com',
  360. token: 'dddddddd',
  361. inviterName: 'Daenerys Targaryen (daenerys@example.com)',
  362. })
  363. })
  364. describe('with standard group', function () {
  365. it('adds the user to the team', async function (ctx) {
  366. await ctx.TeamInvitesHandler.promises.acceptInvite(
  367. 'dddddddd',
  368. ctx.user.id,
  369. ctx.ipAddress
  370. )
  371. ctx.SubscriptionUpdater.promises.addUserToGroup
  372. .calledWith(ctx.subscription._id, ctx.user.id)
  373. .should.eq(true)
  374. })
  375. it('removes the invite from the subscription', async function (ctx) {
  376. await ctx.TeamInvitesHandler.promises.acceptInvite(
  377. 'dddddddd',
  378. ctx.user.id,
  379. ctx.ipAddress
  380. )
  381. ctx.Subscription.updateOne
  382. .calledWith(
  383. { _id: new ObjectId('55153a8014829a865bbf700d') },
  384. { $pull: { teamInvites: { email: 'john.snow@example.com' } } }
  385. )
  386. .should.eq(true)
  387. })
  388. it('removes dashboard notification after they accepted group invitation', async function (ctx) {
  389. const managedUsersEnabled = false
  390. await ctx.TeamInvitesHandler.promises.acceptInvite(
  391. 'dddddddd',
  392. ctx.user.id,
  393. ctx.ipAddress
  394. )
  395. sinon.assert.called(
  396. ctx.NotificationsBuilder.promises.groupInvitation(
  397. ctx.user.id,
  398. ctx.subscription._id,
  399. managedUsersEnabled
  400. ).read
  401. )
  402. })
  403. it('should not schedule an SSO invite reminder', async function (ctx) {
  404. await ctx.TeamInvitesHandler.promises.acceptInvite(
  405. 'dddddddd',
  406. ctx.user.id,
  407. ctx.ipAddress
  408. )
  409. sinon.assert.notCalled(ctx.Modules.promises.hooks.fire)
  410. })
  411. })
  412. describe('with managed group', function () {
  413. it('should enroll the group member', async function (ctx) {
  414. ctx.subscription.managedUsersEnabled = true
  415. await ctx.TeamInvitesHandler.promises.acceptInvite(
  416. 'dddddddd',
  417. ctx.user.id,
  418. ctx.ipAddress
  419. )
  420. sinon.assert.calledWith(
  421. ctx.SubscriptionUpdater.promises.deleteSubscription,
  422. ctx.user_subscription,
  423. { id: ctx.user.id, ip: ctx.ipAddress }
  424. )
  425. sinon.assert.calledWith(
  426. ctx.Modules.promises.hooks.fire,
  427. 'terminateSubscription',
  428. ctx.user_subscription
  429. )
  430. sinon.assert.calledWith(
  431. ctx.Modules.promises.hooks.fire,
  432. 'enrollInManagedSubscription',
  433. ctx.user.id,
  434. ctx.subscription
  435. )
  436. })
  437. it('should not delete the users subscription if that subscription is also the join target', async function (ctx) {
  438. ctx.subscription.managedUsersEnabled = true
  439. ctx.SubscriptionLocator.promises.getUsersSubscription
  440. .withArgs(ctx.user.id)
  441. .resolves(ctx.subscription)
  442. await ctx.TeamInvitesHandler.promises.acceptInvite(
  443. 'dddddddd',
  444. ctx.user.id,
  445. ctx.ipAddress
  446. )
  447. sinon.assert.notCalled(
  448. ctx.SubscriptionUpdater.promises.deleteSubscription
  449. )
  450. })
  451. })
  452. describe('with group SSO enabled', function () {
  453. it('should schedule an SSO invite reminder', async function (ctx) {
  454. ctx.subscription.ssoConfig = 'ssoconfig1'
  455. ctx.SSOConfig.findById
  456. .withArgs('ssoconfig1')
  457. .resolves({ enabled: true })
  458. await ctx.TeamInvitesHandler.promises.acceptInvite(
  459. 'dddddddd',
  460. ctx.user.id,
  461. ctx.ipAddress
  462. )
  463. sinon.assert.calledWith(
  464. ctx.Modules.promises.hooks.fire,
  465. 'scheduleGroupSSOReminder',
  466. ctx.user.id,
  467. ctx.subscription._id
  468. )
  469. })
  470. })
  471. })
  472. describe('revokeInvite', function () {
  473. it('removes the team invite from the subscription', async function (ctx) {
  474. await ctx.TeamInvitesHandler.promises.revokeInvite(
  475. ctx.manager._id,
  476. ctx.subscription,
  477. 'jorah@example.com'
  478. )
  479. ctx.Subscription.updateOne
  480. .calledWith(
  481. { _id: new ObjectId('55153a8014829a865bbf700d') },
  482. { $pull: { teamInvites: { email: 'jorah@example.com' } } }
  483. )
  484. .should.eq(true)
  485. ctx.Subscription.updateOne
  486. .calledWith(
  487. { _id: new ObjectId('55153a8014829a865bbf700d') },
  488. { $pull: { invited_emails: 'jorah@example.com' } }
  489. )
  490. .should.eq(true)
  491. })
  492. it('removes dashboard notification for pending group invitation', async function (ctx) {
  493. const managedUsersEnabled = false
  494. const pendingUser = {
  495. id: '1a2b',
  496. email: 'tyrion@example.com',
  497. }
  498. ctx.UserGetter.promises.getUserByAnyEmail
  499. .withArgs(pendingUser.email)
  500. .resolves(pendingUser)
  501. await ctx.TeamInvitesHandler.promises.revokeInvite(
  502. ctx.manager._id,
  503. ctx.subscription,
  504. pendingUser.email
  505. )
  506. sinon.assert.called(
  507. ctx.NotificationsBuilder.promises.groupInvitation(
  508. pendingUser.id,
  509. ctx.subscription._id,
  510. managedUsersEnabled
  511. ).read
  512. )
  513. })
  514. })
  515. describe('createTeamInvitesForLegacyInvitedEmail', function () {
  516. beforeEach(function (ctx) {
  517. ctx.subscription.invited_emails = [
  518. 'eddard@example.com',
  519. 'robert@example.com',
  520. ]
  521. ctx.TeamInvitesHandler.createInvite = sinon.stub().resolves(null)
  522. ctx.SubscriptionLocator.promises.getGroupsWithEmailInvite = sinon
  523. .stub()
  524. .resolves([ctx.subscription])
  525. })
  526. it('sends an invitation email to addresses in the legacy invited_emails field', async function (ctx) {
  527. const invites =
  528. await ctx.TeamInvitesHandler.promises.createTeamInvitesForLegacyInvitedEmail(
  529. 'eddard@example.com'
  530. )
  531. expect(invites.length).to.eq(1)
  532. const [invite] = invites
  533. expect(invite.token).to.eq(ctx.newToken)
  534. expect(invite.email).to.eq('eddard@example.com')
  535. expect(invite.inviterName).to.eq(
  536. 'Daenerys Targaryen (daenerys@example.com)'
  537. )
  538. expect(invite.invite).to.be.true
  539. expect(ctx.subscription.teamInvites).to.deep.include(invite)
  540. })
  541. })
  542. describe('validation', function () {
  543. it("doesn't create an invite if the team limit has been reached", async function (ctx) {
  544. ctx.LimitationsManager.teamHasReachedMemberLimit = sinon
  545. .stub()
  546. .returns(true)
  547. let error
  548. try {
  549. await ctx.TeamInvitesHandler.promises.createInvite(
  550. ctx.manager._id,
  551. ctx.subscription,
  552. 'John.Snow@example.com'
  553. )
  554. } catch (err) {
  555. error = err
  556. }
  557. expect(error).to.exist
  558. expect(error).to.deep.equal({
  559. limitReached: true,
  560. })
  561. })
  562. it("doesn't create an invite if the subscription is not in a group plan", async function (ctx) {
  563. ctx.subscription.groupPlan = false
  564. let error
  565. try {
  566. await ctx.TeamInvitesHandler.promises.createInvite(
  567. ctx.manager._id,
  568. ctx.subscription,
  569. 'John.Snow@example.com'
  570. )
  571. } catch (err) {
  572. error = err
  573. }
  574. expect(error).to.exist
  575. expect(error).to.deep.equal({
  576. wrongPlan: true,
  577. })
  578. })
  579. it("doesn't create an invite if the user is already part of the team", async function (ctx) {
  580. const member = {
  581. id: '1a2b',
  582. _id: '1a2b',
  583. email: 'tyrion@example.com',
  584. }
  585. ctx.subscription.member_ids = [member.id]
  586. ctx.UserGetter.promises.getUserByAnyEmail
  587. .withArgs(member.email)
  588. .resolves(member)
  589. let error
  590. try {
  591. await ctx.TeamInvitesHandler.promises.createInvite(
  592. ctx.manager._id,
  593. ctx.subscription,
  594. 'tyrion@example.com'
  595. )
  596. } catch (err) {
  597. error = err
  598. }
  599. expect(error).to.exist
  600. expect(error).to.eql({
  601. alreadyInTeam: true,
  602. })
  603. })
  604. })
  605. })