From 1be593f612ca99d331ccafeabd355e880a029de6 Mon Sep 17 00:00:00 2001 From: ilkin-overleaf <100852799+ilkin-overleaf@users.noreply.github.com> Date: Tue, 17 Dec 2024 11:22:35 +0200 Subject: [PATCH] Merge pull request #22448 from overleaf/ii-flexible-group-licensing-add-seats-tests [web] Add seats tests GitOrigin-RevId: 76fb5edc6eba5579fac2d3e05cd1f64fba16046c --- .../SubscriptionGroupController.mjs | 4 +- .../components/add-seats/add-seats.tsx | 3 +- .../components/add-seats/cost-summary.tsx | 26 +- .../components/add-seats.spec.tsx | 351 ++++++++++++++++++ .../components/request-status.spec.tsx | 2 +- .../SubscriptionGroupControllerTests.mjs | 215 ++++++++++- .../SubscriptionGroupHandlerTests.js | 185 ++++++++- 7 files changed, 769 insertions(+), 17 deletions(-) create mode 100644 services/web/test/frontend/features/group-management/components/add-seats.spec.tsx diff --git a/services/web/app/src/Features/Subscription/SubscriptionGroupController.mjs b/services/web/app/src/Features/Subscription/SubscriptionGroupController.mjs index ded144d24c..505010ba98 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionGroupController.mjs +++ b/services/web/app/src/Features/Subscription/SubscriptionGroupController.mjs @@ -171,12 +171,12 @@ async function previewAddSeatsSubscriptionChange(req, res) { */ async function createAddSeatsSubscriptionChange(req, res) { try { - const preview = + const create = await SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange( req ) - res.json(preview) + res.json(create) } catch (error) { logger.err( { error }, diff --git a/services/web/frontend/js/features/group-management/components/add-seats/add-seats.tsx b/services/web/frontend/js/features/group-management/components/add-seats/add-seats.tsx index 7f6742a0b3..c27b638c87 100644 --- a/services/web/frontend/js/features/group-management/components/add-seats/add-seats.tsx +++ b/services/web/frontend/js/features/group-management/components/add-seats/add-seats.tsx @@ -29,7 +29,7 @@ import { } from '../../../../../../types/subscription/subscription-change-preview' import { MergeAndOverride } from '../../../../../../types/utils' -const MAX_NUMBER_OF_USERS = 50 +export const MAX_NUMBER_OF_USERS = 50 function AddSeats() { const { t } = useTranslation() @@ -244,6 +244,7 @@ function AddSeats() { className="d-grid gap-4" onSubmit={handleSubmit} ref={formRef} + data-testid="add-more-users-group-form" >

