From 5da87640e4881226ae9f6ecf6d29d372dcf25676 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Fri, 13 Apr 2018 12:41:41 -0500 Subject: [PATCH] Apple pay tests (#10248) * Added more tests for verifyGemPurchase * Added more tests for subscribe * Added user is subscribed check * Reverted gulp task * Added existence check --- test/api/v3/unit/libs/payments/apple.test.js | 183 +++++++++++++++---- website/server/libs/payments/apple.js | 15 +- 2 files changed, 158 insertions(+), 40 deletions(-) diff --git a/test/api/v3/unit/libs/payments/apple.test.js b/test/api/v3/unit/libs/payments/apple.test.js index 7dcbbf421f..d88967634d 100644 --- a/test/api/v3/unit/libs/payments/apple.test.js +++ b/test/api/v3/unit/libs/payments/apple.test.js @@ -57,6 +57,18 @@ describe('Apple Payments', () => { }); }); + it('should throw an error if getPurchaseData is invalid', async () => { + iapGetPurchaseDataStub.restore(); + iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData').returns([]); + + await expect(applePayments.verifyGemPurchase(user, receipt, headers)) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 401, + name: 'NotAuthorized', + message: applePayments.constants.RESPONSE_NO_ITEM_PURCHASED, + }); + }); + it('errors if the user cannot purchase gems', async () => { sinon.stub(user, 'canGetGems').returnsPromise().resolves(false); await expect(applePayments.verifyGemPurchase(user, receipt, headers)) @@ -69,27 +81,76 @@ describe('Apple Payments', () => { user.canGetGems.restore(); }); - it('purchases gems', async () => { + it('errors if amount does not exist', async () => { sinon.stub(user, 'canGetGems').returnsPromise().resolves(true); - await applePayments.verifyGemPurchase(user, receipt, headers); + iapGetPurchaseDataStub.restore(); + iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData') + .returns([{productId: 'badProduct', + transactionId: token, + }]); - expect(iapSetupStub).to.be.calledOnce; - expect(iapValidateStub).to.be.calledOnce; - expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt); - expect(iapIsValidatedStub).to.be.calledOnce; - expect(iapIsValidatedStub).to.be.calledWith({}); - expect(iapGetPurchaseDataStub).to.be.calledOnce; + await expect(applePayments.verifyGemPurchase(user, receipt, headers)) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 401, + name: 'NotAuthorized', + message: applePayments.constants.RESPONSE_INVALID_ITEM, + }); - expect(paymentBuyGemsStub).to.be.calledOnce; - expect(paymentBuyGemsStub).to.be.calledWith({ - user, - paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE, - amount: 5.25, - headers, - }); - expect(user.canGetGems).to.be.calledOnce; user.canGetGems.restore(); }); + + const gemsCanPurchase = [ + { + productId: 'com.habitrpg.ios.Habitica.4gems', + amount: 1, + }, + { + productId: 'com.habitrpg.ios.Habitica.20gems', + amount: 5.25, + }, + { + productId: 'com.habitrpg.ios.Habitica.21gems', + amount: 5.25, + }, + { + productId: 'com.habitrpg.ios.Habitica.42gems', + amount: 10.5, + }, + { + productId: 'com.habitrpg.ios.Habitica.84gems', + amount: 21, + }, + ]; + + gemsCanPurchase.forEach(gemTest => { + it(`purchases ${gemTest.productId} gems`, async () => { + iapGetPurchaseDataStub.restore(); + iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData') + .returns([{productId: gemTest.productId, + transactionId: token, + }]); + + sinon.stub(user, 'canGetGems').returnsPromise().resolves(true); + await applePayments.verifyGemPurchase(user, receipt, headers); + + expect(iapSetupStub).to.be.calledOnce; + expect(iapValidateStub).to.be.calledOnce; + expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt); + expect(iapIsValidatedStub).to.be.calledOnce; + expect(iapIsValidatedStub).to.be.calledWith({}); + expect(iapGetPurchaseDataStub).to.be.calledOnce; + + expect(paymentBuyGemsStub).to.be.calledOnce; + expect(paymentBuyGemsStub).to.be.calledWith({ + user, + paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE, + amount: gemTest.amount, + headers, + }); + expect(user.canGetGems).to.be.calledOnce; + user.canGetGems.restore(); + }); + }); }); describe('subscribe', () => { @@ -133,7 +194,16 @@ describe('Apple Payments', () => { iapModule.validate.restore(); iapModule.isValidated.restore(); iapModule.getPurchaseData.restore(); - payments.createSubscription.restore(); + if (payments.createSubscription.restore) payments.createSubscription.restore(); + }); + + it('should throw an error if sku is empty', async () => { + await expect(applePayments.subscribe('', user, receipt, headers, nextPaymentProcessing)) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 400, + name: 'BadRequest', + message: i18n.t('missingSubscriptionCode'), + }); }); it('should throw an error if receipt is invalid', async () => { @@ -149,26 +219,69 @@ describe('Apple Payments', () => { }); }); - it('creates a user subscription', async () => { + const subOptions = [ + { + sku: 'subscription1month', + subKey: 'basic_earned', + }, + { + sku: 'com.habitrpg.ios.habitica.subscription.3month', + subKey: 'basic_3mo', + }, + { + sku: 'com.habitrpg.ios.habitica.subscription.6month', + subKey: 'basic_6mo', + }, + { + sku: 'com.habitrpg.ios.habitica.subscription.12month', + subKey: 'basic_12mo', + }, + ]; + subOptions.forEach(option => { + it(`creates a user subscription for ${option.sku}`, async () => { + iapModule.getPurchaseData.restore(); + iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData') + .returns([{ + expirationDate: moment.utc().add({day: 1}).toDate(), + productId: option.sku, + transactionId: token, + }]); + sub = common.content.subscriptionBlocks[option.subKey]; + + await applePayments.subscribe(option.sku, user, receipt, headers, nextPaymentProcessing); + + expect(iapSetupStub).to.be.calledOnce; + expect(iapValidateStub).to.be.calledOnce; + expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt); + expect(iapIsValidatedStub).to.be.calledOnce; + expect(iapIsValidatedStub).to.be.calledWith({}); + expect(iapGetPurchaseDataStub).to.be.calledOnce; + + expect(paymentsCreateSubscritionStub).to.be.calledOnce; + expect(paymentsCreateSubscritionStub).to.be.calledWith({ + user, + customerId: token, + paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE, + sub, + headers, + additionalData: receipt, + nextPaymentProcessing, + }); + }); + }); + + it('errors when a user is already subscribed', async () => { + payments.createSubscription.restore(); + user = new User(); + await applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing); - expect(iapSetupStub).to.be.calledOnce; - expect(iapValidateStub).to.be.calledOnce; - expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt); - expect(iapIsValidatedStub).to.be.calledOnce; - expect(iapIsValidatedStub).to.be.calledWith({}); - expect(iapGetPurchaseDataStub).to.be.calledOnce; - - expect(paymentsCreateSubscritionStub).to.be.calledOnce; - expect(paymentsCreateSubscritionStub).to.be.calledWith({ - user, - customerId: token, - paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE, - sub, - headers, - additionalData: receipt, - nextPaymentProcessing, - }); + await expect(applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing)) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 401, + name: 'NotAuthorized', + message: applePayments.constants.RESPONSE_ALREADY_USED, + }); }); }); diff --git a/website/server/libs/payments/apple.js b/website/server/libs/payments/apple.js index e3db444c03..c235d24c8e 100644 --- a/website/server/libs/payments/apple.js +++ b/website/server/libs/payments/apple.js @@ -28,7 +28,7 @@ api.verifyGemPurchase = async function verifyGemPurchase (user, receipt, headers let appleRes = await iap.validate(iap.APPLE, receipt); let isValidated = iap.isValidated(appleRes); if (!isValidated) throw new NotAuthorized(api.constants.RESPONSE_INVALID_RECEIPT); - let purchaseDataList = iap.getPurchaseData(appleRes); + const purchaseDataList = iap.getPurchaseData(appleRes); if (purchaseDataList.length === 0) throw new NotAuthorized(api.constants.RESPONSE_NO_ITEM_PURCHASED); let correctReceipt = false; @@ -81,8 +81,13 @@ api.verifyGemPurchase = async function verifyGemPurchase (user, receipt, headers return appleRes; }; -api.subscribe = async function subscribe (sku, user, receipt, headers, nextPaymentProcessing = undefined) { +api.subscribe = async function subscribe (sku, user, receipt, headers, nextPaymentProcessing) { + if (user && user.isSubscribed()) { + throw new NotAuthorized(this.constants.RESPONSE_ALREADY_USED); + } + if (!sku) throw new BadRequest(shared.i18n.t('missingSubscriptionCode')); + let subCode; switch (sku) { case 'subscription1month': @@ -98,13 +103,12 @@ api.subscribe = async function subscribe (sku, user, receipt, headers, nextPayme subCode = 'basic_12mo'; break; } - let sub = subCode ? shared.content.subscriptionBlocks[subCode] : false; + const sub = subCode ? shared.content.subscriptionBlocks[subCode] : false; if (!sub) throw new NotAuthorized(this.constants.RESPONSE_INVALID_ITEM); - await iap.setup(); let appleRes = await iap.validate(iap.APPLE, receipt); - let isValidated = iap.isValidated(appleRes); + const isValidated = iap.isValidated(appleRes); if (!isValidated) throw new NotAuthorized(api.constants.RESPONSE_INVALID_RECEIPT); let purchaseDataList = iap.getPurchaseData(appleRes); @@ -121,6 +125,7 @@ api.subscribe = async function subscribe (sku, user, receipt, headers, nextPayme break; } } + if (transactionId) { let existingUser = await User.findOne({ 'purchased.plan.customerId': transactionId,