SubscriptionUpdaterTests.js 18 KB

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