diff --git a/services/web/frontend/js/features/group-management/components/add-seats/cost-summary.tsx b/services/web/frontend/js/features/group-management/components/add-seats/cost-summary.tsx index dc19f165c8..4f8247caa0 100644 --- a/services/web/frontend/js/features/group-management/components/add-seats/cost-summary.tsx +++ b/services/web/frontend/js/features/group-management/components/add-seats/cost-summary.tsx @@ -20,7 +20,10 @@ function CostSummary({ subscriptionChange, totalLicenses }: CostSummaryProps) { const { t } = useTranslation() return ( - +
{t('cost_summary')}
@@ -53,35 +56,44 @@ function CostSummary({ subscriptionChange, totalLicenses }: CostSummaryProps) { <>
- + {subscriptionChange.nextInvoice.plan.name} x{' '} {subscriptionChange.change.addOn.quantity - subscriptionChange.change.addOn.prevQuantity}{' '} {t('seats')} - + {formatCurrencyLocalized( subscriptionChange.immediateCharge.subtotal, subscriptionChange.currency )} - + {t('sales_tax')} ·{' '} {subscriptionChange.nextInvoice.tax.rate * 100}% - + {formatCurrencyLocalized( subscriptionChange.immediateCharge.tax, subscriptionChange.currency )} - + {t('total_due_today')} - + {formatCurrencyLocalized( subscriptionChange.immediateCharge.total, subscriptionChange.currency diff --git a/services/web/test/frontend/features/group-management/components/add-seats.spec.tsx b/services/web/test/frontend/features/group-management/components/add-seats.spec.tsx new file mode 100644 index 0000000000..9aaf3e001e --- /dev/null +++ b/services/web/test/frontend/features/group-management/components/add-seats.spec.tsx @@ -0,0 +1,351 @@ +import '../../../helpers/bootstrap-5' +import AddSeats, { + MAX_NUMBER_OF_USERS, +} from '@/features/group-management/components/add-seats/add-seats' +import { SplitTestProvider } from '@/shared/context/split-test-context' + +describe('', function () { + beforeEach(function () { + this.totalLicenses = 5 + + cy.window().then(win => { + win.metaAttributesCache.set('ol-groupName', 'My Awesome Team') + win.metaAttributesCache.set('ol-subscriptionId', '123') + win.metaAttributesCache.set( + 'ol-subscriptionEndsAt', + '2025-01-01T12:00:00.000Z' + ) + win.metaAttributesCache.set('ol-totalLicenses', this.totalLicenses) + }) + + cy.mount( + + + + ) + + cy.findByRole('button', { name: /add users/i }) + cy.findByTestId('add-more-users-group-form') + }) + + it('renders the back button', function () { + cy.findByTestId('group-heading').within(() => { + cy.findByRole('button', { name: /back to subscription/i }).should( + 'have.attr', + 'href', + '/user/subscription' + ) + }) + }) + + it('shows the group name', function () { + cy.findByTestId('group-heading').within(() => { + cy.findByRole('heading', { name: 'My Awesome Team' }) + }) + }) + + it('shows the "Add more users" label', function () { + cy.findByText(/add more users/i) + }) + + it('shows the maximum supported users', function () { + cy.findByText( + new RegExp( + `your current plan supports up to ${this.totalLicenses} users`, + 'i' + ) + ) + }) + + it('shows instructions on how to reduce users on a plan', function () { + cy.contains( + /if you want to reduce the number of users on your plan, please contact customer support/i + ).within(() => { + cy.findByRole('link', { name: /contact customer support/i }).should( + 'have.attr', + 'href', + '/contact' + ) + }) + }) + + it('shows the "Upgrade my plan" link', function () { + cy.findByRole('link', { name: /upgrade my plan/i }).should( + 'have.attr', + 'href', + '/user/subscription/group/upgrade-subscription' + ) + }) + + it('renders the cancel button', function () { + cy.findByRole('button', { name: /cancel/i }).should( + 'have.attr', + 'href', + '/user/subscription' + ) + }) + + describe('cost summary', function () { + beforeEach(function () { + cy.findByLabelText(/how many users do you want to add/i).as('input') + }) + + it('shows the title', function () { + cy.findByTestId('cost-summary').within(() => { + cy.findByText(/cost summary/i) + }) + }) + + describe('shows default content when', function () { + afterEach(function () { + cy.findByTestId('cost-summary').within(() => { + cy.findByText( + /enter the number of users you’d like to add to see the cost breakdown/i + ) + }) + }) + + it('leaves input empty', function () { + cy.get('@input').should('have.value', '') + }) + + it('fills in a non-numeric value', function () { + cy.get('@input').type('ab') + cy.findByText(/value must be a number/i) + }) + + it('fills in a decimal value', function () { + cy.get('@input').type('1.5') + cy.findByText(/value must be a whole number/i) + }) + + it('fills in a "0" value', function () { + cy.get('@input').type('0') + cy.findByText(/value must be at least 1/i) + }) + + it('fills in a value and clears the input', function () { + cy.get('@input').type('a{backspace}') + cy.get('@input').should('have.text', '') + cy.findByText(/this field is required/i) + }) + }) + + describe('entered more than the maximum allowed number of users', function () { + beforeEach(function () { + this.numberOfUsersExceedingMaxLimit = MAX_NUMBER_OF_USERS + 1 + + cy.get('@input').type(this.numberOfUsersExceedingMaxLimit.toString()) + cy.findByRole('button', { name: /add users/i }).should('not.exist') + cy.findByRole('button', { name: /send request/i }).as('sendRequestBtn') + }) + + it('renders a notification', function () { + cy.findByTestId('cost-summary').should('not.exist') + cy.findByRole('alert').should( + 'contain.text', + `If you want more than ${MAX_NUMBER_OF_USERS} users on your plan, we need to add them for you. Just click Send request below and we’ll be happy to help.` + ) + }) + + describe('request', function () { + afterEach(function () { + cy.findByRole('button', { name: /go to subscriptions/i }).should( + 'have.attr', + 'href', + '/user/subscription' + ) + }) + + function makeRequest(statusCode: number, adding: string) { + cy.intercept( + 'POST', + '/user/subscription/group/add-users/sales-contact-form', + { + statusCode, + } + ).as('addUsersRequest') + cy.get('@sendRequestBtn').click() + cy.get('@addUsersRequest').its('request.body').should('deep.equal', { + adding, + }) + cy.findByTestId('add-more-users-group-form').should('not.exist') + } + + it('sends a request that succeeds', function () { + makeRequest(204, this.numberOfUsersExceedingMaxLimit.toString()) + cy.findByTestId('title').should( + 'contain.text', + 'We’ve got your request' + ) + cy.findByText(/our team will get back to you shortly/i) + }) + + it('sends a request that fails', function () { + makeRequest(400, this.numberOfUsersExceedingMaxLimit.toString()) + cy.findByTestId('title').should( + 'contain.text', + 'Something went wrong' + ) + cy.contains( + /it looks like that didn’t work. You can try again or get in touch with our Support team for more help/i + ).within(() => { + cy.findByRole('link', { name: /get in touch/i }).should( + 'have.attr', + 'href', + '/contact' + ) + }) + }) + }) + }) + + describe('entered less than the maximum allowed number of users', function () { + beforeEach(function () { + this.adding = 1 + + cy.findByRole('button', { name: /add users/i }).as('addUsersBtn') + cy.findByRole('button', { name: /send request/i }).should('not.exist') + }) + + it('renders the preview data', function () { + this.body = { + change: { + type: 'add-on-update', + addOn: { + code: 'additional-license', + quantity: this.totalLicenses + this.adding, + prevQuantity: this.totalLicenses, + }, + }, + currency: 'USD', + immediateCharge: { + subtotal: 100, + tax: 20, + total: 120, + }, + nextInvoice: { + date: '2025-12-01T00:00:00.000Z', + plan: { + name: 'Overleaf Standard Group', + amount: 0, + }, + subtotal: 895, + tax: { + rate: 0.2, + }, + total: 1000, + }, + } + + cy.intercept('POST', '/user/subscription/group/add-users/preview', { + statusCode: 200, + body: this.body, + }).as('addUsersRequest') + cy.get('@input').type(this.adding.toString()) + + cy.findByTestId('cost-summary').within(() => { + cy.contains( + new RegExp( + `you’re adding ${this.adding} users to your plan giving you a total of ${this.body.change.addOn.quantity} users`, + 'i' + ) + ) + + cy.findByTestId('plan').within(() => { + cy.findByText( + `${this.body.nextInvoice.plan.name} x ${this.adding} Seats` + ) + cy.findByTestId('price').should( + 'have.text', + `$${this.body.immediateCharge.subtotal}.00` + ) + }) + + cy.findByTestId('tax').within(() => { + cy.findByText( + new RegExp( + `sales tax · ${this.body.nextInvoice.tax.rate * 100}%`, + 'i' + ) + ) + cy.findByTestId('price').should( + 'have.text', + `$${this.body.immediateCharge.tax}.00` + ) + }) + + cy.findByTestId('total').within(() => { + cy.findByText(/total due today/i) + cy.findByTestId('price').should( + 'have.text', + `$${this.body.immediateCharge.total}.00` + ) + }) + + cy.findByText( + /we’ll charge you now for the cost of your additional users based on the remaining months of your current subscription/i + ) + cy.findByText( + /after that, we’ll bill you \$1,000.00 \+ applicable taxes annually on December 1, unless you cancel/i + ) + }) + }) + + describe('request', function () { + afterEach(function () { + cy.findByRole('button', { name: /go to subscriptions/i }).should( + 'have.attr', + 'href', + '/user/subscription' + ) + }) + + function makeRequest(statusCode: number, adding: string) { + cy.intercept('POST', '/user/subscription/group/add-users/create', { + statusCode, + }).as('addUsersRequest') + cy.get('@input').type(adding) + cy.get('@addUsersBtn').click() + cy.get('@addUsersRequest') + .its('request.body') + .should('deep.equal', { + adding: Number(adding), + }) + cy.findByTestId('add-more-users-group-form').should('not.exist') + } + + it('sends a request that succeeds', function () { + makeRequest(204, this.adding.toString()) + cy.findByTestId('title').should( + 'contain.text', + 'You’ve added more users' + ) + cy.findByText(/you’ve added more users to your subscription/i) + cy.findByRole('link', { name: /invite people/i }).should( + 'have.attr', + 'href', + '/manage/groups/123/members' + ) + }) + + it('sends a request that fails', function () { + makeRequest(400, this.adding.toString()) + cy.findByTestId('title').should( + 'contain.text', + 'Something went wrong' + ) + cy.contains( + /it looks like that didn’t work. You can try again or get in touch with our Support team for more help/i + ).within(() => { + cy.findByRole('link', { name: /get in touch/i }).should( + 'have.attr', + 'href', + '/contact' + ) + }) + }) + }) + }) + }) +}) diff --git a/services/web/test/frontend/features/group-management/components/request-status.spec.tsx b/services/web/test/frontend/features/group-management/components/request-status.spec.tsx index ce3d01d178..e08c9cd131 100644 --- a/services/web/test/frontend/features/group-management/components/request-status.spec.tsx +++ b/services/web/test/frontend/features/group-management/components/request-status.spec.tsx @@ -1,7 +1,7 @@ import '../../../helpers/bootstrap-5' import RequestStatus from '@/features/group-management/components/request-status' -describe('request confirmation page', function () { +describe('', function () { beforeEach(function () { cy.window().then(win => { win.metaAttributesCache.set('ol-groupName', 'My Awesome Team') diff --git a/services/web/test/unit/src/Subscription/SubscriptionGroupControllerTests.mjs b/services/web/test/unit/src/Subscription/SubscriptionGroupControllerTests.mjs index 0abf3596e4..11771f3224 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionGroupControllerTests.mjs +++ b/services/web/test/unit/src/Subscription/SubscriptionGroupControllerTests.mjs @@ -27,11 +27,35 @@ describe('SubscriptionGroupController', function () { _id: this.subscriptionId, teamName: 'Cool group', groupPlan: true, + membersLimit: 5, } + this.plan = { + canUseFlexibleLicensing: true, + } + + this.previewSubscriptionChangeData = { + change: {}, + currency: 'USD', + } + + this.createSubscriptionChangeData = { adding: 1 } + this.SubscriptionGroupHandler = { promises: { removeUserFromGroup: sinon.stub().resolves(), + getUsersGroupSubscriptionDetails: sinon.stub().resolves({ + subscription: this.subscription, + plan: this.plan, + recurlySubscription: {}, + }), + previewAddSeatsSubscriptionChange: sinon + .stub() + .resolves(this.previewSubscriptionChangeData), + createAddSeatsSubscriptionChange: sinon + .stub() + .resolves(this.createSubscriptionChangeData), + ensureFlexibleLicensingEnabled: sinon.stub().resolves(), }, } @@ -66,14 +90,13 @@ describe('SubscriptionGroupController', function () { this.SplitTestHandler = { promises: { - getAssignment: sinon.stub().resolves({ variant: 'default' }), + getAssignment: sinon.stub().resolves(), }, - getAssignment: sinon.stub().yields(null, { variant: 'default' }), } this.UserGetter = { promises: { - getUserEmail: sinon.stub().resolves(this.user), + getUserEmail: sinon.stub().resolves(this.user.email), }, } @@ -105,6 +128,13 @@ describe('SubscriptionGroupController', function () { '../../../../app/src/Features/Subscription/RecurlyClient': this.RecurlyClient, '../../../../app/src/models/Subscription': this.SubscriptionModel, + '@overleaf/logger': { + err: sinon.stub(), + error: sinon.stub(), + warn: sinon.stub(), + log: sinon.stub(), + debug: sinon.stub(), + }, }) }) @@ -287,4 +317,183 @@ describe('SubscriptionGroupController', function () { this.Controller.removeSelfFromGroup(this.req, res, done) }) }) + + describe('addSeatsToGroupSubscription', function () { + it('should render the "add seats" page', function (done) { + const res = { + render: (page, props) => { + this.SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails + .calledWith(this.req) + .should.equal(true) + this.SubscriptionGroupHandler.promises.ensureFlexibleLicensingEnabled + .calledWith(this.plan) + .should.equal(true) + page.should.equal('subscriptions/add-seats') + props.subscriptionId.should.equal(this.subscriptionId) + props.groupName.should.equal(this.subscription.teamName) + props.totalLicenses.should.equal(this.subscription.membersLimit) + done() + }, + } + + this.Controller.addSeatsToGroupSubscription(this.req, res) + }) + + it('should redirect to subscription page when getting subscription details fails', function (done) { + this.SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails = + sinon.stub().rejects() + + const res = { + redirect: url => { + url.should.equal('/user/subscription') + done() + }, + } + + this.Controller.addSeatsToGroupSubscription(this.req, res) + }) + + it('should redirect to subscription page when flexible licensing is not enabled', function (done) { + this.SubscriptionGroupHandler.promises.ensureFlexibleLicensingEnabled = + sinon.stub().rejects() + + const res = { + redirect: url => { + url.should.equal('/user/subscription') + done() + }, + } + + this.Controller.addSeatsToGroupSubscription(this.req, res) + }) + }) + + describe('previewAddSeatsSubscriptionChange', function () { + it('should preview "add seats" change', function (done) { + const res = { + json: data => { + this.SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange + .calledWith(this.req) + .should.equal(true) + data.should.deep.equal(this.previewSubscriptionChangeData) + done() + }, + } + + this.Controller.previewAddSeatsSubscriptionChange(this.req, res) + }) + + it('should fail previewing "add seats" change', function (done) { + this.SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange = + sinon.stub().rejects() + + const res = { + status: statusCode => { + statusCode.should.equal(400) + + return { + end: () => { + done() + }, + } + }, + } + + this.Controller.previewAddSeatsSubscriptionChange(this.req, res) + }) + }) + + describe('createAddSeatsSubscriptionChange', function () { + it('should apply "add seats" change', function (done) { + const res = { + json: data => { + this.SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange + .calledWith(this.req) + .should.equal(true) + data.should.deep.equal(this.createSubscriptionChangeData) + done() + }, + } + + this.Controller.createAddSeatsSubscriptionChange(this.req, res) + }) + + it('should fail applying "add seats" change', function (done) { + this.SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange = + sinon.stub().rejects() + + const res = { + status: statusCode => { + statusCode.should.equal(400) + + return { + end: () => { + done() + }, + } + }, + } + + this.Controller.createAddSeatsSubscriptionChange(this.req, res) + }) + }) + + describe('submitForm', function () { + it('should build and pass the request body to the sales submit handler', function (done) { + const adding = 100 + this.req.body = { adding } + + const res = { + sendStatus: code => { + this.Modules.promises.hooks.fire + .calledWith('sendSupportRequest', { + email: this.user.email, + subject: 'Sales Contact Form', + message: + '\n' + + '**Overleaf Sales Contact Form:**\n' + + '\n' + + '**Subject:** Self-Serve Group User Increase Request\n' + + '\n' + + `**Estimated Number of Users:** ${adding}\n` + + '\n' + + `**Message:** This email has been generated on behalf of user with email **${this.user.email}** to request an increase in the total number of users for their subscription.`, + inbox: 'sales', + }) + .should.equal(true) + sinon.assert.calledOnce(this.Modules.promises.hooks.fire) + code.should.equal(204) + done() + }, + } + this.Controller.submitForm(this.req, res, done) + }) + }) + + describe('flexibleLicensingSplitTest', function () { + it('passes when the variant is "enabled"', function (done) { + const res = sinon.stub() + const next = () => { + this.ErrorController.notFound.notCalled.should.equal(true) + done() + } + this.SplitTestHandler.promises.getAssignment.resolves({ + variant: 'enabled', + }) + this.Controller.flexibleLicensingSplitTest(this.req, res, next, done) + }) + + it('returns error page when the variant is "default"', function (done) { + const res = sinon.stub() + const next = sinon.stub() + this.ErrorController.notFound = sinon.stub().callsFake(() => { + next.notCalled.should.equal(true) + done() + }) + this.SplitTestHandler.promises.getAssignment.resolves({ + variant: 'default', + }) + this.Controller.flexibleLicensingSplitTest(this.req, res, next, done) + }) + }) }) diff --git a/services/web/test/unit/src/Subscription/SubscriptionGroupHandlerTests.js b/services/web/test/unit/src/Subscription/SubscriptionGroupHandlerTests.js index a28d053ca2..a9d06130fd 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionGroupHandlerTests.js +++ b/services/web/test/unit/src/Subscription/SubscriptionGroupHandlerTests.js @@ -1,6 +1,7 @@ const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') const { expect } = require('chai') +const MockRequest = require('../helpers/MockRequest') const modulePath = '../../../../app/src/Features/Subscription/SubscriptionGroupHandler' @@ -12,6 +13,12 @@ describe('SubscriptionGroupHandler', function () { 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.localPlanInSettings = { + membersLimit: 2, + membersLimitAddOn: 'additional-license', + } this.subscription = { admin_id: this.adminUser_id, @@ -19,16 +26,35 @@ describe('SubscriptionGroupHandler', function () { _id: this.subscription_id, } + this.changeRequest = { + timeframe: 'now', + } + + this.recurlySubscription = { + id: 123, + addOns: [ + { + code: 'additional-license', + quantity: 1, + }, + ], + getRequestForAddOnUpdate: sinon.stub().returns(this.changeRequest), + } + this.SubscriptionLocator = { promises: { - getUsersSubscription: sinon.stub(), + getUsersSubscription: sinon.stub().resolves({ groupPlan: true }), getSubscriptionByMemberIdAndId: sinon.stub(), getSubscription: sinon.stub().resolves(this.subscription), }, } + this.changePreview = { + currency: 'USD', + } + this.SubscriptionController = { - makeChangePreview: sinon.stub().resolves(), + makeChangePreview: sinon.stub().resolves(this.changePreview), } this.SubscriptionUpdater = { @@ -44,8 +70,36 @@ describe('SubscriptionGroupHandler', function () { findOne: sinon.stub().returns({ exec: sinon.stub().resolves }), } + this.SessionManager = { + getLoggedInUserId: sinon.stub().returns(this.user._id), + } + + this.previewSubscriptionChange = { + nextAddOns: [ + { + code: 'additional-license', + quantity: this.recurlySubscription.addOns[0].quantity + this.adding, + }, + ], + } + + this.applySubscriptionChange = {} + this.RecurlyClient = { - promises: {}, + 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), + }, + } + + this.PlansLocator = { + findLocalPlanInSettings: sinon.stub(this.localPlanInSettings), } this.SubscriptionHandler = { @@ -64,6 +118,8 @@ describe('SubscriptionGroupHandler', function () { Subscription: this.Subscription, }, './RecurlyClient': this.RecurlyClient, + './PlansLocator': this.PlansLocator, + '../Authentication/SessionManager': this.SessionManager, }, }) }) @@ -183,4 +239,127 @@ describe('SubscriptionGroupHandler', function () { }) }) }) + + 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.req) + ).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.req + ) + + expect(data).to.deep.equal({ + subscription: { groupPlan: true }, + plan: { + membersLimit: 2, + membersLimitAddOn: 'additional-license', + canUseFlexibleLicensing: true, + }, + recurlySubscription: 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, + }) + }) + + afterEach(function () { + this.recurlySubscription.getRequestForAddOnUpdate + .calledWith( + 'additional-license', + 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.req + ) + + this.RecurlyClient.promises.getPaymentMethod + .calledWith(this.user_id) + .should.equal(true) + this.RecurlyClient.promises.previewSubscriptionChange + .calledWith(this.changeRequest) + .should.equal(true) + this.SubscriptionController.makeChangePreview + .calledWith( + { + type: 'add-on-update', + addOn: { + code: 'additional-license', + quantity: + this.recurlySubscription.addOns[0].quantity + this.adding, + prevQuantity: this.adding, + }, + }, + this.previewSubscriptionChange, + this.paymentMethod + ) + .should.equal(true) + preview.should.equal(this.changePreview) + }) + }) + + describe('createAddSeatsSubscriptionChange', function () { + it('should change the subscription', async function () { + const result = + await this.Handler.promises.createAddSeatsSubscriptionChange(this.req) + + this.RecurlyClient.promises.applySubscriptionChangeRequest + .calledWith(this.changeRequest) + .should.equal(true) + this.SubscriptionHandler.promises.syncSubscription + .calledWith({ uuid: this.recurlySubscription.id }, this.user_id) + .should.equal(true) + expect(result).to.deep.equal({ + adding: this.req.body.adding, + }) + }) + }) + }) + + 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 licencing') + }) + }) + + 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 + }) })