SubscriptionUpdater.test.mjs 32 KB

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