Files
overleaf-cep/services/web/test/unit/src/Subscription/LimitationsManagerTests.js
T
M Fahru 8b89c31eed Merge pull request #25166 from overleaf/kh-cancel-subscription
[web] support canceling Stripe subscription

GitOrigin-RevId: a72ccb20fbef9b6662cdfa1dcffacbd76dcb694c
2025-05-02 08:06:02 +00:00

617 lines
20 KiB
JavaScript

const SandboxedModule = require('sandboxed-module')
const sinon = require('sinon')
const { expect } = require('chai')
const modulePath = require('path').join(
__dirname,
'../../../../app/src/Features/Subscription/LimitationsManager'
)
describe('LimitationsManager', function () {
beforeEach(function () {
this.user = {
_id: (this.userId = 'user-id'),
features: { collaborators: 1 },
}
this.project = {
_id: (this.projectId = 'project-id'),
owner_ref: this.userId,
}
this.ProjectGetter = {
promises: {
getProject: sinon.stub().callsFake(async (projectId, fields) => {
if (projectId === this.projectId) {
return this.project
} else {
return null
}
}),
},
}
this.UserGetter = {
promises: {
getUser: sinon.stub().callsFake(async (userId, filter) => {
if (userId === this.userId) {
return this.user
} else {
return null
}
}),
},
}
this.SubscriptionLocator = {
promises: {
getUsersSubscription: sinon.stub().resolves(),
getSubscription: sinon.stub().resolves(),
getMemberSubscriptions: sinon.stub().resolves(),
},
}
this.CollaboratorsGetter = {
promises: {
getInvitedEditCollaboratorCount: sinon.stub().resolves(0),
getMemberIdPrivilegeLevel: sinon.stub(),
},
}
this.CollaboratorsInviteGetter = {
promises: {
getEditInviteCount: sinon.stub().resolves(0),
},
}
this.LimitationsManager = SandboxedModule.require(modulePath, {
requires: {
'../Project/ProjectGetter': this.ProjectGetter,
'../User/UserGetter': this.UserGetter,
'./SubscriptionLocator': this.SubscriptionLocator,
'@overleaf/settings': (this.Settings = {}),
'../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter,
'../Collaborators/CollaboratorsInviteGetter':
this.CollaboratorsInviteGetter,
'./V1SubscriptionManager': this.V1SubscriptionManager,
},
})
})
describe('allowedNumberOfCollaboratorsInProject', function () {
describe('when the project is owned by a user without a subscription', function () {
beforeEach(function () {
this.Settings.defaultFeatures = { collaborators: 23 }
this.project.owner_ref = this.userId
delete this.user.features
})
it('should return the default number', async function () {
const result =
await this.LimitationsManager.promises.allowedNumberOfCollaboratorsInProject(
this.projectId
)
expect(result).to.equal(this.Settings.defaultFeatures.collaborators)
})
})
describe('when the project is owned by a user with a subscription', function () {
beforeEach(function () {
this.project.owner_ref = this.userId
this.user.features = { collaborators: 21 }
})
it('should return the number of collaborators the user is allowed', async function () {
const result =
await this.LimitationsManager.promises.allowedNumberOfCollaboratorsInProject(
this.projectId
)
expect(result).to.equal(this.user.features.collaborators)
})
})
})
describe('allowedNumberOfCollaboratorsForUser', function () {
describe('when the user has no features', function () {
beforeEach(function () {
this.Settings.defaultFeatures = { collaborators: 23 }
delete this.user.features
})
it('should return the default number', async function () {
const result =
await this.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser(
this.userId
)
expect(result).to.equal(this.Settings.defaultFeatures.collaborators)
})
})
describe('when the user has features', function () {
beforeEach(async function () {
this.user.features = { collaborators: 21 }
this.result =
await this.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser(
this.userId
)
})
it('should return the number of collaborators the user is allowed', function () {
expect(this.result).to.equal(this.user.features.collaborators)
})
})
})
describe('canAcceptEditCollaboratorInvite', function () {
describe('when the project has fewer collaborators than allowed', function () {
beforeEach(function () {
this.current_number = 1
this.user.features.collaborators = 2
this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
sinon.stub().resolves(this.current_number)
})
it('should return true', async function () {
const result =
await this.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
this.projectId
)
expect(result).to.be.true
})
})
describe('when accepting the invite would exceed the collaborator limit', function () {
beforeEach(function () {
this.current_number = 2
this.user.features.collaborators = 2
this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
sinon.stub().resolves(this.current_number)
})
it('should return false', async function () {
const result =
await this.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
this.projectId
)
expect(result).to.be.false
})
})
describe('when the project has more collaborators than allowed', function () {
beforeEach(function () {
this.current_number = 3
this.user.features.collaborators = 2
this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
sinon.stub().resolves(this.current_number)
})
it('should return false', async function () {
const result =
await this.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
this.projectId
)
expect(result).to.be.false
})
})
describe('when the project has infinite collaborators', function () {
beforeEach(function () {
this.current_number = 100
this.user.features.collaborators = -1
this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
sinon.stub().resolves(this.current_number)
})
it('should return true', async function () {
const result =
await this.LimitationsManager.promises.canAcceptEditCollaboratorInvite(
this.projectId
)
expect(result).to.be.true
})
})
})
describe('canAddXEditCollaborators', function () {
describe('when the project has fewer collaborators than allowed', function () {
beforeEach(function () {
this.current_number = 1
this.user.features.collaborators = 2
this.invite_count = 0
this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
sinon.stub().resolves(this.current_number)
this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
.stub()
.resolves(this.invite_count)
})
it('should return true', async function () {
const result =
await this.LimitationsManager.promises.canAddXEditCollaborators(
this.projectId,
1
)
expect(result).to.be.true
})
})
describe('when the project has fewer collaborators and invites than allowed', function () {
beforeEach(function () {
this.current_number = 1
this.user.features.collaborators = 4
this.invite_count = 1
this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
sinon.stub().resolves(this.current_number)
this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
.stub()
.resolves(this.invite_count)
})
it('should return true', async function () {
const result =
await this.LimitationsManager.promises.canAddXEditCollaborators(
this.projectId,
1
)
expect(result).to.be.true
})
})
describe('when the project has fewer collaborators than allowed but I want to add more than allowed', function () {
beforeEach(function () {
this.current_number = 1
this.user.features.collaborators = 2
this.invite_count = 0
this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
sinon.stub().resolves(this.current_number)
this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
.stub()
.resolves(this.invite_count)
})
it('should return false', async function () {
const result =
await this.LimitationsManager.promises.canAddXEditCollaborators(
this.projectId,
2
)
expect(result).to.be.false
})
})
describe('when the project has more collaborators than allowed', function () {
beforeEach(function () {
this.current_number = 3
this.user.features.collaborators = 2
this.invite_count = 0
this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
sinon.stub().resolves(this.current_number)
this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
.stub()
.resolves(this.invite_count)
})
it('should return false', async function () {
const result =
await this.LimitationsManager.promises.canAddXEditCollaborators(
this.projectId,
1
)
expect(result).to.be.false
})
})
describe('when the project has infinite collaborators', function () {
beforeEach(function () {
this.current_number = 100
this.user.features.collaborators = -1
this.invite_count = 0
this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
sinon.stub().resolves(this.current_number)
this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
.stub()
.resolves(this.invite_count)
})
it('should return true', async function () {
const result =
await this.LimitationsManager.promises.canAddXEditCollaborators(
this.projectId,
1
)
expect(result).to.be.true
})
})
describe('when the project has more invites than allowed', function () {
beforeEach(function () {
this.current_number = 0
this.user.features.collaborators = 2
this.invite_count = 2
this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
sinon.stub().resolves(this.current_number)
this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
.stub()
.resolves(this.invite_count)
})
it('should return false', async function () {
const result =
await this.LimitationsManager.promises.canAddXEditCollaborators(
this.projectId,
1
)
expect(result).to.be.false
})
})
describe('when the project has more invites and collaborators than allowed', function () {
beforeEach(function () {
this.current_number = 1
this.user.features.collaborators = 2
this.invite_count = 1
this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount =
sinon.stub().resolves(this.current_number)
this.CollaboratorsInviteGetter.promises.getEditInviteCount = sinon
.stub()
.resolves(this.invite_count)
})
it('should return false', async function () {
const result =
await this.LimitationsManager.promises.canAddXEditCollaborators(
this.projectId,
1
)
expect(result).to.be.false
})
})
})
describe('canChangeCollaboratorPrivilegeLevel', function () {
beforeEach(function () {
this.collaboratorId = 'collaborator-id'
this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel.resolves(
'readOnly'
)
})
describe("when the limit hasn't been reached", function () {
it('accepts changing a viewer to an editor', async function () {
const result =
await this.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel(
this.projectId,
this.collaboratorId,
'readAndWrite'
)
expect(result).to.be.true
})
})
describe('when the limit has been reached', function () {
beforeEach(function () {
this.CollaboratorsGetter.promises.getInvitedEditCollaboratorCount.resolves(
1
)
})
it('accepts changing a reviewer to an editor', async function () {
this.CollaboratorsGetter.promises.getMemberIdPrivilegeLevel.resolves(
'review'
)
const result =
await this.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel(
this.projectId,
this.collaboratorId,
'readAndWrite'
)
expect(result).to.be.true
})
it('rejects changing a viewer to a reviewer', async function () {
const result =
await this.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel(
this.projectId,
this.collaboratorId,
'review'
)
expect(result).to.be.false
})
})
})
describe('userHasSubscription', function () {
beforeEach(function () {
this.SubscriptionLocator.promises.getUsersSubscription = sinon
.stub()
.resolves()
})
it('should return true if the recurly token is set', async function () {
this.SubscriptionLocator.promises.getUsersSubscription = sinon
.stub()
.resolves({
recurlySubscription_id: '1234',
})
const { hasSubscription } =
await this.LimitationsManager.promises.userHasSubscription(this.user)
expect(hasSubscription).to.be.true
})
it('should return true if the paymentProvider field is set', async function () {
this.SubscriptionLocator.promises.getUsersSubscription = sinon
.stub()
.resolves({
paymentProvider: {
subscriptionId: '1234',
},
})
const { hasSubscription } =
await this.LimitationsManager.promises.userHasSubscription(this.user)
expect(hasSubscription).to.be.true
})
it('should return false if the recurly token is not set', async function () {
this.SubscriptionLocator.promises.getUsersSubscription.resolves({})
const { hasSubscription } =
await this.LimitationsManager.promises.userHasSubscription(this.user)
expect(hasSubscription).to.be.false
})
it('should return false if the subscription is undefined', async function () {
this.SubscriptionLocator.promises.getUsersSubscription.resolves()
const { hasSubscription } =
await this.LimitationsManager.promises.userHasSubscription(this.user)
expect(hasSubscription).to.be.false
})
it('should return the subscription', async function () {
const stubbedSubscription = { freeTrial: {}, token: '' }
this.SubscriptionLocator.promises.getUsersSubscription.resolves(
stubbedSubscription
)
const { subscription } =
await this.LimitationsManager.promises.userHasSubscription(this.user)
expect(subscription).to.deep.equal(stubbedSubscription)
})
describe('when user has a custom account', function () {
beforeEach(function () {
this.fakeSubscription = { customAccount: true }
this.SubscriptionLocator.promises.getUsersSubscription.resolves(
this.fakeSubscription
)
})
it('should return true', async function () {
const { hasSubscription } =
await this.LimitationsManager.promises.userHasSubscription(this.user)
expect(hasSubscription).to.be.true
})
it('should return the subscription', async function () {
const { subscription } =
await this.LimitationsManager.promises.userHasSubscription(this.user)
expect(subscription).to.deep.equal(this.fakeSubscription)
})
})
})
describe('userIsMemberOfGroupSubscription', function () {
beforeEach(function () {
this.SubscriptionLocator.promises.getMemberSubscriptions = sinon
.stub()
.resolves()
})
it('should return false if there are no groups subcriptions', async function () {
this.SubscriptionLocator.promises.getMemberSubscriptions.resolves([])
const { isMember } =
await this.LimitationsManager.promises.userIsMemberOfGroupSubscription(
this.user
)
expect(isMember).to.be.false
})
it('should return true if there are no groups subcriptions', async function () {
const subscriptions = ['mock-subscription']
this.SubscriptionLocator.promises.getMemberSubscriptions.resolves(
subscriptions
)
const { isMember, subscriptions: retSubscriptions } =
await this.LimitationsManager.promises.userIsMemberOfGroupSubscription(
this.user
)
expect(isMember).to.be.true
expect(retSubscriptions).to.deep.equal(subscriptions)
})
})
describe('hasPaidSubscription', function () {
beforeEach(function () {
this.SubscriptionLocator.promises.getMemberSubscriptions = sinon
.stub()
.resolves([])
this.SubscriptionLocator.promises.getUsersSubscription = sinon
.stub()
.resolves(null)
})
it('should return true if userIsMemberOfGroupSubscription', async function () {
this.SubscriptionLocator.promises.getMemberSubscriptions = sinon
.stub()
.resolves([{ _id: '123' }])
const { hasPaidSubscription } =
await this.LimitationsManager.promises.hasPaidSubscription(this.user)
expect(hasPaidSubscription).to.be.true
})
it('should return true if userHasSubscription', async function () {
this.SubscriptionLocator.promises.getUsersSubscription = sinon
.stub()
.resolves({ recurlySubscription_id: '123' })
const { hasPaidSubscription } =
await this.LimitationsManager.promises.hasPaidSubscription(this.user)
expect(hasPaidSubscription).to.be.true
})
it('should return false if none are true', async function () {
const { hasPaidSubscription } =
await this.LimitationsManager.promises.hasPaidSubscription(this.user)
expect(hasPaidSubscription).to.be.false
})
it('should have userHasSubscriptionOrIsGroupMember alias', async function () {
const { hasPaidSubscription } =
await this.LimitationsManager.promises.userHasSubscriptionOrIsGroupMember(
this.user
)
expect(hasPaidSubscription).to.be.false
})
})
describe('hasGroupMembersLimitReached', function () {
beforeEach(function () {
this.subscriptionId = '12312'
this.subscription = {
membersLimit: 3,
member_ids: ['', ''],
teamInvites: [
{ email: 'bob@example.com', sentAt: new Date(), token: 'hey' },
],
}
})
it('should return true if the limit is hit (including members and invites)', async function () {
this.SubscriptionLocator.promises.getSubscription.resolves(
this.subscription
)
const { limitReached } =
await this.LimitationsManager.promises.hasGroupMembersLimitReached(
this.subscriptionId
)
expect(limitReached).to.be.true
})
it('should return false if the limit is not hit (including members and invites)', async function () {
this.subscription.membersLimit = 4
this.SubscriptionLocator.promises.getSubscription.resolves(
this.subscription
)
const { limitReached } =
await this.LimitationsManager.promises.hasGroupMembersLimitReached(
this.subscriptionId
)
expect(limitReached).to.be.false
})
it('should return true if the limit has been exceded (including members and invites)', async function () {
this.subscription.membersLimit = 2
this.SubscriptionLocator.promises.getSubscription.resolves(
this.subscription
)
const { limitReached } =
await this.LimitationsManager.promises.hasGroupMembersLimitReached(
this.subscriptionId
)
expect(limitReached).to.be.true
})
})
})