overleaf/services/web/test/unit/src/Subscription/SubscriptionUpdaterTests.js

556 lines
18 KiB
JavaScript
Raw Normal View History

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.recurlyPlan = { planCode: 'recurly-plan' }
this.recurlySubscription = {
uuid: '1238uoijdasjhd',
plan: {
plan_code: this.recurlyPlan.planCode,
},
}
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().resolves(),
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().resolves(),
groupPlan: true,
planCode: 'group_subscription',
}
this.betterGroupSubscription = {
_id: '999999999999999999999999',
admin_id: this.adminUser._id,
manager_ids: [this.adminUser._id],
member_ids: [this.otherUserId],
save: sinon.stub().resolves(),
groupPlan: true,
planCode: 'better_group_subscription',
}
const 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
}
save() {
return Promise.resolve(subscription)
}
}
this.SubscriptionModel.deleteOne = sinon
.stub()
.returns({ exec: sinon.stub().resolves() })
this.SubscriptionModel.updateOne = sinon
.stub()
.returns({ exec: sinon.stub().resolves() })
this.SubscriptionModel.updateMany = sinon
.stub()
.returns({ exec: sinon.stub().resolves() })
this.SubscriptionModel.findOneAndUpdate = sinon.stub().returns({
exec: sinon.stub().resolves(this.subscription),
})
this.SubscriptionLocator = {
promises: {
getUsersSubscription: sinon.stub(),
getGroupSubscriptionMemberOf: sinon.stub(),
getMemberSubscriptions: sinon.stub().resolves([]),
getSubscription: sinon.stub(),
},
}
this.SubscriptionLocator.promises.getSubscription
.withArgs(this.subscription._id)
.resolves(this.subscription)
this.Settings = {
defaultPlanCode: 'personal',
defaultFeatures: { default: 'features' },
plans: [
this.recurlyPlan,
{ planCode: this.subscription.planCode, features: {} },
{
planCode: this.groupSubscription.planCode,
features: {
collaborators: 10,
compileTimeout: 60,
dropbox: true,
},
},
{
planCode: this.betterGroupSubscription.planCode,
features: {
collaborators: -1,
compileTimeout: 240,
dropbox: true,
},
},
],
}
this.UserFeaturesUpdater = {
promises: {
updateFeatures: sinon.stub().resolves(),
},
}
this.ReferalFeatures = {
promises: {
getBonusFeatures: sinon.stub().resolves(),
},
}
this.FeaturesUpdater = {
promises: {
scheduleRefreshFeatures: sinon.stub().resolves(),
refreshFeatures: sinon.stub().resolves({}),
},
}
this.DeletedSubscription = {
findOneAndUpdate: sinon.stub().returns({ exec: sinon.stub().resolves() }),
}
this.AnalyticsManager = {
setUserPropertyForUser: sinon.stub(),
}
this.SubscriptionUpdater = SandboxedModule.require(modulePath, {
requires: {
'../../models/Subscription': {
Subscription: this.SubscriptionModel,
},
'./UserFeaturesUpdater': this.UserFeaturesUpdater,
'./SubscriptionLocator': this.SubscriptionLocator,
'@overleaf/settings': this.Settings,
'../../infrastructure/mongodb': { db: {}, ObjectId },
'./FeaturesUpdater': this.FeaturesUpdater,
'../../models/DeletedSubscription': {
DeletedSubscription: this.DeletedSubscription,
},
'../Analytics/AnalyticsManager': this.AnalyticsManager,
},
})
})
describe('updateAdmin', function () {
it('should update the subscription admin', async function () {
this.subscription.groupPlan = true
await this.SubscriptionUpdater.promises.updateAdmin(
this.subscription,
this.otherUserId
)
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
)
})
it('should remove the manager for non-group subscriptions', async function () {
await this.SubscriptionUpdater.promises.updateAdmin(
this.subscription,
this.otherUserId
)
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
)
})
})
describe('syncSubscription', function () {
beforeEach(function () {
this.SubscriptionLocator.promises.getUsersSubscription.resolves(
this.subscription
)
})
it('should update the subscription if the user already is admin of one', async function () {
await this.SubscriptionUpdater.promises.syncSubscription(
this.recurlySubscription,
this.adminUser._id
)
this.SubscriptionLocator.promises.getUsersSubscription
.calledWith(this.adminUser._id)
.should.equal(true)
})
it('should not call updateFeatures with group subscription if recurly subscription is not expired', async function () {
await this.SubscriptionUpdater.promises.syncSubscription(
this.recurlySubscription,
this.adminUser._id
)
this.SubscriptionLocator.promises.getUsersSubscription
.calledWith(this.adminUser._id)
.should.equal(true)
this.UserFeaturesUpdater.promises.updateFeatures.called.should.equal(
false
)
})
})
describe('updateSubscriptionFromRecurly', function () {
afterEach(function () {
this.subscription.member_ids = []
})
it('should update the subscription with token etc when not expired', async function () {
await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
this.recurlySubscription,
this.subscription,
{}
)
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)
expect(
this.FeaturesUpdater.promises.scheduleRefreshFeatures
).to.have.been.calledWith(this.adminUser._id)
})
it('should remove the subscription when expired', async function () {
this.recurlySubscription.state = 'expired'
await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
this.recurlySubscription,
this.subscription,
{}
)
})
it('should update all the users features', async function () {
this.subscription.member_ids = this.allUserIds
await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
this.recurlySubscription,
this.subscription,
{}
)
expect(
this.FeaturesUpdater.promises.scheduleRefreshFeatures
).to.have.been.calledWith(this.adminUser._id)
expect(
this.FeaturesUpdater.promises.scheduleRefreshFeatures
).to.have.been.calledWith(this.allUserIds[0])
expect(
this.FeaturesUpdater.promises.scheduleRefreshFeatures
).to.have.been.calledWith(this.allUserIds[1])
expect(
this.FeaturesUpdater.promises.scheduleRefreshFeatures
).to.have.been.calledWith(this.allUserIds[2])
})
it('should set group to true and save how many members can be added to group', async function () {
this.recurlyPlan.groupPlan = true
this.recurlyPlan.membersLimit = 5
await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
this.recurlySubscription,
this.subscription,
{}
)
this.subscription.membersLimit.should.equal(5)
this.subscription.groupPlan.should.equal(true)
this.subscription.member_ids.should.deep.equal([
this.subscription.admin_id,
])
})
it('should delete and replace subscription when downgrading from group to individual plan', async function () {
this.recurlyPlan.groupPlan = false
await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
this.recurlySubscription,
this.groupSubscription,
{}
)
})
it('should not set group to true or set groupPlan', async function () {
await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
this.recurlySubscription,
this.subscription,
{}
)
assert.notEqual(this.subscription.membersLimit, 5)
assert.notEqual(this.subscription.groupPlan, true)
})
describe('when the plan allows adding more seats', function () {
beforeEach(function () {
this.membersLimitAddOn = 'add_on1'
this.recurlyPlan.groupPlan = true
this.recurlyPlan.membersLimit = 5
this.recurlyPlan.membersLimitAddOn = this.membersLimitAddOn
})
function expectMembersLimit(limit) {
it('should set the membersLimit accordingly', async function () {
await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
this.recurlySubscription,
this.subscription,
{}
)
expect(this.subscription.membersLimit).to.equal(limit)
})
}
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('addUserToGroup', function () {
it('should add the user ids to the group as a set', async function () {
await this.SubscriptionUpdater.promises.addUserToGroup(
this.subscription._id,
this.otherUserId
)
const searchOps = { _id: this.subscription._id }
const insertOperation = {
$addToSet: { member_ids: this.otherUserId },
}
this.SubscriptionModel.updateOne
.calledWith(searchOps, insertOperation)
.should.equal(true)
})
it('should update the users features', async function () {
await this.SubscriptionUpdater.promises.addUserToGroup(
this.subscription._id,
this.otherUserId
)
this.FeaturesUpdater.promises.refreshFeatures
.calledWith(this.otherUserId)
.should.equal(true)
})
it('should set the group plan code user property to the best plan with 1 group subscription', async function () {
this.SubscriptionLocator.promises.getMemberSubscriptions
.withArgs(this.otherUserId)
.resolves([this.groupSubscription])
await this.SubscriptionUpdater.promises.addUserToGroup(
this.groupSubscription._id,
this.otherUserId
)
sinon.assert.calledWith(
this.AnalyticsManager.setUserPropertyForUser,
this.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 () {
this.SubscriptionLocator.promises.getMemberSubscriptions
.withArgs(this.otherUserId)
.resolves([this.groupSubscription, this.betterGroupSubscription])
await this.SubscriptionUpdater.promises.addUserToGroup(
this.betterGroupSubscription._id,
this.otherUserId
)
sinon.assert.calledWith(
this.AnalyticsManager.setUserPropertyForUser,
this.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 () {
this.SubscriptionLocator.promises.getMemberSubscriptions
.withArgs(this.otherUserId)
.resolves([this.betterGroupSubscription, this.groupSubscription])
await this.SubscriptionUpdater.promises.addUserToGroup(
this.betterGroupSubscription._id,
this.otherUserId
)
sinon.assert.calledWith(
this.AnalyticsManager.setUserPropertyForUser,
this.otherUserId,
'group-subscription-plan-code',
'better_group_subscription'
)
})
})
describe('removeUserFromGroups', function () {
beforeEach(function () {
this.fakeSubscriptions = [{ _id: 'fake-id-1' }, { _id: 'fake-id-2' }]
this.SubscriptionLocator.promises.getMemberSubscriptions.resolves(
this.fakeSubscriptions
)
})
it('should pull the users id from the group', async function () {
await this.SubscriptionUpdater.promises.removeUserFromGroup(
this.subscription._id,
this.otherUserId
)
const removeOperation = { $pull: { member_ids: this.otherUserId } }
this.SubscriptionModel.updateOne
.calledWith({ _id: this.subscription._id }, removeOperation)
.should.equal(true)
})
it('should set the group plan code user property when removing user from group', async function () {
await this.SubscriptionUpdater.promises.removeUserFromGroup(
this.subscription._id,
this.otherUserId
)
sinon.assert.calledWith(
this.AnalyticsManager.setUserPropertyForUser,
this.otherUserId,
'group-subscription-plan-code',
null
)
})
it('should set the group plan code user property when removing user from all groups', async function () {
await this.SubscriptionUpdater.promises.removeUserFromAllGroups(
this.otherUserId
)
sinon.assert.calledWith(
this.AnalyticsManager.setUserPropertyForUser,
this.otherUserId,
'group-subscription-plan-code',
null
)
})
it('should pull the users id from all groups', async function () {
await this.SubscriptionUpdater.promises.removeUserFromAllGroups(
this.otherUserId
)
const filter = { _id: ['fake-id-1', 'fake-id-2'] }
const removeOperation = { $pull: { member_ids: this.otherUserId } }
sinon.assert.calledWith(
this.SubscriptionModel.updateMany,
filter,
removeOperation
)
})
it('should update the users features', async function () {
await this.SubscriptionUpdater.promises.removeUserFromGroup(
this.subscription._id,
this.otherUserId
)
this.FeaturesUpdater.promises.refreshFeatures
.calledWith(this.otherUserId)
.should.equal(true)
})
})
describe('deleteSubscription', function () {
beforeEach(async function () {
this.subscription = {
_id: ObjectId().toString(),
mock: 'subscription',
admin_id: ObjectId(),
member_ids: [ObjectId(), ObjectId(), ObjectId()],
}
await this.SubscriptionUpdater.promises.deleteSubscription(
this.subscription,
{}
)
})
it('should remove the subscription', function () {
this.SubscriptionModel.deleteOne
.calledWith({ _id: this.subscription._id })
.should.equal(true)
})
it('should downgrade the admin_id', function () {
expect(
this.FeaturesUpdater.promises.scheduleRefreshFeatures
).to.have.been.calledWith(this.subscription.admin_id)
})
it('should downgrade all of the members', function () {
for (const userId of this.subscription.member_ids) {
expect(
this.FeaturesUpdater.promises.scheduleRefreshFeatures
).to.have.been.calledWith(userId)
}
})
})
})