From 6318b0828fcea3ee4af0134693f87a08f2d7dcee Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Sun, 14 Jun 2015 18:04:47 -0500 Subject: [PATCH 1/5] Add test for iOS in-app purchases --- test/api/inAppPurchases.coffee | 160 +++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 test/api/inAppPurchases.coffee diff --git a/test/api/inAppPurchases.coffee b/test/api/inAppPurchases.coffee new file mode 100644 index 0000000000..bf1cf43eb2 --- /dev/null +++ b/test/api/inAppPurchases.coffee @@ -0,0 +1,160 @@ +'use strict' + +app = require('../../website/src/server') +rewire = require('rewire') +sinon = require('sinon') +inApp = rewire('../../website/src/controllers/payments/iap') +iapMock = { } +inApp.__set__('iap', iapMock) + +describe 'In-App Purchases', -> + describe 'iOS', -> + req = { body: { transaction: { reciept: 'foo' } } } + res = { + locals: { user: { _id: 'user' } } + json: sinon.spy() + } + next = -> true + paymentSpy = sinon.spy() + inApp.__set__('payments.buyGems', paymentSpy) + + afterEach -> + paymentSpy.reset() + res.json.reset() + + context 'successful app purchase', -> + before -> + iapMock.setup = (cb)-> return cb(null) + iapMock.validate = (iapApple, iapBodyReciept, cb)-> return cb(null, true) + iapMock.isValidated = (appleRes)-> return appleRes + iapMock.getPurchaseData = (appleRes)-> + return [{ productId: 'com.habitrpg.ios.Habitica.20gems' }] + iapMock.APPLE = 'apple' + + it 'calls res.json with succesful result object', -> + expectedResObj = { + ok: true + data: true + } + + inApp.iosVerify(req, res, next) + + expect(res.json).to.be.calledOnce + expect(res.json).to.be.calledWith(expectedResObj) + + it 'calls payments.buyGems function', -> + inApp.iosVerify(req, res, next) + + expect(paymentSpy).to.be.calledOnce + expect(paymentSpy).to.be.calledWith({user: res.locals.user, paymentMethod:'IAP AppleStore'}) + + context 'error in setup', -> + before -> + iapMock.setup = (cb)-> return cb("error in setup") + + it 'calls res.json with setup error object', -> + expectedResObj = { + ok: false + data: 'IAP Error' + } + + inApp.iosVerify(req, res, next) + + expect(res.json).to.be.calledOnce + expect(res.json).to.be.calledWith(expectedResObj) + + it 'does not calls payments.buyGems function', -> + inApp.iosVerify(req, res, next) + + expect(paymentSpy).to.not.be.called + + context 'error in validation', -> + before -> + iapMock.setup = (cb)-> return cb(null) + iapMock.validate = (iapApple, iapBodyReciept, cb)-> return cb('error in validation', true) + + it 'calls res.json with validation error object', -> + expectedResObj = { + ok: false + data: { + code: 6778001 + message: 'error in validation' + } + } + + inApp.iosVerify(req, res, next) + + expect(res.json).to.be.calledOnce + expect(res.json).to.be.calledWith(expectedResObj) + + it 'does not calls payments.buyGems function', -> + inApp.iosVerify(req, res, next) + + expect(paymentSpy).to.not.be.called + + context 'iap is not valid', -> + before -> + iapMock.setup = (cb)-> return cb(null) + iapMock.validate = (iapApple, iapBodyReciept, cb)-> return cb(null, false) + iapMock.isValidated = (appleRes)-> return appleRes + + it 'does not call res.json', -> + inApp.iosVerify(req, res, next) + + expect(res.json).to.not.be.called + + context 'iap is valid but has no purchaseDataList', -> + before -> + iapMock.setup = (cb)-> return cb(null) + iapMock.validate = (iapApple, iapBodyReciept, cb)-> return cb(null, true) + iapMock.isValidated = (appleRes)-> return appleRes + iapMock.getPurchaseData = (appleRes)-> + return [] + iapMock.APPLE = 'apple' + + it 'calls res.json with succesful result object', -> + expectedResObj = { + ok: false + data: { + code: 6778001 + message: 'Incorrect receipt' + } + } + + inApp.iosVerify(req, res, next) + + expect(res.json).to.be.calledOnce + expect(res.json).to.be.calledWith(expectedResObj) + + it 'does not calls payments.buyGems function', -> + inApp.iosVerify(req, res, next) + + expect(paymentSpy).to.not.be.called + + context 'iap is valid, has purchaseDataList, but productId does not match', -> + before -> + iapMock.setup = (cb)-> return cb(null) + iapMock.validate = (iapApple, iapBodyReciept, cb)-> return cb(null, true) + iapMock.isValidated = (appleRes)-> return appleRes + iapMock.getPurchaseData = (appleRes)-> + return [{ productId: 'com.another.company' }] + iapMock.APPLE = 'apple' + + it 'calls res.json with incorrect reciept obj', -> + expectedResObj = { + ok: false + data: { + code: 6778001 + message: 'Incorrect receipt' + } + } + + inApp.iosVerify(req, res, next) + + expect(res.json).to.be.calledOnce + expect(res.json).to.be.calledWith(expectedResObj) + + it 'does not calls payments.buyGems function', -> + inApp.iosVerify(req, res, next) + + expect(paymentSpy).to.not.be.called From cd45fc06c662ebb13479f88aef348879d61a96a5 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Sun, 14 Jun 2015 18:07:23 -0500 Subject: [PATCH 2/5] Collapse return and res.json statements --- website/src/controllers/payments/iap.js | 38 ++++++++++--------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/website/src/controllers/payments/iap.js b/website/src/controllers/payments/iap.js index 531ce10bec..bae96b828f 100644 --- a/website/src/controllers/payments/iap.js +++ b/website/src/controllers/payments/iap.js @@ -6,7 +6,7 @@ var nconf = require('nconf'); var inAppPurchase = require('in-app-purchase'); inAppPurchase.config({ // this is the path to the directory containing iap-sanbox/iap-live files - googlePublicKeyPath: nconf.get("IAP_GOOGLE_KEYDIR") + googlePublicKeyPath: nconf.get("IAP_GOOGLE_KEYDIR") }); // Validation ERROR Codes @@ -24,15 +24,14 @@ exports.androidVerify = function(req, res, next) { ok: false, data: 'IAP Error' }; - + console.error('IAP Setup ERROR'); console.error(error); - - res.json(resObj); - - return; + + return res.json(resObj); + } - + /* google receipt must be provided as an object { @@ -44,7 +43,7 @@ exports.androidVerify = function(req, res, next) { data: iapBody.transaction.receipt, signature: iapBody.transaction.signature }; - + // iap is ready iap.validate(iap.GOOGLE, testObj, function (err, googleRes) { if (err) { @@ -56,9 +55,7 @@ exports.androidVerify = function(req, res, next) { } }; - res.json(resObj); - console.error(err); - return; + return res.json(resObj); } if (iap.isValidated(googleRes)) { @@ -78,7 +75,7 @@ exports.androidVerify = function(req, res, next) { exports.iosVerify = function(req, res, next) { console.info(req.body); - + var iapBody = req.body; var user = res.locals.user; @@ -92,11 +89,10 @@ exports.iosVerify = function(req, res, next) { console.error('IAP Setup ERROR'); console.error(error); - res.json(resObj); + return res.json(resObj); - return; } - + // iap is ready iap.validate(iap.APPLE, iapBody.transaction.receipt, function (err, appleRes) { if (err) { @@ -108,9 +104,7 @@ exports.iosVerify = function(req, res, next) { } }; - res.json(resObj); - console.error(err); - return; + return res.json(resObj); } if (iap.isValidated(appleRes)) { @@ -123,8 +117,7 @@ exports.iosVerify = function(req, res, next) { data: appleRes }; // yay good! - res.json(resObj); - return; + return res.json(resObj); } } var resObj = { @@ -135,9 +128,8 @@ exports.iosVerify = function(req, res, next) { } }; - res.json(resObj); - return; + return res.json(resObj); } }); }); -}; \ No newline at end of file +}; From 5b6afde475677e5cfa7482d058db09c1c9c3aa5e Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Sun, 14 Jun 2015 18:08:00 -0500 Subject: [PATCH 3/5] Remove console.logs --- website/src/controllers/payments/iap.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/website/src/controllers/payments/iap.js b/website/src/controllers/payments/iap.js index bae96b828f..cc8360f8f4 100644 --- a/website/src/controllers/payments/iap.js +++ b/website/src/controllers/payments/iap.js @@ -25,9 +25,6 @@ exports.androidVerify = function(req, res, next) { data: 'IAP Error' }; - console.error('IAP Setup ERROR'); - console.error(error); - return res.json(resObj); } @@ -74,8 +71,6 @@ exports.androidVerify = function(req, res, next) { }; exports.iosVerify = function(req, res, next) { - console.info(req.body); - var iapBody = req.body; var user = res.locals.user; @@ -86,9 +81,6 @@ exports.iosVerify = function(req, res, next) { data: 'IAP Error' }; - console.error('IAP Setup ERROR'); - console.error(error); - return res.json(resObj); } From 1ea7193694be3747f887fe0b530366230235a7c8 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Sun, 14 Jun 2015 18:11:07 -0500 Subject: [PATCH 4/5] Add missing test --- test/api/inAppPurchases.coffee | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/api/inAppPurchases.coffee b/test/api/inAppPurchases.coffee index bf1cf43eb2..d40ad41fcc 100644 --- a/test/api/inAppPurchases.coffee +++ b/test/api/inAppPurchases.coffee @@ -103,6 +103,11 @@ describe 'In-App Purchases', -> expect(res.json).to.not.be.called + it 'does not calls payments.buyGems function', -> + inApp.iosVerify(req, res, next) + + expect(paymentSpy).to.not.be.called + context 'iap is valid but has no purchaseDataList', -> before -> iapMock.setup = (cb)-> return cb(null) From bb2f97e802f69f9026afeadb40705091a0fb4345 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Sun, 14 Jun 2015 18:35:19 -0500 Subject: [PATCH 5/5] Added android tests for iap --- test/api/inAppPurchases.coffee | 125 ++++++++++++++++++++++-- website/src/controllers/payments/iap.js | 3 +- 2 files changed, 118 insertions(+), 10 deletions(-) diff --git a/test/api/inAppPurchases.coffee b/test/api/inAppPurchases.coffee index d40ad41fcc..aac9550c10 100644 --- a/test/api/inAppPurchases.coffee +++ b/test/api/inAppPurchases.coffee @@ -8,15 +8,124 @@ iapMock = { } inApp.__set__('iap', iapMock) describe 'In-App Purchases', -> - describe 'iOS', -> - req = { body: { transaction: { reciept: 'foo' } } } - res = { - locals: { user: { _id: 'user' } } + describe 'Android', -> + req = { + body: { + transaction: { + reciept: 'foo' + signature: 'sig' + } + } + } + res = { + locals: { user: { _id: 'user' } } json: sinon.spy() } next = -> true paymentSpy = sinon.spy() - inApp.__set__('payments.buyGems', paymentSpy) + + before -> + inApp.__set__('payments.buyGems', paymentSpy) + + afterEach -> + paymentSpy.reset() + res.json.reset() + + context 'successful app purchase', -> + before -> + iapMock.setup = (cb)-> return cb(null) + iapMock.validate = (iapGoogle, iapBodyReciept, cb)-> return cb(null, true) + iapMock.isValidated = (googleRes)-> return googleRes + iapMock.GOOGLE = 'google' + + it 'calls res.json with succesful result object', -> + expectedResObj = { + ok: true + data: true + } + + inApp.androidVerify(req, res, next) + + expect(res.json).to.be.calledOnce + expect(res.json).to.be.calledWith(expectedResObj) + + it 'calls payments.buyGems function', -> + inApp.androidVerify(req, res, next) + + expect(paymentSpy).to.be.calledOnce + expect(paymentSpy).to.be.calledWith({user: res.locals.user, paymentMethod:'IAP GooglePlay'}) + + context 'error in setup', -> + before -> + iapMock.setup = (cb)-> return cb("error in setup") + + it 'calls res.json with setup error object', -> + expectedResObj = { + ok: false + data: 'IAP Error' + } + + inApp.androidVerify(req, res, next) + + expect(res.json).to.be.calledOnce + expect(res.json).to.be.calledWith(expectedResObj) + + it 'does not calls payments.buyGems function', -> + inApp.androidVerify(req, res, next) + + expect(paymentSpy).to.not.be.called + + context 'error in validation', -> + before -> + iapMock.setup = (cb)-> return cb(null) + iapMock.validate = (iapGoogle, iapBodyReciept, cb)-> return cb('error in validation', true) + + it 'calls res.json with validation error object', -> + expectedResObj = { + ok: false + data: { + code: 6778001 + message: 'error in validation' + } + } + + inApp.androidVerify(req, res, next) + + expect(res.json).to.be.calledOnce + expect(res.json).to.be.calledWith(expectedResObj) + + it 'does not calls payments.buyGems function', -> + inApp.androidVerify(req, res, next) + + expect(paymentSpy).to.not.be.called + + context 'iap is not valid', -> + before -> + iapMock.setup = (cb)-> return cb(null) + iapMock.validate = (iapGoogle, iapBodyReciept, cb)-> return cb(null, false) + iapMock.isValidated = (googleRes)-> return googleRes + + it 'does not call res.json', -> + inApp.androidVerify(req, res, next) + + expect(res.json).to.not.be.called + + it 'does not calls payments.buyGems function', -> + inApp.androidVerify(req, res, next) + + expect(paymentSpy).to.not.be.called + + describe 'iOS', -> + req = { body: { transaction: { reciept: 'foo' } } } + res = { + locals: { user: { _id: 'user' } } + json: sinon.spy() + } + next = -> true + paymentSpy = sinon.spy() + + before -> + inApp.__set__('payments.buyGems', paymentSpy) afterEach -> paymentSpy.reset() @@ -27,7 +136,7 @@ describe 'In-App Purchases', -> iapMock.setup = (cb)-> return cb(null) iapMock.validate = (iapApple, iapBodyReciept, cb)-> return cb(null, true) iapMock.isValidated = (appleRes)-> return appleRes - iapMock.getPurchaseData = (appleRes)-> + iapMock.getPurchaseData = (appleRes)-> return [{ productId: 'com.habitrpg.ios.Habitica.20gems' }] iapMock.APPLE = 'apple' @@ -113,7 +222,7 @@ describe 'In-App Purchases', -> iapMock.setup = (cb)-> return cb(null) iapMock.validate = (iapApple, iapBodyReciept, cb)-> return cb(null, true) iapMock.isValidated = (appleRes)-> return appleRes - iapMock.getPurchaseData = (appleRes)-> + iapMock.getPurchaseData = (appleRes)-> return [] iapMock.APPLE = 'apple' @@ -141,7 +250,7 @@ describe 'In-App Purchases', -> iapMock.setup = (cb)-> return cb(null) iapMock.validate = (iapApple, iapBodyReciept, cb)-> return cb(null, true) iapMock.isValidated = (appleRes)-> return appleRes - iapMock.getPurchaseData = (appleRes)-> + iapMock.getPurchaseData = (appleRes)-> return [{ productId: 'com.another.company' }] iapMock.APPLE = 'apple' diff --git a/website/src/controllers/payments/iap.js b/website/src/controllers/payments/iap.js index cc8360f8f4..ff8a9375b7 100644 --- a/website/src/controllers/payments/iap.js +++ b/website/src/controllers/payments/iap.js @@ -63,8 +63,7 @@ exports.androidVerify = function(req, res, next) { payments.buyGems({user:user, paymentMethod:'IAP GooglePlay'}); - // yay good! - res.json(resObj); + return res.json(resObj); } }); });