const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') const modulePath = '../../../../app/src/Features/Subscription/SubscriptionUpdater' const { assert, expect } = require('chai') const { ObjectId } = require('mongodb') describe('SubscriptionUpdater', function() { beforeEach(function() { this.recurlySubscription = { uuid: '1238uoijdasjhd', plan: { plan_code: 'kjhsakjds' } } this.adminUser = { _id: (this.adminuser_id = '5208dd34438843e2db000007') } this.otherUserId = '5208dd34438842e2db000005' this.allUserIds = ['13213', 'dsadas', 'djsaiud89'] this.subscription = { _id: '111111111111111111111111', admin_id: this.adminUser._id, manager_ids: [this.adminUser._id], member_ids: [], save: sinon.stub().callsArgWith(0), planCode: 'student_or_something' } this.user_id = this.adminuser_id this.groupSubscription = { _id: '222222222222222222222222', admin_id: this.adminUser._id, manager_ids: [this.adminUser._id], member_ids: this.allUserIds, save: sinon.stub().callsArgWith(0), planCode: 'group_subscription' } this.updateStub = sinon.stub().callsArgWith(2, null) this.updateManyStub = sinon.stub().callsArgWith(2, null) this.findOneAndUpdateStub = sinon .stub() .callsArgWith(2, null, this.subscription) let subscription = this.subscription this.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 } } this.SubscriptionModel.deleteOne = sinon.stub().yields() this.SubscriptionModel.updateOne = this.updateStub this.SubscriptionModel.updateMany = this.updateManyStub this.SubscriptionModel.findOneAndUpdate = this.findOneAndUpdateStub this.SubscriptionLocator = { getUsersSubscription: sinon.stub(), getGroupSubscriptionMemberOf: sinon.stub(), getMemberSubscriptions: sinon.stub().yields(null, []) } this.Settings = { defaultPlanCode: 'personal', defaultFeatures: { default: 'features' } } this.UserFeaturesUpdater = { updateFeatures: sinon.stub().yields() } this.PlansLocator = { findLocalPlanInSettings: sinon.stub().returns({}) } this.UserGetter = { getUsers(memberIds, projection, callback) { const users = memberIds.map(id => ({ _id: id })) callback(null, users) }, getUser: sinon.stub() } this.ReferalFeatures = { getBonusFeatures: sinon.stub().callsArgWith(1) } this.Modules = { hooks: { fire: sinon.stub().callsArgWith(2, null, null) } } this.FeaturesUpdater = { refreshFeatures: sinon.stub().yields() } this.DeletedSubscription = { findOneAndUpdate: sinon.stub().yields() } this.SubscriptionUpdater = SandboxedModule.require(modulePath, { requires: { mongodb: { ObjectId }, '../../models/Subscription': { Subscription: this.SubscriptionModel }, './UserFeaturesUpdater': this.UserFeaturesUpdater, './SubscriptionLocator': this.SubscriptionLocator, '../User/UserGetter': this.UserGetter, './PlansLocator': this.PlansLocator, 'settings-sharelatex': this.Settings, '../../infrastructure/mongodb': { db: {}, ObjectId }, './FeaturesUpdater': this.FeaturesUpdater, '../../models/DeletedSubscription': { DeletedSubscription: this.DeletedSubscription } } }) }) describe('updateAdmin', function() { it('should update the subscription admin', function(done) { this.subscription.groupPlan = true this.SubscriptionUpdater.updateAdmin( this.subscription, this.otherUserId, err => { if (err != null) { return done(err) } const query = { _id: ObjectId(this.subscription._id), customAccount: true } const update = { $set: { admin_id: ObjectId(this.otherUserId) }, $addToSet: { manager_ids: ObjectId(this.otherUserId) } } this.SubscriptionModel.updateOne.should.have.been.calledOnce this.SubscriptionModel.updateOne.should.have.been.calledWith( query, update ) done() } ) }) it('should remove the manager for non-group subscriptions', function(done) { this.SubscriptionUpdater.updateAdmin( this.subscription, this.otherUserId, err => { if (err != null) { return done(err) } const query = { _id: ObjectId(this.subscription._id), customAccount: true } const update = { $set: { admin_id: ObjectId(this.otherUserId), manager_ids: [ObjectId(this.otherUserId)] } } this.SubscriptionModel.updateOne.should.have.been.calledOnce this.SubscriptionModel.updateOne.should.have.been.calledWith( query, update ) done() } ) }) }) describe('syncSubscription', function() { beforeEach(function() { this.SubscriptionLocator.getUsersSubscription.callsArgWith( 1, null, this.subscription ) this.SubscriptionUpdater._updateSubscriptionFromRecurly = sinon .stub() .yields() }) it('should update the subscription if the user already is admin of one', function(done) { this.SubscriptionUpdater._createNewSubscription = sinon.stub() this.SubscriptionUpdater.syncSubscription( this.recurlySubscription, this.adminUser._id, err => { if (err != null) { return done(err) } this.SubscriptionLocator.getUsersSubscription .calledWith(this.adminUser._id) .should.equal(true) this.SubscriptionUpdater._updateSubscriptionFromRecurly.called.should.equal( true ) this.SubscriptionUpdater._updateSubscriptionFromRecurly .calledWith(this.recurlySubscription, this.subscription) .should.equal(true) done() } ) }) it('should not call updateFeatures with group subscription if recurly subscription is not expired', function(done) { this.SubscriptionUpdater.syncSubscription( this.recurlySubscription, this.adminUser._id, err => { if (err != null) { return done(err) } this.SubscriptionLocator.getUsersSubscription .calledWith(this.adminUser._id) .should.equal(true) this.SubscriptionUpdater._updateSubscriptionFromRecurly.called.should.equal( true ) this.SubscriptionUpdater._updateSubscriptionFromRecurly .calledWith(this.recurlySubscription, this.subscription) .should.equal(true) this.UserFeaturesUpdater.updateFeatures.called.should.equal(false) done() } ) }) }) describe('_updateSubscriptionFromRecurly', function() { beforeEach(function() { this.FeaturesUpdater.refreshFeatures = sinon.stub().callsArgWith(1) this.SubscriptionUpdater.deleteSubscription = sinon.stub().yields() }) afterEach(function() { this.subscription.member_ids = [] }) it('should update the subscription with token etc when not expired', function(done) { this.SubscriptionUpdater._updateSubscriptionFromRecurly( this.recurlySubscription, this.subscription, {}, err => { if (err != null) { return done(err) } this.subscription.recurlySubscription_id.should.equal( this.recurlySubscription.uuid ) this.subscription.planCode.should.equal( this.recurlySubscription.plan.plan_code ) this.subscription.save.called.should.equal(true) this.FeaturesUpdater.refreshFeatures .calledWith(this.adminUser._id) .should.equal(true) done() } ) }) it('should remove the subscription when expired', function(done) { this.recurlySubscription.state = 'expired' this.SubscriptionUpdater._updateSubscriptionFromRecurly( this.recurlySubscription, this.subscription, {}, err => { if (err != null) { return done(err) } this.SubscriptionUpdater.deleteSubscription .calledWithMatch(this.subscription) .should.equal(true) done() } ) }) it('should update all the users features', function(done) { this.subscription.member_ids = this.allUserIds this.SubscriptionUpdater._updateSubscriptionFromRecurly( this.recurlySubscription, this.subscription, {}, err => { if (err != null) { return done(err) } this.FeaturesUpdater.refreshFeatures .calledWith(this.adminUser._id) .should.equal(true) this.FeaturesUpdater.refreshFeatures .calledWith(this.allUserIds[0]) .should.equal(true) this.FeaturesUpdater.refreshFeatures .calledWith(this.allUserIds[1]) .should.equal(true) this.FeaturesUpdater.refreshFeatures .calledWith(this.allUserIds[2]) .should.equal(true) done() } ) }) it('should set group to true and save how many members can be added to group', function(done) { this.PlansLocator.findLocalPlanInSettings .withArgs(this.recurlySubscription.plan.plan_code) .returns({ groupPlan: true, membersLimit: 5 }) this.SubscriptionUpdater._updateSubscriptionFromRecurly( this.recurlySubscription, this.subscription, {}, err => { if (err != null) { return done(err) } this.subscription.membersLimit.should.equal(5) this.subscription.groupPlan.should.equal(true) this.subscription.member_ids.should.deep.equal([ this.subscription.admin_id ]) done() } ) }) it('should not set group to true or set groupPlan', function(done) { this.SubscriptionUpdater._updateSubscriptionFromRecurly( this.recurlySubscription, this.subscription, {}, err => { if (err != null) { return done(err) } assert.notEqual(this.subscription.membersLimit, 5) assert.notEqual(this.subscription.groupPlan, true) done() } ) }) describe('when the plan allows adding more seats', function() { beforeEach(function() { this.membersLimitAddOn = 'add_on1' this.PlansLocator.findLocalPlanInSettings .withArgs(this.recurlySubscription.plan.plan_code) .returns({ groupPlan: true, membersLimit: 5, membersLimitAddOn: this.membersLimitAddOn }) }) function expectMembersLimit(limit) { it('should set the membersLimit accordingly', function(done) { this.SubscriptionUpdater._updateSubscriptionFromRecurly( this.recurlySubscription, this.subscription, {}, error => { if (error) return done(error) expect(this.subscription.membersLimit).to.equal(limit) done() } ) }) } describe('when the recurlySubscription does not have add ons', function() { beforeEach(function() { delete this.recurlySubscription.subscription_add_ons }) expectMembersLimit(5) }) describe('when the recurlySubscription has non-matching add ons', function() { beforeEach(function() { this.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() { this.recurlySubscription.subscription_add_ons = [ { add_on_code: this.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() { this.recurlySubscription.subscription_add_ons = [ { add_on_code: this.membersLimitAddOn, quantity: 10 }, { add_on_code: this.membersLimitAddOn, quantity: 3 } ] }) expectMembersLimit(18) }) }) }) describe('_createNewSubscription', function() { it('should create a new subscription then update the subscription', function(done) { this.SubscriptionUpdater._createNewSubscription( this.adminUser._id, () => { this.subscription.admin_id.should.equal(this.adminUser._id) this.subscription.manager_ids.should.deep.equal([this.adminUser._id]) this.subscription.save.called.should.equal(true) done() } ) }) }) describe('addUserToGroup', function() { beforeEach(function() { this.SubscriptionUpdater.addUsersToGroup = sinon.stub().yields(null) }) it('delegates to addUsersToGroup', function(done) { this.SubscriptionUpdater.addUserToGroup( this.subscription._id, this.otherUserId, () => { this.SubscriptionUpdater.addUsersToGroup .calledWith(this.subscription._id, [this.otherUserId]) .should.equal(true) done() } ) }) }) describe('addUsersToGroup', function() { beforeEach(function() { this.FeaturesUpdater.refreshFeatures = sinon.stub().callsArgWith(1) }) it('should add the user ids to the group as a set', function(done) { this.SubscriptionUpdater.addUsersToGroup( this.subscription._id, [this.otherUserId], () => { const searchOps = { _id: this.subscription._id } const insertOperation = { $addToSet: { member_ids: { $each: [this.otherUserId] } } } this.updateStub .calledWith(searchOps, insertOperation) .should.equal(true) done() } ) }) it('should update the users features', function(done) { this.SubscriptionUpdater.addUserToGroup( this.subscription._id, this.otherUserId, () => { this.FeaturesUpdater.refreshFeatures .calledWith(this.otherUserId) .should.equal(true) done() } ) }) }) describe('removeUserFromGroups', function() { beforeEach(function() { this.FeaturesUpdater.refreshFeatures = sinon.stub().callsArgWith(1) this.UserGetter.getUser.yields(null, {}) this.fakeSubscriptions = [{ _id: 'fake-id-1' }, { _id: 'fake-id-2' }] this.SubscriptionLocator.getMemberSubscriptions.yields( null, this.fakeSubscriptions ) }) it('should pull the users id from the group', function(done) { this.SubscriptionUpdater.removeUserFromGroup( this.subscription._id, this.otherUserId, () => { const searchOps = { _id: this.subscription._id } const removeOperation = { $pull: { member_ids: this.otherUserId } } this.updateManyStub .calledWith(searchOps, removeOperation) .should.equal(true) done() } ) }) it('should pull the users id from all groups', function(done) { this.SubscriptionUpdater.removeUserFromAllGroups(this.otherUserId, () => { const filter = { _id: ['fake-id-1', 'fake-id-2'] } const removeOperation = { $pull: { member_ids: this.otherUserId } } sinon.assert.calledWith(this.updateManyStub, filter, removeOperation) done() }) }) it('should update the users features', function(done) { this.SubscriptionUpdater.removeUserFromGroup( this.subscription._id, this.otherUserId, () => { this.FeaturesUpdater.refreshFeatures .calledWith(this.otherUserId) .should.equal(true) done() } ) }) }) describe('deleteSubscription', function() { beforeEach(function(done) { this.subscription = { _id: ObjectId().toString(), mock: 'subscription', admin_id: ObjectId(), member_ids: [ObjectId(), ObjectId(), ObjectId()] } this.SubscriptionLocator.getSubscription = sinon .stub() .yields(null, this.subscription) this.FeaturesUpdater.refreshFeatures = sinon.stub().yields() this.SubscriptionUpdater.deleteSubscription(this.subscription, {}, done) }) it('should remove the subscription', function() { this.SubscriptionModel.deleteOne .calledWith({ _id: this.subscription._id }) .should.equal(true) }) it('should downgrade the admin_id', function() { this.FeaturesUpdater.refreshFeatures .calledWith(this.subscription.admin_id) .should.equal(true) }) it('should downgrade all of the members', function() { for (const userId of this.subscription.member_ids) { this.FeaturesUpdater.refreshFeatures .calledWith(userId) .should.equal(true) } }) }) })