From a15de4e18c867dc578e7040b710b32bae95b0706 Mon Sep 17 00:00:00 2001 From: Kristina <7614497+khjrtbrg@users.noreply.github.com> Date: Thu, 3 Jul 2025 09:21:42 +0200 Subject: [PATCH] Merge pull request #26793 from overleaf/mf-add-missing-public-key-on-purchase-addon [web] Add missing publicKey to purchase add-on flow when user need to authenticate their payment via 3ds secure flow GitOrigin-RevId: cc330cb8dad501479bbb3c5c5b4fc32ef9d36921 --- .../Subscription/SubscriptionController.js | 1 + .../SubscriptionControllerTests.js | 109 ++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/services/web/app/src/Features/Subscription/SubscriptionController.js b/services/web/app/src/Features/Subscription/SubscriptionController.js index 4e3ad8be27..0cf1dd959c 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionController.js +++ b/services/web/app/src/Features/Subscription/SubscriptionController.js @@ -453,6 +453,7 @@ async function purchaseAddon(req, res, next) { return res.status(402).json({ message: 'Payment action required', clientSecret: err.info.clientSecret, + publicKey: err.info.publicKey, }) } else { if (err instanceof Error) { diff --git a/services/web/test/unit/src/Subscription/SubscriptionControllerTests.js b/services/web/test/unit/src/Subscription/SubscriptionControllerTests.js index e0b61417f1..61f9debc8d 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionControllerTests.js +++ b/services/web/test/unit/src/Subscription/SubscriptionControllerTests.js @@ -7,6 +7,9 @@ const modulePath = '../../../../app/src/Features/Subscription/SubscriptionController' const SubscriptionErrors = require('../../../../app/src/Features/Subscription/Errors') const SubscriptionHelper = require('../../../../app/src/Features/Subscription/SubscriptionHelper') +const { + AI_ADD_ON_CODE, +} = require('../../../../app/src/Features/Subscription/PaymentProviderEntities') const mockSubscriptions = { 'subscription-123-active': { @@ -59,6 +62,7 @@ describe('SubscriptionController', function () { syncSubscription: sinon.stub().resolves(), attemptPaypalInvoiceCollection: sinon.stub().resolves(), startFreeTrial: sinon.stub().resolves(), + purchaseAddon: sinon.stub().resolves(), }, } @@ -635,4 +639,109 @@ describe('SubscriptionController', function () { }) }) }) + + describe('purchaseAddon', function () { + beforeEach(function () { + this.SessionManager.getSessionUser.returns(this.user) // Make sure getSessionUser returns the user + this.next = sinon.stub() + this.req.params = { addOnCode: AI_ADD_ON_CODE } // Mock add-on code + }) + + it('should return 200 on successful purchase of AI add-on', async function () { + await this.SubscriptionController.purchaseAddon( + this.req, + this.res, + this.next + ) + this.res.sendStatus = sinon.spy() + + await this.SubscriptionController.purchaseAddon( + this.req, + this.res, + this.next + ) + + expect(this.SubscriptionHandler.promises.purchaseAddon).to.have.been + .called + expect( + this.SubscriptionHandler.promises.purchaseAddon + ).to.have.been.calledWith(this.user._id, AI_ADD_ON_CODE, 1) + expect( + this.FeaturesUpdater.promises.refreshFeatures + ).to.have.been.calledWith(this.user._id, 'add-on-purchase') + expect(this.res.sendStatus).to.have.been.calledWith(200) + expect(this.logger.debug).to.have.been.calledWith( + { userId: this.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 () { + this.req.params = { addOnCode: 'some-other-addon' } + this.res.sendStatus = sinon.spy() + + await this.SubscriptionController.purchaseAddon( + this.req, + this.res, + this.next + ) + + expect(this.SubscriptionHandler.promises.purchaseAddon).to.not.have.been + .called + expect(this.FeaturesUpdater.promises.refreshFeatures).to.not.have.been + .called + expect(this.res.sendStatus).to.have.been.calledWith(404) + }) + + it('should handle DuplicateAddOnError and send badRequest while sending 200', async function () { + this.req.params.addOnCode = AI_ADD_ON_CODE + this.SubscriptionHandler.promises.purchaseAddon.rejects( + new SubscriptionErrors.DuplicateAddOnError() + ) + + await this.SubscriptionController.purchaseAddon( + this.req, + this.res, + this.next + ) + + expect(this.HttpErrorHandler.badRequest).to.have.been.calledWith( + this.req, + this.res, + 'Your subscription already includes this add-on', + { addon: AI_ADD_ON_CODE } + ) + expect( + this.FeaturesUpdater.promises.refreshFeatures + ).to.have.been.calledWith(this.user._id, 'add-on-purchase') + expect(this.res.sendStatus).to.have.been.calledWith(200) + }) + + it('should handle PaymentActionRequiredError and return 402 with details', async function () { + this.req.params.addOnCode = AI_ADD_ON_CODE + const paymentError = new SubscriptionErrors.PaymentActionRequiredError({ + clientSecret: 'secret123', + publicKey: 'pubkey456', + }) + this.SubscriptionHandler.promises.purchaseAddon.rejects(paymentError) + + await this.SubscriptionController.purchaseAddon( + this.req, + this.res, + this.next + ) + + this.res.status.calledWith(402).should.equal(true) + this.res.json + .calledWith({ + message: 'Payment action required', + clientSecret: 'secret123', + publicKey: 'pubkey456', + }) + .should.equal(true) + + expect(this.FeaturesUpdater.promises.refreshFeatures).to.not.have.been + .called + }) + }) })