mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-03 22:29:01 +02:00
dece22ba92
Expose remaining marketing properties to customer.io GitOrigin-RevId: 6956e1faf90ecc650108404fe13b2f6de2eb4d0c
868 lines
27 KiB
JavaScript
868 lines
27 KiB
JavaScript
import { vi, expect } from 'vitest'
|
|
import sinon from 'sinon'
|
|
import mongodb from 'mongodb-legacy'
|
|
import { AI_ADD_ON_CODE } from '../../../../app/src/Features/Subscription/AiHelper.mjs'
|
|
|
|
const { ObjectId } = mongodb
|
|
|
|
const MODULE_PATH = '../../../../app/src/Features/Subscription/FeaturesUpdater'
|
|
|
|
describe('FeaturesUpdater', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.renewalDate = new Date('2099-04-01T00:00:00Z')
|
|
ctx.v1UserId = 12345
|
|
ctx.user = {
|
|
_id: new ObjectId(),
|
|
features: {},
|
|
overleaf: { id: ctx.v1UserId },
|
|
}
|
|
ctx.aiAddOn = { addOnCode: AI_ADD_ON_CODE, quantity: 1 }
|
|
ctx.subscriptions = {
|
|
individual: {
|
|
planCode: 'individual-plan',
|
|
groupPlan: false,
|
|
recurlySubscription_id: 'sub-individual',
|
|
recurlyStatus: { state: 'active' },
|
|
},
|
|
group1: { planCode: 'group-plan-1', groupPlan: true },
|
|
group2: { planCode: 'group-plan-2', groupPlan: true },
|
|
noDropbox: { planCode: 'no-dropbox' },
|
|
individualPlusAiAddOn: {
|
|
planCode: 'individual-plan',
|
|
addOns: [ctx.aiAddOn],
|
|
},
|
|
groupPlusAiAddOn: {
|
|
planCode: 'group-plan-1',
|
|
groupPlan: true,
|
|
addOns: [ctx.aiAddOn],
|
|
},
|
|
}
|
|
|
|
ctx.UserFeaturesUpdater = {
|
|
promises: {
|
|
updateFeatures: sinon
|
|
.stub()
|
|
.resolves({ features: { some: 'features' }, featuresChanged: true }),
|
|
},
|
|
}
|
|
|
|
ctx.SubscriptionLocator = {
|
|
promises: {
|
|
getUsersSubscription: sinon.stub(),
|
|
getGroupSubscriptionsMemberOf: sinon.stub(),
|
|
},
|
|
}
|
|
ctx.SubscriptionLocator.promises.getUsersSubscription
|
|
.withArgs(ctx.user._id)
|
|
.resolves(ctx.subscriptions.individual)
|
|
ctx.SubscriptionLocator.promises.getGroupSubscriptionsMemberOf
|
|
.withArgs(ctx.user._id)
|
|
.resolves([ctx.subscriptions.group1, ctx.subscriptions.group2])
|
|
|
|
ctx.Settings = {
|
|
defaultFeatures: { default: 'features' },
|
|
plans: [
|
|
{ planCode: 'individual-plan', features: { individual: 'features' } },
|
|
{ planCode: 'group-plan-1', features: { group1: 'features' } },
|
|
{ planCode: 'group-plan-2', features: { group2: 'features' } },
|
|
{ planCode: 'no-dropbox', features: { dropbox: false } },
|
|
],
|
|
features: {
|
|
all: {
|
|
default: 'features',
|
|
individual: 'features',
|
|
group1: 'features',
|
|
group2: 'features',
|
|
institutions: 'features',
|
|
grandfathered: 'features',
|
|
bonus: 'features',
|
|
},
|
|
},
|
|
writefull: {
|
|
overleafApiUrl: 'https://www.writefull.com',
|
|
quotaTierGranted: 'unlimited',
|
|
},
|
|
aiFeatures: {
|
|
freeQuota: 'free',
|
|
standardQuota: 'standard',
|
|
basicQuota: 'basic',
|
|
unlimitedQuota: 'unlimited',
|
|
},
|
|
quotaGrants: {
|
|
ai: {
|
|
free: 5,
|
|
basic: 5,
|
|
standard: 10,
|
|
unlimited: 200,
|
|
},
|
|
},
|
|
}
|
|
|
|
ctx.ReferalFeatures = {
|
|
promises: {
|
|
getBonusFeatures: sinon.stub().resolves({ bonus: 'features' }),
|
|
},
|
|
}
|
|
ctx.V1SubscriptionManager = {
|
|
getGrandfatheredFeaturesForV1User: sinon.stub(),
|
|
}
|
|
ctx.V1SubscriptionManager.getGrandfatheredFeaturesForV1User
|
|
.withArgs(ctx.v1UserId)
|
|
.returns({ grandfathered: 'features' })
|
|
|
|
ctx.InstitutionsFeatures = {
|
|
promises: {
|
|
getInstitutionsFeatures: sinon
|
|
.stub()
|
|
.resolves({ institutions: 'features' }),
|
|
},
|
|
}
|
|
|
|
ctx.UserGetter = {
|
|
promises: {
|
|
getUser: sinon.stub().resolves(null),
|
|
getWritefullData: sinon.stub().resolves(null),
|
|
},
|
|
}
|
|
ctx.UserGetter.promises.getUser.withArgs(ctx.user._id).resolves(ctx.user)
|
|
ctx.UserGetter.promises.getUser
|
|
.withArgs({ 'overleaf.id': ctx.v1UserId })
|
|
.resolves(ctx.user)
|
|
|
|
ctx.AnalyticsManager = {
|
|
setUserPropertyForUserInBackground: sinon.stub(),
|
|
}
|
|
ctx.Modules = {
|
|
promises: { hooks: { fire: sinon.stub().resolves([]) } },
|
|
}
|
|
ctx.Modules.promises.hooks.fire
|
|
.withArgs('getPaymentFromRecordPromise', ctx.subscriptions.individual)
|
|
.resolves([
|
|
{
|
|
subscription: {
|
|
state: 'active',
|
|
periodEnd: ctx.renewalDate,
|
|
},
|
|
},
|
|
])
|
|
ctx.SubscriptionViewModelBuilder = {
|
|
promises: {
|
|
getUsersSubscriptionDetails: sinon.stub().resolves({
|
|
bestSubscription: { type: 'individual' },
|
|
individualSubscription: ctx.subscriptions.individual,
|
|
memberGroupSubscriptions: [],
|
|
managedGroupSubscriptions: [],
|
|
}),
|
|
},
|
|
}
|
|
ctx.Queues = {
|
|
getQueue: sinon.stub().returns({
|
|
add: sinon.stub().resolves(),
|
|
}),
|
|
}
|
|
|
|
ctx.SplitTestHandler = {
|
|
promises: {
|
|
featureFlagEnabledForUser: sinon.stub().resolves(false),
|
|
},
|
|
}
|
|
|
|
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/Features/Referal/ReferalFeatures', () => ({
|
|
default: ctx.ReferalFeatures,
|
|
}))
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/Subscription/V1SubscriptionManager',
|
|
() => ({
|
|
default: ctx.V1SubscriptionManager,
|
|
})
|
|
)
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/Institutions/InstitutionsFeatures',
|
|
() => ({
|
|
default: ctx.InstitutionsFeatures,
|
|
})
|
|
)
|
|
|
|
vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
|
|
default: ctx.UserGetter,
|
|
}))
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/Analytics/AnalyticsManager',
|
|
() => ({
|
|
default: ctx.AnalyticsManager,
|
|
})
|
|
)
|
|
|
|
vi.doMock('../../../../app/src/infrastructure/Modules', () => ({
|
|
default: ctx.Modules,
|
|
}))
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/Subscription/SubscriptionViewModelBuilder',
|
|
() => ({
|
|
default: ctx.SubscriptionViewModelBuilder,
|
|
})
|
|
)
|
|
|
|
vi.doMock('../../../../app/src/infrastructure/Queues', () => ({
|
|
default: ctx.Queues,
|
|
}))
|
|
|
|
vi.doMock('../../../../app/src/models/Subscription', () => ({}))
|
|
|
|
vi.doMock('@overleaf/fetch-utils', () => ({
|
|
fetchNothing: sinon.stub().resolves(),
|
|
}))
|
|
|
|
vi.doMock(
|
|
'../../../../app/src/Features/SplitTests/SplitTestHandler',
|
|
() => ({
|
|
default: ctx.SplitTestHandler,
|
|
})
|
|
)
|
|
|
|
ctx.FeaturesUpdater = (await import(MODULE_PATH)).default
|
|
})
|
|
|
|
describe('computeFeatures', function () {
|
|
describe('when userFeaturesDisabled is true for individual plan', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.SubscriptionLocator.promises.getUsersSubscription
|
|
.withArgs(ctx.user._id)
|
|
.resolves({
|
|
planCode: 'individual-plan',
|
|
userFeaturesDisabled: true,
|
|
groupPlan: false,
|
|
addOns: [ctx.aiAddOn],
|
|
})
|
|
})
|
|
|
|
it('removes all individual plan features', async function (ctx) {
|
|
const features = await ctx.FeaturesUpdater.promises.computeFeatures(
|
|
ctx.user._id
|
|
)
|
|
expect(features).to.deep.equal({ default: 'features' })
|
|
})
|
|
})
|
|
|
|
describe('when userFeaturesDisabled is true for group plan', function () {
|
|
beforeEach(function (ctx) {
|
|
const groupSubscription = {
|
|
planCode: 'group-plan-1',
|
|
userFeaturesDisabled: true,
|
|
groupPlan: true,
|
|
addOns: [ctx.aiAddOn],
|
|
}
|
|
ctx.SubscriptionLocator.promises.getUsersSubscription
|
|
.withArgs(ctx.user._id)
|
|
.resolves(groupSubscription)
|
|
ctx.SubscriptionLocator.promises.getGroupSubscriptionsMemberOf
|
|
.withArgs(ctx.user._id)
|
|
.resolves([groupSubscription])
|
|
})
|
|
|
|
it('removes all group plan features', async function (ctx) {
|
|
const features = await ctx.FeaturesUpdater.promises.computeFeatures(
|
|
ctx.user._id
|
|
)
|
|
expect(features).to.deep.equal({ default: 'features' })
|
|
})
|
|
})
|
|
beforeEach(function (ctx) {
|
|
ctx.SubscriptionLocator.promises.getUsersSubscription
|
|
.withArgs(ctx.user._id)
|
|
.resolves(null)
|
|
ctx.SubscriptionLocator.promises.getGroupSubscriptionsMemberOf
|
|
.withArgs(ctx.user._id)
|
|
.resolves([])
|
|
ctx.ReferalFeatures.promises.getBonusFeatures.resolves({})
|
|
ctx.V1SubscriptionManager.getGrandfatheredFeaturesForV1User
|
|
.withArgs(ctx.v1UserId)
|
|
.returns({})
|
|
ctx.InstitutionsFeatures.promises.getInstitutionsFeatures.resolves({})
|
|
})
|
|
|
|
describe('individual subscriber', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.SubscriptionLocator.promises.getUsersSubscription
|
|
.withArgs(ctx.user._id)
|
|
.resolves(ctx.subscriptions.individual)
|
|
})
|
|
|
|
it('returns the individual features', async function (ctx) {
|
|
const features = await ctx.FeaturesUpdater.promises.computeFeatures(
|
|
ctx.user._id
|
|
)
|
|
expect(features).to.deep.equal({
|
|
default: 'features',
|
|
individual: 'features',
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('group admin', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.SubscriptionLocator.promises.getUsersSubscription
|
|
.withArgs(ctx.user._id)
|
|
.resolves(ctx.subscriptions.group1)
|
|
})
|
|
|
|
it("doesn't return the group features", async function (ctx) {
|
|
const features = await ctx.FeaturesUpdater.promises.computeFeatures(
|
|
ctx.user._id
|
|
)
|
|
expect(features).to.deep.equal({
|
|
default: 'features',
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('group member', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.SubscriptionLocator.promises.getGroupSubscriptionsMemberOf
|
|
.withArgs(ctx.user._id)
|
|
.resolves([ctx.subscriptions.group1])
|
|
})
|
|
|
|
it('returns the group features', async function (ctx) {
|
|
const features = await ctx.FeaturesUpdater.promises.computeFeatures(
|
|
ctx.user._id
|
|
)
|
|
expect(features).to.deep.equal({
|
|
default: 'features',
|
|
group1: 'features',
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('individual subscription + AI add-on', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.SubscriptionLocator.promises.getUsersSubscription
|
|
.withArgs(ctx.user._id)
|
|
.resolves(ctx.subscriptions.individualPlusAiAddOn)
|
|
})
|
|
|
|
it('returns the individual features and the AI error assistant', async function (ctx) {
|
|
const features = await ctx.FeaturesUpdater.promises.computeFeatures(
|
|
ctx.user._id
|
|
)
|
|
expect(features).to.deep.equal({
|
|
default: 'features',
|
|
individual: 'features',
|
|
aiErrorAssistant: true,
|
|
aiUsageQuota: 'unlimited',
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('group admin + AI add-on', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.SubscriptionLocator.promises.getUsersSubscription
|
|
.withArgs(ctx.user._id)
|
|
.resolves(ctx.subscriptions.groupPlusAiAddOn)
|
|
})
|
|
|
|
it('returns the AI error assistant only', async function (ctx) {
|
|
const features = await ctx.FeaturesUpdater.promises.computeFeatures(
|
|
ctx.user._id
|
|
)
|
|
expect(features).to.deep.equal({
|
|
default: 'features',
|
|
aiErrorAssistant: true,
|
|
aiUsageQuota: 'unlimited',
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('group member + AI add-on', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.SubscriptionLocator.promises.getGroupSubscriptionsMemberOf
|
|
.withArgs(ctx.user._id)
|
|
.resolves([ctx.subscriptions.groupPlusAiAddOn])
|
|
})
|
|
|
|
it('returns the group features without the AI features', async function (ctx) {
|
|
const features = await ctx.FeaturesUpdater.promises.computeFeatures(
|
|
ctx.user._id
|
|
)
|
|
expect(features).to.deep.equal({
|
|
default: 'features',
|
|
group1: 'features',
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('refreshFeatures', function () {
|
|
it('should return features and featuresChanged', async function (ctx) {
|
|
const { features, featuresChanged } =
|
|
await ctx.FeaturesUpdater.promises.refreshFeatures(ctx.user._id, 'test')
|
|
expect(features).to.exist
|
|
expect(featuresChanged).to.exist
|
|
})
|
|
|
|
describe('normally', function () {
|
|
beforeEach(async function (ctx) {
|
|
await ctx.FeaturesUpdater.promises.refreshFeatures(ctx.user._id, 'test')
|
|
})
|
|
|
|
it('should update the user with the merged features', function (ctx) {
|
|
expect(
|
|
ctx.UserFeaturesUpdater.promises.updateFeatures
|
|
).to.have.been.calledWith(ctx.user._id, ctx.Settings.features.all)
|
|
})
|
|
|
|
it('should send the corresponding feature set user property', function (ctx) {
|
|
expect(
|
|
ctx.AnalyticsManager.setUserPropertyForUserInBackground
|
|
).to.have.been.calledWith(ctx.user._id, 'feature-set', 'all')
|
|
})
|
|
|
|
it('should sync subscription properties to customer.io', function (ctx) {
|
|
expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
|
|
'setUserProperties',
|
|
ctx.user._id,
|
|
sinon.match({
|
|
plan_type: 'individual',
|
|
display_plan_type: 'individual',
|
|
ai_plan: 'none',
|
|
next_renewal_date: Math.floor(ctx.renewalDate.getTime() / 1000),
|
|
expiry_date: '',
|
|
group: false,
|
|
commons: false,
|
|
individual_subscription: true,
|
|
payment_provider: 'recurly',
|
|
features: sinon.match.object,
|
|
})
|
|
)
|
|
})
|
|
|
|
it('should not set trial_end_date when no trial is active', function (ctx) {
|
|
const call = ctx.Modules.promises.hooks.fire
|
|
.getCalls()
|
|
.find(c => c.args[0] === 'setUserProperties')
|
|
expect(call).to.exist
|
|
expect(call.args[2]).to.not.have.property('trial_end_date')
|
|
})
|
|
})
|
|
|
|
describe('when the individual subscription is on a trial', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.trialEndsAt = new Date('2099-05-01T00:00:00Z')
|
|
const trialingSubscription = {
|
|
...ctx.subscriptions.individual,
|
|
recurlyStatus: {
|
|
state: 'active',
|
|
trialEndsAt: ctx.trialEndsAt,
|
|
},
|
|
}
|
|
ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves(
|
|
{
|
|
bestSubscription: { type: 'individual' },
|
|
individualSubscription: trialingSubscription,
|
|
memberGroupSubscriptions: [],
|
|
managedGroupSubscriptions: [],
|
|
}
|
|
)
|
|
ctx.Modules.promises.hooks.fire
|
|
.withArgs('getPaymentFromRecordPromise', trialingSubscription)
|
|
.resolves([
|
|
{
|
|
subscription: {
|
|
state: 'active',
|
|
periodEnd: ctx.renewalDate,
|
|
},
|
|
},
|
|
])
|
|
await ctx.FeaturesUpdater.promises.refreshFeatures(ctx.user._id, 'test')
|
|
})
|
|
|
|
it('should sync trial_end_date to customer.io', function (ctx) {
|
|
expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
|
|
'setUserProperties',
|
|
ctx.user._id,
|
|
sinon.match({
|
|
trial_end_date: Math.floor(ctx.trialEndsAt.getTime() / 1000),
|
|
})
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when the individual subscription uses stripe', function () {
|
|
beforeEach(async function (ctx) {
|
|
const stripeSubscription = {
|
|
...ctx.subscriptions.individual,
|
|
paymentProvider: { service: 'stripe-us', state: 'active' },
|
|
}
|
|
ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves(
|
|
{
|
|
bestSubscription: { type: 'individual' },
|
|
individualSubscription: stripeSubscription,
|
|
memberGroupSubscriptions: [],
|
|
managedGroupSubscriptions: [],
|
|
}
|
|
)
|
|
ctx.Modules.promises.hooks.fire
|
|
.withArgs('getPaymentFromRecordPromise', stripeSubscription)
|
|
.resolves([
|
|
{
|
|
subscription: {
|
|
state: 'active',
|
|
periodEnd: ctx.renewalDate,
|
|
},
|
|
},
|
|
])
|
|
await ctx.FeaturesUpdater.promises.refreshFeatures(ctx.user._id, 'test')
|
|
})
|
|
|
|
it('should report stripe as the payment_provider', function (ctx) {
|
|
expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
|
|
'setUserProperties',
|
|
ctx.user._id,
|
|
sinon.match({
|
|
payment_provider: 'stripe',
|
|
individual_subscription: true,
|
|
})
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when the user has a commons institution licence', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves(
|
|
{
|
|
bestSubscription: { type: 'commons' },
|
|
individualSubscription: null,
|
|
memberGroupSubscriptions: [],
|
|
managedGroupSubscriptions: [],
|
|
currentInstitutionsWithLicence: [{ id: 1, name: 'Uni' }],
|
|
}
|
|
)
|
|
await ctx.FeaturesUpdater.promises.refreshFeatures(ctx.user._id, 'test')
|
|
})
|
|
|
|
it('should sync commons=true to customer.io', function (ctx) {
|
|
expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
|
|
'setUserProperties',
|
|
ctx.user._id,
|
|
sinon.match({
|
|
commons: true,
|
|
group: false,
|
|
individual_subscription: false,
|
|
})
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when the user has commons and an individual AI add-on', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves(
|
|
{
|
|
bestSubscription: {
|
|
type: 'individual',
|
|
plan: { planCode: 'individual-plan' },
|
|
},
|
|
individualSubscription: ctx.subscriptions.individual,
|
|
memberGroupSubscriptions: [],
|
|
managedGroupSubscriptions: [],
|
|
currentInstitutionsWithLicence: [{ id: 1, name: 'Uni' }],
|
|
}
|
|
)
|
|
ctx.Modules.promises.hooks.fire
|
|
.withArgs('getPaymentFromRecordPromise', ctx.subscriptions.individual)
|
|
.resolves([
|
|
{
|
|
subscription: {
|
|
state: 'active',
|
|
periodEnd: ctx.renewalDate,
|
|
addOns: [{ code: AI_ADD_ON_CODE }],
|
|
},
|
|
},
|
|
])
|
|
await ctx.FeaturesUpdater.promises.refreshFeatures(ctx.user._id, 'test')
|
|
})
|
|
|
|
it('should set commons, individual_subscription, and ai-assist-add-on together', function (ctx) {
|
|
expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
|
|
'setUserProperties',
|
|
ctx.user._id,
|
|
sinon.match({
|
|
commons: true,
|
|
individual_subscription: true,
|
|
group: false,
|
|
ai_plan: 'ai-assist-add-on',
|
|
})
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when the user has no subscription', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves(
|
|
{
|
|
bestSubscription: null,
|
|
individualSubscription: null,
|
|
memberGroupSubscriptions: [],
|
|
managedGroupSubscriptions: [],
|
|
}
|
|
)
|
|
await ctx.FeaturesUpdater.promises.refreshFeatures(ctx.user._id, 'test')
|
|
})
|
|
|
|
it('should sync false subscription flags and no payment_provider', function (ctx) {
|
|
const call = ctx.Modules.promises.hooks.fire
|
|
.getCalls()
|
|
.find(c => c.args[0] === 'setUserProperties')
|
|
expect(call).to.exist
|
|
expect(call.args[2]).to.include({
|
|
group: false,
|
|
commons: false,
|
|
individual_subscription: false,
|
|
})
|
|
expect(call.args[2]).to.not.have.property('payment_provider')
|
|
expect(call.args[2]).to.not.have.property('trial_end_date')
|
|
})
|
|
})
|
|
|
|
describe('when the individual subscription has a pending cancellation', function () {
|
|
beforeEach(async function (ctx) {
|
|
const pendingCancellationSubscription = {
|
|
...ctx.subscriptions.individual,
|
|
recurlyStatus: { state: 'canceled' },
|
|
}
|
|
ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves(
|
|
{
|
|
bestSubscription: { type: 'individual' },
|
|
individualSubscription: pendingCancellationSubscription,
|
|
memberGroupSubscriptions: [],
|
|
managedGroupSubscriptions: [],
|
|
}
|
|
)
|
|
ctx.Modules.promises.hooks.fire
|
|
.withArgs(
|
|
'getPaymentFromRecordPromise',
|
|
pendingCancellationSubscription
|
|
)
|
|
.resolves([
|
|
{
|
|
subscription: {
|
|
state: 'canceled',
|
|
periodEnd: ctx.renewalDate,
|
|
},
|
|
},
|
|
])
|
|
|
|
await ctx.FeaturesUpdater.promises.refreshFeatures(ctx.user._id, 'test')
|
|
})
|
|
|
|
it('should sync expiry_date and blank next_renewal_date in customer.io', function (ctx) {
|
|
expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
|
|
'setUserProperties',
|
|
ctx.user._id,
|
|
sinon.match({
|
|
plan_type: 'individual',
|
|
display_plan_type: 'individual',
|
|
ai_plan: 'none',
|
|
next_renewal_date: '',
|
|
expiry_date: Math.floor(ctx.renewalDate.getTime() / 1000),
|
|
features: sinon.match.object,
|
|
})
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when the user is in a group subscription', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves(
|
|
{
|
|
bestSubscription: {
|
|
type: 'group',
|
|
plan: {
|
|
planCode: 'group-plan-1',
|
|
groupPlan: true,
|
|
membersLimit: 5,
|
|
},
|
|
subscription: {
|
|
teamName: 'Team Alpha',
|
|
},
|
|
},
|
|
memberGroupSubscriptions: [
|
|
{
|
|
planCode: 'group-plan-1',
|
|
teamName: 'Team Alpha',
|
|
membersLimit: 8,
|
|
},
|
|
],
|
|
managedGroupSubscriptions: [],
|
|
individualSubscription: null,
|
|
}
|
|
)
|
|
await ctx.FeaturesUpdater.promises.refreshFeatures(ctx.user._id, 'test')
|
|
})
|
|
|
|
it('should sync groupSize to customer.io', function (ctx) {
|
|
expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
|
|
'setUserProperties',
|
|
ctx.user._id,
|
|
sinon.match({
|
|
plan_type: 'group-standard',
|
|
display_plan_type: 'Group Standard',
|
|
plan_term_label: 'monthly',
|
|
ai_plan: 'none',
|
|
group_ai_enabled: false,
|
|
group_size: 8,
|
|
next_renewal_date: '',
|
|
expiry_date: '',
|
|
group: true,
|
|
commons: false,
|
|
individual_subscription: false,
|
|
payment_provider: 'recurly',
|
|
features: sinon.match.object,
|
|
overleaf_id: ctx.user._id,
|
|
})
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when the user is in a stripe group subscription', function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves(
|
|
{
|
|
bestSubscription: {
|
|
type: 'group',
|
|
plan: {
|
|
planCode: 'group-plan-1',
|
|
groupPlan: true,
|
|
membersLimit: 5,
|
|
},
|
|
subscription: {
|
|
teamName: 'Team Alpha',
|
|
},
|
|
},
|
|
memberGroupSubscriptions: [
|
|
{
|
|
planCode: 'group-plan-1',
|
|
teamName: 'Team Alpha',
|
|
membersLimit: 8,
|
|
paymentProvider: { service: 'stripe-uk' },
|
|
},
|
|
],
|
|
managedGroupSubscriptions: [],
|
|
individualSubscription: null,
|
|
}
|
|
)
|
|
await ctx.FeaturesUpdater.promises.refreshFeatures(ctx.user._id, 'test')
|
|
})
|
|
|
|
it('should derive payment_provider from the group subscription', function (ctx) {
|
|
expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
|
|
'setUserProperties',
|
|
ctx.user._id,
|
|
sinon.match({
|
|
payment_provider: 'stripe',
|
|
group: true,
|
|
individual_subscription: false,
|
|
})
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('with a non-standard feature set', async function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.SubscriptionLocator.promises.getGroupSubscriptionsMemberOf
|
|
.withArgs(ctx.user._id)
|
|
.resolves(null)
|
|
await ctx.FeaturesUpdater.promises.refreshFeatures(ctx.user._id, 'test')
|
|
})
|
|
|
|
it('should send mixed feature set user property', function (ctx) {
|
|
sinon.assert.calledWith(
|
|
ctx.AnalyticsManager.setUserPropertyForUserInBackground,
|
|
ctx.user._id,
|
|
'feature-set',
|
|
'mixed'
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when losing dropbox feature', async function () {
|
|
beforeEach(async function (ctx) {
|
|
ctx.user.features = { dropbox: true }
|
|
ctx.SubscriptionLocator.promises.getUsersSubscription
|
|
.withArgs(ctx.user._id)
|
|
.resolves(ctx.subscriptions.noDropbox)
|
|
await ctx.FeaturesUpdater.promises.refreshFeatures(ctx.user._id, 'test')
|
|
})
|
|
|
|
it('should fire module hook to unlink dropbox', function (ctx) {
|
|
expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
|
|
'removeDropbox',
|
|
ctx.user._id,
|
|
'test'
|
|
)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('doSyncFromV1', function () {
|
|
describe('when all goes well', function () {
|
|
beforeEach(async function (ctx) {
|
|
await ctx.FeaturesUpdater.promises.doSyncFromV1(ctx.v1UserId)
|
|
})
|
|
|
|
it('should update the user with the merged features', function (ctx) {
|
|
expect(
|
|
ctx.UserFeaturesUpdater.promises.updateFeatures
|
|
).to.have.been.calledWith(ctx.user._id, ctx.Settings.features.all)
|
|
})
|
|
})
|
|
|
|
describe('when getUser produces an error', function () {
|
|
beforeEach(function (ctx) {
|
|
ctx.UserGetter.promises.getUser.rejects(new Error('woops'))
|
|
})
|
|
|
|
it('should propagate the error', async function (ctx) {
|
|
const someId = 9090
|
|
await expect(ctx.FeaturesUpdater.promises.doSyncFromV1(someId)).to.be
|
|
.rejected
|
|
expect(ctx.UserFeaturesUpdater.promises.updateFeatures).not.to.have.been
|
|
.called
|
|
})
|
|
})
|
|
|
|
describe('when getUser does not find a user', function () {
|
|
beforeEach(async function (ctx) {
|
|
const someOtherId = 987
|
|
await ctx.FeaturesUpdater.promises.doSyncFromV1(someOtherId)
|
|
})
|
|
|
|
it('should not update the user', function (ctx) {
|
|
expect(ctx.UserFeaturesUpdater.promises.updateFeatures).not.to.have.been
|
|
.called
|
|
})
|
|
})
|
|
})
|
|
})
|