SubscriptionUpdater.test.mjs 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162
  1. import { beforeEach, describe, it, vi, assert, expect } from 'vitest'
  2. import sinon from 'sinon'
  3. import mongodb from 'mongodb-legacy'
  4. const modulePath =
  5. '../../../../app/src/Features/Subscription/SubscriptionUpdater'
  6. const { ObjectId } = mongodb
  7. describe('SubscriptionUpdater', function () {
  8. beforeEach(async function (ctx) {
  9. ctx.recurlyPlan = { planCode: 'recurly-plan' }
  10. ctx.recurlySubscription = {
  11. uuid: '1238uoijdasjhd',
  12. plan: {
  13. plan_code: ctx.recurlyPlan.planCode,
  14. },
  15. }
  16. ctx.adminUser = { _id: (ctx.adminuser_id = '5208dd34438843e2db000007') }
  17. ctx.otherUserId = '5208dd34438842e2db000005'
  18. ctx.allUserIds = ['13213', 'dsadas', 'djsaiud89']
  19. ctx.subscription = {
  20. _id: '111111111111111111111111',
  21. admin_id: ctx.adminUser._id,
  22. manager_ids: [ctx.adminUser._id],
  23. member_ids: [],
  24. save: sinon.stub().resolves(),
  25. planCode: 'student_or_something',
  26. recurlySubscription_id: 'abc123def456fab789',
  27. }
  28. ctx.user_id = ctx.adminuser_id
  29. ctx.groupSubscription = {
  30. _id: '222222222222222222222222',
  31. admin_id: ctx.adminUser._id,
  32. manager_ids: [ctx.adminUser._id],
  33. member_ids: ctx.allUserIds,
  34. save: sinon.stub().resolves(),
  35. groupPlan: true,
  36. planCode: 'group_subscription',
  37. recurlySubscription_id: '456fab789abc123def',
  38. }
  39. ctx.betterGroupSubscription = {
  40. _id: '999999999999999999999999',
  41. admin_id: ctx.adminUser._id,
  42. manager_ids: [ctx.adminUser._id],
  43. member_ids: [ctx.otherUserId],
  44. save: sinon.stub().resolves(),
  45. groupPlan: true,
  46. planCode: 'better_group_subscription',
  47. recurlySubscription_id: '123def456fab789abc',
  48. }
  49. const subscription = ctx.subscription
  50. ctx.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. ctx.SubscriptionModel.deleteOne = sinon
  62. .stub()
  63. .returns({ exec: sinon.stub().resolves() })
  64. ctx.SubscriptionModel.updateOne = sinon
  65. .stub()
  66. .returns({ exec: sinon.stub().resolves() })
  67. ctx.SubscriptionModel.findOne = sinon.stub().resolves()
  68. ctx.SubscriptionModel.findById = sinon.stub().resolves()
  69. ctx.SubscriptionModel.updateMany = sinon
  70. .stub()
  71. .returns({ exec: sinon.stub().resolves() })
  72. ctx.SubscriptionModel.findOneAndUpdate = sinon.stub().returns({
  73. exec: sinon.stub().resolves(ctx.subscription),
  74. })
  75. ctx.SSOConfigModel = class {}
  76. ctx.SSOConfigModel.findOne = sinon.stub().returns({
  77. lean: sinon.stub().returns({
  78. exec: sinon.stub().resolves({ enabled: true }),
  79. }),
  80. })
  81. ctx.SubscriptionLocator = {
  82. promises: {
  83. getUsersSubscription: sinon.stub(),
  84. getGroupSubscriptionMemberOf: sinon.stub(),
  85. getMemberSubscriptions: sinon.stub().resolves([]),
  86. getManagedGroupSubscriptions: sinon.stub().resolves([]),
  87. getSubscription: sinon.stub(),
  88. },
  89. }
  90. ctx.SubscriptionLocator.promises.getSubscription
  91. .withArgs(ctx.subscription._id)
  92. .resolves(ctx.subscription)
  93. ctx.Settings = {
  94. defaultPlanCode: 'personal',
  95. defaultFeatures: { default: 'features' },
  96. plans: [
  97. ctx.recurlyPlan,
  98. { planCode: ctx.subscription.planCode, features: {} },
  99. {
  100. planCode: ctx.groupSubscription.planCode,
  101. features: {
  102. collaborators: 10,
  103. compileTimeout: 60,
  104. dropbox: true,
  105. },
  106. },
  107. {
  108. planCode: ctx.betterGroupSubscription.planCode,
  109. features: {
  110. collaborators: -1,
  111. compileTimeout: 240,
  112. dropbox: true,
  113. },
  114. },
  115. ],
  116. mongo: {
  117. options: {
  118. appname: 'web',
  119. maxPoolSize: 100,
  120. serverSelectionTimeoutMS: 60000,
  121. socketTimeoutMS: 60000,
  122. monitorCommands: true,
  123. family: 4,
  124. },
  125. url: 'mongodb://mongo/test-overleaf',
  126. hasSecondaries: false,
  127. },
  128. }
  129. ctx.UserFeaturesUpdater = {
  130. promises: {
  131. updateFeatures: sinon.stub().resolves(),
  132. },
  133. }
  134. ctx.ReferalFeatures = {
  135. promises: {
  136. getBonusFeatures: sinon.stub().resolves(),
  137. },
  138. }
  139. ctx.FeaturesUpdater = {
  140. promises: {
  141. scheduleRefreshFeatures: sinon.stub().resolves(),
  142. refreshFeatures: sinon.stub().resolves({}),
  143. },
  144. }
  145. ctx.DeletedSubscription = {
  146. findOneAndUpdate: sinon.stub().returns({ exec: sinon.stub().resolves() }),
  147. }
  148. ctx.AnalyticsManager = {
  149. recordEventForUserInBackground: sinon.stub().resolves(),
  150. setUserPropertyForUserInBackground: sinon.stub(),
  151. registerAccountMapping: sinon.stub(),
  152. }
  153. ctx.Features = {
  154. hasFeature: sinon.stub().returns(false),
  155. }
  156. ctx.UserAuditLogHandler = {
  157. promises: {
  158. addEntry: sinon.stub().resolves(),
  159. },
  160. }
  161. ctx.UserUpdater = {
  162. promises: {
  163. updateUser: sinon.stub().resolves(),
  164. },
  165. }
  166. vi.doMock('../../../../app/src/models/Subscription', () => ({
  167. Subscription: ctx.SubscriptionModel,
  168. }))
  169. vi.doMock('../../../../app/src/models/SSOConfig', () => ({
  170. SSOConfig: ctx.SSOConfigModel,
  171. }))
  172. vi.doMock(
  173. '../../../../app/src/Features/Subscription/UserFeaturesUpdater',
  174. () => ({
  175. default: ctx.UserFeaturesUpdater,
  176. })
  177. )
  178. vi.doMock(
  179. '../../../../app/src/Features/Subscription/SubscriptionLocator',
  180. () => ({
  181. default: ctx.SubscriptionLocator,
  182. })
  183. )
  184. vi.doMock('@overleaf/settings', () => ({
  185. default: ctx.Settings,
  186. }))
  187. vi.doMock('../../../../app/src/infrastructure/mongodb', () => ({
  188. db: {},
  189. ObjectId,
  190. }))
  191. vi.doMock(
  192. '../../../../app/src/Features/Subscription/FeaturesUpdater',
  193. () => ({
  194. default: ctx.FeaturesUpdater,
  195. })
  196. )
  197. vi.doMock('../../../../app/src/models/DeletedSubscription', () => ({
  198. DeletedSubscription: ctx.DeletedSubscription,
  199. }))
  200. vi.doMock(
  201. '../../../../app/src/Features/Analytics/AnalyticsManager',
  202. () => ({
  203. default: ctx.AnalyticsManager,
  204. })
  205. )
  206. vi.doMock(
  207. '../../../../app/src/Features/Analytics/AccountMappingHelper',
  208. () => ({
  209. default: (ctx.AccountMappingHelper = {
  210. generateSubscriptionToRecurlyMapping: sinon.stub(),
  211. }),
  212. })
  213. )
  214. vi.doMock('../../../../app/src/infrastructure/Features', () => ({
  215. default: ctx.Features,
  216. }))
  217. vi.doMock('../../../../app/src/Features/User/UserAuditLogHandler', () => ({
  218. default: ctx.UserAuditLogHandler,
  219. }))
  220. vi.doMock('../../../../app/src/Features/User/UserUpdater', () => ({
  221. default: ctx.UserUpdater,
  222. }))
  223. vi.doMock('../../../../app/src/infrastructure/Modules', () => ({
  224. default: (ctx.Modules = {
  225. promises: {
  226. hooks: {
  227. fire: sinon.stub().resolves(),
  228. },
  229. },
  230. }),
  231. }))
  232. ctx.SubscriptionUpdater = (await import(modulePath)).default
  233. })
  234. describe('updateAdmin', function () {
  235. it('should update the subscription admin', async function (ctx) {
  236. ctx.subscription.groupPlan = true
  237. await ctx.SubscriptionUpdater.promises.updateAdmin(
  238. ctx.subscription,
  239. ctx.otherUserId
  240. )
  241. const query = {
  242. _id: new ObjectId(ctx.subscription._id),
  243. customAccount: true,
  244. }
  245. const update = {
  246. $set: { admin_id: new ObjectId(ctx.otherUserId) },
  247. $addToSet: { manager_ids: new ObjectId(ctx.otherUserId) },
  248. }
  249. ctx.SubscriptionModel.updateOne.should.have.been.calledOnce
  250. ctx.SubscriptionModel.updateOne.should.have.been.calledWith(query, update)
  251. })
  252. it("should fire setUserProperties for both old and new admin's group_role on group subscriptions", async function (ctx) {
  253. ctx.subscription.groupPlan = true
  254. ctx.SubscriptionLocator.promises.getManagedGroupSubscriptions
  255. .withArgs(ctx.otherUserId)
  256. .resolves([{ admin_id: { _id: ctx.otherUserId } }])
  257. ctx.SubscriptionLocator.promises.getManagedGroupSubscriptions
  258. .withArgs(ctx.adminUser._id)
  259. .resolves([{ admin_id: { _id: ctx.otherUserId } }])
  260. await ctx.SubscriptionUpdater.promises.updateAdmin(
  261. ctx.subscription,
  262. ctx.otherUserId
  263. )
  264. expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
  265. 'setUserProperties',
  266. ctx.adminUser._id,
  267. { group_role: 'manager' }
  268. )
  269. expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
  270. 'setUserProperties',
  271. ctx.otherUserId,
  272. { group_role: 'admin' }
  273. )
  274. })
  275. it('should not fire setUserProperties for group_role on non-group subscriptions', async function (ctx) {
  276. await ctx.SubscriptionUpdater.promises.updateAdmin(
  277. ctx.subscription,
  278. ctx.otherUserId
  279. )
  280. expect(ctx.Modules.promises.hooks.fire).to.not.have.been.calledWith(
  281. 'setUserProperties',
  282. sinon.match.any,
  283. sinon.match.has('group_role')
  284. )
  285. })
  286. it('should remove the manager for non-group subscriptions', async function (ctx) {
  287. await ctx.SubscriptionUpdater.promises.updateAdmin(
  288. ctx.subscription,
  289. ctx.otherUserId
  290. )
  291. const query = {
  292. _id: new ObjectId(ctx.subscription._id),
  293. customAccount: true,
  294. }
  295. const update = {
  296. $set: {
  297. admin_id: new ObjectId(ctx.otherUserId),
  298. manager_ids: [new ObjectId(ctx.otherUserId)],
  299. },
  300. }
  301. ctx.SubscriptionModel.updateOne.should.have.been.calledOnce
  302. ctx.SubscriptionModel.updateOne.should.have.been.calledWith(query, update)
  303. })
  304. })
  305. describe('transferSubscriptionOwnership', function () {
  306. it('should transfer the subscription ownership for group subscriptions', async function (ctx) {
  307. ctx.subscription.groupPlan = true
  308. ctx.subscription.paymentProvider = {
  309. id: 'stripe-123',
  310. name: 'stripe-us',
  311. }
  312. await ctx.SubscriptionUpdater.promises.transferSubscriptionOwnership(
  313. ctx.subscription,
  314. ctx.otherUserId,
  315. false
  316. )
  317. const query = {
  318. _id: new ObjectId(ctx.subscription._id),
  319. }
  320. const update = {
  321. $set: {
  322. admin_id: new ObjectId(ctx.otherUserId),
  323. previousPaymentProvider: ctx.subscription.paymentProvider,
  324. },
  325. $addToSet: { manager_ids: new ObjectId(ctx.otherUserId) },
  326. }
  327. ctx.SubscriptionModel.updateOne.should.have.been.calledOnce
  328. ctx.SubscriptionModel.updateOne.should.have.been.calledWith(query, update)
  329. })
  330. it('should transfer the subscription ownership for non-group subscriptions', async function (ctx) {
  331. ctx.subscription.paymentProvider = {
  332. id: 'stripe-123',
  333. name: 'stripe-us',
  334. }
  335. await ctx.SubscriptionUpdater.promises.transferSubscriptionOwnership(
  336. ctx.subscription,
  337. ctx.otherUserId,
  338. false
  339. )
  340. const query = {
  341. _id: new ObjectId(ctx.subscription._id),
  342. }
  343. const update = {
  344. $set: {
  345. admin_id: new ObjectId(ctx.otherUserId),
  346. manager_ids: [new ObjectId(ctx.otherUserId)],
  347. previousPaymentProvider: ctx.subscription.paymentProvider,
  348. },
  349. }
  350. ctx.SubscriptionModel.updateOne.should.have.been.calledOnce
  351. ctx.SubscriptionModel.updateOne.should.have.been.calledWith(query, update)
  352. })
  353. it("should fire setUserProperties for both old and new admin's group_role on group subscriptions", async function (ctx) {
  354. ctx.subscription.groupPlan = true
  355. ctx.SubscriptionLocator.promises.getManagedGroupSubscriptions
  356. .withArgs(ctx.otherUserId)
  357. .resolves([{ admin_id: { _id: ctx.otherUserId } }])
  358. ctx.SubscriptionLocator.promises.getManagedGroupSubscriptions
  359. .withArgs(ctx.adminUser._id)
  360. .resolves([{ admin_id: { _id: ctx.otherUserId } }])
  361. await ctx.SubscriptionUpdater.promises.transferSubscriptionOwnership(
  362. ctx.subscription,
  363. ctx.otherUserId,
  364. false
  365. )
  366. expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
  367. 'setUserProperties',
  368. ctx.adminUser._id,
  369. { group_role: 'manager' }
  370. )
  371. expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
  372. 'setUserProperties',
  373. ctx.otherUserId,
  374. { group_role: 'admin' }
  375. )
  376. })
  377. it('should not fire setUserProperties for group_role on non-group subscriptions', async function (ctx) {
  378. await ctx.SubscriptionUpdater.promises.transferSubscriptionOwnership(
  379. ctx.subscription,
  380. ctx.otherUserId,
  381. false
  382. )
  383. expect(ctx.Modules.promises.hooks.fire).to.not.have.been.calledWith(
  384. 'setUserProperties',
  385. sinon.match.any,
  386. sinon.match.has('group_role')
  387. )
  388. })
  389. it('should clear previousPaymentProvider when clearPreviousPaymentProvider is true', async function (ctx) {
  390. ctx.subscription.paymentProvider = {
  391. id: 'stripe-123',
  392. name: 'stripe-us',
  393. }
  394. await ctx.SubscriptionUpdater.promises.transferSubscriptionOwnership(
  395. ctx.subscription,
  396. ctx.otherUserId,
  397. true
  398. )
  399. const query = {
  400. _id: new ObjectId(ctx.subscription._id),
  401. }
  402. const update = {
  403. $set: {
  404. admin_id: new ObjectId(ctx.otherUserId),
  405. manager_ids: [new ObjectId(ctx.otherUserId)],
  406. },
  407. $unset: { previousPaymentProvider: 1 },
  408. }
  409. ctx.SubscriptionModel.updateOne.should.have.been.calledOnce
  410. ctx.SubscriptionModel.updateOne.should.have.been.calledWith(query, update)
  411. })
  412. })
  413. describe('syncSubscription', function () {
  414. beforeEach(function (ctx) {
  415. ctx.SubscriptionLocator.promises.getUsersSubscription.resolves(
  416. ctx.subscription
  417. )
  418. })
  419. it('should update the subscription if the user already is admin of one', async function (ctx) {
  420. await ctx.SubscriptionUpdater.promises.syncSubscription(
  421. ctx.recurlySubscription,
  422. ctx.adminUser._id
  423. )
  424. ctx.SubscriptionLocator.promises.getUsersSubscription
  425. .calledWith(ctx.adminUser._id)
  426. .should.equal(true)
  427. })
  428. it('should not call updateFeatures with group subscription if recurly subscription is not expired', async function (ctx) {
  429. await ctx.SubscriptionUpdater.promises.syncSubscription(
  430. ctx.recurlySubscription,
  431. ctx.adminUser._id
  432. )
  433. ctx.SubscriptionLocator.promises.getUsersSubscription
  434. .calledWith(ctx.adminUser._id)
  435. .should.equal(true)
  436. ctx.UserFeaturesUpdater.promises.updateFeatures.called.should.equal(false)
  437. })
  438. })
  439. describe('updateSubscriptionFromRecurly', function () {
  440. afterEach(function (ctx) {
  441. ctx.subscription.member_ids = []
  442. delete ctx.subscription.paymentProvider
  443. })
  444. it('should update the subscription with token etc when not expired', async function (ctx) {
  445. await ctx.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  446. ctx.recurlySubscription,
  447. ctx.subscription,
  448. {}
  449. )
  450. ctx.subscription.recurlySubscription_id.should.equal(
  451. ctx.recurlySubscription.uuid
  452. )
  453. ctx.subscription.planCode.should.equal(
  454. ctx.recurlySubscription.plan.plan_code
  455. )
  456. ctx.subscription.save.called.should.equal(true)
  457. expect(
  458. ctx.FeaturesUpdater.promises.scheduleRefreshFeatures
  459. ).to.have.been.calledWith(ctx.adminUser._id)
  460. })
  461. it('should send a recurly account mapping event', async function (ctx) {
  462. const createdAt = new Date().toISOString()
  463. ctx.AccountMappingHelper.generateSubscriptionToRecurlyMapping.returns({
  464. source: 'recurly',
  465. sourceEntity: 'subscription',
  466. sourceEntityId: ctx.recurlySubscription.uuid,
  467. target: 'v2',
  468. targetEntity: 'subscription',
  469. targetEntityId: ctx.subscription._id,
  470. createdAt,
  471. })
  472. await ctx.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  473. ctx.recurlySubscription,
  474. ctx.subscription,
  475. {}
  476. )
  477. expect(
  478. ctx.AccountMappingHelper.generateSubscriptionToRecurlyMapping
  479. ).to.have.been.calledWith(
  480. ctx.subscription._id,
  481. ctx.recurlySubscription.uuid
  482. )
  483. expect(
  484. ctx.AnalyticsManager.registerAccountMapping
  485. ).to.have.been.calledWith({
  486. source: 'recurly',
  487. sourceEntity: 'subscription',
  488. sourceEntityId: ctx.recurlySubscription.uuid,
  489. target: 'v2',
  490. targetEntity: 'subscription',
  491. targetEntityId: ctx.subscription._id,
  492. createdAt,
  493. })
  494. })
  495. it('should not update subscription when paymentProvider service contains stripe', async function (ctx) {
  496. ctx.subscription.paymentProvider = {
  497. service: 'stripe-uk',
  498. }
  499. await ctx.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  500. ctx.recurlySubscription,
  501. ctx.subscription,
  502. {}
  503. )
  504. ctx.subscription.save.called.should.equal(false)
  505. expect(ctx.FeaturesUpdater.promises.scheduleRefreshFeatures).to.not.have
  506. .been.called
  507. })
  508. it('should remove the subscription when expired', async function (ctx) {
  509. ctx.recurlySubscription.state = 'expired'
  510. await ctx.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  511. ctx.recurlySubscription,
  512. ctx.subscription,
  513. {}
  514. )
  515. ctx.SubscriptionModel.deleteOne.should.have.been.calledWith({
  516. _id: ctx.subscription._id,
  517. })
  518. })
  519. it('should not remove the subscription when expired if it has Managed Users enabled', async function (ctx) {
  520. ctx.Features.hasFeature.withArgs('saas').returns(true)
  521. ctx.subscription.managedUsersEnabled = true
  522. ctx.recurlySubscription.state = 'expired'
  523. await ctx.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  524. ctx.recurlySubscription,
  525. ctx.subscription,
  526. {}
  527. )
  528. ctx.SubscriptionModel.deleteOne.should.not.have.been.called
  529. })
  530. it('should not remove the subscription when expired if it has Group SSO enabled', async function (ctx) {
  531. ctx.Features.hasFeature.withArgs('saas').returns(true)
  532. ctx.subscription.ssoConfig = new ObjectId('abc123abc123abc123abc123')
  533. ctx.recurlySubscription.state = 'expired'
  534. await ctx.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  535. ctx.recurlySubscription,
  536. ctx.subscription,
  537. {}
  538. )
  539. ctx.SubscriptionModel.deleteOne.should.not.have.been.called
  540. })
  541. it('should update all the users features', async function (ctx) {
  542. ctx.subscription.member_ids = ctx.allUserIds
  543. await ctx.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  544. ctx.recurlySubscription,
  545. ctx.subscription,
  546. {}
  547. )
  548. expect(
  549. ctx.FeaturesUpdater.promises.scheduleRefreshFeatures
  550. ).to.have.been.calledWith(ctx.adminUser._id)
  551. expect(
  552. ctx.FeaturesUpdater.promises.scheduleRefreshFeatures
  553. ).to.have.been.calledWith(ctx.allUserIds[0])
  554. expect(
  555. ctx.FeaturesUpdater.promises.scheduleRefreshFeatures
  556. ).to.have.been.calledWith(ctx.allUserIds[1])
  557. expect(
  558. ctx.FeaturesUpdater.promises.scheduleRefreshFeatures
  559. ).to.have.been.calledWith(ctx.allUserIds[2])
  560. })
  561. it('should set group to true and save how many members can be added to group', async function (ctx) {
  562. ctx.recurlyPlan.groupPlan = true
  563. ctx.recurlyPlan.membersLimit = 5
  564. await ctx.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  565. ctx.recurlySubscription,
  566. ctx.subscription,
  567. {}
  568. )
  569. ctx.subscription.membersLimit.should.equal(5)
  570. ctx.subscription.groupPlan.should.equal(true)
  571. ctx.subscription.member_ids.should.deep.equal([ctx.subscription.admin_id])
  572. })
  573. it('should delete and replace subscription when downgrading from group to individual plan', async function (ctx) {
  574. ctx.recurlyPlan.groupPlan = false
  575. await ctx.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  576. ctx.recurlySubscription,
  577. ctx.groupSubscription,
  578. {}
  579. )
  580. })
  581. it('should not set group to true or set groupPlan', async function (ctx) {
  582. await ctx.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  583. ctx.recurlySubscription,
  584. ctx.subscription,
  585. {}
  586. )
  587. assert.notEqual(ctx.subscription.membersLimit, 5)
  588. assert.notEqual(ctx.subscription.groupPlan, true)
  589. })
  590. describe('when the plan allows adding more seats', function () {
  591. beforeEach(function (ctx) {
  592. ctx.membersLimitAddOn = 'add_on1'
  593. ctx.recurlyPlan.groupPlan = true
  594. ctx.recurlyPlan.membersLimit = 5
  595. ctx.recurlyPlan.membersLimitAddOn = ctx.membersLimitAddOn
  596. })
  597. function expectMembersLimit(limit) {
  598. it('should set the membersLimit accordingly', async function (ctx) {
  599. await ctx.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
  600. ctx.recurlySubscription,
  601. ctx.subscription,
  602. {}
  603. )
  604. expect(ctx.subscription.membersLimit).to.equal(limit)
  605. })
  606. }
  607. describe('when the recurlySubscription does not have add ons', function () {
  608. beforeEach(function (ctx) {
  609. delete ctx.recurlySubscription.subscription_add_ons
  610. })
  611. expectMembersLimit(5)
  612. })
  613. describe('when the recurlySubscription has non-matching add ons', function () {
  614. beforeEach(function (ctx) {
  615. ctx.recurlySubscription.subscription_add_ons = [
  616. { add_on_code: 'add_on_99', quantity: 3 },
  617. ]
  618. })
  619. expectMembersLimit(5)
  620. })
  621. describe('when the recurlySubscription has a matching add on', function () {
  622. beforeEach(function (ctx) {
  623. ctx.recurlySubscription.subscription_add_ons = [
  624. { add_on_code: ctx.membersLimitAddOn, quantity: 10 },
  625. ]
  626. })
  627. expectMembersLimit(15)
  628. })
  629. // NOTE: This is unexpected, but we are going to support it anyways.
  630. describe('when the recurlySubscription has multiple matching add ons', function () {
  631. beforeEach(function (ctx) {
  632. ctx.recurlySubscription.subscription_add_ons = [
  633. { add_on_code: ctx.membersLimitAddOn, quantity: 10 },
  634. { add_on_code: ctx.membersLimitAddOn, quantity: 3 },
  635. ]
  636. })
  637. expectMembersLimit(18)
  638. })
  639. })
  640. })
  641. describe('handleExpiredSubscription', function () {
  642. it("should set the admin's previous_plan_type in customer.io to the expired plan when the subscription is deleted", async function (ctx) {
  643. await ctx.SubscriptionUpdater.promises.handleExpiredSubscription(
  644. ctx.subscription,
  645. {}
  646. )
  647. expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
  648. 'setUserProperties',
  649. ctx.subscription.admin_id,
  650. { previous_plan_type: 'student' }
  651. )
  652. })
  653. it('should not set previous_plan_type when the subscription is not deleted (managed users)', async function (ctx) {
  654. ctx.Features.hasFeature.withArgs('saas').returns(true)
  655. ctx.subscription.managedUsersEnabled = true
  656. await ctx.SubscriptionUpdater.promises.handleExpiredSubscription(
  657. ctx.subscription,
  658. {}
  659. )
  660. expect(ctx.Modules.promises.hooks.fire).to.not.have.been.calledWith(
  661. 'setUserProperties',
  662. ctx.subscription.admin_id,
  663. { previous_plan_type: 'student' }
  664. )
  665. })
  666. it('should not set previous_plan_type when the subscription is not deleted (group SSO)', async function (ctx) {
  667. ctx.Features.hasFeature.withArgs('saas').returns(true)
  668. ctx.subscription.ssoConfig = new ObjectId('abc123abc123abc123abc123')
  669. await ctx.SubscriptionUpdater.promises.handleExpiredSubscription(
  670. ctx.subscription,
  671. {}
  672. )
  673. expect(ctx.Modules.promises.hooks.fire).to.not.have.been.calledWith(
  674. 'setUserProperties',
  675. ctx.subscription.admin_id,
  676. { previous_plan_type: 'student' }
  677. )
  678. })
  679. })
  680. describe('addUserToGroup', function () {
  681. it('should add the user ids to the group as a set', async function (ctx) {
  682. ctx.SubscriptionModel.findOne = sinon
  683. .stub()
  684. .resolves(ctx.groupSubscription)
  685. await ctx.SubscriptionUpdater.promises.addUserToGroup(
  686. ctx.groupSubscription._id,
  687. ctx.otherUserId
  688. )
  689. const searchOps = { _id: ctx.groupSubscription._id }
  690. const insertOperation = {
  691. $addToSet: { member_ids: ctx.otherUserId },
  692. }
  693. ctx.SubscriptionModel.updateOne
  694. .calledWith(searchOps, insertOperation)
  695. .should.equal(true)
  696. expect(ctx.SubscriptionModel.updateOne.lastCall.args[2].session).to.exist
  697. sinon.assert.calledWith(
  698. ctx.AnalyticsManager.recordEventForUserInBackground,
  699. ctx.otherUserId,
  700. 'group-subscription-joined',
  701. {
  702. groupId: ctx.groupSubscription._id,
  703. subscriptionId: ctx.groupSubscription.recurlySubscription_id,
  704. }
  705. )
  706. })
  707. it('should update the users features', async function (ctx) {
  708. await ctx.SubscriptionUpdater.promises.addUserToGroup(
  709. ctx.subscription._id,
  710. ctx.otherUserId
  711. )
  712. ctx.FeaturesUpdater.promises.refreshFeatures
  713. .calledWith(ctx.otherUserId)
  714. .should.equal(true)
  715. })
  716. it('should set the group plan code user property to the best plan with 1 group subscription', async function (ctx) {
  717. ctx.SubscriptionLocator.promises.getMemberSubscriptions
  718. .withArgs(ctx.otherUserId)
  719. .resolves([ctx.groupSubscription])
  720. await ctx.SubscriptionUpdater.promises.addUserToGroup(
  721. ctx.groupSubscription._id,
  722. ctx.otherUserId
  723. )
  724. sinon.assert.calledWith(
  725. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  726. ctx.otherUserId,
  727. 'group-subscription-plan-code',
  728. 'group_subscription'
  729. )
  730. })
  731. it('should set the group plan code user property to the best plan with 2 group subscriptions', async function (ctx) {
  732. ctx.SubscriptionLocator.promises.getMemberSubscriptions
  733. .withArgs(ctx.otherUserId)
  734. .resolves([ctx.groupSubscription, ctx.betterGroupSubscription])
  735. await ctx.SubscriptionUpdater.promises.addUserToGroup(
  736. ctx.betterGroupSubscription._id,
  737. ctx.otherUserId
  738. )
  739. sinon.assert.calledWith(
  740. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  741. ctx.otherUserId,
  742. 'group-subscription-plan-code',
  743. 'better_group_subscription'
  744. )
  745. })
  746. it('should set the group plan code user property to the best plan with 2 group subscriptions in reverse order', async function (ctx) {
  747. ctx.SubscriptionLocator.promises.getMemberSubscriptions
  748. .withArgs(ctx.otherUserId)
  749. .resolves([ctx.betterGroupSubscription, ctx.groupSubscription])
  750. await ctx.SubscriptionUpdater.promises.addUserToGroup(
  751. ctx.betterGroupSubscription._id,
  752. ctx.otherUserId
  753. )
  754. sinon.assert.calledWith(
  755. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  756. ctx.otherUserId,
  757. 'group-subscription-plan-code',
  758. 'better_group_subscription'
  759. )
  760. })
  761. it('should add an entry to the user audit log when joining a group', async function (ctx) {
  762. await ctx.SubscriptionUpdater.promises.addUserToGroup(
  763. ctx.subscription._id,
  764. ctx.otherUserId
  765. )
  766. sinon.assert.calledWith(
  767. ctx.UserAuditLogHandler.promises.addEntry,
  768. ctx.otherUserId,
  769. 'join-group-subscription',
  770. undefined,
  771. undefined,
  772. {
  773. subscriptionId: ctx.subscription._id,
  774. }
  775. )
  776. })
  777. it('should add an entry to the group audit log when joining a group', async function (ctx) {
  778. await ctx.SubscriptionUpdater.promises.addUserToGroup(
  779. ctx.subscription._id,
  780. ctx.otherUserId,
  781. { ipAddress: '0:0:0:0', initiatorId: 'user123' }
  782. )
  783. expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
  784. 'addGroupAuditLogEntry',
  785. {
  786. groupId: ctx.subscription._id,
  787. initiatorId: 'user123',
  788. ipAddress: '0:0:0:0',
  789. operation: 'join-group',
  790. }
  791. )
  792. })
  793. })
  794. describe('removeUserFromGroup', function () {
  795. beforeEach(function (ctx) {
  796. ctx.fakeSubscriptions = [
  797. {
  798. _id: 'fake-id-1',
  799. },
  800. {
  801. _id: 'fake-id-2',
  802. },
  803. ]
  804. ctx.SubscriptionModel.findOne.resolves(ctx.groupSubscription)
  805. ctx.SubscriptionModel.findById = sinon
  806. .stub()
  807. .resolves(ctx.groupSubscription)
  808. ctx.SubscriptionLocator.promises.getMemberSubscriptions.resolves(
  809. ctx.fakeSubscriptions
  810. )
  811. })
  812. it('should pull the users id from the group', async function (ctx) {
  813. await ctx.SubscriptionUpdater.promises.removeUserFromGroup(
  814. ctx.subscription._id,
  815. ctx.otherUserId
  816. )
  817. const removeOperation = { $pull: { member_ids: ctx.otherUserId } }
  818. ctx.SubscriptionModel.updateOne
  819. .calledWith({ _id: ctx.subscription._id }, removeOperation)
  820. .should.equal(true)
  821. })
  822. it('should remove user enrollment if the group is managed', async function (ctx) {
  823. ctx.SubscriptionModel.findById.resolves({
  824. ...ctx.groupSubscription,
  825. managedUsersEnabled: true,
  826. })
  827. await ctx.SubscriptionUpdater.promises.removeUserFromGroup(
  828. ctx.groupSubscription._id,
  829. ctx.otherUserId
  830. )
  831. ctx.UserUpdater.promises.updateUser
  832. .calledWith(
  833. { _id: ctx.otherUserId },
  834. {
  835. $unset: {
  836. 'enrollment.managedBy': 1,
  837. 'enrollment.enrolledAt': 1,
  838. },
  839. }
  840. )
  841. .should.equal(true)
  842. })
  843. it('should send a group-subscription-left event', async function (ctx) {
  844. await ctx.SubscriptionUpdater.promises.removeUserFromGroup(
  845. ctx.groupSubscription._id,
  846. ctx.otherUserId
  847. )
  848. sinon.assert.calledWith(
  849. ctx.AnalyticsManager.recordEventForUserInBackground,
  850. ctx.otherUserId,
  851. 'group-subscription-left',
  852. {
  853. groupId: ctx.groupSubscription._id,
  854. subscriptionId: ctx.groupSubscription.recurlySubscription_id,
  855. }
  856. )
  857. })
  858. it('should set the group plan code user property when removing user from group', async function (ctx) {
  859. await ctx.SubscriptionUpdater.promises.removeUserFromGroup(
  860. ctx.subscription._id,
  861. ctx.otherUserId
  862. )
  863. sinon.assert.calledWith(
  864. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  865. ctx.otherUserId,
  866. 'group-subscription-plan-code',
  867. null
  868. )
  869. })
  870. it('should update the users features', async function (ctx) {
  871. await ctx.SubscriptionUpdater.promises.removeUserFromGroup(
  872. ctx.subscription._id,
  873. ctx.otherUserId
  874. )
  875. ctx.FeaturesUpdater.promises.refreshFeatures
  876. .calledWith(ctx.otherUserId)
  877. .should.equal(true)
  878. })
  879. it('should add an audit log when a user leaves a group', async function (ctx) {
  880. await ctx.SubscriptionUpdater.promises.removeUserFromGroup(
  881. ctx.subscription._id,
  882. ctx.otherUserId
  883. )
  884. sinon.assert.calledWith(
  885. ctx.UserAuditLogHandler.promises.addEntry,
  886. ctx.otherUserId,
  887. 'leave-group-subscription',
  888. undefined,
  889. undefined,
  890. {
  891. subscriptionId: ctx.subscription._id,
  892. }
  893. )
  894. })
  895. })
  896. describe('removeUserFromAllGroups', function () {
  897. beforeEach(function (ctx) {
  898. ctx.SubscriptionLocator.promises.getMemberSubscriptions.resolves([
  899. {
  900. _id: 'fake-id-1',
  901. },
  902. {
  903. _id: 'fake-id-2',
  904. },
  905. ])
  906. })
  907. it('should set the group plan code user property when removing user from all groups', async function (ctx) {
  908. await ctx.SubscriptionUpdater.promises.removeUserFromAllGroups(
  909. ctx.otherUserId
  910. )
  911. sinon.assert.calledWith(
  912. ctx.AnalyticsManager.setUserPropertyForUserInBackground,
  913. ctx.otherUserId,
  914. 'group-subscription-plan-code',
  915. null
  916. )
  917. })
  918. it('should pull the users id from all groups', async function (ctx) {
  919. await ctx.SubscriptionUpdater.promises.removeUserFromAllGroups(
  920. ctx.otherUserId
  921. )
  922. const filter = { _id: ['fake-id-1', 'fake-id-2'] }
  923. const removeOperation = { $pull: { member_ids: ctx.otherUserId } }
  924. sinon.assert.calledWith(
  925. ctx.SubscriptionModel.updateMany,
  926. filter,
  927. removeOperation
  928. )
  929. })
  930. it('should send a group-subscription-left event for each group', async function (ctx) {
  931. ctx.fakeSub1 = {
  932. _id: 'fake-id-1',
  933. groupPlan: true,
  934. recurlySubscription_id: 'fake-sub-1',
  935. }
  936. ctx.fakeSub2 = {
  937. _id: 'fake-id-2',
  938. groupPlan: true,
  939. recurlySubscription_id: 'fake-sub-2',
  940. }
  941. ctx.SubscriptionModel.findOne
  942. .withArgs(
  943. { _id: 'fake-id-1' },
  944. { recurlySubscription_id: 1, groupPlan: 1 }
  945. )
  946. .resolves(ctx.fakeSub1)
  947. .withArgs(
  948. { _id: 'fake-id-2' },
  949. { recurlySubscription_id: 1, groupPlan: 1 }
  950. )
  951. .resolves(ctx.fakeSub2)
  952. await ctx.SubscriptionUpdater.promises.removeUserFromAllGroups(
  953. ctx.otherUserId
  954. )
  955. sinon.assert.calledWith(
  956. ctx.AnalyticsManager.recordEventForUserInBackground,
  957. ctx.otherUserId,
  958. 'group-subscription-left',
  959. {
  960. groupId: 'fake-id-1',
  961. subscriptionId: 'fake-sub-1',
  962. }
  963. )
  964. sinon.assert.calledWith(
  965. ctx.AnalyticsManager.recordEventForUserInBackground,
  966. ctx.otherUserId,
  967. 'group-subscription-left',
  968. {
  969. groupId: 'fake-id-2',
  970. subscriptionId: 'fake-sub-2',
  971. }
  972. )
  973. })
  974. it('should add an audit log entry for each group the user leaves', async function (ctx) {
  975. await ctx.SubscriptionUpdater.promises.removeUserFromAllGroups(
  976. ctx.otherUserId
  977. )
  978. sinon.assert.calledWith(
  979. ctx.UserAuditLogHandler.promises.addEntry,
  980. ctx.otherUserId,
  981. 'leave-group-subscription',
  982. undefined,
  983. undefined,
  984. {
  985. subscriptionId: 'fake-id-1',
  986. }
  987. )
  988. sinon.assert.calledWith(
  989. ctx.UserAuditLogHandler.promises.addEntry,
  990. ctx.otherUserId,
  991. 'leave-group-subscription',
  992. undefined,
  993. undefined,
  994. {
  995. subscriptionId: 'fake-id-2',
  996. }
  997. )
  998. })
  999. })
  1000. describe('deleteSubscription', function () {
  1001. beforeEach(async function (ctx) {
  1002. ctx.subscription = {
  1003. _id: new ObjectId().toString(),
  1004. mock: 'subscription',
  1005. admin_id: new ObjectId(),
  1006. member_ids: [new ObjectId(), new ObjectId(), new ObjectId()],
  1007. }
  1008. await ctx.SubscriptionUpdater.promises.deleteSubscription(
  1009. ctx.subscription,
  1010. {}
  1011. )
  1012. })
  1013. it('should remove the subscription', function (ctx) {
  1014. ctx.SubscriptionModel.deleteOne
  1015. .calledWith({ _id: ctx.subscription._id })
  1016. .should.equal(true)
  1017. })
  1018. it('should downgrade the admin_id', function (ctx) {
  1019. expect(
  1020. ctx.FeaturesUpdater.promises.scheduleRefreshFeatures
  1021. ).to.have.been.calledWith(ctx.subscription.admin_id)
  1022. })
  1023. it('should downgrade all of the members', function (ctx) {
  1024. for (const userId of ctx.subscription.member_ids) {
  1025. expect(
  1026. ctx.FeaturesUpdater.promises.scheduleRefreshFeatures
  1027. ).to.have.been.calledWith(userId)
  1028. }
  1029. })
  1030. })
  1031. describe('scheduleRefreshFeatures', function () {
  1032. it('should call upgrades feature for personal subscription from admin_id', async function (ctx) {
  1033. ctx.subscription = {
  1034. _id: new ObjectId().toString(),
  1035. mock: 'subscription',
  1036. admin_id: new ObjectId(),
  1037. }
  1038. await ctx.SubscriptionUpdater.promises.scheduleRefreshFeatures(
  1039. ctx.subscription
  1040. )
  1041. expect(
  1042. ctx.FeaturesUpdater.promises.scheduleRefreshFeatures
  1043. ).to.have.been.calledOnceWith(ctx.subscription.admin_id)
  1044. })
  1045. it('should call upgrades feature for group subscription from admin_id and member_ids', async function (ctx) {
  1046. ctx.subscription = {
  1047. _id: new ObjectId().toString(),
  1048. mock: 'subscription',
  1049. admin_id: new ObjectId(),
  1050. member_ids: [new ObjectId(), new ObjectId(), new ObjectId()],
  1051. }
  1052. await ctx.SubscriptionUpdater.promises.scheduleRefreshFeatures(
  1053. ctx.subscription
  1054. )
  1055. expect(
  1056. ctx.FeaturesUpdater.promises.scheduleRefreshFeatures.callCount
  1057. ).to.equal(4)
  1058. })
  1059. })
  1060. })