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
+ })
})