SubscriptionUpdaterTests.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909
  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-legacy')
  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. recurlySubscription_id: 'abc123def456fab789',
  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().resolves(),
  35. groupPlan: true,
  36. planCode: 'group_subscription',
  37. recurlySubscription_id: '456fab789abc123def',
  38. }
  39. this.betterGroupSubscription = {
  40. _id: '999999999999999999999999',
  41. admin_id: this.adminUser._id,
  42. manager_ids: [this.adminUser._id],
  43. member_ids: [this.otherUserId],
  44. save: sinon.stub().resolves(),
  45. groupPlan: true,
  46. planCode: 'better_group_subscription',
  47. recurlySubscription_id: '123def456fab789abc',
  48. }
  49. const subscription = this.subscription
  50. this.SubscriptionModel = class {
  51. constructor(opts) {
  52. // Always return our mock subscription when creating a new one
  53. subscription.admin_id = opts.admin_id
  54. subscription.manager_ids = [opts.admin_id]
  55. return subscription
  56. }
  57. save() {
  58. return Promise.resolve(subscription)
  59. }
  60. }
  61. this.SubscriptionModel.deleteOne = sinon
  62. .stub()
  63. .returns({ exec: sinon.stub().resolves() })
  64. this.SubscriptionModel.updateOne = sinon
  65. .stub()
  66. .returns({ exec: sinon.stub().resolves() })
  67. this.SubscriptionModel.findOne = sinon.stub().resolves()
  68. this.SubscriptionModel.findById = sinon.stub().resolves()
  69. this.SubscriptionModel.updateMany = sinon
  70. .stub()
  71. .returns({ exec: sinon.stub().resolves() })
  72. this.SubscriptionModel.findOneAndUpdate = sinon.stub().returns({
  73. exec: sinon.stub().resolves(this.subscription),
  74. })
  75. this.SSOConfigModel = class {}
  76. this.SSOConfigModel.findOne = sinon.stub().returns({
  77. lean: sinon.stub().returns({
  78. exec: sinon.stub().resolves({ enabled: true }),
  79. }),
  80. })
  81. this.SubscriptionLocator = {
  82. promises: {
  83. getUsersSubscription: sinon.stub(),
  84. getGroupSubscriptionMemberOf: sinon.stub(),
  85. getMemberSubscriptions: sinon.stub().resolves([]),
  86. getSubscription: sinon.stub(),
  87. },
  88. }
  89. this.SubscriptionLocator.promises.getSubscription
  90. .withArgs(this.subscription._id)
  91. .resolves(this.subscription)
  92. this.Settings = {
  93. defaultPlanCode: 'personal',
  94. defaultFeatures: { default: 'features' },
  95. plans: [
  96. this.recurlyPlan,
  97. { planCode: this.subscription.planCode, features: {} },
  98. {
  99. planCode: this.groupSubscription.planCode,
  100. features: {
  101. collaborators: 10,
  102. compileTimeout: 60,
  103. dropbox: true,
  104. },
  105. },
  106. {
  107. planCode: this.betterGroupSubscription.planCode,
  108. features: {
  109. collaborators: -1,
  110. compileTimeout: 240,
  111. dropbox: true,
  112. },
  113. },
  114. ],
  115. mongo: {
  116. options: {
  117. appname: 'web',
  118. maxPoolSize: 100,
  119. serverSelectionTimeoutMS: 60000,
  120. socketTimeoutMS: 60000,
  121. monitorCommands: true,
  122. family: 4,
  123. },
  124. url: 'mongodb://mongo/test-overleaf',
  125. hasSecondaries: false,
  126. },
  127. }
  128. this.UserFeaturesUpdater = {
  129. promises: {
  130. updateFeatures: sinon.stub().resolves(),
  131. },
  132. }
  133. this.ReferalFeatures = {
  134. promises: {
  135. getBonusFeatures: sinon.stub().resolves(),
  136. },
  137. }
  138. this.FeaturesUpdater = {
  139. promises: {
  140. scheduleRefreshFeatures: sinon.stub().resolves(),
  141. refreshFeatures: sinon.stub().resolves({}),
  142. },
  143. }
  144. this.DeletedSubscription = {
  145. findOneAndUpdate: sinon.stub().returns({ exec: sinon.stub().resolves() }),
  146. }
  147. this.AnalyticsManager = {
  148. recordEventForUserInBackground: sinon.stub().resolves(),
  149. setUserPropertyForUserInBackground: sinon.stub(),
  150. registerAccountMapping: sinon.stub(),
  151. }
  152. this.Features = {
  153. hasFeature: sinon.stub().returns(false),
  154. }
  155. this.UserAuditLogHandler = {
  156. promises: {
  157. addEntry: sinon.stub().resolves(),
  158. },
  159. }
  160. this.UserUpdater = {
  161. promises: {
  162. updateUser: sinon.stub().resolves(),
  163. },
  164. }
  165. this.SubscriptionUpdater = SandboxedModule.require(modulePath, {
  166. requires: {
  167. '../../models/Subscription': {
  168. Subscription: this.SubscriptionModel,
  169. },
  170. '../../models/SSOConfig': { SSOConfig: this.SSOConfigModel },
  171. './UserFeaturesUpdater': this.UserFeaturesUpdater,
  172. './SubscriptionLocator': this.SubscriptionLocator,
  173. '@overleaf/settings': this.Settings,
  174. '../../infrastructure/mongodb': { db: {}, ObjectId },
  175. './FeaturesUpdater': this.FeaturesUpdater,
  176. '../../models/DeletedSubscription': {
  177. DeletedSubscription: this.DeletedSubscription,
  178. },
  179. '../Analytics/AnalyticsManager': this.AnalyticsManager,
  180. '../Analytics/AccountMappingHelper': (this.AccountMappingHelper = {
  181. generateSubscriptionToRecurlyMapping: sinon.stub(),
  182. }),
  183. '../../infrastructure/Features': this.Features,
  184. '../User/UserAuditLogHandler': this.UserAuditLogHandler,
  185. '../User/UserUpdater': this.UserUpdater,
  186. '../../infrastructure/Modules': (this.Modules = {
  187. promises: {
  188. hooks: {
  189. fire: sinon.stub().resolves(),
  190. },
  191. },
  192. }),
  193. },
  194. })
  195. })
  196. describe('updateAdmin', function () {
  197. it('should update the subscription admin', async function () {
  198. this.subscription.groupPlan = true
  199. await this.SubscriptionUpdater.promises.updateAdmin(
  200. this.subscription,
  201. this.otherUserId
  202. )
  203. const query = {
  204. _id: new ObjectId(this.subscription._id),
  205. customAccount: true,
  206. }
  207. const update = {
  208. $set: { admin_id: new ObjectId(this.otherUserId) },
  209. $addToSet: { manager_ids: new ObjectId(this.otherUserId) },
  210. }
  211. this.SubscriptionModel.updateOne.should.have.been.calledOnce
  212. this.SubscriptionModel.updateOne.should.have.been.calledWith(
  213. query,
  214. update
  215. )
  216. })
  217. it('should remove the manager for non-group subscriptions', async function () {
  218. await this.SubscriptionUpdater.promises.updateAdmin(
  219. this.subscription,
  220. this.otherUserId
  221. )
  222. const query = {
  223. _id: new ObjectId(this.subscription._id),
  224. customAccount: true,
  225. }
  226. const update = {
  227. $set: {
  228. admin_id: new ObjectId(this.otherUserId),
  229. manager_ids: [new ObjectId(this.otherUserId)],
  230. },
  231. }
  232. this.SubscriptionModel.updateOne.should.have.been.calledOnce
  233. this.SubscriptionModel.updateOne.should.have.been.calledWith(
  234. query,
  235. update
  236. )
  237. })
  238. })
  239. describe('syncSubscription', function () {
  240. beforeEach(function () {
  241. this.SubscriptionLocator.promises.getUsersSubscription.resolves(
  242. this.subscription
  243. )
  244. })
  245. it('should update the subscription if the user already is admin of one', async function () {
  246. await this.SubscriptionUpdater.promises.syncSubscription(
  247. this.recurlySubscription,
  248. this.adminUser._id
  249. )
  250. this.SubscriptionLocator.promises.getUsersSubscription
  251. .calledWith(this.adminUser._id)
  252. .should.equal(true)
  253. })
  254. it('should not call updateFeatures with group subscription if recurly subscription is not expired', async function () {
  255. await this.SubscriptionUpdater.promises.syncSubscription(
  256. this.recurlySubscription,
  257. this.adminUser._id
  258. )
  259. this.SubscriptionLocator.promises.getUsersSubscription
  260. .calledWith(this.adminUser._id)
  261. .should.equal(true)
  262. this.UserFeaturesUpdater.promises.updateFeatures.called.should.equal(
  263. false
  264. )
  265. })
  266. })
  267. describe('updateSubscriptionFromRecurly', function () {
  268. afterEach(function () {
  269. this.subscription.member_ids = []
  270. })
  271. it('should update the subscription with token etc when not expired', async function () {
  272. await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  273. this.recurlySubscription,
  274. this.subscription,
  275. {}
  276. )
  277. this.subscription.recurlySubscription_id.should.equal(
  278. this.recurlySubscription.uuid
  279. )
  280. this.subscription.planCode.should.equal(
  281. this.recurlySubscription.plan.plan_code
  282. )
  283. this.subscription.save.called.should.equal(true)
  284. expect(
  285. this.FeaturesUpdater.promises.scheduleRefreshFeatures
  286. ).to.have.been.calledWith(this.adminUser._id)
  287. })
  288. it('should send a recurly account mapping event', async function () {
  289. const createdAt = new Date().toISOString()
  290. this.AccountMappingHelper.generateSubscriptionToRecurlyMapping.returns({
  291. source: 'recurly',
  292. sourceEntity: 'subscription',
  293. sourceEntityId: this.recurlySubscription.uuid,
  294. target: 'v2',
  295. targetEntity: 'subscription',
  296. targetEntityId: this.subscription._id,
  297. createdAt,
  298. })
  299. await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  300. this.recurlySubscription,
  301. this.subscription,
  302. {}
  303. )
  304. expect(
  305. this.AccountMappingHelper.generateSubscriptionToRecurlyMapping
  306. ).to.have.been.calledWith(
  307. this.subscription._id,
  308. this.recurlySubscription.uuid
  309. )
  310. expect(
  311. this.AnalyticsManager.registerAccountMapping
  312. ).to.have.been.calledWith({
  313. source: 'recurly',
  314. sourceEntity: 'subscription',
  315. sourceEntityId: this.recurlySubscription.uuid,
  316. target: 'v2',
  317. targetEntity: 'subscription',
  318. targetEntityId: this.subscription._id,
  319. createdAt,
  320. })
  321. })
  322. it('should remove the subscription when expired', async function () {
  323. this.recurlySubscription.state = 'expired'
  324. await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  325. this.recurlySubscription,
  326. this.subscription,
  327. {}
  328. )
  329. this.SubscriptionModel.deleteOne.should.have.been.calledWith({
  330. _id: this.subscription._id,
  331. })
  332. })
  333. it('should not remove the subscription when expired if it has Managed Users enabled', async function () {
  334. this.Features.hasFeature.withArgs('saas').returns(true)
  335. this.subscription.managedUsersEnabled = true
  336. this.recurlySubscription.state = 'expired'
  337. await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  338. this.recurlySubscription,
  339. this.subscription,
  340. {}
  341. )
  342. this.SubscriptionModel.deleteOne.should.not.have.been.called
  343. })
  344. it('should not remove the subscription when expired if it has Group SSO enabled', async function () {
  345. this.Features.hasFeature.withArgs('saas').returns(true)
  346. this.subscription.ssoConfig = new ObjectId('abc123abc123abc123abc123')
  347. this.recurlySubscription.state = 'expired'
  348. await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  349. this.recurlySubscription,
  350. this.subscription,
  351. {}
  352. )
  353. this.SubscriptionModel.deleteOne.should.not.have.been.called
  354. })
  355. it('should update all the users features', async function () {
  356. this.subscription.member_ids = this.allUserIds
  357. await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  358. this.recurlySubscription,
  359. this.subscription,
  360. {}
  361. )
  362. expect(
  363. this.FeaturesUpdater.promises.scheduleRefreshFeatures
  364. ).to.have.been.calledWith(this.adminUser._id)
  365. expect(
  366. this.FeaturesUpdater.promises.scheduleRefreshFeatures
  367. ).to.have.been.calledWith(this.allUserIds[0])
  368. expect(
  369. this.FeaturesUpdater.promises.scheduleRefreshFeatures
  370. ).to.have.been.calledWith(this.allUserIds[1])
  371. expect(
  372. this.FeaturesUpdater.promises.scheduleRefreshFeatures
  373. ).to.have.been.calledWith(this.allUserIds[2])
  374. })
  375. it('should set group to true and save how many members can be added to group', async function () {
  376. this.recurlyPlan.groupPlan = true
  377. this.recurlyPlan.membersLimit = 5
  378. await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  379. this.recurlySubscription,
  380. this.subscription,
  381. {}
  382. )
  383. this.subscription.membersLimit.should.equal(5)
  384. this.subscription.groupPlan.should.equal(true)
  385. this.subscription.member_ids.should.deep.equal([
  386. this.subscription.admin_id,
  387. ])
  388. })
  389. it('should delete and replace subscription when downgrading from group to individual plan', async function () {
  390. this.recurlyPlan.groupPlan = false
  391. await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  392. this.recurlySubscription,
  393. this.groupSubscription,
  394. {}
  395. )
  396. })
  397. it('should not set group to true or set groupPlan', async function () {
  398. await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  399. this.recurlySubscription,
  400. this.subscription,
  401. {}
  402. )
  403. assert.notEqual(this.subscription.membersLimit, 5)
  404. assert.notEqual(this.subscription.groupPlan, true)
  405. })
  406. describe('when the plan allows adding more seats', function () {
  407. beforeEach(function () {
  408. this.membersLimitAddOn = 'add_on1'
  409. this.recurlyPlan.groupPlan = true
  410. this.recurlyPlan.membersLimit = 5
  411. this.recurlyPlan.membersLimitAddOn = this.membersLimitAddOn
  412. })
  413. function expectMembersLimit(limit) {
  414. it('should set the membersLimit accordingly', async function () {
  415. await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  416. this.recurlySubscription,
  417. this.subscription,
  418. {}
  419. )
  420. expect(this.subscription.membersLimit).to.equal(limit)
  421. })
  422. }
  423. describe('when the recurlySubscription does not have add ons', function () {
  424. beforeEach(function () {
  425. delete this.recurlySubscription.subscription_add_ons
  426. })
  427. expectMembersLimit(5)
  428. })
  429. describe('when the recurlySubscription has non-matching add ons', function () {
  430. beforeEach(function () {
  431. this.recurlySubscription.subscription_add_ons = [
  432. { add_on_code: 'add_on_99', quantity: 3 },
  433. ]
  434. })
  435. expectMembersLimit(5)
  436. })
  437. describe('when the recurlySubscription has a matching add on', function () {
  438. beforeEach(function () {
  439. this.recurlySubscription.subscription_add_ons = [
  440. { add_on_code: this.membersLimitAddOn, quantity: 10 },
  441. ]
  442. })
  443. expectMembersLimit(15)
  444. })
  445. // NOTE: This is unexpected, but we are going to support it anyways.
  446. describe('when the recurlySubscription has multiple matching add ons', function () {
  447. beforeEach(function () {
  448. this.recurlySubscription.subscription_add_ons = [
  449. { add_on_code: this.membersLimitAddOn, quantity: 10 },
  450. { add_on_code: this.membersLimitAddOn, quantity: 3 },
  451. ]
  452. })
  453. expectMembersLimit(18)
  454. })
  455. })
  456. })
  457. describe('addUserToGroup', function () {
  458. it('should add the user ids to the group as a set', async function () {
  459. this.SubscriptionModel.findOne = sinon
  460. .stub()
  461. .resolves(this.groupSubscription)
  462. await this.SubscriptionUpdater.promises.addUserToGroup(
  463. this.groupSubscription._id,
  464. this.otherUserId
  465. )
  466. const searchOps = { _id: this.groupSubscription._id }
  467. const insertOperation = {
  468. $addToSet: { member_ids: this.otherUserId },
  469. }
  470. this.SubscriptionModel.updateOne
  471. .calledWith(searchOps, insertOperation)
  472. .should.equal(true)
  473. expect(this.SubscriptionModel.updateOne.lastCall.args[2].session).to.exist
  474. sinon.assert.calledWith(
  475. this.AnalyticsManager.recordEventForUserInBackground,
  476. this.otherUserId,
  477. 'group-subscription-joined',
  478. {
  479. groupId: this.groupSubscription._id,
  480. subscriptionId: this.groupSubscription.recurlySubscription_id,
  481. }
  482. )
  483. })
  484. it('should update the users features', async function () {
  485. await this.SubscriptionUpdater.promises.addUserToGroup(
  486. this.subscription._id,
  487. this.otherUserId
  488. )
  489. this.FeaturesUpdater.promises.refreshFeatures
  490. .calledWith(this.otherUserId)
  491. .should.equal(true)
  492. })
  493. it('should set the group plan code user property to the best plan with 1 group subscription', async function () {
  494. this.SubscriptionLocator.promises.getMemberSubscriptions
  495. .withArgs(this.otherUserId)
  496. .resolves([this.groupSubscription])
  497. await this.SubscriptionUpdater.promises.addUserToGroup(
  498. this.groupSubscription._id,
  499. this.otherUserId
  500. )
  501. sinon.assert.calledWith(
  502. this.AnalyticsManager.setUserPropertyForUserInBackground,
  503. this.otherUserId,
  504. 'group-subscription-plan-code',
  505. 'group_subscription'
  506. )
  507. })
  508. it('should set the group plan code user property to the best plan with 2 group subscriptions', async function () {
  509. this.SubscriptionLocator.promises.getMemberSubscriptions
  510. .withArgs(this.otherUserId)
  511. .resolves([this.groupSubscription, this.betterGroupSubscription])
  512. await this.SubscriptionUpdater.promises.addUserToGroup(
  513. this.betterGroupSubscription._id,
  514. this.otherUserId
  515. )
  516. sinon.assert.calledWith(
  517. this.AnalyticsManager.setUserPropertyForUserInBackground,
  518. this.otherUserId,
  519. 'group-subscription-plan-code',
  520. 'better_group_subscription'
  521. )
  522. })
  523. it('should set the group plan code user property to the best plan with 2 group subscriptions in reverse order', async function () {
  524. this.SubscriptionLocator.promises.getMemberSubscriptions
  525. .withArgs(this.otherUserId)
  526. .resolves([this.betterGroupSubscription, this.groupSubscription])
  527. await this.SubscriptionUpdater.promises.addUserToGroup(
  528. this.betterGroupSubscription._id,
  529. this.otherUserId
  530. )
  531. sinon.assert.calledWith(
  532. this.AnalyticsManager.setUserPropertyForUserInBackground,
  533. this.otherUserId,
  534. 'group-subscription-plan-code',
  535. 'better_group_subscription'
  536. )
  537. })
  538. it('should add an entry to the user audit log when joining a group', async function () {
  539. await this.SubscriptionUpdater.promises.addUserToGroup(
  540. this.subscription._id,
  541. this.otherUserId
  542. )
  543. sinon.assert.calledWith(
  544. this.UserAuditLogHandler.promises.addEntry,
  545. this.otherUserId,
  546. 'join-group-subscription',
  547. undefined,
  548. undefined,
  549. {
  550. subscriptionId: this.subscription._id,
  551. }
  552. )
  553. })
  554. it('should add an entry to the group audit log when joining a group', async function () {
  555. await this.SubscriptionUpdater.promises.addUserToGroup(
  556. this.subscription._id,
  557. this.otherUserId,
  558. { ipAddress: '0:0:0:0', initiatorId: 'user123' }
  559. )
  560. expect(this.Modules.promises.hooks.fire).to.have.been.calledWith(
  561. 'addGroupAuditLogEntry',
  562. {
  563. groupId: this.subscription._id,
  564. initiatorId: 'user123',
  565. ipAddress: '0:0:0:0',
  566. operation: 'join-group',
  567. }
  568. )
  569. })
  570. })
  571. describe('removeUserFromGroup', function () {
  572. beforeEach(function () {
  573. this.fakeSubscriptions = [
  574. {
  575. _id: 'fake-id-1',
  576. },
  577. {
  578. _id: 'fake-id-2',
  579. },
  580. ]
  581. this.SubscriptionModel.findOne.resolves(this.groupSubscription)
  582. this.SubscriptionModel.findById = sinon
  583. .stub()
  584. .resolves(this.groupSubscription)
  585. this.SubscriptionLocator.promises.getMemberSubscriptions.resolves(
  586. this.fakeSubscriptions
  587. )
  588. })
  589. it('should pull the users id from the group', async function () {
  590. await this.SubscriptionUpdater.promises.removeUserFromGroup(
  591. this.subscription._id,
  592. this.otherUserId
  593. )
  594. const removeOperation = { $pull: { member_ids: this.otherUserId } }
  595. this.SubscriptionModel.updateOne
  596. .calledWith({ _id: this.subscription._id }, removeOperation)
  597. .should.equal(true)
  598. })
  599. it('should remove user enrollment if the group is managed', async function () {
  600. this.SubscriptionModel.findById.resolves({
  601. ...this.groupSubscription,
  602. managedUsersEnabled: true,
  603. })
  604. await this.SubscriptionUpdater.promises.removeUserFromGroup(
  605. this.groupSubscription._id,
  606. this.otherUserId
  607. )
  608. this.UserUpdater.promises.updateUser
  609. .calledWith(
  610. { _id: this.otherUserId },
  611. {
  612. $unset: {
  613. 'enrollment.managedBy': 1,
  614. 'enrollment.enrolledAt': 1,
  615. },
  616. }
  617. )
  618. .should.equal(true)
  619. })
  620. it('should send a group-subscription-left event', async function () {
  621. await this.SubscriptionUpdater.promises.removeUserFromGroup(
  622. this.groupSubscription._id,
  623. this.otherUserId
  624. )
  625. sinon.assert.calledWith(
  626. this.AnalyticsManager.recordEventForUserInBackground,
  627. this.otherUserId,
  628. 'group-subscription-left',
  629. {
  630. groupId: this.groupSubscription._id,
  631. subscriptionId: this.groupSubscription.recurlySubscription_id,
  632. }
  633. )
  634. })
  635. it('should set the group plan code user property when removing user from group', async function () {
  636. await this.SubscriptionUpdater.promises.removeUserFromGroup(
  637. this.subscription._id,
  638. this.otherUserId
  639. )
  640. sinon.assert.calledWith(
  641. this.AnalyticsManager.setUserPropertyForUserInBackground,
  642. this.otherUserId,
  643. 'group-subscription-plan-code',
  644. null
  645. )
  646. })
  647. it('should update the users features', async function () {
  648. await this.SubscriptionUpdater.promises.removeUserFromGroup(
  649. this.subscription._id,
  650. this.otherUserId
  651. )
  652. this.FeaturesUpdater.promises.refreshFeatures
  653. .calledWith(this.otherUserId)
  654. .should.equal(true)
  655. })
  656. it('should add an audit log when a user leaves a group', async function () {
  657. await this.SubscriptionUpdater.promises.removeUserFromGroup(
  658. this.subscription._id,
  659. this.otherUserId
  660. )
  661. sinon.assert.calledWith(
  662. this.UserAuditLogHandler.promises.addEntry,
  663. this.otherUserId,
  664. 'leave-group-subscription',
  665. undefined,
  666. undefined,
  667. {
  668. subscriptionId: this.subscription._id,
  669. }
  670. )
  671. })
  672. })
  673. describe('removeUserFromAllGroups', function () {
  674. beforeEach(function () {
  675. this.SubscriptionLocator.promises.getMemberSubscriptions.resolves([
  676. {
  677. _id: 'fake-id-1',
  678. },
  679. {
  680. _id: 'fake-id-2',
  681. },
  682. ])
  683. })
  684. it('should set the group plan code user property when removing user from all groups', async function () {
  685. await this.SubscriptionUpdater.promises.removeUserFromAllGroups(
  686. this.otherUserId
  687. )
  688. sinon.assert.calledWith(
  689. this.AnalyticsManager.setUserPropertyForUserInBackground,
  690. this.otherUserId,
  691. 'group-subscription-plan-code',
  692. null
  693. )
  694. })
  695. it('should pull the users id from all groups', async function () {
  696. await this.SubscriptionUpdater.promises.removeUserFromAllGroups(
  697. this.otherUserId
  698. )
  699. const filter = { _id: ['fake-id-1', 'fake-id-2'] }
  700. const removeOperation = { $pull: { member_ids: this.otherUserId } }
  701. sinon.assert.calledWith(
  702. this.SubscriptionModel.updateMany,
  703. filter,
  704. removeOperation
  705. )
  706. })
  707. it('should send a group-subscription-left event for each group', async function () {
  708. this.fakeSub1 = {
  709. _id: 'fake-id-1',
  710. groupPlan: true,
  711. recurlySubscription_id: 'fake-sub-1',
  712. }
  713. this.fakeSub2 = {
  714. _id: 'fake-id-2',
  715. groupPlan: true,
  716. recurlySubscription_id: 'fake-sub-2',
  717. }
  718. this.SubscriptionModel.findOne
  719. .withArgs(
  720. { _id: 'fake-id-1' },
  721. { recurlySubscription_id: 1, groupPlan: 1 }
  722. )
  723. .resolves(this.fakeSub1)
  724. .withArgs(
  725. { _id: 'fake-id-2' },
  726. { recurlySubscription_id: 1, groupPlan: 1 }
  727. )
  728. .resolves(this.fakeSub2)
  729. await this.SubscriptionUpdater.promises.removeUserFromAllGroups(
  730. this.otherUserId
  731. )
  732. sinon.assert.calledWith(
  733. this.AnalyticsManager.recordEventForUserInBackground,
  734. this.otherUserId,
  735. 'group-subscription-left',
  736. {
  737. groupId: 'fake-id-1',
  738. subscriptionId: 'fake-sub-1',
  739. }
  740. )
  741. sinon.assert.calledWith(
  742. this.AnalyticsManager.recordEventForUserInBackground,
  743. this.otherUserId,
  744. 'group-subscription-left',
  745. {
  746. groupId: 'fake-id-2',
  747. subscriptionId: 'fake-sub-2',
  748. }
  749. )
  750. })
  751. it('should add an audit log entry for each group the user leaves', async function () {
  752. await this.SubscriptionUpdater.promises.removeUserFromAllGroups(
  753. this.otherUserId
  754. )
  755. sinon.assert.calledWith(
  756. this.UserAuditLogHandler.promises.addEntry,
  757. this.otherUserId,
  758. 'leave-group-subscription',
  759. undefined,
  760. undefined,
  761. {
  762. subscriptionId: 'fake-id-1',
  763. }
  764. )
  765. sinon.assert.calledWith(
  766. this.UserAuditLogHandler.promises.addEntry,
  767. this.otherUserId,
  768. 'leave-group-subscription',
  769. undefined,
  770. undefined,
  771. {
  772. subscriptionId: 'fake-id-2',
  773. }
  774. )
  775. })
  776. })
  777. describe('deleteSubscription', function () {
  778. beforeEach(async function () {
  779. this.subscription = {
  780. _id: new ObjectId().toString(),
  781. mock: 'subscription',
  782. admin_id: new ObjectId(),
  783. member_ids: [new ObjectId(), new ObjectId(), new ObjectId()],
  784. }
  785. await this.SubscriptionUpdater.promises.deleteSubscription(
  786. this.subscription,
  787. {}
  788. )
  789. })
  790. it('should remove the subscription', function () {
  791. this.SubscriptionModel.deleteOne
  792. .calledWith({ _id: this.subscription._id })
  793. .should.equal(true)
  794. })
  795. it('should downgrade the admin_id', function () {
  796. expect(
  797. this.FeaturesUpdater.promises.scheduleRefreshFeatures
  798. ).to.have.been.calledWith(this.subscription.admin_id)
  799. })
  800. it('should downgrade all of the members', function () {
  801. for (const userId of this.subscription.member_ids) {
  802. expect(
  803. this.FeaturesUpdater.promises.scheduleRefreshFeatures
  804. ).to.have.been.calledWith(userId)
  805. }
  806. })
  807. })
  808. describe('scheduleRefreshFeatures', function () {
  809. it('should call upgrades feature for personal subscription from admin_id', async function () {
  810. this.subscription = {
  811. _id: new ObjectId().toString(),
  812. mock: 'subscription',
  813. admin_id: new ObjectId(),
  814. }
  815. await this.SubscriptionUpdater.promises.scheduleRefreshFeatures(
  816. this.subscription
  817. )
  818. expect(
  819. this.FeaturesUpdater.promises.scheduleRefreshFeatures
  820. ).to.have.been.calledOnceWith(this.subscription.admin_id)
  821. })
  822. it('should call upgrades feature for group subscription from admin_id and member_ids', async function () {
  823. this.subscription = {
  824. _id: new ObjectId().toString(),
  825. mock: 'subscription',
  826. admin_id: new ObjectId(),
  827. member_ids: [new ObjectId(), new ObjectId(), new ObjectId()],
  828. }
  829. await this.SubscriptionUpdater.promises.scheduleRefreshFeatures(
  830. this.subscription
  831. )
  832. expect(
  833. this.FeaturesUpdater.promises.scheduleRefreshFeatures.callCount
  834. ).to.equal(4)
  835. })
  836. })
  837. })