| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918 |
- 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),
- },
- }
- ctx.GroupPolicy = {
- find: sinon.stub().returns({ exec: sinon.stub().resolves([]) }),
- }
- 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('../../../../app/src/models/GroupPolicy', () => ({
- GroupPolicy: ctx.GroupPolicy,
- }))
- 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: true,
- 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 group subscription has a policy that blocks AI', function () {
- beforeEach(async function (ctx) {
- const policyId = new ObjectId()
- 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,
- groupPolicy: policyId,
- },
- ],
- managedGroupSubscriptions: [],
- individualSubscription: null,
- }
- )
- ctx.GroupPolicy.find.returns({
- exec: sinon
- .stub()
- .resolves([{ _id: policyId, userCannotUseAIFeatures: true }]),
- })
- await ctx.FeaturesUpdater.promises.refreshFeatures(ctx.user._id, 'test')
- })
- it('should set group_ai_enabled to false', function (ctx) {
- expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
- 'setUserProperties',
- ctx.user._id,
- sinon.match({ group_ai_enabled: false })
- )
- })
- })
- 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
- })
- })
- })
- })
|