LimitationsManagerTests.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  1. const SandboxedModule = require('sandboxed-module')
  2. const sinon = require('sinon')
  3. const { expect } = require('chai')
  4. const modulePath = require('path').join(
  5. __dirname,
  6. '../../../../app/src/Features/Subscription/LimitationsManager'
  7. )
  8. describe('LimitationsManager', function () {
  9. beforeEach(function () {
  10. this.user = {
  11. _id: (this.userId = 'user-id'),
  12. features: { collaborators: 1 },
  13. }
  14. this.project = {
  15. _id: (this.projectId = 'project-id'),
  16. owner_ref: this.userId,
  17. }
  18. this.ProjectGetter = {
  19. promises: {
  20. getProject: sinon.stub().callsFake(async (projectId, fields) => {
  21. if (projectId === this.projectId) {
  22. return this.project
  23. } else {
  24. return null
  25. }
  26. }),
  27. },
  28. }
  29. this.UserGetter = {
  30. promises: {
  31. getUser: sinon.stub().callsFake(async (userId, filter) => {
  32. if (userId === this.userId) {
  33. return this.user
  34. } else {
  35. return null
  36. }
  37. }),
  38. },
  39. }
  40. this.SubscriptionLocator = {
  41. promises: {
  42. getUsersSubscription: sinon.stub().resolves(),
  43. getSubscription: sinon.stub().resolves(),
  44. getMemberSubscriptions: sinon.stub().resolves(),
  45. },
  46. }
  47. this.CollaboratorsGetter = {
  48. promises: {
  49. getInvitedEditCollaboratorCount: sinon.stub().resolves(0),
  50. getMemberIdPrivilegeLevel: sinon.stub(),
  51. },
  52. }
  53. this.CollaboratorsInviteGetter = {
  54. promises: {
  55. getEditInviteCount: sinon.stub().resolves(0),
  56. },
  57. }
  58. this.LimitationsManager = SandboxedModule.require(modulePath, {
  59. requires: {
  60. '../Project/ProjectGetter': this.ProjectGetter,
  61. '../User/UserGetter': this.UserGetter,
  62. './SubscriptionLocator': this.SubscriptionLocator,
  63. '@overleaf/settings': (this.Settings = {}),
  64. '../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter,
  65. '../Collaborators/CollaboratorsInviteGetter':
  66. this.CollaboratorsInviteGetter,
  67. './V1SubscriptionManager': this.V1SubscriptionManager,
  68. },
  69. })
  70. })
  71. describe('allowedNumberOfCollaboratorsInProject', function () {
  72. describe('when the project is owned by a user without a subscription', function () {
  73. beforeEach(function () {
  74. this.Settings.defaultFeatures = { collaborators: 23 }
  75. this.project.owner_ref = this.userId
  76. delete this.user.features
  77. })
  78. it('should return the default number', async function () {
  79. const result =
  80. await this.LimitationsManager.promises.allowedNumberOfCollaboratorsInProject(
  81. this.projectId
  82. )
  83. expect(result).to.equal(this.Settings.defaultFeatures.collaborators)
  84. })
  85. })
  86. describe('when the project is owned by a user with a subscription', function () {
  87. beforeEach(function () {
  88. this.project.owner_ref = this.userId
  89. this.user.features = { collaborators: 21 }
  90. })
  91. it('should return the number of collaborators the user is allowed', async function () {
  92. const result =
  93. await this.LimitationsManager.promises.allowedNumberOfCollaboratorsInProject(
  94. this.projectId
  95. )
  96. expect(result).to.equal(this.user.features.collaborators)
  97. })
  98. })
  99. })
  100. describe('allowedNumberOfCollaboratorsForUser', function () {
  101. describe('when the user has no features', function () {
  102. beforeEach(function () {
  103. this.Settings.defaultFeatures = { collaborators: 23 }
  104. delete this.user.features
  105. })
  106. it('should return the default number', async function () {
  107. const result =
  108. await this.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser(
  109. this.userId
  110. )
  111. expect(result).to.equal(this.Settings.defaultFeatures.collaborators)
  112. })
  113. })
  114. describe('when the user has features', function () {
  115. beforeEach(async function () {
  116. this.user.features = { collaborators: 21 }
  117. this.result =
  118. await this.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser(
  119. this.userId
  120. )
  121. })
  122. it('should return the number of collaborators the user is allowed', function () {
  123. expect(this.result).to.equal(this.user.features.collaborators)
  124. })
  125. })
  126. })
  127. describe('canAcceptEditCollaboratorInvite', function () {
  128. describe('when the project has fewer collaborators than allowed', function () {
  129. beforeEach(function () {
  130. this.current_number = 1
  131. this.user.features.collaborators = 2
  132. this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
  133. sinon.stub().resolves(this.current_number)
  134. })
  135. it('should return true', async function () {
  136. const result =
  137. await this.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
  138. this.projectId
  139. )
  140. expect(result).to.be.true
  141. })
  142. })
  143. describe('when accepting the invite would exceed the collaborator limit', function () {
  144. beforeEach(function () {
  145. this.current_number = 2
  146. this.user.features.collaborators = 2
  147. this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
  148. sinon.stub().resolves(this.current_number)
  149. })
  150. it('should return false', async function () {
  151. const result =
  152. await this.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
  153. this.projectId
  154. )
  155. expect(result).to.be.false
  156. })
  157. })
  158. describe('when the project has more collaborators than allowed', function () {
  159. beforeEach(function () {
  160. this.current_number = 3
  161. this.user.features.collaborators = 2
  162. this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
  163. sinon.stub().resolves(this.current_number)
  164. })
  165. it('should return false', async function () {
  166. const result =
  167. await this.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
  168. this.projectId
  169. )
  170. expect(result).to.be.false
  171. })
  172. })
  173. describe('when the project has infinite collaborators', function () {
  174. beforeEach(function () {
  175. this.current_number = 100
  176. this.user.features.collaborators = -1
  177. this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
  178. sinon.stub().resolves(this.current_number)
  179. })
  180. it('should return true', async function () {
  181. const result =
  182. await this.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
  183. this.projectId
  184. )
  185. expect(result).to.be.true
  186. })
  187. })
  188. })
  189. describe('canAddXEditCollaborators', function () {
  190. describe('when the project has fewer collaborators than allowed', function () {
  191. beforeEach(function () {
  192. this.current_number = 1
  193. this.user.features.collaborators = 2
  194. this.invite_count = 0
  195. this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
  196. sinon.stub().resolves(this.current_number)
  197. this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
  198. .stub()
  199. .resolves(this.invite_count)
  200. })
  201. it('should return true', async function () {
  202. const result =
  203. await this.LimitationsManager.promises.canAddXEditCollaborators(
  204. this.projectId,
  205. 1
  206. )
  207. expect(result).to.be.true
  208. })
  209. })
  210. describe('when the project has fewer collaborators and invites than allowed', function () {
  211. beforeEach(function () {
  212. this.current_number = 1
  213. this.user.features.collaborators = 4
  214. this.invite_count = 1
  215. this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
  216. sinon.stub().resolves(this.current_number)
  217. this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
  218. .stub()
  219. .resolves(this.invite_count)
  220. })
  221. it('should return true', async function () {
  222. const result =
  223. await this.LimitationsManager.promises.canAddXEditCollaborators(
  224. this.projectId,
  225. 1
  226. )
  227. expect(result).to.be.true
  228. })
  229. })
  230. describe('when the project has fewer collaborators than allowed but I want to add more than allowed', function () {
  231. beforeEach(function () {
  232. this.current_number = 1
  233. this.user.features.collaborators = 2
  234. this.invite_count = 0
  235. this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
  236. sinon.stub().resolves(this.current_number)
  237. this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
  238. .stub()
  239. .resolves(this.invite_count)
  240. })
  241. it('should return false', async function () {
  242. const result =
  243. await this.LimitationsManager.promises.canAddXEditCollaborators(
  244. this.projectId,
  245. 2
  246. )
  247. expect(result).to.be.false
  248. })
  249. })
  250. describe('when the project has more collaborators than allowed', function () {
  251. beforeEach(function () {
  252. this.current_number = 3
  253. this.user.features.collaborators = 2
  254. this.invite_count = 0
  255. this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
  256. sinon.stub().resolves(this.current_number)
  257. this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
  258. .stub()
  259. .resolves(this.invite_count)
  260. })
  261. it('should return false', async function () {
  262. const result =
  263. await this.LimitationsManager.promises.canAddXEditCollaborators(
  264. this.projectId,
  265. 1
  266. )
  267. expect(result).to.be.false
  268. })
  269. })
  270. describe('when the project has infinite collaborators', function () {
  271. beforeEach(function () {
  272. this.current_number = 100
  273. this.user.features.collaborators = -1
  274. this.invite_count = 0
  275. this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
  276. sinon.stub().resolves(this.current_number)
  277. this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
  278. .stub()
  279. .resolves(this.invite_count)
  280. })
  281. it('should return true', async function () {
  282. const result =
  283. await this.LimitationsManager.promises.canAddXEditCollaborators(
  284. this.projectId,
  285. 1
  286. )
  287. expect(result).to.be.true
  288. })
  289. })
  290. describe('when the project has more invites than allowed', function () {
  291. beforeEach(function () {
  292. this.current_number = 0
  293. this.user.features.collaborators = 2
  294. this.invite_count = 2
  295. this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
  296. sinon.stub().resolves(this.current_number)
  297. this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
  298. .stub()
  299. .resolves(this.invite_count)
  300. })
  301. it('should return false', async function () {
  302. const result =
  303. await this.LimitationsManager.promises.canAddXEditCollaborators(
  304. this.projectId,
  305. 1
  306. )
  307. expect(result).to.be.false
  308. })
  309. })
  310. describe('when the project has more invites and collaborators than allowed', function () {
  311. beforeEach(function () {
  312. this.current_number = 1
  313. this.user.features.collaborators = 2
  314. this.invite_count = 1
  315. this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
  316. sinon.stub().resolves(this.current_number)
  317. this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
  318. .stub()
  319. .resolves(this.invite_count)
  320. })
  321. it('should return false', async function () {
  322. const result =
  323. await this.LimitationsManager.promises.canAddXEditCollaborators(
  324. this.projectId,
  325. 1
  326. )
  327. expect(result).to.be.false
  328. })
  329. })
  330. })
  331. describe('canChangeCollaboratorPrivilegeLevel', function () {
  332. beforeEach(function () {
  333. this.collaboratorId = 'collaborator-id'
  334. this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel.resolves(
  335. 'readOnly'
  336. )
  337. })
  338. describe("when the limit hasn't been reached", function () {
  339. it('accepts changing a viewer to an editor', async function () {
  340. const result =
  341. await this.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel(
  342. this.projectId,
  343. this.collaboratorId,
  344. 'readAndWrite'
  345. )
  346. expect(result).to.be.true
  347. })
  348. })
  349. describe('when the limit has been reached', function () {
  350. beforeEach(function () {
  351. this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount.resolves(
  352. 1
  353. )
  354. })
  355. it('accepts changing a reviewer to an editor', async function () {
  356. this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel.resolves(
  357. 'review'
  358. )
  359. const result =
  360. await this.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel(
  361. this.projectId,
  362. this.collaboratorId,
  363. 'readAndWrite'
  364. )
  365. expect(result).to.be.true
  366. })
  367. it('rejects changing a viewer to a reviewer', async function () {
  368. const result =
  369. await this.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel(
  370. this.projectId,
  371. this.collaboratorId,
  372. 'review'
  373. )
  374. expect(result).to.be.false
  375. })
  376. })
  377. })
  378. describe('userHasSubscription', function () {
  379. beforeEach(function () {
  380. this.SubscriptionLocator.promises.getUsersSubscription = sinon
  381. .stub()
  382. .resolves()
  383. })
  384. it('should return true if the recurly token is set', async function () {
  385. this.SubscriptionLocator.promises.getUsersSubscription = sinon
  386. .stub()
  387. .resolves({
  388. recurlySubscription_id: '1234',
  389. })
  390. const { hasSubscription } =
  391. await this.LimitationsManager.promises.userHasSubscription(this.user)
  392. expect(hasSubscription).to.be.true
  393. })
  394. it('should return true if the paymentProvider field is set', async function () {
  395. this.SubscriptionLocator.promises.getUsersSubscription = sinon
  396. .stub()
  397. .resolves({
  398. paymentProvider: {
  399. subscriptionId: '1234',
  400. },
  401. })
  402. const { hasSubscription } =
  403. await this.LimitationsManager.promises.userHasSubscription(this.user)
  404. expect(hasSubscription).to.be.true
  405. })
  406. it('should return false if the recurly token is not set', async function () {
  407. this.SubscriptionLocator.promises.getUsersSubscription.resolves({})
  408. const { hasSubscription } =
  409. await this.LimitationsManager.promises.userHasSubscription(this.user)
  410. expect(hasSubscription).to.be.false
  411. })
  412. it('should return false if the subscription is undefined', async function () {
  413. this.SubscriptionLocator.promises.getUsersSubscription.resolves()
  414. const { hasSubscription } =
  415. await this.LimitationsManager.promises.userHasSubscription(this.user)
  416. expect(hasSubscription).to.be.false
  417. })
  418. it('should return the subscription', async function () {
  419. const stubbedSubscription = { freeTrial: {}, token: '' }
  420. this.SubscriptionLocator.promises.getUsersSubscription.resolves(
  421. stubbedSubscription
  422. )
  423. const { subscription } =
  424. await this.LimitationsManager.promises.userHasSubscription(this.user)
  425. expect(subscription).to.deep.equal(stubbedSubscription)
  426. })
  427. describe('when user has a custom account', function () {
  428. beforeEach(function () {
  429. this.fakeSubscription = { customAccount: true }
  430. this.SubscriptionLocator.promises.getUsersSubscription.resolves(
  431. this.fakeSubscription
  432. )
  433. })
  434. it('should return true', async function () {
  435. const { hasSubscription } =
  436. await this.LimitationsManager.promises.userHasSubscription(this.user)
  437. expect(hasSubscription).to.be.true
  438. })
  439. it('should return the subscription', async function () {
  440. const { subscription } =
  441. await this.LimitationsManager.promises.userHasSubscription(this.user)
  442. expect(subscription).to.deep.equal(this.fakeSubscription)
  443. })
  444. })
  445. })
  446. describe('userIsMemberOfGroupSubscription', function () {
  447. beforeEach(function () {
  448. this.SubscriptionLocator.promises.getMemberSubscriptions = sinon
  449. .stub()
  450. .resolves()
  451. })
  452. it('should return false if there are no groups subcriptions', async function () {
  453. this.SubscriptionLocator.promises.getMemberSubscriptions.resolves([])
  454. const { isMember } =
  455. await this.LimitationsManager.promises.userIsMemberOfGroupSubscription(
  456. this.user
  457. )
  458. expect(isMember).to.be.false
  459. })
  460. it('should return true if there are no groups subcriptions', async function () {
  461. const subscriptions = ['mock-subscription']
  462. this.SubscriptionLocator.promises.getMemberSubscriptions.resolves(
  463. subscriptions
  464. )
  465. const { isMember, subscriptions: retSubscriptions } =
  466. await this.LimitationsManager.promises.userIsMemberOfGroupSubscription(
  467. this.user
  468. )
  469. expect(isMember).to.be.true
  470. expect(retSubscriptions).to.deep.equal(subscriptions)
  471. })
  472. })
  473. describe('hasPaidSubscription', function () {
  474. beforeEach(function () {
  475. this.SubscriptionLocator.promises.getMemberSubscriptions = sinon
  476. .stub()
  477. .resolves([])
  478. this.SubscriptionLocator.promises.getUsersSubscription = sinon
  479. .stub()
  480. .resolves(null)
  481. })
  482. it('should return true if userIsMemberOfGroupSubscription', async function () {
  483. this.SubscriptionLocator.promises.getMemberSubscriptions = sinon
  484. .stub()
  485. .resolves([{ _id: '123' }])
  486. const { hasPaidSubscription } =
  487. await this.LimitationsManager.promises.hasPaidSubscription(this.user)
  488. expect(hasPaidSubscription).to.be.true
  489. })
  490. it('should return true if userHasSubscription', async function () {
  491. this.SubscriptionLocator.promises.getUsersSubscription = sinon
  492. .stub()
  493. .resolves({ recurlySubscription_id: '123' })
  494. const { hasPaidSubscription } =
  495. await this.LimitationsManager.promises.hasPaidSubscription(this.user)
  496. expect(hasPaidSubscription).to.be.true
  497. })
  498. it('should return false if none are true', async function () {
  499. const { hasPaidSubscription } =
  500. await this.LimitationsManager.promises.hasPaidSubscription(this.user)
  501. expect(hasPaidSubscription).to.be.false
  502. })
  503. it('should have userHasSubscriptionOrIsGroupMember alias', async function () {
  504. const { hasPaidSubscription } =
  505. await this.LimitationsManager.promises.userHasSubscriptionOrIsGroupMember(
  506. this.user
  507. )
  508. expect(hasPaidSubscription).to.be.false
  509. })
  510. })
  511. describe('hasGroupMembersLimitReached', function () {
  512. beforeEach(function () {
  513. this.subscriptionId = '12312'
  514. this.subscription = {
  515. membersLimit: 3,
  516. member_ids: ['', ''],
  517. teamInvites: [
  518. { email: 'bob@example.com', sentAt: new Date(), token: 'hey' },
  519. ],
  520. }
  521. })
  522. it('should return true if the limit is hit (including members and invites)', async function () {
  523. this.SubscriptionLocator.promises.getSubscription.resolves(
  524. this.subscription
  525. )
  526. const { limitReached } =
  527. await this.LimitationsManager.promises.hasGroupMembersLimitReached(
  528. this.subscriptionId
  529. )
  530. expect(limitReached).to.be.true
  531. })
  532. it('should return false if the limit is not hit (including members and invites)', async function () {
  533. this.subscription.membersLimit = 4
  534. this.SubscriptionLocator.promises.getSubscription.resolves(
  535. this.subscription
  536. )
  537. const { limitReached } =
  538. await this.LimitationsManager.promises.hasGroupMembersLimitReached(
  539. this.subscriptionId
  540. )
  541. expect(limitReached).to.be.false
  542. })
  543. it('should return true if the limit has been exceded (including members and invites)', async function () {
  544. this.subscription.membersLimit = 2
  545. this.SubscriptionLocator.promises.getSubscription.resolves(
  546. this.subscription
  547. )
  548. const { limitReached } =
  549. await this.LimitationsManager.promises.hasGroupMembersLimitReached(
  550. this.subscriptionId
  551. )
  552. expect(limitReached).to.be.true
  553. })
  554. })
  555. })