LimitationsManagerTests.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  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 false if the recurly token is not set', async function () {
  395. this.SubscriptionLocator.promises.getUsersSubscription.resolves({})
  396. const { hasSubscription } =
  397. await this.LimitationsManager.promises.userHasSubscription(this.user)
  398. expect(hasSubscription).to.be.false
  399. })
  400. it('should return false if the subscription is undefined', async function () {
  401. this.SubscriptionLocator.promises.getUsersSubscription.resolves()
  402. const { hasSubscription } =
  403. await this.LimitationsManager.promises.userHasSubscription(this.user)
  404. expect(hasSubscription).to.be.false
  405. })
  406. it('should return the subscription', async function () {
  407. const stubbedSubscription = { freeTrial: {}, token: '' }
  408. this.SubscriptionLocator.promises.getUsersSubscription.resolves(
  409. stubbedSubscription
  410. )
  411. const { subscription } =
  412. await this.LimitationsManager.promises.userHasSubscription(this.user)
  413. expect(subscription).to.deep.equal(stubbedSubscription)
  414. })
  415. describe('when user has a custom account', function () {
  416. beforeEach(function () {
  417. this.fakeSubscription = { customAccount: true }
  418. this.SubscriptionLocator.promises.getUsersSubscription.resolves(
  419. this.fakeSubscription
  420. )
  421. })
  422. it('should return true', async function () {
  423. const { hasSubscription } =
  424. await this.LimitationsManager.promises.userHasSubscription(this.user)
  425. expect(hasSubscription).to.be.true
  426. })
  427. it('should return the subscription', async function () {
  428. const { subscription } =
  429. await this.LimitationsManager.promises.userHasSubscription(this.user)
  430. expect(subscription).to.deep.equal(this.fakeSubscription)
  431. })
  432. })
  433. })
  434. describe('userIsMemberOfGroupSubscription', function () {
  435. beforeEach(function () {
  436. this.SubscriptionLocator.promises.getMemberSubscriptions = sinon
  437. .stub()
  438. .resolves()
  439. })
  440. it('should return false if there are no groups subcriptions', async function () {
  441. this.SubscriptionLocator.promises.getMemberSubscriptions.resolves([])
  442. const { isMember } =
  443. await this.LimitationsManager.promises.userIsMemberOfGroupSubscription(
  444. this.user
  445. )
  446. expect(isMember).to.be.false
  447. })
  448. it('should return true if there are no groups subcriptions', async function () {
  449. const subscriptions = ['mock-subscription']
  450. this.SubscriptionLocator.promises.getMemberSubscriptions.resolves(
  451. subscriptions
  452. )
  453. const { isMember, subscriptions: retSubscriptions } =
  454. await this.LimitationsManager.promises.userIsMemberOfGroupSubscription(
  455. this.user
  456. )
  457. expect(isMember).to.be.true
  458. expect(retSubscriptions).to.deep.equal(subscriptions)
  459. })
  460. })
  461. describe('hasPaidSubscription', function () {
  462. beforeEach(function () {
  463. this.SubscriptionLocator.promises.getMemberSubscriptions = sinon
  464. .stub()
  465. .resolves([])
  466. this.SubscriptionLocator.promises.getUsersSubscription = sinon
  467. .stub()
  468. .resolves(null)
  469. })
  470. it('should return true if userIsMemberOfGroupSubscription', async function () {
  471. this.SubscriptionLocator.promises.getMemberSubscriptions = sinon
  472. .stub()
  473. .resolves([{ _id: '123' }])
  474. const { hasPaidSubscription } =
  475. await this.LimitationsManager.promises.hasPaidSubscription(this.user)
  476. expect(hasPaidSubscription).to.be.true
  477. })
  478. it('should return true if userHasSubscription', async function () {
  479. this.SubscriptionLocator.promises.getUsersSubscription = sinon
  480. .stub()
  481. .resolves({ recurlySubscription_id: '123' })
  482. const { hasPaidSubscription } =
  483. await this.LimitationsManager.promises.hasPaidSubscription(this.user)
  484. expect(hasPaidSubscription).to.be.true
  485. })
  486. it('should return false if none are true', async function () {
  487. const { hasPaidSubscription } =
  488. await this.LimitationsManager.promises.hasPaidSubscription(this.user)
  489. expect(hasPaidSubscription).to.be.false
  490. })
  491. it('should have userHasSubscriptionOrIsGroupMember alias', async function () {
  492. const { hasPaidSubscription } =
  493. await this.LimitationsManager.promises.userHasSubscriptionOrIsGroupMember(
  494. this.user
  495. )
  496. expect(hasPaidSubscription).to.be.false
  497. })
  498. })
  499. describe('hasGroupMembersLimitReached', function () {
  500. beforeEach(function () {
  501. this.subscriptionId = '12312'
  502. this.subscription = {
  503. membersLimit: 3,
  504. member_ids: ['', ''],
  505. teamInvites: [
  506. { email: 'bob@example.com', sentAt: new Date(), token: 'hey' },
  507. ],
  508. }
  509. })
  510. it('should return true if the limit is hit (including members and invites)', async function () {
  511. this.SubscriptionLocator.promises.getSubscription.resolves(
  512. this.subscription
  513. )
  514. const { limitReached } =
  515. await this.LimitationsManager.promises.hasGroupMembersLimitReached(
  516. this.subscriptionId
  517. )
  518. expect(limitReached).to.be.true
  519. })
  520. it('should return false if the limit is not hit (including members and invites)', async function () {
  521. this.subscription.membersLimit = 4
  522. this.SubscriptionLocator.promises.getSubscription.resolves(
  523. this.subscription
  524. )
  525. const { limitReached } =
  526. await this.LimitationsManager.promises.hasGroupMembersLimitReached(
  527. this.subscriptionId
  528. )
  529. expect(limitReached).to.be.false
  530. })
  531. it('should return true if the limit has been exceded (including members and invites)', async function () {
  532. this.subscription.membersLimit = 2
  533. this.SubscriptionLocator.promises.getSubscription.resolves(
  534. this.subscription
  535. )
  536. const { limitReached } =
  537. await this.LimitationsManager.promises.hasGroupMembersLimitReached(
  538. this.subscriptionId
  539. )
  540. expect(limitReached).to.be.true
  541. })
  542. })
  543. })