TeamInvitesHandlerTests.js 19 KB

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