| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207 |
- const SandboxedModule = require('sandboxed-module')
- const { ObjectId } = require('mongodb-legacy')
- const sinon = require('sinon')
- const { expect } = require('chai')
- const MockRequest = require('../helpers/MockRequest')
- const {
- InvalidEmailError,
- } = require('../../../../app/src/Features/Errors/Errors')
- const modulePath =
- '../../../../app/src/Features/Subscription/SubscriptionGroupHandler'
- describe('SubscriptionGroupHandler', function () {
- beforeEach(function () {
- this.adminUser_id = '12321'
- this.newEmail = 'bob@smith.com'
- this.user_id = '3121321'
- this.email = 'jim@example.com'
- this.user = { _id: this.user_id, email: this.newEmail }
- this.subscription_id = '31DSd1123D'
- this.adding = 1
- this.paymentMethod = { cardType: 'Visa', lastFour: '1111' }
- this.PaymentProviderEntities = {
- MEMBERS_LIMIT_ADD_ON_CODE: 'additional-license',
- }
- this.localPlanInSettings = {
- membersLimit: 5,
- membersLimitAddOn: this.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE,
- }
- this.subscription = {
- admin_id: this.adminUser_id,
- manager_ids: [this.adminUser_id],
- _id: this.subscription_id,
- membersLimit: 100,
- }
- this.changeRequest = {
- timeframe: 'now',
- subscription: {
- id: 'test_id',
- },
- }
- this.termsAndConditionsUpdate = {
- termsAndConditions: 'T&C copy',
- }
- this.poNumberAndTermsAndConditionsUpdate = {
- poNumber: '4444',
- ...this.termsAndConditionsUpdate,
- }
- this.recurlySubscription = {
- id: 123,
- addOns: [
- {
- code: this.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE,
- quantity: 1,
- },
- ],
- getRequestForAddOnUpdate: sinon.stub().returns(this.changeRequest),
- getRequestForGroupPlanUpgrade: sinon.stub().returns(this.changeRequest),
- getRequestForAddOnPurchase: sinon.stub().returns(this.changeRequest),
- getRequestForFlexibleLicensingGroupPlanUpgrade: sinon
- .stub()
- .returns(this.changeRequest),
- getRequestForPoNumberAndTermsAndConditionsUpdate: sinon
- .stub()
- .returns(this.poNumberAndTermsAndConditionsUpdate),
- getRequestForTermsAndConditionsUpdate: sinon
- .stub()
- .returns(this.termsAndConditionsUpdate),
- currency: 'USD',
- hasAddOn(code) {
- return this.addOns.some(addOn => addOn.code === code)
- },
- get isCollectionMethodManual() {
- return false
- },
- }
- this.SubscriptionLocator = {
- promises: {
- getUsersSubscription: sinon.stub().resolves({
- groupPlan: true,
- recurlyStatus: {
- state: 'active',
- },
- }),
- getSubscriptionByMemberIdAndId: sinon.stub(),
- getSubscription: sinon.stub().resolves(this.subscription),
- },
- }
- this.changePreview = {
- currency: 'USD',
- }
- this.SubscriptionController = {
- makeChangePreview: sinon.stub().resolves(this.changePreview),
- getPlanNameForDisplay: sinon.stub().resolves(),
- }
- this.SubscriptionUpdater = {
- promises: {
- removeUserFromGroup: sinon.stub().resolves(),
- getSubscription: sinon.stub().resolves(),
- },
- }
- this.Subscription = {
- updateOne: sinon.stub().returns({ exec: sinon.stub().resolves }),
- updateMany: sinon.stub().returns({ exec: sinon.stub().resolves }),
- findOne: sinon.stub().returns({ exec: sinon.stub().resolves }),
- }
- this.User = {
- find: sinon.stub().returns({ exec: sinon.stub().resolves }),
- }
- this.SessionManager = {
- getLoggedInUserId: sinon.stub().returns(this.user._id),
- }
- this.previewSubscriptionChange = {
- nextAddOns: [
- {
- code: this.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE,
- quantity: this.recurlySubscription.addOns[0].quantity + this.adding,
- },
- ],
- subscription: {
- planName: 'test plan',
- },
- }
- this.applySubscriptionChange = {}
- this.RecurlyClient = {
- promises: {
- getSubscription: sinon.stub().resolves(this.recurlySubscription),
- getPaymentMethod: sinon.stub().resolves(this.paymentMethod),
- previewSubscriptionChange: sinon
- .stub()
- .resolves(this.previewSubscriptionChange),
- applySubscriptionChangeRequest: sinon
- .stub()
- .resolves(this.applySubscriptionChange),
- updateSubscriptionDetails: sinon.stub().resolves(),
- },
- }
- this.PlansLocator = {
- findLocalPlanInSettings: sinon.stub().returns(this.localPlanInSettings),
- }
- this.SubscriptionHandler = {
- promises: {
- syncSubscription: sinon.stub().resolves(),
- },
- }
- this.TeamInvitesHandler = {
- promises: {
- revokeInvite: sinon.stub().resolves(),
- createInvite: sinon.stub().resolves(),
- },
- }
- this.GroupPlansData = {
- enterprise: {
- collaborator: {
- USD: {
- 5: {
- price_in_cents: 10000,
- additional_license_legacy_price_in_cents: 5000,
- },
- },
- },
- },
- educational: {
- collaborator: {
- USD: {
- 5: {
- price_in_cents: 10000,
- additional_license_legacy_price_in_cents: 5000,
- },
- },
- },
- },
- }
- this.Modules = {
- promises: {
- hooks: {
- fire: sinon.stub(),
- },
- },
- }
- this.Modules.promises.hooks.fire
- .withArgs('generateTermsAndConditions')
- .resolves(['T&Cs'])
- .withArgs('getPaymentFromRecord')
- .resolves([
- {
- subscription: this.recurlySubscription,
- account: { hasPastDueInvoice: false },
- },
- ])
- .withArgs('previewSubscriptionChangeRequest')
- .resolves([this.previewSubscriptionChange])
- .withArgs('previewGroupPlanUpgrade')
- .resolves([{ subscriptionChange: this.previewSubscriptionChange }])
- this.Handler = SandboxedModule.require(modulePath, {
- requires: {
- './SubscriptionUpdater': this.SubscriptionUpdater,
- './SubscriptionLocator': this.SubscriptionLocator,
- './SubscriptionController': this.SubscriptionController,
- './SubscriptionHandler': this.SubscriptionHandler,
- './TeamInvitesHandler': this.TeamInvitesHandler,
- '../../models/Subscription': {
- Subscription: this.Subscription,
- },
- '../../models/User': {
- User: this.User,
- },
- './RecurlyClient': this.RecurlyClient,
- './PlansLocator': this.PlansLocator,
- './PaymentProviderEntities': this.PaymentProviderEntities,
- '../Authentication/SessionManager': this.SessionManager,
- './GroupPlansData': this.GroupPlansData,
- '../../infrastructure/Modules': this.Modules,
- },
- })
- })
- describe('removeUserFromGroup', function () {
- it('should call the subscription updater to remove the user', async function () {
- const auditLog = { ipAddress: '0:0:0:0', initiatorId: this.user._id }
- await this.Handler.promises.removeUserFromGroup(
- this.adminUser_id,
- this.user._id,
- auditLog
- )
- this.SubscriptionUpdater.promises.removeUserFromGroup
- .calledWith(this.adminUser_id, this.user._id, auditLog)
- .should.equal(true)
- })
- })
- describe('replaceUserReferencesInGroups', function () {
- beforeEach(async function () {
- this.oldId = 'ba5eba11'
- this.newId = '5ca1ab1e'
- await this.Handler.promises.replaceUserReferencesInGroups(
- this.oldId,
- this.newId
- )
- })
- it('replaces the admin_id', function () {
- this.Subscription.updateOne
- .calledWith({ admin_id: this.oldId }, { admin_id: this.newId })
- .should.equal(true)
- })
- it('replaces the manager_ids', function () {
- this.Subscription.updateMany
- .calledWith(
- { manager_ids: 'ba5eba11' },
- { $addToSet: { manager_ids: '5ca1ab1e' } }
- )
- .should.equal(true)
- this.Subscription.updateMany
- .calledWith(
- { manager_ids: 'ba5eba11' },
- { $pull: { manager_ids: 'ba5eba11' } }
- )
- .should.equal(true)
- })
- it('replaces the member ids', function () {
- this.Subscription.updateMany
- .calledWith(
- { member_ids: this.oldId },
- { $addToSet: { member_ids: this.newId } }
- )
- .should.equal(true)
- this.Subscription.updateMany
- .calledWith(
- { member_ids: this.oldId },
- { $pull: { member_ids: this.oldId } }
- )
- .should.equal(true)
- })
- })
- describe('isUserPartOfGroup', function () {
- beforeEach(function () {
- this.subscription_id = '123ed13123'
- })
- it('should return true when user is part of subscription', async function () {
- this.SubscriptionLocator.promises.getSubscriptionByMemberIdAndId.resolves(
- {
- _id: this.subscription_id,
- }
- )
- const partOfGroup = await this.Handler.promises.isUserPartOfGroup(
- this.user_id,
- this.subscription_id
- )
- partOfGroup.should.equal(true)
- })
- it('should return false when no subscription is found', async function () {
- this.SubscriptionLocator.promises.getSubscriptionByMemberIdAndId.resolves(
- null
- )
- const partOfGroup = await this.Handler.promises.isUserPartOfGroup(
- this.user_id,
- this.subscription_id
- )
- partOfGroup.should.equal(false)
- })
- })
- describe('getTotalConfirmedUsersInGroup', function () {
- describe('for existing subscriptions', function () {
- beforeEach(function () {
- this.subscription.member_ids = ['12321', '3121321']
- })
- it('should call the subscription locator and return 2 users', async function () {
- const count = await this.Handler.promises.getTotalConfirmedUsersInGroup(
- this.subscription_id
- )
- this.SubscriptionLocator.promises.getSubscription
- .calledWith(this.subscription_id)
- .should.equal(true)
- count.should.equal(2)
- })
- })
- describe('for nonexistent subscriptions', function () {
- it('should return undefined', async function () {
- const count =
- await this.Handler.promises.getTotalConfirmedUsersInGroup('fake-id')
- expect(count).not.to.exist
- })
- })
- })
- describe('getUsersGroupSubscriptionDetails', function () {
- beforeEach(function () {
- this.req = new MockRequest()
- this.PlansLocator.findLocalPlanInSettings = sinon.stub().returns({
- ...this.localPlanInSettings,
- canUseFlexibleLicensing: true,
- })
- })
- it('should throw if the subscription is not a group plan', async function () {
- this.SubscriptionLocator.promises.getUsersSubscription = sinon
- .stub()
- .resolves({ groupPlan: false })
- await expect(
- this.Handler.promises.getUsersGroupSubscriptionDetails(
- this.adminUser_id
- )
- ).to.be.rejectedWith('User subscription is not a group plan')
- })
- it('should return users group subscription details', async function () {
- const data = await this.Handler.promises.getUsersGroupSubscriptionDetails(
- this.adminUser_id
- )
- expect(data).to.deep.equal({
- userId: this.adminUser_id,
- subscription: {
- groupPlan: true,
- recurlyStatus: {
- state: 'active',
- },
- },
- plan: {
- membersLimit: 5,
- membersLimitAddOn:
- this.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE,
- canUseFlexibleLicensing: true,
- },
- paymentProviderSubscription: this.recurlySubscription,
- })
- })
- })
- describe('add seats subscription change', function () {
- beforeEach(function () {
- this.req = new MockRequest()
- Object.assign(this.req.body, { adding: this.adding })
- this.PlansLocator.findLocalPlanInSettings = sinon.stub().returns({
- ...this.localPlanInSettings,
- canUseFlexibleLicensing: true,
- })
- })
- describe('has "additional-license" add-on', function () {
- beforeEach(function () {
- this.recurlySubscription.addOns = [
- {
- code: this.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE,
- quantity: 6,
- },
- ]
- this.prevQuantity = this.recurlySubscription.addOns[0].quantity
- this.previewSubscriptionChange.nextAddOns = [
- {
- code: this.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE,
- quantity: this.prevQuantity + this.adding,
- },
- ]
- })
- afterEach(function () {
- sinon.assert.notCalled(
- this.recurlySubscription.getRequestForAddOnPurchase
- )
- this.recurlySubscription.getRequestForAddOnUpdate
- .calledWith(
- this.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE,
- this.recurlySubscription.addOns[0].quantity + this.adding
- )
- .should.equal(true)
- })
- describe('previewAddSeatsSubscriptionChange', function () {
- it('should return the subscription change preview', async function () {
- const preview =
- await this.Handler.promises.previewAddSeatsSubscriptionChange(
- this.adminUser_id,
- this.adding
- )
- this.Modules.promises.hooks.fire
- .calledWith('getPaymentFromRecord', {
- groupPlan: true,
- recurlyStatus: {
- state: 'active',
- },
- })
- .should.equal(true)
- this.Modules.promises.hooks.fire
- .calledWith('previewSubscriptionChangeRequest', this.changeRequest)
- .should.equal(true)
- this.SubscriptionController.makeChangePreview
- .calledWith(
- {
- type: 'add-on-update',
- addOn: {
- code: this.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE,
- quantity:
- this.previewSubscriptionChange.nextAddOns[0].quantity,
- prevQuantity: this.prevQuantity,
- },
- },
- this.previewSubscriptionChange
- )
- .should.equal(true)
- preview.should.equal(this.changePreview)
- })
- })
- describe('createAddSeatsSubscriptionChange', function () {
- it('should change the subscription', async function () {
- this.recurlySubscription = {
- ...this.recurlySubscription,
- get isCollectionMethodManual() {
- return true
- },
- }
- this.Modules.promises.hooks.fire
- .withArgs('getPaymentFromRecord')
- .resolves([
- {
- subscription: this.recurlySubscription,
- account: { hasPastDueInvoice: false },
- },
- ])
- const result =
- await this.Handler.promises.createAddSeatsSubscriptionChange(
- this.adminUser_id,
- this.adding,
- '123'
- )
- this.Modules.promises.hooks.fire
- .calledWith(
- 'updateSubscriptionDetails',
- sinon.match
- .has('poNumber')
- .and(sinon.match.has('termsAndConditions'))
- )
- .should.equal(true)
- this.Modules.promises.hooks.fire
- .calledWith(
- 'applySubscriptionChangeRequestAndSync',
- this.changeRequest
- )
- .should.equal(true)
- expect(result).to.deep.equal({
- adding: this.req.body.adding,
- })
- })
- })
- })
- describe('updateSubscriptionPaymentTerms', function () {
- describe('accounts with PO number', function () {
- it('should update the subscription PO number and T&C', async function () {
- await this.Handler.promises.updateSubscriptionPaymentTerms(
- this.recurlySubscription,
- this.poNumberAndTermsAndConditionsUpdate.poNumber
- )
- this.recurlySubscription.getRequestForPoNumberAndTermsAndConditionsUpdate
- .calledWithMatch(
- this.poNumberAndTermsAndConditionsUpdate.poNumber,
- 'T&Cs'
- )
- .should.equal(true)
- this.Modules.promises.hooks.fire
- .calledWith(
- 'updateSubscriptionDetails',
- this.poNumberAndTermsAndConditionsUpdate
- )
- .should.equal(true)
- })
- })
- describe('accounts with no PO number', function () {
- it('should update the subscription T&C only', async function () {
- await this.Handler.promises.updateSubscriptionPaymentTerms(
- this.recurlySubscription
- )
- this.recurlySubscription.getRequestForTermsAndConditionsUpdate
- .calledWithMatch('T&Cs')
- .should.equal(true)
- this.Modules.promises.hooks.fire
- .calledWith(
- 'updateSubscriptionDetails',
- this.termsAndConditionsUpdate
- )
- .should.equal(true)
- })
- })
- })
- describe('has no "additional-license" add-on', function () {
- beforeEach(function () {
- this.recurlySubscription.addOns = []
- this.prevQuantity = this.recurlySubscription.addOns[0]?.quantity ?? 0
- this.previewSubscriptionChange.nextAddOns = [
- {
- code: this.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE,
- quantity: this.prevQuantity + this.adding,
- },
- ]
- this.PlansLocator.findLocalPlanInSettings = sinon.stub().returns({
- ...this.localPlanInSettings,
- planCode: 'group_collaborator_5_enterprise',
- canUseFlexibleLicensing: true,
- })
- })
- afterEach(function () {
- sinon.assert.notCalled(
- this.recurlySubscription.getRequestForAddOnUpdate
- )
- })
- describe('previewAddSeatsSubscriptionChange', function () {
- let preview
- afterEach(function () {
- this.Modules.promises.hooks.fire
- .calledWith('getPaymentFromRecord', {
- groupPlan: true,
- recurlyStatus: {
- state: 'active',
- },
- })
- .should.equal(true)
- this.Modules.promises.hooks.fire
- .calledWith('previewSubscriptionChangeRequest', this.changeRequest)
- .should.equal(true)
- this.SubscriptionController.makeChangePreview
- .calledWith(
- {
- type: 'add-on-update',
- addOn: {
- code: this.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE,
- quantity:
- this.previewSubscriptionChange.nextAddOns[0].quantity,
- prevQuantity: this.prevQuantity,
- },
- },
- this.previewSubscriptionChange
- )
- .should.equal(true)
- preview.should.equal(this.changePreview)
- })
- it('should return the subscription change preview with legacy add-on price', async function () {
- this.recurlySubscription.planPrice =
- this.GroupPlansData.enterprise.collaborator.USD[5].price_in_cents /
- 100 -
- 1
- preview =
- await this.Handler.promises.previewAddSeatsSubscriptionChange(
- this.adminUser_id,
- this.adding
- )
- this.recurlySubscription.getRequestForAddOnPurchase
- .calledWithExactly(
- this.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE,
- this.adding,
- this.GroupPlansData.enterprise.collaborator.USD[5]
- .additional_license_legacy_price_in_cents / 100
- )
- .should.equal(true)
- })
- it('should return the subscription change preview with non-legacy add-on price', async function () {
- this.recurlySubscription.planPrice =
- this.GroupPlansData.enterprise.collaborator.USD[5].price_in_cents /
- 100
- preview =
- await this.Handler.promises.previewAddSeatsSubscriptionChange(
- this.adminUser_id,
- this.adding
- )
- this.recurlySubscription.getRequestForAddOnPurchase
- .calledWithExactly(
- this.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE,
- this.adding,
- undefined
- )
- .should.equal(true)
- })
- it('should return the subscription change preview with legacy add-on price for small educational group', async function () {
- this.PlansLocator.findLocalPlanInSettings = sinon.stub().returns({
- ...this.localPlanInSettings,
- planCode: 'group_collaborator_5_educational',
- canUseFlexibleLicensing: true,
- })
- this.recurlySubscription.planPrice =
- this.GroupPlansData.enterprise.collaborator.USD[5].price_in_cents /
- 100 +
- 1
- preview =
- await this.Handler.promises.previewAddSeatsSubscriptionChange(
- this.adminUser_id,
- this.adding
- )
- this.recurlySubscription.getRequestForAddOnPurchase
- .calledWithExactly(
- this.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE,
- this.adding,
- this.GroupPlansData.enterprise.collaborator.USD[5]
- .additional_license_legacy_price_in_cents / 100
- )
- .should.equal(true)
- })
- it('should return the subscription change preview with non-legacy add-on price for small educational group', async function () {
- this.PlansLocator.findLocalPlanInSettings = sinon.stub().returns({
- ...this.localPlanInSettings,
- planCode: 'group_collaborator_5_educational',
- canUseFlexibleLicensing: true,
- })
- this.recurlySubscription.planPrice =
- this.GroupPlansData.enterprise.collaborator.USD[5].price_in_cents /
- 100
- preview =
- await this.Handler.promises.previewAddSeatsSubscriptionChange(
- this.adminUser_id,
- this.adding
- )
- this.recurlySubscription.getRequestForAddOnPurchase
- .calledWithExactly(
- this.PaymentProviderEntities.MEMBERS_LIMIT_ADD_ON_CODE,
- this.adding,
- undefined
- )
- .should.equal(true)
- })
- })
- })
- })
- describe('ensureFlexibleLicensingEnabled', function () {
- it('should throw if the subscription can not use flexible licensing', async function () {
- await expect(
- this.Handler.promises.ensureFlexibleLicensingEnabled({
- canUseFlexibleLicensing: false,
- })
- ).to.be.rejectedWith('The group plan does not support flexible licensing')
- })
- it('should not throw if the subscription can use flexible licensing', async function () {
- await expect(
- this.Handler.promises.ensureFlexibleLicensingEnabled({
- canUseFlexibleLicensing: true,
- })
- ).to.not.be.rejected
- })
- })
- describe('ensureSubscriptionIsActive', function () {
- it('should throw if the subscription is not active', async function () {
- await expect(
- this.Handler.promises.ensureSubscriptionIsActive(this.subscription)
- ).to.be.rejectedWith('The subscription is not active')
- })
- it('should not throw if the subscription is active', async function () {
- await expect(
- this.Handler.promises.ensureSubscriptionIsActive({
- recurlyStatus: { state: 'active' },
- })
- ).to.not.be.rejected
- })
- })
- describe('ensureSubscriptionCollectionMethodIsNotManual', function () {
- it('should throw if the subscription is manually collected', async function () {
- await expect(
- this.Handler.promises.ensureSubscriptionCollectionMethodIsNotManual({
- get isCollectionMethodManual() {
- return true
- },
- })
- ).to.be.rejectedWith('This subscription is being collected manually')
- })
- it('should not throw if the subscription is automatically collected', async function () {
- await expect(
- this.Handler.promises.ensureSubscriptionCollectionMethodIsNotManual({
- get isCollectionMethodManual() {
- return false
- },
- })
- ).to.not.be.rejected
- })
- })
- describe('ensureSubscriptionHasNoPendingChanges', function () {
- it('should throw if the subscription has pending change', async function () {
- await expect(
- this.Handler.promises.ensureSubscriptionHasNoPendingChanges({
- pendingChange: {},
- })
- ).to.be.rejectedWith('This subscription has a pending change')
- })
- it('should not throw if the subscription has no pending change', async function () {
- await expect(
- this.Handler.promises.ensureSubscriptionHasNoPendingChanges({})
- ).to.not.be.rejected
- })
- })
- describe('ensureSubscriptionHasNoPastDueInvoice', function () {
- it('should throw if the subscription has past due invoice', async function () {
- this.Modules.promises.hooks.fire
- .withArgs('getPaymentFromRecord')
- .resolves([{ account: { hasPastDueInvoice: true } }])
- await expect(
- this.Handler.promises.ensureSubscriptionHasNoPastDueInvoice(
- this.subscription
- )
- ).to.be.rejectedWith('This subscription has a past due invoice')
- })
- it('should not throw if the subscription has no past due invoice', async function () {
- await expect(
- this.Handler.promises.ensureSubscriptionHasNoPastDueInvoice(
- this.subscription
- )
- ).to.not.be.rejected
- })
- })
- describe('ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual', function () {
- it('should throw if the subscription is manually collected and has no additional license add-on', async function () {
- await expect(
- this.Handler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual(
- {
- isCollectionMethodManual: true,
- hasAddOn: sinon
- .stub()
- .withArgs('additional-license')
- .returns(false),
- }
- )
- ).to.be.rejectedWith(
- 'This subscription is being collected manually has no "additional-license" add-on'
- )
- })
- it('should not throw if the subscription is not manually collected and has no additional license add-on and ', async function () {
- await expect(
- this.Handler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual(
- {
- isCollectionMethodManual: false,
- hasAddOn: sinon
- .stub()
- .withArgs('additional-license')
- .returns(false),
- }
- )
- ).to.not.be.rejected
- })
- it('should not throw if the subscription is not manually collected and has additional license add-on', async function () {
- await expect(
- this.Handler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual(
- {
- isCollectionMethodManual: true,
- hasAddOn: sinon.stub().withArgs('additional-license').returns(true),
- }
- )
- ).to.not.be.rejected
- })
- })
- describe('getGroupPlanUpgradePreview', function () {
- it('should generate preview for subscription upgrade', async function () {
- const result = await this.Handler.promises.getGroupPlanUpgradePreview(
- this.user_id
- )
- result.should.equal(this.changePreview)
- })
- })
- describe('checkBillingInfoExistence', function () {
- it('should invoke the payment method function when collection method is "automatic"', async function () {
- await this.Handler.promises.checkBillingInfoExistence(
- this.recurlySubscription,
- this.adminUser_id
- )
- this.Modules.promises.hooks.fire
- .calledWith('getPaymentMethod', this.adminUser_id)
- .should.equal(true)
- })
- it('shouldn’t invoke the payment method function when collection method is "manual"', async function () {
- const recurlySubscription = {
- ...this.recurlySubscription,
- get isCollectionMethodManual() {
- return true
- },
- }
- await this.Handler.promises.checkBillingInfoExistence(
- recurlySubscription,
- this.adminUser_id
- )
- this.RecurlyClient.promises.getPaymentMethod.should.not.have.been.called
- })
- })
- describe('updateGroupMembersBulk', function () {
- const inviterId = new ObjectId()
- let members
- let emailList
- let callUpdateGroupMembersBulk
- beforeEach(function () {
- members = [
- {
- _id: new ObjectId(),
- email: 'user1@example.com',
- emails: [{ email: 'user1@example.com' }],
- },
- {
- _id: new ObjectId(),
- email: 'user2-alias@example.com',
- emails: [
- {
- email: 'user2-alias@example.com',
- },
- {
- email: 'user2@example.com',
- },
- ],
- },
- {
- _id: new ObjectId(),
- email: 'user3@example.com',
- emails: [{ email: 'user3@example.com' }],
- },
- ]
- emailList = [
- 'user1@example.com',
- 'user2@example.com',
- 'new-user@example.com', // primary email of existing user
- 'new-user-2@example.com', // secondary email of existing user
- ]
- callUpdateGroupMembersBulk = async (options = {}) => {
- this.Subscription.findOne = sinon
- .stub()
- .returns({ exec: sinon.stub().resolves(this.subscription) })
- this.User.find = sinon
- .stub()
- .returns({ exec: sinon.stub().resolves(members) })
- return await this.Handler.promises.updateGroupMembersBulk(
- inviterId,
- this.subscription._id,
- emailList,
- options
- )
- }
- })
- it('throws an error when any of the emails is invalid', async function () {
- emailList.push('invalid@email')
- await expect(
- callUpdateGroupMembersBulk({ commit: true })
- ).to.be.rejectedWith(InvalidEmailError)
- })
- describe('with commit = false', function () {
- describe('with removeMembersNotIncluded = false', function () {
- it('should preview zero users to delete, and should not send invites', async function () {
- const result = await callUpdateGroupMembersBulk()
- expect(result).to.deep.equal({
- emailsToSendInvite: [
- 'new-user@example.com',
- 'new-user-2@example.com',
- ],
- emailsToRevokeInvite: [],
- membersToRemove: [],
- currentMemberCount: 3,
- newTotalCount: 5,
- membersLimit: this.subscription.membersLimit,
- })
- expect(this.TeamInvitesHandler.promises.createInvite).not.to.have.been
- .called
- expect(this.SubscriptionUpdater.promises.removeUserFromGroup).not.to
- .have.been.called
- })
- })
- describe('with removeMembersNotIncluded = true', function () {
- it('should preview the users to be deleted, and should not send invites', async function () {
- const result = await callUpdateGroupMembersBulk({
- removeMembersNotIncluded: true,
- })
- expect(result).to.deep.equal({
- emailsToSendInvite: [
- 'new-user@example.com',
- 'new-user-2@example.com',
- ],
- emailsToRevokeInvite: [],
- membersToRemove: [members[2]._id],
- currentMemberCount: 3,
- newTotalCount: 4,
- membersLimit: this.subscription.membersLimit,
- })
- expect(this.TeamInvitesHandler.promises.createInvite).not.to.have.been
- .called
- expect(this.SubscriptionUpdater.promises.removeUserFromGroup).not.to
- .have.been.called
- })
- it('should preview but not revoke invites to emails that are no longer invited', async function () {
- this.subscription.teamInvites = [
- { email: 'new-user@example.com' },
- { email: 'no-longer-invited@example.com' },
- ]
- const result = await callUpdateGroupMembersBulk({
- removeMembersNotIncluded: true,
- })
- expect(result.emailsToRevokeInvite).to.deep.equal([
- 'no-longer-invited@example.com',
- ])
- expect(this.TeamInvitesHandler.promises.revokeInvite).not.to.have.been
- .called
- })
- })
- it('does not throw an error when the member limit is reached', async function () {
- this.subscription.membersLimit = 3
- const result = await callUpdateGroupMembersBulk()
- expect(result.membersLimit).to.equal(3)
- expect(result.newTotalCount).to.equal(5)
- })
- })
- describe('with commit = true', function () {
- describe('with removeMembersNotIncluded = false', function () {
- it('should preview zero users to delete, and should send invites', async function () {
- const result = await callUpdateGroupMembersBulk({ commit: true })
- expect(result).to.deep.equal({
- emailsToSendInvite: [
- 'new-user@example.com',
- 'new-user-2@example.com',
- ],
- emailsToRevokeInvite: [],
- membersToRemove: [],
- currentMemberCount: 3,
- newTotalCount: 5,
- membersLimit: this.subscription.membersLimit,
- })
- expect(this.SubscriptionUpdater.promises.removeUserFromGroup).not.to
- .have.been.called
- expect(
- this.TeamInvitesHandler.promises.createInvite.callCount
- ).to.equal(2)
- expect(
- this.TeamInvitesHandler.promises.createInvite
- ).to.have.been.calledWith(
- inviterId,
- this.subscription,
- 'new-user@example.com'
- )
- expect(
- this.TeamInvitesHandler.promises.createInvite
- ).to.have.been.calledWith(
- inviterId,
- this.subscription,
- 'new-user-2@example.com'
- )
- })
- it('should not send invites to emails already invited', async function () {
- this.subscription.teamInvites = [{ email: 'new-user@example.com' }]
- const result = await callUpdateGroupMembersBulk({ commit: true })
- expect(result.emailsToSendInvite).to.deep.equal([
- 'new-user-2@example.com',
- ])
- expect(
- this.TeamInvitesHandler.promises.createInvite.callCount
- ).to.equal(1)
- expect(
- this.TeamInvitesHandler.promises.createInvite
- ).to.have.been.calledWith(
- inviterId,
- this.subscription,
- 'new-user-2@example.com'
- )
- })
- it('should preview and not revoke invites to emails that are no longer invited', async function () {
- this.subscription.teamInvites = [
- { email: 'new-user@example.com' },
- { email: 'no-longer-invited@example.com' },
- ]
- const result = await callUpdateGroupMembersBulk({
- commit: true,
- })
- expect(result.emailsToRevokeInvite).to.deep.equal([])
- expect(this.TeamInvitesHandler.promises.revokeInvite).not.to.have.been
- .called
- })
- })
- describe('with removeMembersNotIncluded = true', function () {
- it('should remove users from group, and should send invites', async function () {
- const result = await callUpdateGroupMembersBulk({
- commit: true,
- removeMembersNotIncluded: true,
- })
- expect(result).to.deep.equal({
- emailsToSendInvite: [
- 'new-user@example.com',
- 'new-user-2@example.com',
- ],
- emailsToRevokeInvite: [],
- membersToRemove: [members[2]._id],
- currentMemberCount: 3,
- newTotalCount: 4,
- membersLimit: this.subscription.membersLimit,
- })
- expect(
- this.SubscriptionUpdater.promises.removeUserFromGroup.callCount
- ).to.equal(1)
- expect(
- this.SubscriptionUpdater.promises.removeUserFromGroup
- ).to.have.been.calledWith(this.subscription._id, members[2]._id, {
- initiatorId: inviterId,
- })
- expect(
- this.TeamInvitesHandler.promises.createInvite.callCount
- ).to.equal(2)
- expect(
- this.TeamInvitesHandler.promises.createInvite
- ).to.have.been.calledWith(
- inviterId,
- this.subscription,
- 'new-user@example.com'
- )
- expect(
- this.TeamInvitesHandler.promises.createInvite
- ).to.have.been.calledWith(
- inviterId,
- this.subscription,
- 'new-user-2@example.com'
- )
- })
- it('should send invites and revoke invites to emails no longer invited', async function () {
- this.subscription.teamInvites = [
- { email: 'new-user@example.com' },
- { email: 'no-longer-invited@example.com' },
- ]
- const result = await callUpdateGroupMembersBulk({
- commit: true,
- removeMembersNotIncluded: true,
- })
- expect(result.emailsToSendInvite).to.deep.equal([
- 'new-user-2@example.com',
- ])
- expect(result.emailsToRevokeInvite).to.deep.equal([
- 'no-longer-invited@example.com',
- ])
- expect(
- this.TeamInvitesHandler.promises.createInvite.callCount
- ).to.equal(1)
- expect(
- this.TeamInvitesHandler.promises.createInvite
- ).to.have.been.calledWith(
- inviterId,
- this.subscription,
- 'new-user-2@example.com'
- )
- expect(
- this.TeamInvitesHandler.promises.revokeInvite.callCount
- ).to.equal(1)
- expect(
- this.TeamInvitesHandler.promises.revokeInvite
- ).to.have.been.calledWith(
- inviterId,
- this.subscription,
- 'no-longer-invited@example.com'
- )
- })
- })
- it('throws an error when the member limit is reached', async function () {
- this.subscription.membersLimit = 3
- await expect(
- callUpdateGroupMembersBulk({ commit: true })
- ).to.be.rejectedWith('limit reached')
- })
- })
- })
- })
|