LimitationsManager.test.mjs 21 KB

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