TeamInvitesHandler.test.mjs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844
  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', appName: 'Overleaf' },
  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. it('creates an audit log entry for group-invite-sent for managed subscription', async function (ctx) {
  310. ctx.subscription.managedUsersEnabled = true
  311. const auditLog = {
  312. initiatorId: ctx.manager._id,
  313. ipAddress: '192.0.2.1',
  314. }
  315. await ctx.TeamInvitesHandler.promises.createInvite(
  316. ctx.manager._id,
  317. ctx.subscription,
  318. 'John.Snow@example.com',
  319. auditLog
  320. )
  321. sinon.assert.calledWith(
  322. ctx.Modules.promises.hooks.fire,
  323. 'addGroupAuditLogEntry',
  324. sinon.match({
  325. initiatorId: auditLog.initiatorId,
  326. ipAddress: auditLog.ipAddress,
  327. groupId: ctx.subscription._id,
  328. operation: 'group-invite-sent',
  329. info: { invitedEmail: 'john.snow@example.com' },
  330. })
  331. )
  332. })
  333. it('does not create an audit log entry for non-managed subscription', async function (ctx) {
  334. ctx.subscription.managedUsersEnabled = false
  335. const auditLog = {
  336. initiatorId: ctx.manager._id,
  337. ipAddress: '192.0.2.1',
  338. }
  339. await ctx.TeamInvitesHandler.promises.createInvite(
  340. ctx.manager._id,
  341. ctx.subscription,
  342. 'John.Snow@example.com',
  343. auditLog
  344. )
  345. sinon.assert.neverCalledWith(
  346. ctx.Modules.promises.hooks.fire,
  347. 'addGroupAuditLogEntry'
  348. )
  349. })
  350. describe('when domain capture is enabled', function () {
  351. it('creates a domain capture invite', async function (ctx) {
  352. const initPath = '/saml/ukamf/init?group_id=12345'
  353. ctx.Modules.promises.hooks.fire.resolves([initPath])
  354. ctx.UserGetter.promises.getUser.resolves(ctx.manager)
  355. ctx.subscription.domainCaptureEnabled = true
  356. const invite = await ctx.TeamInvitesHandler.promises.createInvite(
  357. ctx.manager._id,
  358. ctx.subscription,
  359. 'user@example.com',
  360. { domainCapture: true }
  361. )
  362. expect(invite.token).to.be.undefined
  363. expect(invite.domainCapture).to.be.true
  364. expect(invite.email).to.eq('user@example.com')
  365. sinon.assert.calledWith(
  366. ctx.Modules.promises.hooks.fire,
  367. 'getGroupSSOInitPath',
  368. ctx.subscription,
  369. invite.email
  370. )
  371. ctx.EmailHandler.promises.sendEmail
  372. .calledWith(
  373. 'verifyEmailToJoinTeam',
  374. sinon.match({
  375. to: 'user@example.com',
  376. inviter: ctx.manager,
  377. acceptInviteUrl: `http://example.com${initPath}`,
  378. appName: 'Overleaf',
  379. })
  380. )
  381. .should.equal(true)
  382. })
  383. describe('when managed users is also enabled', function () {
  384. it('creates a domain capture invite', async function (ctx) {
  385. ctx.SubscriptionLocator.promises.getAdminEmailAndName = sinon
  386. .stub()
  387. .resolves(ctx.manager)
  388. ctx.UserGetter.promises.getUserByAnyEmail
  389. .withArgs('user@example.com')
  390. .resolves(ctx.user)
  391. const initPath = '/saml/ukamf/init?group_id=12345'
  392. ctx.Modules.promises.hooks.fire.resolves([initPath])
  393. ctx.UserGetter.promises.getUser.resolves(ctx.manager)
  394. ctx.subscription.managedUsersEnabled = true
  395. ctx.subscription.domainCaptureEnabled = true
  396. const invite = await ctx.TeamInvitesHandler.promises.createInvite(
  397. ctx.manager._id,
  398. ctx.subscription,
  399. 'user@example.com',
  400. { domainCapture: true }
  401. )
  402. expect(invite.token).to.be.undefined
  403. expect(invite.domainCapture).to.be.true
  404. expect(invite.email).to.eq('user@example.com')
  405. sinon.assert.calledWith(
  406. ctx.Modules.promises.hooks.fire,
  407. 'getGroupSSOInitPath',
  408. ctx.subscription,
  409. invite.email
  410. )
  411. ctx.EmailHandler.promises.sendEmail
  412. .calledWith(
  413. 'inviteNewUserToJoinManagedUsers',
  414. sinon.match({
  415. to: 'user@example.com',
  416. inviter: ctx.manager,
  417. acceptInviteUrl: `http://example.com${initPath}`,
  418. appName: 'Overleaf',
  419. admin: ctx.manager,
  420. })
  421. )
  422. .should.equal(true)
  423. })
  424. })
  425. })
  426. })
  427. describe('importInvite', function () {
  428. beforeEach(function (ctx) {
  429. ctx.sentAt = new Date()
  430. })
  431. it('can imports an invite from v1', function (ctx) {
  432. ctx.TeamInvitesHandler.importInvite(
  433. ctx.subscription,
  434. 'A-Team',
  435. 'hannibal@a-team.org',
  436. 'secret',
  437. ctx.sentAt,
  438. error => {
  439. expect(error).not.to.exist
  440. ctx.subscription.save.calledOnce.should.eq(true)
  441. const invite = ctx.subscription.teamInvites.find(
  442. i => i.email === 'hannibal@a-team.org'
  443. )
  444. expect(invite.token).to.eq('secret')
  445. expect(invite.sentAt).to.eq(ctx.sentAt)
  446. }
  447. )
  448. })
  449. })
  450. describe('acceptInvite', function () {
  451. beforeEach(function (ctx) {
  452. ctx.user = {
  453. id: '123456789',
  454. first_name: 'Tyrion',
  455. last_name: 'Lannister',
  456. email: 'tyrion@example.com',
  457. }
  458. ctx.user_subscription = {
  459. id: '66264b9125930b976cc0811e',
  460. _id: new ObjectId('66264b9125930b976cc0811e'),
  461. groupPlan: false,
  462. recurlySubscription_id: 'fa1b2cfa156gh',
  463. admin_id: '123456789',
  464. member_ids: [],
  465. teamInvites: [],
  466. save: sinon.stub().resolves(),
  467. }
  468. ctx.ipAddress = '127.0.0.1'
  469. ctx.UserGetter.promises.getUserByAnyEmail
  470. .withArgs(ctx.user.email)
  471. .resolves(ctx.user)
  472. ctx.SubscriptionLocator.promises.getUsersSubscription
  473. .withArgs(ctx.user.id)
  474. .resolves(ctx.user_subscription)
  475. ctx.subscription.teamInvites.push({
  476. email: 'john.snow@example.com',
  477. token: 'dddddddd',
  478. inviterName: 'Daenerys Targaryen (daenerys@example.com)',
  479. })
  480. })
  481. describe('with standard group', function () {
  482. it('adds the user to the team', async function (ctx) {
  483. await ctx.TeamInvitesHandler.promises.acceptInvite(
  484. 'dddddddd',
  485. ctx.user.id,
  486. ctx.ipAddress
  487. )
  488. ctx.SubscriptionUpdater.promises.addUserToGroup
  489. .calledWith(ctx.subscription._id, ctx.user.id)
  490. .should.eq(true)
  491. })
  492. it('removes the invite from the subscription', async function (ctx) {
  493. await ctx.TeamInvitesHandler.promises.acceptInvite(
  494. 'dddddddd',
  495. ctx.user.id,
  496. ctx.ipAddress
  497. )
  498. ctx.Subscription.updateOne
  499. .calledWith(
  500. { _id: new ObjectId('55153a8014829a865bbf700d') },
  501. { $pull: { teamInvites: { email: 'john.snow@example.com' } } }
  502. )
  503. .should.eq(true)
  504. })
  505. it('removes dashboard notification after they accepted group invitation', async function (ctx) {
  506. const managedUsersEnabled = false
  507. await ctx.TeamInvitesHandler.promises.acceptInvite(
  508. 'dddddddd',
  509. ctx.user.id,
  510. ctx.ipAddress
  511. )
  512. sinon.assert.called(
  513. ctx.NotificationsBuilder.promises.groupInvitation(
  514. ctx.user.id,
  515. ctx.subscription._id,
  516. managedUsersEnabled
  517. ).read
  518. )
  519. })
  520. it('should not schedule an SSO invite reminder', async function (ctx) {
  521. await ctx.TeamInvitesHandler.promises.acceptInvite(
  522. 'dddddddd',
  523. ctx.user.id,
  524. ctx.ipAddress
  525. )
  526. sinon.assert.notCalled(ctx.Modules.promises.hooks.fire)
  527. })
  528. })
  529. describe('with managed group', function () {
  530. it('should enroll the group member', async function (ctx) {
  531. ctx.subscription.managedUsersEnabled = true
  532. await ctx.TeamInvitesHandler.promises.acceptInvite(
  533. 'dddddddd',
  534. ctx.user.id,
  535. ctx.ipAddress
  536. )
  537. sinon.assert.calledWith(
  538. ctx.SubscriptionUpdater.promises.deleteSubscription,
  539. ctx.user_subscription,
  540. { id: ctx.user.id, ip: ctx.ipAddress }
  541. )
  542. sinon.assert.calledWith(
  543. ctx.Modules.promises.hooks.fire,
  544. 'terminateSubscription',
  545. ctx.user_subscription
  546. )
  547. sinon.assert.calledWith(
  548. ctx.Modules.promises.hooks.fire,
  549. 'enrollInManagedSubscription',
  550. ctx.user.id,
  551. ctx.subscription
  552. )
  553. })
  554. it('should not delete the users subscription if that subscription is also the join target', async function (ctx) {
  555. ctx.subscription.managedUsersEnabled = true
  556. ctx.SubscriptionLocator.promises.getUsersSubscription
  557. .withArgs(ctx.user.id)
  558. .resolves(ctx.subscription)
  559. await ctx.TeamInvitesHandler.promises.acceptInvite(
  560. 'dddddddd',
  561. ctx.user.id,
  562. ctx.ipAddress
  563. )
  564. sinon.assert.notCalled(
  565. ctx.SubscriptionUpdater.promises.deleteSubscription
  566. )
  567. })
  568. })
  569. describe('with group SSO enabled', function () {
  570. it('should schedule an SSO invite reminder', async function (ctx) {
  571. ctx.subscription.ssoConfig = 'ssoconfig1'
  572. ctx.SSOConfig.findById
  573. .withArgs('ssoconfig1')
  574. .resolves({ enabled: true })
  575. await ctx.TeamInvitesHandler.promises.acceptInvite(
  576. 'dddddddd',
  577. ctx.user.id,
  578. ctx.ipAddress
  579. )
  580. sinon.assert.calledWith(
  581. ctx.Modules.promises.hooks.fire,
  582. 'scheduleGroupSSOReminder',
  583. ctx.user.id,
  584. ctx.subscription._id
  585. )
  586. })
  587. })
  588. })
  589. describe('revokeInvite', function () {
  590. it('removes the team invite from the subscription', async function (ctx) {
  591. await ctx.TeamInvitesHandler.promises.revokeInvite(
  592. ctx.manager._id,
  593. ctx.subscription,
  594. 'jorah@example.com'
  595. )
  596. ctx.Subscription.updateOne
  597. .calledWith(
  598. { _id: new ObjectId('55153a8014829a865bbf700d') },
  599. { $pull: { teamInvites: { email: 'jorah@example.com' } } }
  600. )
  601. .should.eq(true)
  602. ctx.Subscription.updateOne
  603. .calledWith(
  604. { _id: new ObjectId('55153a8014829a865bbf700d') },
  605. { $pull: { invited_emails: 'jorah@example.com' } }
  606. )
  607. .should.eq(true)
  608. })
  609. it('removes dashboard notification for pending group invitation', async function (ctx) {
  610. const managedUsersEnabled = false
  611. const pendingUser = {
  612. id: '1a2b',
  613. email: 'tyrion@example.com',
  614. }
  615. ctx.UserGetter.promises.getUserByAnyEmail
  616. .withArgs(pendingUser.email)
  617. .resolves(pendingUser)
  618. await ctx.TeamInvitesHandler.promises.revokeInvite(
  619. ctx.manager._id,
  620. ctx.subscription,
  621. pendingUser.email
  622. )
  623. sinon.assert.called(
  624. ctx.NotificationsBuilder.promises.groupInvitation(
  625. pendingUser.id,
  626. ctx.subscription._id,
  627. managedUsersEnabled
  628. ).read
  629. )
  630. })
  631. })
  632. describe('createTeamInvitesForLegacyInvitedEmail', function () {
  633. beforeEach(function (ctx) {
  634. ctx.subscription.invited_emails = [
  635. 'eddard@example.com',
  636. 'robert@example.com',
  637. ]
  638. ctx.TeamInvitesHandler.createInvite = sinon.stub().resolves(null)
  639. ctx.SubscriptionLocator.promises.getGroupsWithEmailInvite = sinon
  640. .stub()
  641. .resolves([ctx.subscription])
  642. })
  643. it('sends an invitation email to addresses in the legacy invited_emails field', async function (ctx) {
  644. const invites =
  645. await ctx.TeamInvitesHandler.promises.createTeamInvitesForLegacyInvitedEmail(
  646. 'eddard@example.com'
  647. )
  648. expect(invites.length).to.eq(1)
  649. const [invite] = invites
  650. expect(invite.token).to.eq(ctx.newToken)
  651. expect(invite.email).to.eq('eddard@example.com')
  652. expect(invite.inviterName).to.eq(
  653. 'Daenerys Targaryen (daenerys@example.com)'
  654. )
  655. expect(invite.invite).to.be.true
  656. expect(ctx.subscription.teamInvites).to.deep.include(invite)
  657. })
  658. })
  659. describe('validation', function () {
  660. it("doesn't create an invite if the team limit has been reached", async function (ctx) {
  661. ctx.LimitationsManager.teamHasReachedMemberLimit = sinon
  662. .stub()
  663. .returns(true)
  664. let error
  665. try {
  666. await ctx.TeamInvitesHandler.promises.createInvite(
  667. ctx.manager._id,
  668. ctx.subscription,
  669. 'John.Snow@example.com'
  670. )
  671. } catch (err) {
  672. error = err
  673. }
  674. expect(error).to.exist
  675. expect(error).to.deep.equal({
  676. limitReached: true,
  677. })
  678. })
  679. it("doesn't create an invite if the subscription is not in a group plan", async function (ctx) {
  680. ctx.subscription.groupPlan = false
  681. let error
  682. try {
  683. await ctx.TeamInvitesHandler.promises.createInvite(
  684. ctx.manager._id,
  685. ctx.subscription,
  686. 'John.Snow@example.com'
  687. )
  688. } catch (err) {
  689. error = err
  690. }
  691. expect(error).to.exist
  692. expect(error).to.deep.equal({
  693. wrongPlan: true,
  694. })
  695. })
  696. it("doesn't create an invite if the user is already part of the team", async function (ctx) {
  697. const member = {
  698. id: '1a2b',
  699. _id: '1a2b',
  700. email: 'tyrion@example.com',
  701. }
  702. ctx.subscription.member_ids = [member.id]
  703. ctx.UserGetter.promises.getUserByAnyEmail
  704. .withArgs(member.email)
  705. .resolves(member)
  706. let error
  707. try {
  708. await ctx.TeamInvitesHandler.promises.createInvite(
  709. ctx.manager._id,
  710. ctx.subscription,
  711. 'tyrion@example.com'
  712. )
  713. } catch (err) {
  714. error = err
  715. }
  716. expect(error).to.exist
  717. expect(error).to.eql({
  718. alreadyInTeam: true,
  719. })
  720. })
  721. })
  722. })