SubscriptionUpdaterTests.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. const SandboxedModule = require('sandboxed-module')
  2. const sinon = require('sinon')
  3. const modulePath =
  4. '../../../../app/src/Features/Subscription/SubscriptionUpdater'
  5. const { assert, expect } = require('chai')
  6. const { ObjectId } = require('mongodb')
  7. describe('SubscriptionUpdater', function () {
  8. beforeEach(function () {
  9. this.recurlyPlan = { planCode: 'recurly-plan' }
  10. this.recurlySubscription = {
  11. uuid: '1238uoijdasjhd',
  12. plan: {
  13. plan_code: this.recurlyPlan.planCode,
  14. },
  15. }
  16. this.adminUser = { _id: (this.adminuser_id = '5208dd34438843e2db000007') }
  17. this.otherUserId = '5208dd34438842e2db000005'
  18. this.allUserIds = ['13213', 'dsadas', 'djsaiud89']
  19. this.subscription = {
  20. _id: '111111111111111111111111',
  21. admin_id: this.adminUser._id,
  22. manager_ids: [this.adminUser._id],
  23. member_ids: [],
  24. save: sinon.stub().resolves(),
  25. planCode: 'student_or_something',
  26. }
  27. this.user_id = this.adminuser_id
  28. this.groupSubscription = {
  29. _id: '222222222222222222222222',
  30. admin_id: this.adminUser._id,
  31. manager_ids: [this.adminUser._id],
  32. member_ids: this.allUserIds,
  33. save: sinon.stub().resolves(),
  34. groupPlan: true,
  35. planCode: 'group_subscription',
  36. }
  37. this.betterGroupSubscription = {
  38. _id: '999999999999999999999999',
  39. admin_id: this.adminUser._id,
  40. manager_ids: [this.adminUser._id],
  41. member_ids: [this.otherUserId],
  42. save: sinon.stub().resolves(),
  43. groupPlan: true,
  44. planCode: 'better_group_subscription',
  45. }
  46. const subscription = this.subscription
  47. this.SubscriptionModel = class {
  48. constructor(opts) {
  49. // Always return our mock subscription when creating a new one
  50. subscription.admin_id = opts.admin_id
  51. subscription.manager_ids = [opts.admin_id]
  52. return subscription
  53. }
  54. save() {
  55. return Promise.resolve(subscription)
  56. }
  57. }
  58. this.SubscriptionModel.deleteOne = sinon
  59. .stub()
  60. .returns({ exec: sinon.stub().resolves() })
  61. this.SubscriptionModel.updateOne = sinon
  62. .stub()
  63. .returns({ exec: sinon.stub().resolves() })
  64. this.SubscriptionModel.updateMany = sinon
  65. .stub()
  66. .returns({ exec: sinon.stub().resolves() })
  67. this.SubscriptionModel.findOneAndUpdate = sinon.stub().returns({
  68. exec: sinon.stub().resolves(this.subscription),
  69. })
  70. this.SubscriptionLocator = {
  71. promises: {
  72. getUsersSubscription: sinon.stub(),
  73. getGroupSubscriptionMemberOf: sinon.stub(),
  74. getMemberSubscriptions: sinon.stub().resolves([]),
  75. getSubscription: sinon.stub(),
  76. },
  77. }
  78. this.SubscriptionLocator.promises.getSubscription
  79. .withArgs(this.subscription._id)
  80. .resolves(this.subscription)
  81. this.Settings = {
  82. defaultPlanCode: 'personal',
  83. defaultFeatures: { default: 'features' },
  84. plans: [
  85. this.recurlyPlan,
  86. { planCode: this.subscription.planCode, features: {} },
  87. {
  88. planCode: this.groupSubscription.planCode,
  89. features: {
  90. collaborators: 10,
  91. compileTimeout: 60,
  92. dropbox: true,
  93. },
  94. },
  95. {
  96. planCode: this.betterGroupSubscription.planCode,
  97. features: {
  98. collaborators: -1,
  99. compileTimeout: 240,
  100. dropbox: true,
  101. },
  102. },
  103. ],
  104. }
  105. this.UserFeaturesUpdater = {
  106. promises: {
  107. updateFeatures: sinon.stub().resolves(),
  108. },
  109. }
  110. this.ReferalFeatures = {
  111. promises: {
  112. getBonusFeatures: sinon.stub().resolves(),
  113. },
  114. }
  115. this.FeaturesUpdater = {
  116. promises: {
  117. scheduleRefreshFeatures: sinon.stub().resolves(),
  118. refreshFeatures: sinon.stub().resolves({}),
  119. },
  120. }
  121. this.DeletedSubscription = {
  122. findOneAndUpdate: sinon.stub().returns({ exec: sinon.stub().resolves() }),
  123. }
  124. this.AnalyticsManager = {
  125. setUserPropertyForUser: sinon.stub(),
  126. }
  127. this.SubscriptionUpdater = SandboxedModule.require(modulePath, {
  128. requires: {
  129. '../../models/Subscription': {
  130. Subscription: this.SubscriptionModel,
  131. },
  132. './UserFeaturesUpdater': this.UserFeaturesUpdater,
  133. './SubscriptionLocator': this.SubscriptionLocator,
  134. '@overleaf/settings': this.Settings,
  135. '../../infrastructure/mongodb': { db: {}, ObjectId },
  136. './FeaturesUpdater': this.FeaturesUpdater,
  137. '../../models/DeletedSubscription': {
  138. DeletedSubscription: this.DeletedSubscription,
  139. },
  140. '../Analytics/AnalyticsManager': this.AnalyticsManager,
  141. },
  142. })
  143. })
  144. describe('updateAdmin', function () {
  145. it('should update the subscription admin', async function () {
  146. this.subscription.groupPlan = true
  147. await this.SubscriptionUpdater.promises.updateAdmin(
  148. this.subscription,
  149. this.otherUserId
  150. )
  151. const query = {
  152. _id: ObjectId(this.subscription._id),
  153. customAccount: true,
  154. }
  155. const update = {
  156. $set: { admin_id: ObjectId(this.otherUserId) },
  157. $addToSet: { manager_ids: ObjectId(this.otherUserId) },
  158. }
  159. this.SubscriptionModel.updateOne.should.have.been.calledOnce
  160. this.SubscriptionModel.updateOne.should.have.been.calledWith(
  161. query,
  162. update
  163. )
  164. })
  165. it('should remove the manager for non-group subscriptions', async function () {
  166. await this.SubscriptionUpdater.promises.updateAdmin(
  167. this.subscription,
  168. this.otherUserId
  169. )
  170. const query = {
  171. _id: ObjectId(this.subscription._id),
  172. customAccount: true,
  173. }
  174. const update = {
  175. $set: {
  176. admin_id: ObjectId(this.otherUserId),
  177. manager_ids: [ObjectId(this.otherUserId)],
  178. },
  179. }
  180. this.SubscriptionModel.updateOne.should.have.been.calledOnce
  181. this.SubscriptionModel.updateOne.should.have.been.calledWith(
  182. query,
  183. update
  184. )
  185. })
  186. })
  187. describe('syncSubscription', function () {
  188. beforeEach(function () {
  189. this.SubscriptionLocator.promises.getUsersSubscription.resolves(
  190. this.subscription
  191. )
  192. })
  193. it('should update the subscription if the user already is admin of one', async function () {
  194. await this.SubscriptionUpdater.promises.syncSubscription(
  195. this.recurlySubscription,
  196. this.adminUser._id
  197. )
  198. this.SubscriptionLocator.promises.getUsersSubscription
  199. .calledWith(this.adminUser._id)
  200. .should.equal(true)
  201. })
  202. it('should not call updateFeatures with group subscription if recurly subscription is not expired', async function () {
  203. await this.SubscriptionUpdater.promises.syncSubscription(
  204. this.recurlySubscription,
  205. this.adminUser._id
  206. )
  207. this.SubscriptionLocator.promises.getUsersSubscription
  208. .calledWith(this.adminUser._id)
  209. .should.equal(true)
  210. this.UserFeaturesUpdater.promises.updateFeatures.called.should.equal(
  211. false
  212. )
  213. })
  214. })
  215. describe('updateSubscriptionFromRecurly', function () {
  216. afterEach(function () {
  217. this.subscription.member_ids = []
  218. })
  219. it('should update the subscription with token etc when not expired', async function () {
  220. await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  221. this.recurlySubscription,
  222. this.subscription,
  223. {}
  224. )
  225. this.subscription.recurlySubscription_id.should.equal(
  226. this.recurlySubscription.uuid
  227. )
  228. this.subscription.planCode.should.equal(
  229. this.recurlySubscription.plan.plan_code
  230. )
  231. this.subscription.save.called.should.equal(true)
  232. expect(
  233. this.FeaturesUpdater.promises.scheduleRefreshFeatures
  234. ).to.have.been.calledWith(this.adminUser._id)
  235. })
  236. it('should remove the subscription when expired', async function () {
  237. this.recurlySubscription.state = 'expired'
  238. await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  239. this.recurlySubscription,
  240. this.subscription,
  241. {}
  242. )
  243. })
  244. it('should update all the users features', async function () {
  245. this.subscription.member_ids = this.allUserIds
  246. await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  247. this.recurlySubscription,
  248. this.subscription,
  249. {}
  250. )
  251. expect(
  252. this.FeaturesUpdater.promises.scheduleRefreshFeatures
  253. ).to.have.been.calledWith(this.adminUser._id)
  254. expect(
  255. this.FeaturesUpdater.promises.scheduleRefreshFeatures
  256. ).to.have.been.calledWith(this.allUserIds[0])
  257. expect(
  258. this.FeaturesUpdater.promises.scheduleRefreshFeatures
  259. ).to.have.been.calledWith(this.allUserIds[1])
  260. expect(
  261. this.FeaturesUpdater.promises.scheduleRefreshFeatures
  262. ).to.have.been.calledWith(this.allUserIds[2])
  263. })
  264. it('should set group to true and save how many members can be added to group', async function () {
  265. this.recurlyPlan.groupPlan = true
  266. this.recurlyPlan.membersLimit = 5
  267. await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  268. this.recurlySubscription,
  269. this.subscription,
  270. {}
  271. )
  272. this.subscription.membersLimit.should.equal(5)
  273. this.subscription.groupPlan.should.equal(true)
  274. this.subscription.member_ids.should.deep.equal([
  275. this.subscription.admin_id,
  276. ])
  277. })
  278. it('should delete and replace subscription when downgrading from group to individual plan', async function () {
  279. this.recurlyPlan.groupPlan = false
  280. await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  281. this.recurlySubscription,
  282. this.groupSubscription,
  283. {}
  284. )
  285. })
  286. it('should not set group to true or set groupPlan', async function () {
  287. await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  288. this.recurlySubscription,
  289. this.subscription,
  290. {}
  291. )
  292. assert.notEqual(this.subscription.membersLimit, 5)
  293. assert.notEqual(this.subscription.groupPlan, true)
  294. })
  295. describe('when the plan allows adding more seats', function () {
  296. beforeEach(function () {
  297. this.membersLimitAddOn = 'add_on1'
  298. this.recurlyPlan.groupPlan = true
  299. this.recurlyPlan.membersLimit = 5
  300. this.recurlyPlan.membersLimitAddOn = this.membersLimitAddOn
  301. })
  302. function expectMembersLimit(limit) {
  303. it('should set the membersLimit accordingly', async function () {
  304. await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  305. this.recurlySubscription,
  306. this.subscription,
  307. {}
  308. )
  309. expect(this.subscription.membersLimit).to.equal(limit)
  310. })
  311. }
  312. describe('when the recurlySubscription does not have add ons', function () {
  313. beforeEach(function () {
  314. delete this.recurlySubscription.subscription_add_ons
  315. })
  316. expectMembersLimit(5)
  317. })
  318. describe('when the recurlySubscription has non-matching add ons', function () {
  319. beforeEach(function () {
  320. this.recurlySubscription.subscription_add_ons = [
  321. { add_on_code: 'add_on_99', quantity: 3 },
  322. ]
  323. })
  324. expectMembersLimit(5)
  325. })
  326. describe('when the recurlySubscription has a matching add on', function () {
  327. beforeEach(function () {
  328. this.recurlySubscription.subscription_add_ons = [
  329. { add_on_code: this.membersLimitAddOn, quantity: 10 },
  330. ]
  331. })
  332. expectMembersLimit(15)
  333. })
  334. // NOTE: This is unexpected, but we are going to support it anyways.
  335. describe('when the recurlySubscription has multiple matching add ons', function () {
  336. beforeEach(function () {
  337. this.recurlySubscription.subscription_add_ons = [
  338. { add_on_code: this.membersLimitAddOn, quantity: 10 },
  339. { add_on_code: this.membersLimitAddOn, quantity: 3 },
  340. ]
  341. })
  342. expectMembersLimit(18)
  343. })
  344. })
  345. })
  346. describe('addUserToGroup', function () {
  347. it('should add the user ids to the group as a set', async function () {
  348. await this.SubscriptionUpdater.promises.addUserToGroup(
  349. this.subscription._id,
  350. this.otherUserId
  351. )
  352. const searchOps = { _id: this.subscription._id }
  353. const insertOperation = {
  354. $addToSet: { member_ids: this.otherUserId },
  355. }
  356. this.SubscriptionModel.updateOne
  357. .calledWith(searchOps, insertOperation)
  358. .should.equal(true)
  359. })
  360. it('should update the users features', async function () {
  361. await this.SubscriptionUpdater.promises.addUserToGroup(
  362. this.subscription._id,
  363. this.otherUserId
  364. )
  365. this.FeaturesUpdater.promises.refreshFeatures
  366. .calledWith(this.otherUserId)
  367. .should.equal(true)
  368. })
  369. it('should set the group plan code user property to the best plan with 1 group subscription', async function () {
  370. this.SubscriptionLocator.promises.getMemberSubscriptions
  371. .withArgs(this.otherUserId)
  372. .resolves([this.groupSubscription])
  373. await this.SubscriptionUpdater.promises.addUserToGroup(
  374. this.groupSubscription._id,
  375. this.otherUserId
  376. )
  377. sinon.assert.calledWith(
  378. this.AnalyticsManager.setUserPropertyForUser,
  379. this.otherUserId,
  380. 'group-subscription-plan-code',
  381. 'group_subscription'
  382. )
  383. })
  384. it('should set the group plan code user property to the best plan with 2 group subscriptions', async function () {
  385. this.SubscriptionLocator.promises.getMemberSubscriptions
  386. .withArgs(this.otherUserId)
  387. .resolves([this.groupSubscription, this.betterGroupSubscription])
  388. await this.SubscriptionUpdater.promises.addUserToGroup(
  389. this.betterGroupSubscription._id,
  390. this.otherUserId
  391. )
  392. sinon.assert.calledWith(
  393. this.AnalyticsManager.setUserPropertyForUser,
  394. this.otherUserId,
  395. 'group-subscription-plan-code',
  396. 'better_group_subscription'
  397. )
  398. })
  399. it('should set the group plan code user property to the best plan with 2 group subscriptions in reverse order', async function () {
  400. this.SubscriptionLocator.promises.getMemberSubscriptions
  401. .withArgs(this.otherUserId)
  402. .resolves([this.betterGroupSubscription, this.groupSubscription])
  403. await this.SubscriptionUpdater.promises.addUserToGroup(
  404. this.betterGroupSubscription._id,
  405. this.otherUserId
  406. )
  407. sinon.assert.calledWith(
  408. this.AnalyticsManager.setUserPropertyForUser,
  409. this.otherUserId,
  410. 'group-subscription-plan-code',
  411. 'better_group_subscription'
  412. )
  413. })
  414. })
  415. describe('removeUserFromGroups', function () {
  416. beforeEach(function () {
  417. this.fakeSubscriptions = [{ _id: 'fake-id-1' }, { _id: 'fake-id-2' }]
  418. this.SubscriptionLocator.promises.getMemberSubscriptions.resolves(
  419. this.fakeSubscriptions
  420. )
  421. })
  422. it('should pull the users id from the group', async function () {
  423. await this.SubscriptionUpdater.promises.removeUserFromGroup(
  424. this.subscription._id,
  425. this.otherUserId
  426. )
  427. const removeOperation = { $pull: { member_ids: this.otherUserId } }
  428. this.SubscriptionModel.updateOne
  429. .calledWith({ _id: this.subscription._id }, removeOperation)
  430. .should.equal(true)
  431. })
  432. it('should set the group plan code user property when removing user from group', async function () {
  433. await this.SubscriptionUpdater.promises.removeUserFromGroup(
  434. this.subscription._id,
  435. this.otherUserId
  436. )
  437. sinon.assert.calledWith(
  438. this.AnalyticsManager.setUserPropertyForUser,
  439. this.otherUserId,
  440. 'group-subscription-plan-code',
  441. null
  442. )
  443. })
  444. it('should set the group plan code user property when removing user from all groups', async function () {
  445. await this.SubscriptionUpdater.promises.removeUserFromAllGroups(
  446. this.otherUserId
  447. )
  448. sinon.assert.calledWith(
  449. this.AnalyticsManager.setUserPropertyForUser,
  450. this.otherUserId,
  451. 'group-subscription-plan-code',
  452. null
  453. )
  454. })
  455. it('should pull the users id from all groups', async function () {
  456. await this.SubscriptionUpdater.promises.removeUserFromAllGroups(
  457. this.otherUserId
  458. )
  459. const filter = { _id: ['fake-id-1', 'fake-id-2'] }
  460. const removeOperation = { $pull: { member_ids: this.otherUserId } }
  461. sinon.assert.calledWith(
  462. this.SubscriptionModel.updateMany,
  463. filter,
  464. removeOperation
  465. )
  466. })
  467. it('should update the users features', async function () {
  468. await this.SubscriptionUpdater.promises.removeUserFromGroup(
  469. this.subscription._id,
  470. this.otherUserId
  471. )
  472. this.FeaturesUpdater.promises.refreshFeatures
  473. .calledWith(this.otherUserId)
  474. .should.equal(true)
  475. })
  476. })
  477. describe('deleteSubscription', function () {
  478. beforeEach(async function () {
  479. this.subscription = {
  480. _id: ObjectId().toString(),
  481. mock: 'subscription',
  482. admin_id: ObjectId(),
  483. member_ids: [ObjectId(), ObjectId(), ObjectId()],
  484. }
  485. await this.SubscriptionUpdater.promises.deleteSubscription(
  486. this.subscription,
  487. {}
  488. )
  489. })
  490. it('should remove the subscription', function () {
  491. this.SubscriptionModel.deleteOne
  492. .calledWith({ _id: this.subscription._id })
  493. .should.equal(true)
  494. })
  495. it('should downgrade the admin_id', function () {
  496. expect(
  497. this.FeaturesUpdater.promises.scheduleRefreshFeatures
  498. ).to.have.been.calledWith(this.subscription.admin_id)
  499. })
  500. it('should downgrade all of the members', function () {
  501. for (const userId of this.subscription.member_ids) {
  502. expect(
  503. this.FeaturesUpdater.promises.scheduleRefreshFeatures
  504. ).to.have.been.calledWith(userId)
  505. }
  506. })
  507. })
  508. })