TeamInvitesHandlerTests.js 18 KB

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