| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162 |
- import { beforeEach, describe, it, vi, assert, expect } from 'vitest'
- import sinon from 'sinon'
- import mongodb from 'mongodb-legacy'
- const modulePath =
- '../../../../app/src/Features/Subscription/SubscriptionUpdater'
- const { ObjectId } = mongodb
- describe('SubscriptionUpdater', function () {
- beforeEach(async function (ctx) {
- ctx.recurlyPlan = { planCode: 'recurly-plan' }
- ctx.recurlySubscription = {
- uuid: '1238uoijdasjhd',
- plan: {
- plan_code: ctx.recurlyPlan.planCode,
- },
- }
- ctx.adminUser = { _id: (ctx.adminuser_id = '5208dd34438843e2db000007') }
- ctx.otherUserId = '5208dd34438842e2db000005'
- ctx.allUserIds = ['13213', 'dsadas', 'djsaiud89']
- ctx.subscription = {
- _id: '111111111111111111111111',
- admin_id: ctx.adminUser._id,
- manager_ids: [ctx.adminUser._id],
- member_ids: [],
- save: sinon.stub().resolves(),
- planCode: 'student_or_something',
- recurlySubscription_id: 'abc123def456fab789',
- }
- ctx.user_id = ctx.adminuser_id
- ctx.groupSubscription = {
- _id: '222222222222222222222222',
- admin_id: ctx.adminUser._id,
- manager_ids: [ctx.adminUser._id],
- member_ids: ctx.allUserIds,
- save: sinon.stub().resolves(),
- groupPlan: true,
- planCode: 'group_subscription',
- recurlySubscription_id: '456fab789abc123def',
- }
- ctx.betterGroupSubscription = {
- _id: '999999999999999999999999',
- admin_id: ctx.adminUser._id,
- manager_ids: [ctx.adminUser._id],
- member_ids: [ctx.otherUserId],
- save: sinon.stub().resolves(),
- groupPlan: true,
- planCode: 'better_group_subscription',
- recurlySubscription_id: '123def456fab789abc',
- }
- const subscription = ctx.subscription
- ctx.SubscriptionModel = class {
- constructor(opts) {
- // Always return our mock subscription when creating a new one
- subscription.admin_id = opts.admin_id
- subscription.manager_ids = [opts.admin_id]
- return subscription
- }
- save() {
- return Promise.resolve(subscription)
- }
- }
- ctx.SubscriptionModel.deleteOne = sinon
- .stub()
- .returns({ exec: sinon.stub().resolves() })
- ctx.SubscriptionModel.updateOne = sinon
- .stub()
- .returns({ exec: sinon.stub().resolves() })
- ctx.SubscriptionModel.findOne = sinon.stub().resolves()
- ctx.SubscriptionModel.findById = sinon.stub().resolves()
- ctx.SubscriptionModel.updateMany = sinon
- .stub()
- .returns({ exec: sinon.stub().resolves() })
- ctx.SubscriptionModel.findOneAndUpdate = sinon.stub().returns({
- exec: sinon.stub().resolves(ctx.subscription),
- })
- ctx.SSOConfigModel = class {}
- ctx.SSOConfigModel.findOne = sinon.stub().returns({
- lean: sinon.stub().returns({
- exec: sinon.stub().resolves({ enabled: true }),
- }),
- })
- ctx.SubscriptionLocator = {
- promises: {
- getUsersSubscription: sinon.stub(),
- getGroupSubscriptionMemberOf: sinon.stub(),
- getMemberSubscriptions: sinon.stub().resolves([]),
- getManagedGroupSubscriptions: sinon.stub().resolves([]),
- getSubscription: sinon.stub(),
- },
- }
- ctx.SubscriptionLocator.promises.getSubscription
- .withArgs(ctx.subscription._id)
- .resolves(ctx.subscription)
- ctx.Settings = {
- defaultPlanCode: 'personal',
- defaultFeatures: { default: 'features' },
- plans: [
- ctx.recurlyPlan,
- { planCode: ctx.subscription.planCode, features: {} },
- {
- planCode: ctx.groupSubscription.planCode,
- features: {
- collaborators: 10,
- compileTimeout: 60,
- dropbox: true,
- },
- },
- {
- planCode: ctx.betterGroupSubscription.planCode,
- features: {
- collaborators: -1,
- compileTimeout: 240,
- dropbox: true,
- },
- },
- ],
- mongo: {
- options: {
- appname: 'web',
- maxPoolSize: 100,
- serverSelectionTimeoutMS: 60000,
- socketTimeoutMS: 60000,
- monitorCommands: true,
- family: 4,
- },
- url: 'mongodb://mongo/test-overleaf',
- hasSecondaries: false,
- },
- }
- ctx.UserFeaturesUpdater = {
- promises: {
- updateFeatures: sinon.stub().resolves(),
- },
- }
- ctx.ReferalFeatures = {
- promises: {
- getBonusFeatures: sinon.stub().resolves(),
- },
- }
- ctx.FeaturesUpdater = {
- promises: {
- scheduleRefreshFeatures: sinon.stub().resolves(),
- refreshFeatures: sinon.stub().resolves({}),
- },
- }
- ctx.DeletedSubscription = {
- findOneAndUpdate: sinon.stub().returns({ exec: sinon.stub().resolves() }),
- }
- ctx.AnalyticsManager = {
- recordEventForUserInBackground: sinon.stub().resolves(),
- setUserPropertyForUserInBackground: sinon.stub(),
- registerAccountMapping: sinon.stub(),
- }
- ctx.Features = {
- hasFeature: sinon.stub().returns(false),
- }
- ctx.UserAuditLogHandler = {
- promises: {
- addEntry: sinon.stub().resolves(),
- },
- }
- ctx.UserUpdater = {
- promises: {
- updateUser: sinon.stub().resolves(),
- },
- }
- vi.doMock('../../../../app/src/models/Subscription', () => ({
- Subscription: ctx.SubscriptionModel,
- }))
- vi.doMock('../../../../app/src/models/SSOConfig', () => ({
- SSOConfig: ctx.SSOConfigModel,
- }))
- vi.doMock(
- '../../../../app/src/Features/Subscription/UserFeaturesUpdater',
- () => ({
- default: ctx.UserFeaturesUpdater,
- })
- )
- vi.doMock(
- '../../../../app/src/Features/Subscription/SubscriptionLocator',
- () => ({
- default: ctx.SubscriptionLocator,
- })
- )
- vi.doMock('@overleaf/settings', () => ({
- default: ctx.Settings,
- }))
- vi.doMock('../../../../app/src/infrastructure/mongodb', () => ({
- db: {},
- ObjectId,
- }))
- vi.doMock(
- '../../../../app/src/Features/Subscription/FeaturesUpdater',
- () => ({
- default: ctx.FeaturesUpdater,
- })
- )
- vi.doMock('../../../../app/src/models/DeletedSubscription', () => ({
- DeletedSubscription: ctx.DeletedSubscription,
- }))
- vi.doMock(
- '../../../../app/src/Features/Analytics/AnalyticsManager',
- () => ({
- default: ctx.AnalyticsManager,
- })
- )
- vi.doMock(
- '../../../../app/src/Features/Analytics/AccountMappingHelper',
- () => ({
- default: (ctx.AccountMappingHelper = {
- generateSubscriptionToRecurlyMapping: sinon.stub(),
- }),
- })
- )
- vi.doMock('../../../../app/src/infrastructure/Features', () => ({
- default: ctx.Features,
- }))
- vi.doMock('../../../../app/src/Features/User/UserAuditLogHandler', () => ({
- default: ctx.UserAuditLogHandler,
- }))
- vi.doMock('../../../../app/src/Features/User/UserUpdater', () => ({
- default: ctx.UserUpdater,
- }))
- vi.doMock('../../../../app/src/infrastructure/Modules', () => ({
- default: (ctx.Modules = {
- promises: {
- hooks: {
- fire: sinon.stub().resolves(),
- },
- },
- }),
- }))
- ctx.SubscriptionUpdater = (await import(modulePath)).default
- })
- describe('updateAdmin', function () {
- it('should update the subscription admin', async function (ctx) {
- ctx.subscription.groupPlan = true
- await ctx.SubscriptionUpdater.promises.updateAdmin(
- ctx.subscription,
- ctx.otherUserId
- )
- const query = {
- _id: new ObjectId(ctx.subscription._id),
- customAccount: true,
- }
- const update = {
- $set: { admin_id: new ObjectId(ctx.otherUserId) },
- $addToSet: { manager_ids: new ObjectId(ctx.otherUserId) },
- }
- ctx.SubscriptionModel.updateOne.should.have.been.calledOnce
- ctx.SubscriptionModel.updateOne.should.have.been.calledWith(query, update)
- })
- it("should fire setUserProperties for both old and new admin's group_role on group subscriptions", async function (ctx) {
- ctx.subscription.groupPlan = true
- ctx.SubscriptionLocator.promises.getManagedGroupSubscriptions
- .withArgs(ctx.otherUserId)
- .resolves([{ admin_id: { _id: ctx.otherUserId } }])
- ctx.SubscriptionLocator.promises.getManagedGroupSubscriptions
- .withArgs(ctx.adminUser._id)
- .resolves([{ admin_id: { _id: ctx.otherUserId } }])
- await ctx.SubscriptionUpdater.promises.updateAdmin(
- ctx.subscription,
- ctx.otherUserId
- )
- expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
- 'setUserProperties',
- ctx.adminUser._id,
- { group_role: 'manager' }
- )
- expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
- 'setUserProperties',
- ctx.otherUserId,
- { group_role: 'admin' }
- )
- })
- it('should not fire setUserProperties for group_role on non-group subscriptions', async function (ctx) {
- await ctx.SubscriptionUpdater.promises.updateAdmin(
- ctx.subscription,
- ctx.otherUserId
- )
- expect(ctx.Modules.promises.hooks.fire).to.not.have.been.calledWith(
- 'setUserProperties',
- sinon.match.any,
- sinon.match.has('group_role')
- )
- })
- it('should remove the manager for non-group subscriptions', async function (ctx) {
- await ctx.SubscriptionUpdater.promises.updateAdmin(
- ctx.subscription,
- ctx.otherUserId
- )
- const query = {
- _id: new ObjectId(ctx.subscription._id),
- customAccount: true,
- }
- const update = {
- $set: {
- admin_id: new ObjectId(ctx.otherUserId),
- manager_ids: [new ObjectId(ctx.otherUserId)],
- },
- }
- ctx.SubscriptionModel.updateOne.should.have.been.calledOnce
- ctx.SubscriptionModel.updateOne.should.have.been.calledWith(query, update)
- })
- })
- describe('transferSubscriptionOwnership', function () {
- it('should transfer the subscription ownership for group subscriptions', async function (ctx) {
- ctx.subscription.groupPlan = true
- ctx.subscription.paymentProvider = {
- id: 'stripe-123',
- name: 'stripe-us',
- }
- await ctx.SubscriptionUpdater.promises.transferSubscriptionOwnership(
- ctx.subscription,
- ctx.otherUserId,
- false
- )
- const query = {
- _id: new ObjectId(ctx.subscription._id),
- }
- const update = {
- $set: {
- admin_id: new ObjectId(ctx.otherUserId),
- previousPaymentProvider: ctx.subscription.paymentProvider,
- },
- $addToSet: { manager_ids: new ObjectId(ctx.otherUserId) },
- }
- ctx.SubscriptionModel.updateOne.should.have.been.calledOnce
- ctx.SubscriptionModel.updateOne.should.have.been.calledWith(query, update)
- })
- it('should transfer the subscription ownership for non-group subscriptions', async function (ctx) {
- ctx.subscription.paymentProvider = {
- id: 'stripe-123',
- name: 'stripe-us',
- }
- await ctx.SubscriptionUpdater.promises.transferSubscriptionOwnership(
- ctx.subscription,
- ctx.otherUserId,
- false
- )
- const query = {
- _id: new ObjectId(ctx.subscription._id),
- }
- const update = {
- $set: {
- admin_id: new ObjectId(ctx.otherUserId),
- manager_ids: [new ObjectId(ctx.otherUserId)],
- previousPaymentProvider: ctx.subscription.paymentProvider,
- },
- }
- ctx.SubscriptionModel.updateOne.should.have.been.calledOnce
- ctx.SubscriptionModel.updateOne.should.have.been.calledWith(query, update)
- })
- it("should fire setUserProperties for both old and new admin's group_role on group subscriptions", async function (ctx) {
- ctx.subscription.groupPlan = true
- ctx.SubscriptionLocator.promises.getManagedGroupSubscriptions
- .withArgs(ctx.otherUserId)
- .resolves([{ admin_id: { _id: ctx.otherUserId } }])
- ctx.SubscriptionLocator.promises.getManagedGroupSubscriptions
- .withArgs(ctx.adminUser._id)
- .resolves([{ admin_id: { _id: ctx.otherUserId } }])
- await ctx.SubscriptionUpdater.promises.transferSubscriptionOwnership(
- ctx.subscription,
- ctx.otherUserId,
- false
- )
- expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
- 'setUserProperties',
- ctx.adminUser._id,
- { group_role: 'manager' }
- )
- expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
- 'setUserProperties',
- ctx.otherUserId,
- { group_role: 'admin' }
- )
- })
- it('should not fire setUserProperties for group_role on non-group subscriptions', async function (ctx) {
- await ctx.SubscriptionUpdater.promises.transferSubscriptionOwnership(
- ctx.subscription,
- ctx.otherUserId,
- false
- )
- expect(ctx.Modules.promises.hooks.fire).to.not.have.been.calledWith(
- 'setUserProperties',
- sinon.match.any,
- sinon.match.has('group_role')
- )
- })
- it('should clear previousPaymentProvider when clearPreviousPaymentProvider is true', async function (ctx) {
- ctx.subscription.paymentProvider = {
- id: 'stripe-123',
- name: 'stripe-us',
- }
- await ctx.SubscriptionUpdater.promises.transferSubscriptionOwnership(
- ctx.subscription,
- ctx.otherUserId,
- true
- )
- const query = {
- _id: new ObjectId(ctx.subscription._id),
- }
- const update = {
- $set: {
- admin_id: new ObjectId(ctx.otherUserId),
- manager_ids: [new ObjectId(ctx.otherUserId)],
- },
- $unset: { previousPaymentProvider: 1 },
- }
- ctx.SubscriptionModel.updateOne.should.have.been.calledOnce
- ctx.SubscriptionModel.updateOne.should.have.been.calledWith(query, update)
- })
- })
- describe('syncSubscription', function () {
- beforeEach(function (ctx) {
- ctx.SubscriptionLocator.promises.getUsersSubscription.resolves(
- ctx.subscription
- )
- })
- it('should update the subscription if the user already is admin of one', async function (ctx) {
- await ctx.SubscriptionUpdater.promises.syncSubscription(
- ctx.recurlySubscription,
- ctx.adminUser._id
- )
- ctx.SubscriptionLocator.promises.getUsersSubscription
- .calledWith(ctx.adminUser._id)
- .should.equal(true)
- })
- it('should not call updateFeatures with group subscription if recurly subscription is not expired', async function (ctx) {
- await ctx.SubscriptionUpdater.promises.syncSubscription(
- ctx.recurlySubscription,
- ctx.adminUser._id
- )
- ctx.SubscriptionLocator.promises.getUsersSubscription
- .calledWith(ctx.adminUser._id)
- .should.equal(true)
- ctx.UserFeaturesUpdater.promises.updateFeatures.called.should.equal(false)
- })
- })
- describe('updateSubscriptionFromRecurly', function () {
- afterEach(function (ctx) {
- ctx.subscription.member_ids = []
- delete ctx.subscription.paymentProvider
- })
- it('should update the subscription with token etc when not expired', async function (ctx) {
- await ctx.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
- ctx.recurlySubscription,
- ctx.subscription,
- {}
- )
- ctx.subscription.recurlySubscription_id.should.equal(
- ctx.recurlySubscription.uuid
- )
- ctx.subscription.planCode.should.equal(
- ctx.recurlySubscription.plan.plan_code
- )
- ctx.subscription.save.called.should.equal(true)
- expect(
- ctx.FeaturesUpdater.promises.scheduleRefreshFeatures
- ).to.have.been.calledWith(ctx.adminUser._id)
- })
- it('should send a recurly account mapping event', async function (ctx) {
- const createdAt = new Date().toISOString()
- ctx.AccountMappingHelper.generateSubscriptionToRecurlyMapping.returns({
- source: 'recurly',
- sourceEntity: 'subscription',
- sourceEntityId: ctx.recurlySubscription.uuid,
- target: 'v2',
- targetEntity: 'subscription',
- targetEntityId: ctx.subscription._id,
- createdAt,
- })
- await ctx.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
- ctx.recurlySubscription,
- ctx.subscription,
- {}
- )
- expect(
- ctx.AccountMappingHelper.generateSubscriptionToRecurlyMapping
- ).to.have.been.calledWith(
- ctx.subscription._id,
- ctx.recurlySubscription.uuid
- )
- expect(
- ctx.AnalyticsManager.registerAccountMapping
- ).to.have.been.calledWith({
- source: 'recurly',
- sourceEntity: 'subscription',
- sourceEntityId: ctx.recurlySubscription.uuid,
- target: 'v2',
- targetEntity: 'subscription',
- targetEntityId: ctx.subscription._id,
- createdAt,
- })
- })
- it('should not update subscription when paymentProvider service contains stripe', async function (ctx) {
- ctx.subscription.paymentProvider = {
- service: 'stripe-uk',
- }
- await ctx.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
- ctx.recurlySubscription,
- ctx.subscription,
- {}
- )
- ctx.subscription.save.called.should.equal(false)
- expect(ctx.FeaturesUpdater.promises.scheduleRefreshFeatures).to.not.have
- .been.called
- })
- it('should remove the subscription when expired', async function (ctx) {
- ctx.recurlySubscription.state = 'expired'
- await ctx.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
- ctx.recurlySubscription,
- ctx.subscription,
- {}
- )
- ctx.SubscriptionModel.deleteOne.should.have.been.calledWith({
- _id: ctx.subscription._id,
- })
- })
- it('should not remove the subscription when expired if it has Managed Users enabled', async function (ctx) {
- ctx.Features.hasFeature.withArgs('saas').returns(true)
- ctx.subscription.managedUsersEnabled = true
- ctx.recurlySubscription.state = 'expired'
- await ctx.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
- ctx.recurlySubscription,
- ctx.subscription,
- {}
- )
- ctx.SubscriptionModel.deleteOne.should.not.have.been.called
- })
- it('should not remove the subscription when expired if it has Group SSO enabled', async function (ctx) {
- ctx.Features.hasFeature.withArgs('saas').returns(true)
- ctx.subscription.ssoConfig = new ObjectId('abc123abc123abc123abc123')
- ctx.recurlySubscription.state = 'expired'
- await ctx.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
- ctx.recurlySubscription,
- ctx.subscription,
- {}
- )
- ctx.SubscriptionModel.deleteOne.should.not.have.been.called
- })
- it('should update all the users features', async function (ctx) {
- ctx.subscription.member_ids = ctx.allUserIds
- await ctx.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
- ctx.recurlySubscription,
- ctx.subscription,
- {}
- )
- expect(
- ctx.FeaturesUpdater.promises.scheduleRefreshFeatures
- ).to.have.been.calledWith(ctx.adminUser._id)
- expect(
- ctx.FeaturesUpdater.promises.scheduleRefreshFeatures
- ).to.have.been.calledWith(ctx.allUserIds[0])
- expect(
- ctx.FeaturesUpdater.promises.scheduleRefreshFeatures
- ).to.have.been.calledWith(ctx.allUserIds[1])
- expect(
- ctx.FeaturesUpdater.promises.scheduleRefreshFeatures
- ).to.have.been.calledWith(ctx.allUserIds[2])
- })
- it('should set group to true and save how many members can be added to group', async function (ctx) {
- ctx.recurlyPlan.groupPlan = true
- ctx.recurlyPlan.membersLimit = 5
- await ctx.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
- ctx.recurlySubscription,
- ctx.subscription,
- {}
- )
- ctx.subscription.membersLimit.should.equal(5)
- ctx.subscription.groupPlan.should.equal(true)
- ctx.subscription.member_ids.should.deep.equal([ctx.subscription.admin_id])
- })
- it('should delete and replace subscription when downgrading from group to individual plan', async function (ctx) {
- ctx.recurlyPlan.groupPlan = false
- await ctx.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
- ctx.recurlySubscription,
- ctx.groupSubscription,
- {}
- )
- })
- it('should not set group to true or set groupPlan', async function (ctx) {
- await ctx.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
- ctx.recurlySubscription,
- ctx.subscription,
- {}
- )
- assert.notEqual(ctx.subscription.membersLimit, 5)
- assert.notEqual(ctx.subscription.groupPlan, true)
- })
- describe('when the plan allows adding more seats', function () {
- beforeEach(function (ctx) {
- ctx.membersLimitAddOn = 'add_on1'
- ctx.recurlyPlan.groupPlan = true
- ctx.recurlyPlan.membersLimit = 5
- ctx.recurlyPlan.membersLimitAddOn = ctx.membersLimitAddOn
- })
- function expectMembersLimit(limit) {
- it('should set the membersLimit accordingly', async function (ctx) {
- await ctx.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
- ctx.recurlySubscription,
- ctx.subscription,
- {}
- )
- expect(ctx.subscription.membersLimit).to.equal(limit)
- })
- }
- describe('when the recurlySubscription does not have add ons', function () {
- beforeEach(function (ctx) {
- delete ctx.recurlySubscription.subscription_add_ons
- })
- expectMembersLimit(5)
- })
- describe('when the recurlySubscription has non-matching add ons', function () {
- beforeEach(function (ctx) {
- ctx.recurlySubscription.subscription_add_ons = [
- { add_on_code: 'add_on_99', quantity: 3 },
- ]
- })
- expectMembersLimit(5)
- })
- describe('when the recurlySubscription has a matching add on', function () {
- beforeEach(function (ctx) {
- ctx.recurlySubscription.subscription_add_ons = [
- { add_on_code: ctx.membersLimitAddOn, quantity: 10 },
- ]
- })
- expectMembersLimit(15)
- })
- // NOTE: This is unexpected, but we are going to support it anyways.
- describe('when the recurlySubscription has multiple matching add ons', function () {
- beforeEach(function (ctx) {
- ctx.recurlySubscription.subscription_add_ons = [
- { add_on_code: ctx.membersLimitAddOn, quantity: 10 },
- { add_on_code: ctx.membersLimitAddOn, quantity: 3 },
- ]
- })
- expectMembersLimit(18)
- })
- })
- })
- describe('handleExpiredSubscription', function () {
- it("should set the admin's previous_plan_type in customer.io to the expired plan when the subscription is deleted", async function (ctx) {
- await ctx.SubscriptionUpdater.promises.handleExpiredSubscription(
- ctx.subscription,
- {}
- )
- expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
- 'setUserProperties',
- ctx.subscription.admin_id,
- { previous_plan_type: 'student' }
- )
- })
- it('should not set previous_plan_type when the subscription is not deleted (managed users)', async function (ctx) {
- ctx.Features.hasFeature.withArgs('saas').returns(true)
- ctx.subscription.managedUsersEnabled = true
- await ctx.SubscriptionUpdater.promises.handleExpiredSubscription(
- ctx.subscription,
- {}
- )
- expect(ctx.Modules.promises.hooks.fire).to.not.have.been.calledWith(
- 'setUserProperties',
- ctx.subscription.admin_id,
- { previous_plan_type: 'student' }
- )
- })
- it('should not set previous_plan_type when the subscription is not deleted (group SSO)', async function (ctx) {
- ctx.Features.hasFeature.withArgs('saas').returns(true)
- ctx.subscription.ssoConfig = new ObjectId('abc123abc123abc123abc123')
- await ctx.SubscriptionUpdater.promises.handleExpiredSubscription(
- ctx.subscription,
- {}
- )
- expect(ctx.Modules.promises.hooks.fire).to.not.have.been.calledWith(
- 'setUserProperties',
- ctx.subscription.admin_id,
- { previous_plan_type: 'student' }
- )
- })
- })
- describe('addUserToGroup', function () {
- it('should add the user ids to the group as a set', async function (ctx) {
- ctx.SubscriptionModel.findOne = sinon
- .stub()
- .resolves(ctx.groupSubscription)
- await ctx.SubscriptionUpdater.promises.addUserToGroup(
- ctx.groupSubscription._id,
- ctx.otherUserId
- )
- const searchOps = { _id: ctx.groupSubscription._id }
- const insertOperation = {
- $addToSet: { member_ids: ctx.otherUserId },
- }
- ctx.SubscriptionModel.updateOne
- .calledWith(searchOps, insertOperation)
- .should.equal(true)
- expect(ctx.SubscriptionModel.updateOne.lastCall.args[2].session).to.exist
- sinon.assert.calledWith(
- ctx.AnalyticsManager.recordEventForUserInBackground,
- ctx.otherUserId,
- 'group-subscription-joined',
- {
- groupId: ctx.groupSubscription._id,
- subscriptionId: ctx.groupSubscription.recurlySubscription_id,
- }
- )
- })
- it('should update the users features', async function (ctx) {
- await ctx.SubscriptionUpdater.promises.addUserToGroup(
- ctx.subscription._id,
- ctx.otherUserId
- )
- ctx.FeaturesUpdater.promises.refreshFeatures
- .calledWith(ctx.otherUserId)
- .should.equal(true)
- })
- it('should set the group plan code user property to the best plan with 1 group subscription', async function (ctx) {
- ctx.SubscriptionLocator.promises.getMemberSubscriptions
- .withArgs(ctx.otherUserId)
- .resolves([ctx.groupSubscription])
- await ctx.SubscriptionUpdater.promises.addUserToGroup(
- ctx.groupSubscription._id,
- ctx.otherUserId
- )
- sinon.assert.calledWith(
- ctx.AnalyticsManager.setUserPropertyForUserInBackground,
- ctx.otherUserId,
- 'group-subscription-plan-code',
- 'group_subscription'
- )
- })
- it('should set the group plan code user property to the best plan with 2 group subscriptions', async function (ctx) {
- ctx.SubscriptionLocator.promises.getMemberSubscriptions
- .withArgs(ctx.otherUserId)
- .resolves([ctx.groupSubscription, ctx.betterGroupSubscription])
- await ctx.SubscriptionUpdater.promises.addUserToGroup(
- ctx.betterGroupSubscription._id,
- ctx.otherUserId
- )
- sinon.assert.calledWith(
- ctx.AnalyticsManager.setUserPropertyForUserInBackground,
- ctx.otherUserId,
- 'group-subscription-plan-code',
- 'better_group_subscription'
- )
- })
- it('should set the group plan code user property to the best plan with 2 group subscriptions in reverse order', async function (ctx) {
- ctx.SubscriptionLocator.promises.getMemberSubscriptions
- .withArgs(ctx.otherUserId)
- .resolves([ctx.betterGroupSubscription, ctx.groupSubscription])
- await ctx.SubscriptionUpdater.promises.addUserToGroup(
- ctx.betterGroupSubscription._id,
- ctx.otherUserId
- )
- sinon.assert.calledWith(
- ctx.AnalyticsManager.setUserPropertyForUserInBackground,
- ctx.otherUserId,
- 'group-subscription-plan-code',
- 'better_group_subscription'
- )
- })
- it('should add an entry to the user audit log when joining a group', async function (ctx) {
- await ctx.SubscriptionUpdater.promises.addUserToGroup(
- ctx.subscription._id,
- ctx.otherUserId
- )
- sinon.assert.calledWith(
- ctx.UserAuditLogHandler.promises.addEntry,
- ctx.otherUserId,
- 'join-group-subscription',
- undefined,
- undefined,
- {
- subscriptionId: ctx.subscription._id,
- }
- )
- })
- it('should add an entry to the group audit log when joining a group', async function (ctx) {
- await ctx.SubscriptionUpdater.promises.addUserToGroup(
- ctx.subscription._id,
- ctx.otherUserId,
- { ipAddress: '0:0:0:0', initiatorId: 'user123' }
- )
- expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
- 'addGroupAuditLogEntry',
- {
- groupId: ctx.subscription._id,
- initiatorId: 'user123',
- ipAddress: '0:0:0:0',
- operation: 'join-group',
- }
- )
- })
- })
- describe('removeUserFromGroup', function () {
- beforeEach(function (ctx) {
- ctx.fakeSubscriptions = [
- {
- _id: 'fake-id-1',
- },
- {
- _id: 'fake-id-2',
- },
- ]
- ctx.SubscriptionModel.findOne.resolves(ctx.groupSubscription)
- ctx.SubscriptionModel.findById = sinon
- .stub()
- .resolves(ctx.groupSubscription)
- ctx.SubscriptionLocator.promises.getMemberSubscriptions.resolves(
- ctx.fakeSubscriptions
- )
- })
- it('should pull the users id from the group', async function (ctx) {
- await ctx.SubscriptionUpdater.promises.removeUserFromGroup(
- ctx.subscription._id,
- ctx.otherUserId
- )
- const removeOperation = { $pull: { member_ids: ctx.otherUserId } }
- ctx.SubscriptionModel.updateOne
- .calledWith({ _id: ctx.subscription._id }, removeOperation)
- .should.equal(true)
- })
- it('should remove user enrollment if the group is managed', async function (ctx) {
- ctx.SubscriptionModel.findById.resolves({
- ...ctx.groupSubscription,
- managedUsersEnabled: true,
- })
- await ctx.SubscriptionUpdater.promises.removeUserFromGroup(
- ctx.groupSubscription._id,
- ctx.otherUserId
- )
- ctx.UserUpdater.promises.updateUser
- .calledWith(
- { _id: ctx.otherUserId },
- {
- $unset: {
- 'enrollment.managedBy': 1,
- 'enrollment.enrolledAt': 1,
- },
- }
- )
- .should.equal(true)
- })
- it('should send a group-subscription-left event', async function (ctx) {
- await ctx.SubscriptionUpdater.promises.removeUserFromGroup(
- ctx.groupSubscription._id,
- ctx.otherUserId
- )
- sinon.assert.calledWith(
- ctx.AnalyticsManager.recordEventForUserInBackground,
- ctx.otherUserId,
- 'group-subscription-left',
- {
- groupId: ctx.groupSubscription._id,
- subscriptionId: ctx.groupSubscription.recurlySubscription_id,
- }
- )
- })
- it('should set the group plan code user property when removing user from group', async function (ctx) {
- await ctx.SubscriptionUpdater.promises.removeUserFromGroup(
- ctx.subscription._id,
- ctx.otherUserId
- )
- sinon.assert.calledWith(
- ctx.AnalyticsManager.setUserPropertyForUserInBackground,
- ctx.otherUserId,
- 'group-subscription-plan-code',
- null
- )
- })
- it('should update the users features', async function (ctx) {
- await ctx.SubscriptionUpdater.promises.removeUserFromGroup(
- ctx.subscription._id,
- ctx.otherUserId
- )
- ctx.FeaturesUpdater.promises.refreshFeatures
- .calledWith(ctx.otherUserId)
- .should.equal(true)
- })
- it('should add an audit log when a user leaves a group', async function (ctx) {
- await ctx.SubscriptionUpdater.promises.removeUserFromGroup(
- ctx.subscription._id,
- ctx.otherUserId
- )
- sinon.assert.calledWith(
- ctx.UserAuditLogHandler.promises.addEntry,
- ctx.otherUserId,
- 'leave-group-subscription',
- undefined,
- undefined,
- {
- subscriptionId: ctx.subscription._id,
- }
- )
- })
- })
- describe('removeUserFromAllGroups', function () {
- beforeEach(function (ctx) {
- ctx.SubscriptionLocator.promises.getMemberSubscriptions.resolves([
- {
- _id: 'fake-id-1',
- },
- {
- _id: 'fake-id-2',
- },
- ])
- })
- it('should set the group plan code user property when removing user from all groups', async function (ctx) {
- await ctx.SubscriptionUpdater.promises.removeUserFromAllGroups(
- ctx.otherUserId
- )
- sinon.assert.calledWith(
- ctx.AnalyticsManager.setUserPropertyForUserInBackground,
- ctx.otherUserId,
- 'group-subscription-plan-code',
- null
- )
- })
- it('should pull the users id from all groups', async function (ctx) {
- await ctx.SubscriptionUpdater.promises.removeUserFromAllGroups(
- ctx.otherUserId
- )
- const filter = { _id: ['fake-id-1', 'fake-id-2'] }
- const removeOperation = { $pull: { member_ids: ctx.otherUserId } }
- sinon.assert.calledWith(
- ctx.SubscriptionModel.updateMany,
- filter,
- removeOperation
- )
- })
- it('should send a group-subscription-left event for each group', async function (ctx) {
- ctx.fakeSub1 = {
- _id: 'fake-id-1',
- groupPlan: true,
- recurlySubscription_id: 'fake-sub-1',
- }
- ctx.fakeSub2 = {
- _id: 'fake-id-2',
- groupPlan: true,
- recurlySubscription_id: 'fake-sub-2',
- }
- ctx.SubscriptionModel.findOne
- .withArgs(
- { _id: 'fake-id-1' },
- { recurlySubscription_id: 1, groupPlan: 1 }
- )
- .resolves(ctx.fakeSub1)
- .withArgs(
- { _id: 'fake-id-2' },
- { recurlySubscription_id: 1, groupPlan: 1 }
- )
- .resolves(ctx.fakeSub2)
- await ctx.SubscriptionUpdater.promises.removeUserFromAllGroups(
- ctx.otherUserId
- )
- sinon.assert.calledWith(
- ctx.AnalyticsManager.recordEventForUserInBackground,
- ctx.otherUserId,
- 'group-subscription-left',
- {
- groupId: 'fake-id-1',
- subscriptionId: 'fake-sub-1',
- }
- )
- sinon.assert.calledWith(
- ctx.AnalyticsManager.recordEventForUserInBackground,
- ctx.otherUserId,
- 'group-subscription-left',
- {
- groupId: 'fake-id-2',
- subscriptionId: 'fake-sub-2',
- }
- )
- })
- it('should add an audit log entry for each group the user leaves', async function (ctx) {
- await ctx.SubscriptionUpdater.promises.removeUserFromAllGroups(
- ctx.otherUserId
- )
- sinon.assert.calledWith(
- ctx.UserAuditLogHandler.promises.addEntry,
- ctx.otherUserId,
- 'leave-group-subscription',
- undefined,
- undefined,
- {
- subscriptionId: 'fake-id-1',
- }
- )
- sinon.assert.calledWith(
- ctx.UserAuditLogHandler.promises.addEntry,
- ctx.otherUserId,
- 'leave-group-subscription',
- undefined,
- undefined,
- {
- subscriptionId: 'fake-id-2',
- }
- )
- })
- })
- describe('deleteSubscription', function () {
- beforeEach(async function (ctx) {
- ctx.subscription = {
- _id: new ObjectId().toString(),
- mock: 'subscription',
- admin_id: new ObjectId(),
- member_ids: [new ObjectId(), new ObjectId(), new ObjectId()],
- }
- await ctx.SubscriptionUpdater.promises.deleteSubscription(
- ctx.subscription,
- {}
- )
- })
- it('should remove the subscription', function (ctx) {
- ctx.SubscriptionModel.deleteOne
- .calledWith({ _id: ctx.subscription._id })
- .should.equal(true)
- })
- it('should downgrade the admin_id', function (ctx) {
- expect(
- ctx.FeaturesUpdater.promises.scheduleRefreshFeatures
- ).to.have.been.calledWith(ctx.subscription.admin_id)
- })
- it('should downgrade all of the members', function (ctx) {
- for (const userId of ctx.subscription.member_ids) {
- expect(
- ctx.FeaturesUpdater.promises.scheduleRefreshFeatures
- ).to.have.been.calledWith(userId)
- }
- })
- })
- describe('scheduleRefreshFeatures', function () {
- it('should call upgrades feature for personal subscription from admin_id', async function (ctx) {
- ctx.subscription = {
- _id: new ObjectId().toString(),
- mock: 'subscription',
- admin_id: new ObjectId(),
- }
- await ctx.SubscriptionUpdater.promises.scheduleRefreshFeatures(
- ctx.subscription
- )
- expect(
- ctx.FeaturesUpdater.promises.scheduleRefreshFeatures
- ).to.have.been.calledOnceWith(ctx.subscription.admin_id)
- })
- it('should call upgrades feature for group subscription from admin_id and member_ids', async function (ctx) {
- ctx.subscription = {
- _id: new ObjectId().toString(),
- mock: 'subscription',
- admin_id: new ObjectId(),
- member_ids: [new ObjectId(), new ObjectId(), new ObjectId()],
- }
- await ctx.SubscriptionUpdater.promises.scheduleRefreshFeatures(
- ctx.subscription
- )
- expect(
- ctx.FeaturesUpdater.promises.scheduleRefreshFeatures.callCount
- ).to.equal(4)
- })
- })
- })
|