SubscriptionControllerTests.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747
  1. const SandboxedModule = require('sandboxed-module')
  2. const sinon = require('sinon')
  3. const { assert, expect } = require('chai')
  4. const MockRequest = require('../helpers/MockRequest')
  5. const MockResponse = require('../helpers/MockResponse')
  6. const modulePath =
  7. '../../../../app/src/Features/Subscription/SubscriptionController'
  8. const SubscriptionErrors = require('../../../../app/src/Features/Subscription/Errors')
  9. const SubscriptionHelper = require('../../../../app/src/Features/Subscription/SubscriptionHelper')
  10. const {
  11. AI_ADD_ON_CODE,
  12. } = require('../../../../app/src/Features/Subscription/AiHelper')
  13. const mockSubscriptions = {
  14. 'subscription-123-active': {
  15. uuid: 'subscription-123-active',
  16. plan: {
  17. name: 'Gold',
  18. plan_code: 'gold',
  19. },
  20. current_period_ends_at: new Date(),
  21. state: 'active',
  22. unit_amount_in_cents: 999,
  23. account: {
  24. account_code: 'user-123',
  25. },
  26. },
  27. }
  28. describe('SubscriptionController', function () {
  29. beforeEach(function () {
  30. this.user = {
  31. email: 'tom@yahoo.com',
  32. _id: 'one',
  33. signUpDate: new Date('2000-10-01'),
  34. emails: [{ email: 'tom@yahoo.com', confirmedAt: new Date('2000-10-02') }],
  35. }
  36. this.activeRecurlySubscription =
  37. mockSubscriptions['subscription-123-active']
  38. this.SessionManager = {
  39. getLoggedInUser: sinon.stub().callsArgWith(1, null, this.user),
  40. getLoggedInUserId: sinon.stub().returns(this.user._id),
  41. getSessionUser: sinon.stub().returns(this.user),
  42. isUserLoggedIn: sinon.stub().returns(true),
  43. }
  44. this.SubscriptionHandler = {
  45. createSubscription: sinon.stub().callsArgWith(3),
  46. updateSubscription: sinon.stub().callsArgWith(3),
  47. reactivateSubscription: sinon.stub().callsArgWith(1),
  48. cancelSubscription: sinon.stub().callsArgWith(1),
  49. syncSubscription: sinon.stub().yields(),
  50. attemptPaypalInvoiceCollection: sinon.stub().yields(),
  51. startFreeTrial: sinon.stub(),
  52. promises: {
  53. createSubscription: sinon.stub().resolves(),
  54. updateSubscription: sinon.stub().resolves(),
  55. reactivateSubscription: sinon.stub().resolves(),
  56. cancelSubscription: sinon.stub().resolves(),
  57. pauseSubscription: sinon.stub().resolves(),
  58. resumeSubscription: sinon.stub().resolves(),
  59. syncSubscription: sinon.stub().resolves(),
  60. attemptPaypalInvoiceCollection: sinon.stub().resolves(),
  61. startFreeTrial: sinon.stub().resolves(),
  62. purchaseAddon: sinon.stub().resolves(),
  63. },
  64. }
  65. this.LimitationsManager = {
  66. hasPaidSubscription: sinon.stub(),
  67. userHasSubscription: sinon
  68. .stub()
  69. .yields(null, { hasSubscription: false }),
  70. promises: {
  71. hasPaidSubscription: sinon.stub().resolves(),
  72. userHasSubscription: sinon.stub().resolves({ hasSubscription: false }),
  73. },
  74. }
  75. this.SubscriptionViewModelBuilder = {
  76. buildUsersSubscriptionViewModel: sinon.stub().callsArgWith(1, null, {}),
  77. buildPlansList: sinon.stub(),
  78. promises: {
  79. buildUsersSubscriptionViewModel: sinon.stub().resolves({}),
  80. },
  81. buildPlansListForSubscriptionDash: sinon
  82. .stub()
  83. .returns({ plans: [], planCodesChangingAtTermEnd: [] }),
  84. }
  85. this.settings = {
  86. coupon_codes: {
  87. upgradeToAnnualPromo: {
  88. student: 'STUDENTCODEHERE',
  89. collaborator: 'COLLABORATORCODEHERE',
  90. },
  91. },
  92. groupPlanModalOptions: {
  93. plan_codes: [],
  94. currencies: [
  95. {
  96. display: 'GBP (£)',
  97. code: 'GBP',
  98. },
  99. ],
  100. sizes: ['42'],
  101. usages: [{ code: 'foo', display: 'Foo' }],
  102. },
  103. apis: {
  104. recurly: {
  105. subdomain: 'sl',
  106. },
  107. },
  108. siteUrl: 'http://de.overleaf.dev:3000',
  109. }
  110. this.AuthorizationManager = {
  111. promises: {
  112. isUserSiteAdmin: sinon.stub().resolves(false),
  113. },
  114. }
  115. this.GeoIpLookup = {
  116. isValidCurrencyParam: sinon.stub().returns(true),
  117. getCurrencyCode: sinon.stub().yields('USD', 'US'),
  118. promises: {
  119. getCurrencyCode: sinon.stub().resolves({
  120. countryCode: 'US',
  121. currencyCode: 'USD',
  122. }),
  123. },
  124. }
  125. this.UserGetter = {
  126. getUser: sinon.stub().callsArgWith(2, null, this.user),
  127. promises: {
  128. getUser: sinon.stub().resolves(this.user),
  129. getWritefullData: sinon
  130. .stub()
  131. .resolves({ isPremium: false, premiumSource: null }),
  132. },
  133. }
  134. this.SplitTestV2Hander = {
  135. promises: {
  136. getAssignment: sinon.stub().resolves({ variant: 'default' }),
  137. },
  138. }
  139. this.Features = {
  140. hasFeature: sinon.stub().returns(false),
  141. }
  142. this.SubscriptionController = SandboxedModule.require(modulePath, {
  143. requires: {
  144. '../Authorization/AuthorizationManager': this.AuthorizationManager,
  145. '../SplitTests/SplitTestHandler': this.SplitTestV2Hander,
  146. '../Authentication/SessionManager': this.SessionManager,
  147. './SubscriptionHandler': this.SubscriptionHandler,
  148. './SubscriptionHelper': SubscriptionHelper,
  149. './SubscriptionViewModelBuilder': this.SubscriptionViewModelBuilder,
  150. './LimitationsManager': this.LimitationsManager,
  151. '../../infrastructure/GeoIpLookup': this.GeoIpLookup,
  152. '@overleaf/settings': this.settings,
  153. '../User/UserGetter': this.UserGetter,
  154. './RecurlyWrapper': (this.RecurlyWrapper = {
  155. promises: {
  156. updateAccountEmailAddress: sinon.stub().resolves(),
  157. },
  158. }),
  159. './RecurlyEventHandler': {
  160. sendRecurlyAnalyticsEvent: sinon.stub().resolves(),
  161. },
  162. './FeaturesUpdater': (this.FeaturesUpdater = {
  163. promises: {
  164. refreshFeatures: sinon.stub().resolves({ features: {} }),
  165. },
  166. }),
  167. './GroupPlansData': (this.GroupPlansData = {}),
  168. './V1SubscriptionManager': (this.V1SubscriptionManager = {}),
  169. '../Errors/HttpErrorHandler': (this.HttpErrorHandler = {
  170. unprocessableEntity: sinon.stub().callsFake((req, res, message) => {
  171. res.status(422)
  172. res.json({ message })
  173. }),
  174. badRequest: sinon.stub().callsFake((req, res, message) => {
  175. res.status(400)
  176. res.json({ message })
  177. }),
  178. }),
  179. './Errors': SubscriptionErrors,
  180. '../Analytics/AnalyticsManager': (this.AnalyticsManager = {
  181. recordEventForUser: sinon.stub(),
  182. recordEventForUserInBackground: sinon.stub(),
  183. recordEventForSession: sinon.stub(),
  184. setUserPropertyForUser: sinon.stub(),
  185. }),
  186. '../../infrastructure/Modules': (this.Modules = {
  187. promises: { hooks: { fire: sinon.stub().resolves() } },
  188. }),
  189. '../../infrastructure/Features': this.Features,
  190. '../../util/currency': (this.currency = {
  191. formatCurrency: sinon.stub(),
  192. }),
  193. '../../models/User': {
  194. User: {
  195. findById: sinon.stub().resolves(this.user),
  196. },
  197. },
  198. },
  199. })
  200. this.res = new MockResponse()
  201. this.req = new MockRequest()
  202. this.req.body = {}
  203. this.req.query = { planCode: '123123' }
  204. this.stubbedCurrencyCode = 'GBP'
  205. })
  206. describe('successfulSubscription', function () {
  207. it('without a personal subscription', function (done) {
  208. this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel.resolves(
  209. {}
  210. )
  211. this.res.redirect = url => {
  212. url.should.equal('/user/subscription/plans')
  213. done()
  214. }
  215. this.SubscriptionController.successfulSubscription(this.req, this.res)
  216. })
  217. it('with a personal subscription', function (done) {
  218. this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel.resolves(
  219. {
  220. personalSubscription: 'foo',
  221. }
  222. )
  223. this.res.render = (url, variables) => {
  224. url.should.equal('subscriptions/successful-subscription-react')
  225. assert.deepEqual(variables, {
  226. title: 'thank_you',
  227. personalSubscription: 'foo',
  228. postCheckoutRedirect: undefined,
  229. user: {
  230. _id: this.user._id,
  231. features: this.user.features,
  232. },
  233. })
  234. done()
  235. }
  236. this.SubscriptionController.successfulSubscription(this.req, this.res)
  237. })
  238. it('with an error', function (done) {
  239. this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel.resolves(
  240. undefined
  241. )
  242. this.SubscriptionController.successfulSubscription(
  243. this.req,
  244. this.res,
  245. error => {
  246. assert.isNotNull(error)
  247. done()
  248. }
  249. )
  250. })
  251. })
  252. describe('userSubscriptionPage', function () {
  253. beforeEach(function (done) {
  254. this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel.resolves(
  255. {
  256. personalSubscription: (this.personalSubscription = {
  257. 'personal-subscription': 'mock',
  258. }),
  259. memberGroupSubscriptions: (this.memberGroupSubscriptions = {
  260. 'group-subscriptions': 'mock',
  261. }),
  262. }
  263. )
  264. this.SubscriptionViewModelBuilder.buildPlansList.returns(
  265. (this.plans = { plans: 'mock' })
  266. )
  267. this.SubscriptionViewModelBuilder.buildPlansListForSubscriptionDash.returns(
  268. {
  269. plans: this.plans,
  270. planCodesChangingAtTermEnd: [],
  271. }
  272. )
  273. this.LimitationsManager.promises.userHasSubscription.resolves({
  274. hasSubscription: false,
  275. })
  276. this.res.render = (view, data) => {
  277. this.data = data
  278. expect(view).to.equal('subscriptions/dashboard-react')
  279. done()
  280. }
  281. this.SubscriptionController.userSubscriptionPage(this.req, this.res, done)
  282. })
  283. it('should load the personal, groups and v1 subscriptions', function () {
  284. expect(this.data.personalSubscription).to.deep.equal(
  285. this.personalSubscription
  286. )
  287. expect(this.data.memberGroupSubscriptions).to.deep.equal(
  288. this.memberGroupSubscriptions
  289. )
  290. })
  291. it('should load the user', function () {
  292. expect(this.data.user).to.deep.equal(this.user)
  293. })
  294. it('should load the plans', function () {
  295. expect(this.data.plans).to.deep.equal(this.plans)
  296. })
  297. it('should load an empty list of groups with settings available', function () {
  298. expect(this.data.groupSettingsEnabledFor).to.deep.equal([])
  299. })
  300. })
  301. describe('updateAccountEmailAddress via put', function () {
  302. beforeEach(function () {
  303. this.req.body = {
  304. account_email: 'current_account_email@overleaf.com',
  305. }
  306. })
  307. it('should send the user and subscriptionId to "updateAccountEmailAddress" hooks', async function () {
  308. this.res.sendStatus = sinon.spy()
  309. await this.SubscriptionController.updateAccountEmailAddress(
  310. this.req,
  311. this.res
  312. )
  313. expect(this.Modules.promises.hooks.fire).to.have.been.calledWith(
  314. 'updateAccountEmailAddress',
  315. this.user._id,
  316. this.user.email
  317. )
  318. })
  319. it('should respond with 200', async function () {
  320. this.res.sendStatus = sinon.spy()
  321. await this.SubscriptionController.updateAccountEmailAddress(
  322. this.req,
  323. this.res
  324. )
  325. this.res.sendStatus.calledWith(200).should.equal(true)
  326. })
  327. it('should send the error to the next handler when updating recurly account email fails', async function () {
  328. this.Modules.promises.hooks.fire
  329. .withArgs('updateAccountEmailAddress', this.user._id, this.user.email)
  330. .rejects(new Error())
  331. this.next = sinon.spy(error => {
  332. expect(error).to.be.instanceOf(Error)
  333. })
  334. await this.SubscriptionController.updateAccountEmailAddress(
  335. this.req,
  336. this.res,
  337. this.next
  338. )
  339. expect(this.next.calledOnce).to.be.true
  340. })
  341. })
  342. describe('reactivateSubscription', function () {
  343. describe('when the user has permission', function () {
  344. beforeEach(function (done) {
  345. this.res = {
  346. redirect() {
  347. done()
  348. },
  349. }
  350. this.req.assertPermission = sinon.stub()
  351. this.next = sinon.stub().callsFake(error => {
  352. done(error)
  353. })
  354. sinon.spy(this.res, 'redirect')
  355. this.SubscriptionController.reactivateSubscription(
  356. this.req,
  357. this.res,
  358. this.next
  359. )
  360. })
  361. it('should assert the user has permission to reactivate their subscription', function (done) {
  362. this.req.assertPermission
  363. .calledWith('reactivate-subscription')
  364. .should.equal(true)
  365. done()
  366. })
  367. it('should tell the handler to reactivate this user', function (done) {
  368. this.SubscriptionHandler.reactivateSubscription
  369. .calledWith(this.user)
  370. .should.equal(true)
  371. done()
  372. })
  373. it('should redurect to the subscription page', function (done) {
  374. this.res.redirect.calledWith('/user/subscription').should.equal(true)
  375. done()
  376. })
  377. })
  378. describe('when the user does not have permission', function () {
  379. beforeEach(function (done) {
  380. this.res = {
  381. redirect() {
  382. done()
  383. },
  384. }
  385. this.req.assertPermission = sinon.stub().throws()
  386. this.next = sinon.stub().callsFake(() => {
  387. done()
  388. })
  389. sinon.spy(this.res, 'redirect')
  390. this.SubscriptionController.reactivateSubscription(
  391. this.req,
  392. this.res,
  393. this.next
  394. )
  395. })
  396. it('should not reactivate the user', function (done) {
  397. this.req.assertPermission = sinon.stub().throws()
  398. this.SubscriptionHandler.reactivateSubscription.called.should.equal(
  399. false
  400. )
  401. done()
  402. })
  403. it('should call next with an error', function (done) {
  404. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  405. done()
  406. })
  407. })
  408. })
  409. describe('pauseSubscription', function () {
  410. it('should throw an error if no pause length is provided', async function () {
  411. this.res = new MockResponse()
  412. this.req = new MockRequest()
  413. this.next = sinon.stub()
  414. await this.SubscriptionController.pauseSubscription(
  415. this.req,
  416. this.res,
  417. this.next
  418. )
  419. expect(this.res.statusCode).to.equal(400)
  420. })
  421. it('should throw an error if an invalid pause length is provided', async function () {
  422. this.res = new MockResponse()
  423. this.req = new MockRequest()
  424. this.req.params = { pauseCycles: -10 }
  425. this.next = sinon.stub()
  426. await this.SubscriptionController.pauseSubscription(
  427. this.req,
  428. this.res,
  429. this.next
  430. )
  431. expect(this.res.statusCode).to.equal(400)
  432. })
  433. it('should return a 200 when requesting a pause', async function () {
  434. this.res = new MockResponse()
  435. this.req = new MockRequest()
  436. this.req.params = { pauseCycles: 3 }
  437. this.next = sinon.stub()
  438. await this.SubscriptionController.pauseSubscription(
  439. this.req,
  440. this.res,
  441. this.next
  442. )
  443. expect(this.res.statusCode).to.equal(200)
  444. })
  445. })
  446. describe('resumeSubscription', function () {
  447. it('should return a 200 when resuming a subscription', async function () {
  448. this.res = new MockResponse()
  449. this.req = new MockRequest()
  450. this.next = sinon.stub()
  451. await this.SubscriptionController.resumeSubscription(
  452. this.req,
  453. this.res,
  454. this.next
  455. )
  456. expect(this.res.statusCode).to.equal(200)
  457. })
  458. })
  459. describe('cancelSubscription', function () {
  460. it('should tell the handler to cancel this user', async function () {
  461. this.next = sinon.stub()
  462. await this.SubscriptionController.cancelSubscription(
  463. this.req,
  464. this.res,
  465. this.next
  466. )
  467. this.SubscriptionHandler.promises.cancelSubscription
  468. .calledWith(this.user)
  469. .should.equal(true)
  470. })
  471. it('should return a 200 on success', async function () {
  472. this.next = sinon.stub()
  473. await this.SubscriptionController.cancelSubscription(
  474. this.req,
  475. this.res,
  476. this.next
  477. )
  478. expect(this.res.statusCode).to.equal(200)
  479. })
  480. it('should call next with error', async function () {
  481. this.SubscriptionHandler.promises.cancelSubscription.rejects(
  482. new Error('cancel error')
  483. )
  484. this.next = sinon.stub()
  485. await this.SubscriptionController.cancelSubscription(
  486. this.req,
  487. this.res,
  488. this.next
  489. )
  490. this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
  491. })
  492. })
  493. describe('recurly callback', function () {
  494. describe('with a sync subscription request', function () {
  495. beforeEach(function (done) {
  496. this.req = {
  497. body: {
  498. expired_subscription_notification: {
  499. account: {
  500. account_code: this.user._id,
  501. },
  502. subscription: {
  503. uuid: this.activeRecurlySubscription.uuid,
  504. plan: {
  505. plan_code: 'collaborator',
  506. state: 'active',
  507. },
  508. },
  509. },
  510. },
  511. }
  512. this.res = {
  513. sendStatus() {
  514. done()
  515. },
  516. }
  517. sinon.spy(this.res, 'sendStatus')
  518. this.SubscriptionController.recurlyCallback(this.req, this.res)
  519. })
  520. it('should tell the SubscriptionHandler to process the recurly callback', function (done) {
  521. this.SubscriptionHandler.syncSubscription.called.should.equal(true)
  522. done()
  523. })
  524. it('should send a 200', function (done) {
  525. this.res.sendStatus.calledWith(200)
  526. done()
  527. })
  528. })
  529. describe('with a billing info updated request', function () {
  530. beforeEach(function (done) {
  531. this.req = {
  532. body: {
  533. billing_info_updated_notification: {
  534. account: {
  535. account_code: 'mock-account-code',
  536. },
  537. },
  538. },
  539. }
  540. this.res = {
  541. sendStatus() {
  542. done()
  543. },
  544. }
  545. sinon.spy(this.res, 'sendStatus')
  546. this.SubscriptionController.recurlyCallback(this.req, this.res)
  547. })
  548. it('should call attemptPaypalInvoiceCollection', function (done) {
  549. this.SubscriptionHandler.attemptPaypalInvoiceCollection
  550. .calledWith('mock-account-code')
  551. .should.equal(true)
  552. done()
  553. })
  554. it('should send a 200', function (done) {
  555. this.res.sendStatus.calledWith(200)
  556. done()
  557. })
  558. })
  559. describe('with a non-actionable request', function () {
  560. beforeEach(function (done) {
  561. this.user.id = this.activeRecurlySubscription.account.account_code
  562. this.req = {
  563. body: {
  564. renewed_subscription_notification: {
  565. account: {
  566. account_code: this.user._id,
  567. },
  568. subscription: {
  569. uuid: this.activeRecurlySubscription.uuid,
  570. plan: {
  571. plan_code: 'collaborator',
  572. state: 'active',
  573. },
  574. },
  575. },
  576. },
  577. }
  578. this.res = {
  579. sendStatus() {
  580. done()
  581. },
  582. }
  583. sinon.spy(this.res, 'sendStatus')
  584. this.SubscriptionController.recurlyCallback(this.req, this.res)
  585. })
  586. it('should not call the subscriptionshandler', function () {
  587. this.SubscriptionHandler.syncSubscription.called.should.equal(false)
  588. this.SubscriptionHandler.attemptPaypalInvoiceCollection.called.should.equal(
  589. false
  590. )
  591. })
  592. it('should respond with a 200 status', function () {
  593. this.res.sendStatus.calledWith(200)
  594. })
  595. })
  596. })
  597. describe('purchaseAddon', function () {
  598. beforeEach(function () {
  599. this.SessionManager.getSessionUser.returns(this.user) // Make sure getSessionUser returns the user
  600. this.next = sinon.stub()
  601. this.req.params = { addOnCode: AI_ADD_ON_CODE } // Mock add-on code
  602. })
  603. it('should return 200 on successful purchase of AI add-on', async function () {
  604. await this.SubscriptionController.purchaseAddon(
  605. this.req,
  606. this.res,
  607. this.next
  608. )
  609. this.res.sendStatus = sinon.spy()
  610. await this.SubscriptionController.purchaseAddon(
  611. this.req,
  612. this.res,
  613. this.next
  614. )
  615. expect(this.SubscriptionHandler.promises.purchaseAddon).to.have.been
  616. .called
  617. expect(
  618. this.SubscriptionHandler.promises.purchaseAddon
  619. ).to.have.been.calledWith(this.user._id, AI_ADD_ON_CODE, 1)
  620. expect(
  621. this.FeaturesUpdater.promises.refreshFeatures
  622. ).to.have.been.calledWith(this.user._id, 'add-on-purchase')
  623. expect(this.res.sendStatus).to.have.been.calledWith(200)
  624. expect(this.logger.debug).to.have.been.calledWith(
  625. { userId: this.user._id, addOnCode: AI_ADD_ON_CODE },
  626. 'purchasing add-ons'
  627. )
  628. })
  629. it('should return 404 if the add-on code is not AI_ADD_ON_CODE', async function () {
  630. this.req.params = { addOnCode: 'some-other-addon' }
  631. this.res.sendStatus = sinon.spy()
  632. await this.SubscriptionController.purchaseAddon(
  633. this.req,
  634. this.res,
  635. this.next
  636. )
  637. expect(this.SubscriptionHandler.promises.purchaseAddon).to.not.have.been
  638. .called
  639. expect(this.FeaturesUpdater.promises.refreshFeatures).to.not.have.been
  640. .called
  641. expect(this.res.sendStatus).to.have.been.calledWith(404)
  642. })
  643. it('should handle DuplicateAddOnError and send badRequest while sending 200', async function () {
  644. this.req.params.addOnCode = AI_ADD_ON_CODE
  645. this.SubscriptionHandler.promises.purchaseAddon.rejects(
  646. new SubscriptionErrors.DuplicateAddOnError()
  647. )
  648. await this.SubscriptionController.purchaseAddon(
  649. this.req,
  650. this.res,
  651. this.next
  652. )
  653. expect(this.HttpErrorHandler.badRequest).to.have.been.calledWith(
  654. this.req,
  655. this.res,
  656. 'Your subscription already includes this add-on',
  657. { addon: AI_ADD_ON_CODE }
  658. )
  659. expect(
  660. this.FeaturesUpdater.promises.refreshFeatures
  661. ).to.have.been.calledWith(this.user._id, 'add-on-purchase')
  662. expect(this.res.sendStatus).to.have.been.calledWith(200)
  663. })
  664. it('should handle PaymentActionRequiredError and return 402 with details', async function () {
  665. this.req.params.addOnCode = AI_ADD_ON_CODE
  666. const paymentError = new SubscriptionErrors.PaymentActionRequiredError({
  667. clientSecret: 'secret123',
  668. publicKey: 'pubkey456',
  669. })
  670. this.SubscriptionHandler.promises.purchaseAddon.rejects(paymentError)
  671. await this.SubscriptionController.purchaseAddon(
  672. this.req,
  673. this.res,
  674. this.next
  675. )
  676. this.res.status.calledWith(402).should.equal(true)
  677. this.res.json
  678. .calledWith({
  679. message: 'Payment action required',
  680. clientSecret: 'secret123',
  681. publicKey: 'pubkey456',
  682. })
  683. .should.equal(true)
  684. expect(this.FeaturesUpdater.promises.refreshFeatures).to.not.have.been
  685. .called
  686. })
  687. })
  688. })