| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566 |
- import { vi, assert, expect } from 'vitest'
- import sinon from 'sinon'
- import MockRequest from '../helpers/MockRequest.mjs'
- import MockResponse from '../helpers/MockResponse.mjs'
- import SubscriptionErrors from '../../../../app/src/Features/Subscription/Errors.mjs'
- import SubscriptionHelper from '../../../../app/src/Features/Subscription/SubscriptionHelper.mjs'
- import { AI_ADD_ON_CODE } from '../../../../app/src/Features/Subscription/AiHelper.mjs'
- const modulePath =
- '../../../../app/src/Features/Subscription/SubscriptionController.mjs'
- const mockSubscriptions = {
- 'subscription-123-active': {
- uuid: 'subscription-123-active',
- plan: {
- name: 'Gold',
- plan_code: 'gold',
- },
- current_period_ends_at: new Date(),
- state: 'active',
- unit_amount_in_cents: 999,
- account: {
- account_code: 'user-123',
- },
- },
- }
- describe('SubscriptionController', function () {
- beforeEach(async function (ctx) {
- ctx.logger = {
- debug: sinon.stub(),
- warn: sinon.stub(),
- }
- vi.doMock('@overleaf/logger', () => ({
- default: ctx.logger,
- }))
- ctx.user = {
- email: 'tom@yahoo.com',
- _id: 'one',
- signUpDate: new Date('2000-10-01'),
- emails: [{ email: 'tom@yahoo.com', confirmedAt: new Date('2000-10-02') }],
- }
- ctx.activeRecurlySubscription = mockSubscriptions['subscription-123-active']
- ctx.SessionManager = {
- getLoggedInUser: sinon.stub().callsArgWith(1, null, ctx.user),
- getLoggedInUserId: sinon.stub().returns(ctx.user._id),
- getSessionUser: sinon.stub().returns(ctx.user),
- isUserLoggedIn: sinon.stub().returns(true),
- }
- ctx.SubscriptionHandler = {
- createSubscription: sinon.stub().callsArgWith(3),
- updateSubscription: sinon.stub().callsArgWith(3),
- reactivateSubscription: sinon.stub().callsArgWith(1),
- cancelSubscription: sinon.stub().callsArgWith(1),
- syncSubscription: sinon.stub().yields(),
- attemptPaypalInvoiceCollection: sinon.stub().yields(),
- startFreeTrial: sinon.stub(),
- promises: {
- createSubscription: sinon.stub().resolves(),
- updateSubscription: sinon.stub().resolves(),
- reactivateSubscription: sinon.stub().resolves(),
- cancelSubscription: sinon.stub().resolves(),
- pauseSubscription: sinon.stub().resolves(),
- resumeSubscription: sinon.stub().resolves(),
- syncSubscription: sinon.stub().resolves(),
- attemptPaypalInvoiceCollection: sinon.stub().resolves(),
- startFreeTrial: sinon.stub().resolves(),
- purchaseAddon: sinon.stub().resolves(),
- previewAddonPurchase: sinon.stub().resolves({
- subscription: {
- currency: 'USD',
- netTerms: 0,
- periodEnd: new Date(),
- taxRate: 0,
- },
- immediateCharge: { amount: 0 },
- nextPlanCode: 'professional',
- nextPlanName: 'Professional',
- nextPlanPrice: 2000,
- nextAddOns: [],
- subtotal: 2000,
- tax: 0,
- total: 2000,
- }),
- },
- }
- ctx.LimitationsManager = {
- hasPaidSubscription: sinon.stub(),
- userHasSubscription: sinon
- .stub()
- .yields(null, { hasSubscription: false }),
- promises: {
- hasPaidSubscription: sinon.stub().resolves(),
- userHasSubscription: sinon.stub().resolves({ hasSubscription: false }),
- },
- }
- ctx.SubscriptionViewModelBuilder = {
- buildUsersSubscriptionViewModel: sinon.stub().callsArgWith(1, null, {}),
- buildPlansList: sinon.stub(),
- promises: {
- buildUsersSubscriptionViewModel: sinon.stub().resolves({}),
- },
- buildPlansListForSubscriptionDash: sinon
- .stub()
- .returns({ plans: [], planCodesChangingAtTermEnd: [] }),
- }
- ctx.settings = {
- coupon_codes: {
- upgradeToAnnualPromo: {
- student: 'STUDENTCODEHERE',
- collaborator: 'COLLABORATORCODEHERE',
- },
- },
- groupPlanModalOptions: {
- plan_codes: [],
- currencies: [
- {
- display: 'GBP (£)',
- code: 'GBP',
- },
- ],
- sizes: ['42'],
- usages: [{ code: 'foo', display: 'Foo' }],
- },
- apis: {
- recurly: {
- subdomain: 'sl',
- },
- },
- siteUrl: 'http://de.overleaf.dev:3000',
- }
- ctx.AuthorizationManager = {
- promises: {
- isUserSiteAdmin: sinon.stub().resolves(false),
- },
- }
- ctx.GeoIpLookup = {
- isValidCurrencyParam: sinon.stub().returns(true),
- getCurrencyCode: sinon.stub().yields('USD', 'US'),
- promises: {
- getCurrencyCode: sinon.stub().resolves({
- countryCode: 'US',
- currencyCode: 'USD',
- }),
- },
- }
- ctx.UserGetter = {
- getUser: sinon.stub().callsArgWith(2, null, ctx.user),
- promises: {
- getUser: sinon.stub().resolves(ctx.user),
- getWritefullData: sinon
- .stub()
- .resolves({ isPremium: false, premiumSource: null }),
- },
- }
- ctx.SplitTestV2Hander = {
- promises: {
- getAssignment: sinon.stub().resolves({ variant: 'default' }),
- },
- }
- ctx.Features = {
- hasFeature: sinon.stub().returns(false),
- }
- vi.doMock(
- '../../../../app/src/Features/Authorization/AuthorizationManager',
- () => ({
- default: ctx.AuthorizationManager,
- })
- )
- vi.doMock(
- '../../../../app/src/Features/SplitTests/SplitTestHandler',
- () => ({
- default: ctx.SplitTestV2Hander,
- })
- )
- vi.doMock(
- '../../../../app/src/Features/Authentication/SessionManager',
- () => ({
- default: ctx.SessionManager,
- })
- )
- vi.doMock(
- '../../../../app/src/Features/Subscription/SubscriptionHandler',
- () => ({
- default: ctx.SubscriptionHandler,
- })
- )
- vi.doMock(
- '../../../../app/src/Features/Subscription/SubscriptionHelper',
- () => ({
- default: SubscriptionHelper,
- })
- )
- vi.doMock(
- '../../../../app/src/Features/Subscription/SubscriptionViewModelBuilder',
- () => ({
- default: ctx.SubscriptionViewModelBuilder,
- })
- )
- vi.doMock(
- '../../../../app/src/Features/Subscription/LimitationsManager',
- () => ({
- default: ctx.LimitationsManager,
- })
- )
- vi.doMock('../../../../app/src/infrastructure/GeoIpLookup', () => ({
- default: ctx.GeoIpLookup,
- }))
- vi.doMock('@overleaf/settings', () => ({
- default: ctx.settings,
- }))
- vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
- default: ctx.UserGetter,
- }))
- vi.doMock(
- '../../../../app/src/Features/Subscription/RecurlyWrapper',
- () => ({
- default: (ctx.RecurlyWrapper = {
- promises: {
- updateAccountEmailAddress: sinon.stub().resolves(),
- getSubscription: sinon.stub().resolves({}),
- },
- }),
- })
- )
- vi.doMock(
- '../../../../app/src/Features/Subscription/RecurlyEventHandler',
- () => ({
- default: {
- sendRecurlyAnalyticsEvent: sinon.stub().resolves(),
- },
- })
- )
- vi.doMock(
- '../../../../app/src/Features/Subscription/FeaturesUpdater',
- () => ({
- default: (ctx.FeaturesUpdater = {
- promises: {
- refreshFeatures: sinon.stub().resolves({ features: {} }),
- },
- }),
- })
- )
- vi.doMock(
- '../../../../app/src/Features/Subscription/GroupPlansData',
- () => ({
- default: (ctx.GroupPlansData = {}),
- })
- )
- vi.doMock(
- '../../../../app/src/Features/Subscription/V1SubscriptionManager',
- () => ({
- default: (ctx.V1SubscriptionManager = {}),
- })
- )
- vi.doMock('../../../../app/src/Features/Errors/HttpErrorHandler', () => ({
- default: (ctx.HttpErrorHandler = {
- unprocessableEntity: sinon.stub().callsFake((req, res, message) => {
- res.status(422)
- res.json({ message })
- }),
- badRequest: sinon.stub().callsFake((req, res, message) => {
- res.status(400)
- res.json({ message })
- }),
- }),
- }))
- vi.doMock('../../../../app/src/Features/Subscription/Errors', () => ({
- default: SubscriptionErrors,
- }))
- vi.doMock(
- '../../../../app/src/Features/Analytics/AnalyticsManager',
- () => ({
- default: (ctx.AnalyticsManager = {
- recordEventForUser: sinon.stub(),
- recordEventForUserInBackground: sinon.stub(),
- recordEventForSession: sinon.stub(),
- setUserPropertyForUser: sinon.stub(),
- }),
- })
- )
- vi.doMock('../../../../app/src/infrastructure/Modules', () => ({
- default: (ctx.Modules = {
- promises: { hooks: { fire: sinon.stub().resolves() } },
- }),
- }))
- vi.doMock('../../../../app/src/infrastructure/Features', () => ({
- default: ctx.Features,
- }))
- vi.doMock('../../../../app/src/util/currency', () => ({
- default: (ctx.currency = {
- formatCurrency: sinon.stub(),
- }),
- }))
- vi.doMock('../../../../app/src/models/User', () => ({
- User: {
- findById: sinon.stub().resolves(ctx.user),
- },
- }))
- vi.doMock(
- '../../../../app/src/Features/Subscription/SubscriptionLocator',
- () => ({
- default: (ctx.SubscriptionLocator = {
- promises: {
- getUsersSubscription: sinon.stub().resolves(null),
- },
- }),
- })
- )
- vi.doMock(
- '../../../../app/src/Features/Authorization/PermissionsManager',
- () => ({
- default: (ctx.PermissionsManager = {
- promises: {
- checkUserPermissions: sinon.stub().resolves(true),
- },
- }),
- })
- )
- vi.doMock(
- '../../../../app/src/Features/Subscription/RecurlyClient',
- () => ({
- default: (ctx.RecurlyClient = {
- promises: {
- getAddOn: sinon.stub().resolves({
- code: 'ai-assistant',
- name: 'AI Assistant',
- }),
- },
- }),
- })
- )
- vi.doMock('../../../../app/src/Features/Subscription/PlansLocator', () => ({
- default: (ctx.PlansLocator = {
- findLocalPlanInSettings: sinon.stub().returns({
- annual: false,
- }),
- }),
- }))
- ctx.SubscriptionController = (await import(modulePath)).default
- ctx.res = new MockResponse(vi)
- ctx.req = new MockRequest(vi)
- ctx.req.body = {}
- ctx.req.query = { planCode: '123123' }
- ctx.stubbedCurrencyCode = 'GBP'
- })
- describe('successfulSubscription', function () {
- it('without a personal subscription', async function (ctx) {
- await new Promise(resolve => {
- ctx.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel.resolves(
- {}
- )
- ctx.res.redirect = url => {
- url.should.equal('/user/subscription/plans')
- resolve()
- }
- ctx.SubscriptionController.successfulSubscription(ctx.req, ctx.res)
- })
- })
- it('with a personal subscription', async function (ctx) {
- await new Promise(resolve => {
- ctx.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel.resolves(
- {
- personalSubscription: 'foo',
- }
- )
- ctx.res.render = (url, variables) => {
- url.should.equal('subscriptions/successful-subscription-react')
- assert.deepEqual(variables, {
- title: 'thank_you',
- personalSubscription: 'foo',
- postCheckoutRedirect: undefined,
- user: {
- _id: ctx.user._id,
- features: ctx.user.features,
- },
- })
- resolve()
- }
- ctx.SubscriptionController.successfulSubscription(ctx.req, ctx.res)
- })
- })
- it('with an error', async function (ctx) {
- await new Promise(resolve => {
- ctx.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel.resolves(
- undefined
- )
- ctx.SubscriptionController.successfulSubscription(
- ctx.req,
- ctx.res,
- error => {
- assert.isNotNull(error)
- resolve()
- }
- )
- })
- })
- })
- describe('userSubscriptionPage', function () {
- beforeEach(async function (ctx) {
- await new Promise((resolve, reject) => {
- ctx.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel.resolves(
- {
- personalSubscription: (ctx.personalSubscription = {
- 'personal-subscription': 'mock',
- }),
- memberGroupSubscriptions: (ctx.memberGroupSubscriptions = {
- 'group-subscriptions': 'mock',
- }),
- }
- )
- ctx.SubscriptionViewModelBuilder.buildPlansList.returns(
- (ctx.plans = { plans: 'mock' })
- )
- ctx.SubscriptionViewModelBuilder.buildPlansListForSubscriptionDash.returns(
- {
- plans: ctx.plans,
- planCodesChangingAtTermEnd: [],
- }
- )
- ctx.LimitationsManager.promises.userHasSubscription.resolves({
- hasSubscription: false,
- })
- ctx.res.render = (view, data) => {
- ctx.data = data
- expect(view).to.equal('subscriptions/dashboard-react')
- resolve()
- }
- ctx.SubscriptionController.userSubscriptionPage(
- ctx.req,
- ctx.res,
- ctx.rejectOnError(reject)
- )
- })
- })
- it('should load the personal, groups and v1 subscriptions', function (ctx) {
- expect(ctx.data.personalSubscription).to.deep.equal(
- ctx.personalSubscription
- )
- expect(ctx.data.memberGroupSubscriptions).to.deep.equal(
- ctx.memberGroupSubscriptions
- )
- })
- it('should load the user', function (ctx) {
- expect(ctx.data.user).to.deep.equal(ctx.user)
- })
- it('should load the plans', function (ctx) {
- expect(ctx.data.plans).to.deep.equal(ctx.plans)
- })
- it('should load an empty list of groups with settings available', function (ctx) {
- expect(ctx.data.groupSettingsEnabledFor).to.deep.equal([])
- })
- it('should pass isManagedGroupAdmin as false when not set', function (ctx) {
- expect(ctx.data.isManagedGroupAdmin).to.equal(false)
- })
- describe('when user is a managed group admin', function () {
- beforeEach(async function (ctx) {
- ctx.req.isManagedGroupAdmin = true
- await new Promise((resolve, reject) => {
- ctx.res.render = (view, data) => {
- ctx.data = data
- expect(view).to.equal('subscriptions/dashboard-react')
- resolve()
- }
- ctx.SubscriptionController.userSubscriptionPage(
- ctx.req,
- ctx.res,
- ctx.rejectOnError(reject)
- )
- })
- })
- it('should pass isManagedGroupAdmin as true', function (ctx) {
- expect(ctx.data.isManagedGroupAdmin).to.equal(true)
- })
- })
- describe('when errorCode query param is present', function () {
- beforeEach(async function (ctx) {
- ctx.req.query.errorCode = 'payment_failed'
- await new Promise((resolve, reject) => {
- ctx.res.render = (view, data) => {
- ctx.data = data
- expect(view).to.equal('subscriptions/dashboard-react')
- resolve()
- }
- ctx.SubscriptionController.userSubscriptionPage(
- ctx.req,
- ctx.res,
- ctx.rejectOnError(reject)
- )
- })
- })
- it('should pass redirectedPaymentErrorCode to the view', function (ctx) {
- expect(ctx.data.redirectedPaymentErrorCode).to.equal('payment_failed')
- })
- })
- })
- describe('updateAccountEmailAddress via put', function () {
- beforeEach(function (ctx) {
- ctx.req.body = {
- account_email: 'current_account_email@overleaf.com',
- }
- })
- it('should send the user and subscriptionId to "updateAccountEmailAddress" hooks', async function (ctx) {
- ctx.res.sendStatus = sinon.spy()
- await ctx.SubscriptionController.updateAccountEmailAddress(
- ctx.req,
- ctx.res
- )
- expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
- 'updateAccountEmailAddress',
- ctx.user._id,
- ctx.user.email
- )
- })
- it('should respond with 200', async function (ctx) {
- ctx.res.sendStatus = sinon.spy()
- await ctx.SubscriptionController.updateAccountEmailAddress(
- ctx.req,
- ctx.res
- )
- ctx.res.sendStatus.calledWith(200).should.equal(true)
- })
- it('should send the error to the next handler when updating recurly account email fails', async function (ctx) {
- ctx.Modules.promises.hooks.fire
- .withArgs('updateAccountEmailAddress', ctx.user._id, ctx.user.email)
- .rejects(new Error())
- ctx.next = sinon.spy(error => {
- expect(error).to.be.instanceOf(Error)
- })
- await ctx.SubscriptionController.updateAccountEmailAddress(
- ctx.req,
- ctx.res,
- ctx.next
- )
- expect(ctx.next.calledOnce).to.be.true
- })
- })
- describe('reactivateSubscription', function () {
- describe('when the user has permission', function () {
- beforeEach(async function (ctx) {
- await new Promise(resolve => {
- ctx.res = {
- redirect() {
- resolve()
- },
- }
- ctx.req.assertPermission = sinon.stub()
- ctx.next = sinon.stub().callsFake(error => {
- resolve(error)
- })
- sinon.spy(ctx.res, 'redirect')
- ctx.SubscriptionController.reactivateSubscription(
- ctx.req,
- ctx.res,
- ctx.next
- )
- })
- })
- it('should assert the user has permission to reactivate their subscription', async function (ctx) {
- await new Promise(resolve => {
- ctx.req.assertPermission
- .calledWith('reactivate-subscription')
- .should.equal(true)
- resolve()
- })
- })
- it('should tell the handler to reactivate this user', async function (ctx) {
- await new Promise(resolve => {
- ctx.SubscriptionHandler.reactivateSubscription
- .calledWith(ctx.user)
- .should.equal(true)
- resolve()
- })
- })
- it('should redurect to the subscription page', async function (ctx) {
- await new Promise(resolve => {
- ctx.res.redirect.calledWith('/user/subscription').should.equal(true)
- resolve()
- })
- })
- })
- describe('when the user does not have permission', function () {
- beforeEach(async function (ctx) {
- await new Promise(resolve => {
- ctx.res = {
- redirect() {
- resolve()
- },
- }
- ctx.req.assertPermission = sinon.stub().throws()
- ctx.next = sinon.stub().callsFake(() => {
- resolve()
- })
- sinon.spy(ctx.res, 'redirect')
- ctx.SubscriptionController.reactivateSubscription(
- ctx.req,
- ctx.res,
- ctx.next
- )
- })
- })
- it('should not reactivate the user', async function (ctx) {
- await new Promise(resolve => {
- ctx.req.assertPermission = sinon.stub().throws()
- ctx.SubscriptionHandler.reactivateSubscription.called.should.equal(
- false
- )
- resolve()
- })
- })
- it('should call next with an error', async function (ctx) {
- await new Promise(resolve => {
- ctx.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
- resolve()
- })
- })
- })
- })
- describe('pauseSubscription', function () {
- it('should throw an error if no pause length is provided', async function (ctx) {
- ctx.res = new MockResponse(vi)
- ctx.req = new MockRequest(vi)
- ctx.next = sinon.stub()
- await expect(
- ctx.SubscriptionController.pauseSubscription(ctx.req, ctx.res, ctx.next)
- ).to.be.rejectedWith('Not found')
- })
- it('should throw an error if an invalid pause length is provided', async function (ctx) {
- ctx.res = new MockResponse(vi)
- ctx.req = new MockRequest(vi)
- ctx.req.params = { pauseCycles: '-10' }
- ctx.next = sinon.stub()
- await ctx.SubscriptionController.pauseSubscription(
- ctx.req,
- ctx.res,
- ctx.next
- )
- expect(ctx.res.statusCode).to.equal(400)
- })
- it('should return a 200 when requesting a pause', async function (ctx) {
- ctx.res = new MockResponse(vi)
- ctx.req = new MockRequest(vi)
- ctx.req.params = { pauseCycles: '3' }
- ctx.next = sinon.stub()
- await ctx.SubscriptionController.pauseSubscription(
- ctx.req,
- ctx.res,
- ctx.next
- )
- expect(ctx.res.statusCode).to.equal(200)
- })
- })
- describe('resumeSubscription', function () {
- it('should return a 200 when resuming a subscription', async function (ctx) {
- ctx.res = new MockResponse(vi)
- ctx.req = new MockRequest(vi)
- ctx.next = sinon.stub()
- await ctx.SubscriptionController.resumeSubscription(
- ctx.req,
- ctx.res,
- ctx.next
- )
- expect(ctx.res.statusCode).to.equal(200)
- })
- })
- describe('cancelSubscription', function () {
- it('should tell the handler to cancel this user', async function (ctx) {
- ctx.next = sinon.stub()
- await ctx.SubscriptionController.cancelSubscription(
- ctx.req,
- ctx.res,
- ctx.next
- )
- ctx.SubscriptionHandler.promises.cancelSubscription
- .calledWith(ctx.user)
- .should.equal(true)
- })
- it('should return a 200 on success', async function (ctx) {
- ctx.next = sinon.stub()
- await ctx.SubscriptionController.cancelSubscription(
- ctx.req,
- ctx.res,
- ctx.next
- )
- expect(ctx.res.statusCode).to.equal(200)
- })
- it('should call next with error', async function (ctx) {
- ctx.SubscriptionHandler.promises.cancelSubscription.rejects(
- new Error('cancel error')
- )
- ctx.next = sinon.stub()
- await ctx.SubscriptionController.cancelSubscription(
- ctx.req,
- ctx.res,
- ctx.next
- )
- ctx.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
- })
- })
- describe('recurly callback', function () {
- describe('with a sync subscription request', function () {
- beforeEach(async function (ctx) {
- await new Promise(resolve => {
- ctx.req = {
- body: {
- expired_subscription_notification: {
- account: {
- account_code: ctx.user._id,
- },
- subscription: {
- uuid: ctx.activeRecurlySubscription.uuid,
- plan: {
- plan_code: 'collaborator',
- state: 'active',
- },
- },
- },
- },
- }
- ctx.res = {
- sendStatus() {
- resolve()
- },
- }
- sinon.spy(ctx.res, 'sendStatus')
- ctx.SubscriptionController.recurlyCallback(ctx.req, ctx.res)
- })
- })
- it('should tell the SubscriptionHandler to process the recurly callback', async function (ctx) {
- await new Promise(resolve => {
- ctx.SubscriptionHandler.syncSubscription.called.should.equal(true)
- resolve()
- })
- })
- it('should send a 200', async function (ctx) {
- await new Promise(resolve => {
- ctx.res.sendStatus.calledWith(200)
- resolve()
- })
- })
- })
- describe('with a billing info updated request', function () {
- beforeEach(async function (ctx) {
- await new Promise(resolve => {
- ctx.req = {
- body: {
- billing_info_updated_notification: {
- account: {
- account_code: 'mock-account-code',
- },
- },
- },
- }
- ctx.res = {
- sendStatus() {
- resolve()
- },
- }
- sinon.spy(ctx.res, 'sendStatus')
- ctx.SubscriptionController.recurlyCallback(ctx.req, ctx.res)
- })
- })
- it('should call attemptPaypalInvoiceCollection', async function (ctx) {
- await new Promise(resolve => {
- ctx.SubscriptionHandler.attemptPaypalInvoiceCollection
- .calledWith('mock-account-code')
- .should.equal(true)
- resolve()
- })
- })
- it('should send a 200', async function (ctx) {
- await new Promise(resolve => {
- ctx.res.sendStatus.calledWith(200)
- resolve()
- })
- })
- })
- describe('with a non-actionable request', function () {
- beforeEach(async function (ctx) {
- await new Promise(resolve => {
- ctx.user.id = ctx.activeRecurlySubscription.account.account_code
- ctx.req = {
- body: {
- renewed_subscription_notification: {
- account: {
- account_code: ctx.user._id,
- },
- subscription: {
- uuid: ctx.activeRecurlySubscription.uuid,
- plan: {
- plan_code: 'collaborator',
- state: 'active',
- },
- },
- },
- },
- }
- ctx.res = {
- sendStatus() {
- resolve()
- },
- }
- sinon.spy(ctx.res, 'sendStatus')
- ctx.SubscriptionController.recurlyCallback(ctx.req, ctx.res)
- })
- })
- it('should not call the subscriptionshandler', function (ctx) {
- ctx.SubscriptionHandler.syncSubscription.called.should.equal(false)
- ctx.SubscriptionHandler.attemptPaypalInvoiceCollection.called.should.equal(
- false
- )
- })
- it('should respond with a 200 status', function (ctx) {
- ctx.res.sendStatus.calledWith(200)
- })
- })
- })
- describe('purchaseAddon', function () {
- beforeEach(function (ctx) {
- ctx.SessionManager.getSessionUser.returns(ctx.user) // Make sure getSessionUser returns the user
- ctx.next = sinon.stub()
- ctx.req.params = { addOnCode: AI_ADD_ON_CODE } // Mock add-on code
- })
- it('should return 200 on successful purchase of AI add-on', async function (ctx) {
- await ctx.SubscriptionController.purchaseAddon(ctx.req, ctx.res, ctx.next)
- ctx.res.sendStatus = sinon.spy()
- await ctx.SubscriptionController.purchaseAddon(ctx.req, ctx.res, ctx.next)
- expect(ctx.SubscriptionHandler.promises.purchaseAddon).to.have.been.called
- expect(
- ctx.SubscriptionHandler.promises.purchaseAddon
- ).to.have.been.calledWith(ctx.user._id, AI_ADD_ON_CODE, 1)
- expect(
- ctx.FeaturesUpdater.promises.refreshFeatures
- ).to.have.been.calledWith(ctx.user._id, 'add-on-purchase')
- expect(ctx.res.sendStatus).to.have.been.calledWith(200)
- expect(ctx.logger.debug).to.have.been.calledWith(
- { userId: ctx.user._id, addOnCode: AI_ADD_ON_CODE },
- 'purchasing add-ons'
- )
- })
- it('should return 404 if the add-on code is not AI_ADD_ON_CODE', async function (ctx) {
- ctx.req.params = { addOnCode: 'some-other-addon' }
- ctx.res.sendStatus = sinon.spy()
- await ctx.SubscriptionController.purchaseAddon(ctx.req, ctx.res, ctx.next)
- expect(ctx.SubscriptionHandler.promises.purchaseAddon).to.not.have.been
- .called
- expect(ctx.FeaturesUpdater.promises.refreshFeatures).to.not.have.been
- .called
- expect(ctx.res.sendStatus).to.have.been.calledWith(404)
- })
- it('should handle DuplicateAddOnError and send badRequest while sending 200', async function (ctx) {
- ctx.req.params.addOnCode = AI_ADD_ON_CODE
- ctx.SubscriptionHandler.promises.purchaseAddon.rejects(
- new SubscriptionErrors.DuplicateAddOnError()
- )
- await ctx.SubscriptionController.purchaseAddon(ctx.req, ctx.res, ctx.next)
- expect(ctx.HttpErrorHandler.badRequest).to.have.been.calledWith(
- ctx.req,
- ctx.res,
- 'Your subscription already includes this add-on',
- { addon: AI_ADD_ON_CODE }
- )
- expect(
- ctx.FeaturesUpdater.promises.refreshFeatures
- ).to.have.been.calledWith(ctx.user._id, 'add-on-purchase')
- expect(ctx.res.sendStatus).toHaveBeenCalledWith(200)
- })
- it('should handle PaymentActionRequiredError and return 402 with details', async function (ctx) {
- ctx.req.params.addOnCode = AI_ADD_ON_CODE
- const paymentError = new SubscriptionErrors.PaymentActionRequiredError({
- clientSecret: 'secret123',
- publicKey: 'pubkey456',
- })
- ctx.SubscriptionHandler.promises.purchaseAddon.rejects(paymentError)
- await ctx.SubscriptionController.purchaseAddon(ctx.req, ctx.res, ctx.next)
- expect(ctx.res.status).toHaveBeenCalledWith(402)
- expect(ctx.res.json).toHaveBeenCalledWith({
- message: 'Payment action required',
- clientSecret: 'secret123',
- publicKey: 'pubkey456',
- })
- expect(ctx.FeaturesUpdater.promises.refreshFeatures).to.not.have.been
- .called
- })
- it('should refresh features', async function (ctx) {
- ctx.req.params.addOnCode = 'assistant'
- ctx.SubscriptionHandler.promises.purchaseAddon = sinon.stub().resolves()
- ctx.FeaturesUpdater.promises.refreshFeatures = sinon.stub().resolves()
- await ctx.SubscriptionController.purchaseAddon(ctx.req, ctx.res)
- expect(
- ctx.FeaturesUpdater.promises.refreshFeatures.calledWith(
- ctx.user._id,
- 'add-on-purchase'
- )
- ).to.be.true
- })
- it('should respond with a bad request if the subscription already includes the addOn', async function (ctx) {
- ctx.req.params.addOnCode = 'assistant'
- ctx.SubscriptionHandler.promises.purchaseAddon = sinon
- .stub()
- .rejects(new SubscriptionErrors.DuplicateAddOnError())
- await ctx.SubscriptionController.purchaseAddon(ctx.req, ctx.res)
- expect(
- ctx.HttpErrorHandler.badRequest.calledWith(
- ctx.req,
- ctx.res,
- 'Your subscription already includes this add-on',
- { addon: 'assistant' }
- )
- ).to.be.true
- })
- it('should handle MultiplePendingChangesError and return 422 with JSON response', async function (ctx) {
- ctx.req.params.addOnCode = AI_ADD_ON_CODE
- ctx.SubscriptionHandler.promises.purchaseAddon.rejects(
- new SubscriptionErrors.MultiplePendingChangesError()
- )
- await ctx.SubscriptionController.purchaseAddon(ctx.req, ctx.res, ctx.next)
- expect(ctx.res.status).toHaveBeenCalledWith(422)
- expect(ctx.res.json).toHaveBeenCalledWith({
- code: 'multiple_pending_changes',
- message:
- 'Cannot complete purchase while there are multiple pending subscription changes. Please contact support.',
- })
- expect(ctx.FeaturesUpdater.promises.refreshFeatures).to.not.have.been
- .called
- })
- })
- describe('removeAddon', function () {
- beforeEach(function (ctx) {
- ctx.SessionManager.getSessionUser.returns(ctx.user)
- ctx.next = sinon.stub()
- ctx.req.params = { addOnCode: AI_ADD_ON_CODE }
- ctx.SubscriptionHandler.promises.removeAddon = sinon.stub().resolves()
- })
- it('should return 200 on successful removal of AI add-on', async function (ctx) {
- ctx.res.sendStatus = sinon.spy()
- await ctx.SubscriptionController.removeAddon(ctx.req, ctx.res, ctx.next)
- expect(ctx.SubscriptionHandler.promises.removeAddon).to.have.been.called
- expect(
- ctx.SubscriptionHandler.promises.removeAddon
- ).to.have.been.calledWith(ctx.user, AI_ADD_ON_CODE)
- expect(ctx.res.sendStatus).to.have.been.calledWith(200)
- expect(ctx.logger.debug).to.have.been.calledWith(
- { userId: ctx.user._id, addOnCode: AI_ADD_ON_CODE },
- 'removing add-ons'
- )
- })
- it('should return 404 if the add-on code is not AI_ADD_ON_CODE', async function (ctx) {
- ctx.req.params = { addOnCode: 'some-other-addon' }
- ctx.res.sendStatus = sinon.spy()
- await ctx.SubscriptionController.removeAddon(ctx.req, ctx.res, ctx.next)
- expect(ctx.SubscriptionHandler.promises.removeAddon).to.not.have.been
- .called
- expect(ctx.res.sendStatus).to.have.been.calledWith(404)
- })
- it('should handle AddOnNotPresentError and send badRequest', async function (ctx) {
- ctx.SubscriptionHandler.promises.removeAddon.rejects(
- new SubscriptionErrors.AddOnNotPresentError()
- )
- await ctx.SubscriptionController.removeAddon(ctx.req, ctx.res, ctx.next)
- expect(ctx.HttpErrorHandler.badRequest).to.have.been.calledWith(
- ctx.req,
- ctx.res,
- 'Your subscription does not contain the requested add-on',
- { addon: AI_ADD_ON_CODE }
- )
- })
- it('should handle MultiplePendingChangesError and return 422 with JSON response', async function (ctx) {
- ctx.SubscriptionHandler.promises.removeAddon.rejects(
- new SubscriptionErrors.MultiplePendingChangesError()
- )
- await ctx.SubscriptionController.removeAddon(ctx.req, ctx.res, ctx.next)
- expect(ctx.res.status).toHaveBeenCalledWith(422)
- expect(ctx.res.json).toHaveBeenCalledWith({
- code: 'multiple_pending_changes',
- message:
- 'Cannot remove add-on while there are multiple pending subscription changes. Please contact support.',
- })
- })
- })
- describe('checkSubscriptionPauseStatus', function () {
- beforeEach(function (ctx) {
- ctx.user = {
- _id: 'user-id-123',
- email: 'test@example.com',
- }
- })
- it('should return isPaused: false when user has no subscription', async function (ctx) {
- ctx.LimitationsManager.promises.userHasSubscription.resolves({
- subscription: null,
- })
- const result =
- await ctx.SubscriptionController.checkSubscriptionPauseStatus(ctx.user)
- expect(result).to.deep.equal({ isPaused: false })
- })
- it('should return isPaused: false when subscription has no paymentProvider', async function (ctx) {
- const subscription = {
- planCode: 'professional',
- }
- ctx.LimitationsManager.promises.userHasSubscription.resolves({
- subscription,
- })
- const result =
- await ctx.SubscriptionController.checkSubscriptionPauseStatus(ctx.user)
- expect(result).to.deep.equal({ isPaused: false })
- })
- it('should return isPaused: false when subscription has no subscriptionId', async function (ctx) {
- const subscription = {
- paymentProvider: {
- service: 'stripe',
- subscriptionId: null,
- },
- }
- ctx.LimitationsManager.promises.userHasSubscription.resolves({
- subscription,
- })
- const result =
- await ctx.SubscriptionController.checkSubscriptionPauseStatus(ctx.user)
- expect(result).to.deep.equal({ isPaused: false })
- })
- it('should return isPaused: false when Stripe subscription has no remaining pause cycles', async function (ctx) {
- const subscription = {
- paymentProvider: {
- service: 'stripe',
- subscriptionId: 'sub-123',
- },
- }
- ctx.LimitationsManager.promises.userHasSubscription.resolves({
- subscription,
- })
- const paymentRecord = {
- subscription: {
- remainingPauseCycles: 0,
- },
- }
- ctx.Modules.promises.hooks.fire
- .withArgs('getPaymentFromRecord', subscription)
- .resolves([paymentRecord])
- const result =
- await ctx.SubscriptionController.checkSubscriptionPauseStatus(ctx.user)
- expect(result).to.deep.equal({ isPaused: false })
- })
- it('should return isPaused: false when Stripe subscription has no remainingPauseCycles property', async function (ctx) {
- const subscription = {
- paymentProvider: {
- service: 'stripe',
- subscriptionId: 'sub-123',
- },
- }
- ctx.LimitationsManager.promises.userHasSubscription.resolves({
- subscription,
- })
- const paymentRecord = {
- subscription: {},
- }
- ctx.Modules.promises.hooks.fire
- .withArgs('getPaymentFromRecord', subscription)
- .resolves([paymentRecord])
- const result =
- await ctx.SubscriptionController.checkSubscriptionPauseStatus(ctx.user)
- expect(result).to.deep.equal({ isPaused: false })
- })
- it('should return isPaused: true with redirect path when Stripe subscription has remaining pause cycles', async function (ctx) {
- const subscription = {
- paymentProvider: {
- service: 'stripe',
- subscriptionId: 'sub-123',
- },
- }
- ctx.LimitationsManager.promises.userHasSubscription.resolves({
- subscription,
- })
- const paymentRecord = {
- subscription: {
- remainingPauseCycles: 2,
- },
- }
- ctx.Modules.promises.hooks.fire
- .withArgs('getPaymentFromRecord', subscription)
- .resolves([paymentRecord])
- const result =
- await ctx.SubscriptionController.checkSubscriptionPauseStatus(ctx.user)
- expect(result).to.deep.equal({
- isPaused: true,
- redirectPath: '/user/subscription?redirect-reason=subscription-paused',
- })
- })
- it('should return isPaused: true when remainingPauseCycles is exactly 1', async function (ctx) {
- const subscription = {
- paymentProvider: {
- service: 'stripe',
- subscriptionId: 'sub-123',
- },
- }
- ctx.LimitationsManager.promises.userHasSubscription.resolves({
- subscription,
- })
- const paymentRecord = {
- subscription: {
- remainingPauseCycles: 1,
- },
- }
- ctx.Modules.promises.hooks.fire
- .withArgs('getPaymentFromRecord', subscription)
- .resolves([paymentRecord])
- const result =
- await ctx.SubscriptionController.checkSubscriptionPauseStatus(ctx.user)
- expect(result).to.deep.equal({
- isPaused: true,
- redirectPath: '/user/subscription?redirect-reason=subscription-paused',
- })
- })
- it('should return isPaused: false when userHasSubscription throws error', async function (ctx) {
- const error = new Error('Something bad happened')
- ctx.LimitationsManager.promises.userHasSubscription.rejects(error)
- const result =
- await ctx.SubscriptionController.checkSubscriptionPauseStatus(ctx.user)
- expect(result).to.deep.equal({ isPaused: false })
- })
- it('should return isPaused: false when getPaymentFromRecord throws error', async function (ctx) {
- const subscription = {
- paymentProvider: {
- service: 'stripe',
- subscriptionId: 'sub-123',
- },
- }
- ctx.LimitationsManager.promises.userHasSubscription.resolves({
- subscription,
- })
- const error = new Error('Something bad happened')
- ctx.Modules.promises.hooks.fire
- .withArgs('getPaymentFromRecord', subscription)
- .rejects(error)
- const result =
- await ctx.SubscriptionController.checkSubscriptionPauseStatus(ctx.user)
- expect(result).to.deep.equal({ isPaused: false })
- })
- it('should return isPaused: false when Recurly subscription is not paused', async function (ctx) {
- const subscription = {
- recurlySubscription_id: 'uuid-123',
- recurlyStatus: {
- state: 'active',
- },
- }
- ctx.LimitationsManager.promises.userHasSubscription.resolves({
- subscription,
- })
- const result =
- await ctx.SubscriptionController.checkSubscriptionPauseStatus(ctx.user)
- expect(result).to.deep.equal({ isPaused: false })
- })
- it('should return isPaused: true when Recurly subscription is paused', async function (ctx) {
- const subscription = {
- recurlySubscription_id: 'uuid-123',
- recurlyStatus: {
- state: 'paused',
- },
- }
- ctx.LimitationsManager.promises.userHasSubscription.resolves({
- subscription,
- })
- const result =
- await ctx.SubscriptionController.checkSubscriptionPauseStatus(ctx.user)
- expect(result).to.deep.equal({
- isPaused: true,
- redirectPath: '/user/subscription?redirect-reason=subscription-paused',
- })
- })
- it('should return isPaused: true when Recurly subscription has pending pause cycles', async function (ctx) {
- const subscription = {
- recurlySubscription_id: 'uuid-123',
- recurlyStatus: {
- state: 'active',
- },
- }
- ctx.LimitationsManager.promises.userHasSubscription.resolves({
- subscription,
- })
- const recurlySubscriptionData = {
- remaining_pause_cycles: 2,
- }
- ctx.RecurlyWrapper.promises.getSubscription.resolves(
- recurlySubscriptionData
- )
- const result =
- await ctx.SubscriptionController.checkSubscriptionPauseStatus(ctx.user)
- expect(result).to.deep.equal({
- isPaused: true,
- redirectPath: '/user/subscription?redirect-reason=subscription-paused',
- })
- expect(
- ctx.RecurlyWrapper.promises.getSubscription
- ).to.have.been.calledWith('uuid-123')
- })
- it('should return isPaused: false when Recurly subscription has no remaining pause cycles', async function (ctx) {
- const subscription = {
- recurlySubscription_id: 'uuid-123',
- recurlyStatus: {
- state: 'active',
- },
- }
- ctx.LimitationsManager.promises.userHasSubscription.resolves({
- subscription,
- })
- const recurlySubscriptionData = {
- remaining_pause_cycles: 0,
- }
- ctx.RecurlyWrapper.promises.getSubscription.resolves(
- recurlySubscriptionData
- )
- const result =
- await ctx.SubscriptionController.checkSubscriptionPauseStatus(ctx.user)
- expect(result).to.deep.equal({ isPaused: false })
- })
- it('should return isPaused: false when Recurly subscription has no remaining_pause_cycles property', async function (ctx) {
- const subscription = {
- recurlySubscription_id: 'uuid-123',
- recurlyStatus: {
- state: 'active',
- },
- }
- ctx.LimitationsManager.promises.userHasSubscription.resolves({
- subscription,
- })
- const recurlySubscriptionData = {}
- ctx.RecurlyWrapper.promises.getSubscription.resolves(
- recurlySubscriptionData
- )
- const result =
- await ctx.SubscriptionController.checkSubscriptionPauseStatus(ctx.user)
- expect(result).to.deep.equal({ isPaused: false })
- })
- it('should return isPaused: false when Recurly API call fails', async function (ctx) {
- const subscription = {
- recurlySubscription_id: 'uuid-123',
- recurlyStatus: {
- state: 'active',
- },
- }
- ctx.LimitationsManager.promises.userHasSubscription.resolves({
- subscription,
- })
- const error = new Error('Recurly API failed')
- ctx.RecurlyWrapper.promises.getSubscription.rejects(error)
- const result =
- await ctx.SubscriptionController.checkSubscriptionPauseStatus(ctx.user)
- expect(result).to.deep.equal({ isPaused: false })
- })
- })
- describe('previewAddonPurchase', function () {
- beforeEach(function (ctx) {
- ctx.req = new MockRequest(vi)
- ctx.req.params = { addOnCode: 'assistant' }
- ctx.req.query = { purchaseReferrer: 'fake-referrer' }
- ctx.res = new MockResponse(vi)
- ctx.Modules.promises.hooks.fire
- .withArgs('getPaymentMethod')
- .resolves(['fake-method'])
- ctx.SubscriptionLocator.promises.getUsersSubscription.resolves(null)
- })
- describe('when user has manual or custom subscription', function () {
- it('should redirect with ai-assist-unavailable when subscription has customAccount = true', async function (ctx) {
- const customSubscription = {
- _id: 'sub-123',
- customAccount: true,
- collectionMethod: 'automatic',
- }
- ctx.SubscriptionLocator.promises.getUsersSubscription.resolves(
- customSubscription
- )
- ctx.res.redirect = sinon.stub()
- await ctx.SubscriptionController.previewAddonPurchase(ctx.req, ctx.res)
- expect(ctx.res.redirect).to.have.been.calledWith(
- '/user/subscription?redirect-reason=ai-assist-unavailable'
- )
- })
- it('should redirect with ai-assist-unavailable when subscription has collectionMethod = manual', async function (ctx) {
- const manualSubscription = {
- _id: 'sub-123',
- customAccount: false,
- collectionMethod: 'manual',
- }
- ctx.SubscriptionLocator.promises.getUsersSubscription.resolves(
- manualSubscription
- )
- ctx.res.redirect = sinon.stub()
- await ctx.SubscriptionController.previewAddonPurchase(ctx.req, ctx.res)
- expect(ctx.res.redirect).to.have.been.calledWith(
- '/user/subscription?redirect-reason=ai-assist-unavailable'
- )
- })
- it('should redirect with ai-assist-unavailable when subscription has both customAccount and manual collection', async function (ctx) {
- const customManualSubscription = {
- _id: 'sub-123',
- customAccount: true,
- collectionMethod: 'manual',
- }
- ctx.SubscriptionLocator.promises.getUsersSubscription.resolves(
- customManualSubscription
- )
- ctx.res.redirect = sinon.stub()
- await ctx.SubscriptionController.previewAddonPurchase(ctx.req, ctx.res)
- expect(ctx.res.redirect).to.have.been.calledWith(
- '/user/subscription?redirect-reason=ai-assist-unavailable'
- )
- })
- })
- describe('when user has normal subscription', function () {
- it('should proceed with preview when subscription is not manual or custom', async function (ctx) {
- const normalSubscription = {
- _id: 'sub-123',
- customAccount: false,
- collectionMethod: 'automatic',
- }
- ctx.SubscriptionLocator.promises.getUsersSubscription.resolves(
- normalSubscription
- )
- ctx.res.render = sinon.stub()
- await ctx.SubscriptionController.previewAddonPurchase(ctx.req, ctx.res)
- expect(ctx.res.render).to.have.been.calledWith(
- 'subscriptions/preview-change',
- sinon.match({
- changePreview: sinon.match.object,
- purchaseReferrer: 'fake-referrer',
- redirectedPaymentErrorCode: undefined,
- })
- )
- expect(
- ctx.SubscriptionHandler.promises.previewAddonPurchase
- ).to.have.been.calledWith(ctx.user._id, 'assistant')
- })
- it('should pass redirectedPaymentErrorCode to the view when errorCode query param is present', async function (ctx) {
- const normalSubscription = {
- _id: 'sub-123',
- customAccount: false,
- collectionMethod: 'automatic',
- }
- ctx.SubscriptionLocator.promises.getUsersSubscription.resolves(
- normalSubscription
- )
- ctx.req.query.errorCode = 'payment_failed'
- ctx.res.render = sinon.stub()
- await ctx.SubscriptionController.previewAddonPurchase(ctx.req, ctx.res)
- expect(ctx.res.render).to.have.been.calledWith(
- 'subscriptions/preview-change',
- sinon.match({
- changePreview: sinon.match.object,
- purchaseReferrer: 'fake-referrer',
- redirectedPaymentErrorCode: 'payment_failed',
- })
- )
- })
- it('should proceed with preview when customAccount is undefined and collectionMethod is automatic', async function (ctx) {
- const normalSubscription = {
- _id: 'sub-123',
- // customAccount: undefined (not set)
- collectionMethod: 'automatic',
- }
- ctx.SubscriptionLocator.promises.getUsersSubscription.resolves(
- normalSubscription
- )
- ctx.res.render = sinon.stub()
- await ctx.SubscriptionController.previewAddonPurchase(ctx.req, ctx.res)
- expect(ctx.res.render).to.have.been.calledWith(
- 'subscriptions/preview-change'
- )
- expect(
- ctx.SubscriptionHandler.promises.previewAddonPurchase
- ).to.have.been.calledWith(ctx.user._id, 'assistant')
- })
- })
- })
- })
|