From 0e3a979d423d82b18a4ca7fc6d22c4da8eed9697 Mon Sep 17 00:00:00 2001 From: Kumar McMillan Date: Thu, 11 Dec 2014 17:50:43 -0600 Subject: [PATCH] Open a payment popup window (bug 1110228) --- Gruntfile.js | 1 + example/shared/js/index.js | 25 ++++++-- lib/fxpay/index.js | 15 ++++- lib/fxpay/jwt.js | 64 +++++++++++++++++++ lib/fxpay/pay.js | 26 ++++++-- lib/fxpay/products.js | 2 +- lib/fxpay/settings.js | 8 +++ lib/fxpay/utils.js | 8 ++- package.json | 4 +- tests/helper.js | 68 ++++++++++++++++++++ tests/test-jwt.js | 120 +++++++++++++++++++++++++++++++++++ tests/test-purchase.js | 124 ++++++------------------------------- tests/test-utils.js | 22 +++---- tests/test-web-purchase.js | 51 +++++++++++++++ 14 files changed, 406 insertions(+), 132 deletions(-) create mode 100644 lib/fxpay/jwt.js create mode 100644 tests/test-jwt.js create mode 100644 tests/test-web-purchase.js diff --git a/Gruntfile.js b/Gruntfile.js index 75a0d24..fda63e2 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -8,6 +8,7 @@ var libFiles = [ __dirname + '/lib/fxpay/utils.js', __dirname + '/lib/fxpay/settings.js', __dirname + '/lib/fxpay/api.js', + __dirname + '/lib/fxpay/jwt.js', __dirname + '/lib/fxpay/pay.js', __dirname + '/lib/fxpay/products.js', __dirname + '/lib/fxpay/receipts.js', diff --git a/example/shared/js/index.js b/example/shared/js/index.js index d90262b..a596563 100644 --- a/example/shared/js/index.js +++ b/example/shared/js/index.js @@ -92,7 +92,8 @@ $(function() { // DOM handling: // - $('ul').on('click', '.product button', function() { + $('ul').on('click', '.product button', function(evt) { + evt.preventDefault(); clearError(); var prod = $(this).data('product'); console.log('purchasing', prod.name, prod.productId); @@ -146,22 +147,38 @@ $(function() { fxpay.configure({ receiptCheckSites: [ - // Whitelist the production service. + // Allow the production service. 'https://receiptcheck.marketplace.firefox.com', 'https://marketplace.firefox.com', // The following would not be needed in a live app. These our some test // services for development of the fxpay library only. - // Whitelist our test servers. + // Allow our test servers. 'https://receiptcheck-dev.allizom.org', 'https://marketplace-dev.allizom.org', 'https://receiptcheck-payments-alt.allizom.org', 'https://payments-alt.allizom.org', - // Whitelist a local development server I use. + // Allow some common local servers.. + 'http://mp.dev', 'http://fireplace.loc', ], + payProviderUrls: { + // Map the production site. + 'mozilla/payments/pay/v1': + 'https://marketplace.firefox.com/mozpay/?req={jwt}', + + // Map some development sites. + 'mozilla-dev/payments/pay/v1': + 'https://marketplace-dev.allizom.org/mozpay/?req={jwt}', + 'mozilla-stage/payments/pay/v1': + 'https://marketplace.allizom.org/mozpay/?req={jwt}', + 'mozilla-alt/payments/pay/v1': + 'https://payments-alt.allizom.org/mozpay/?req={jwt}', + 'mozilla-local/payments/pay/v1': + 'http://fireplace.loc/mozpay/?req={jwt}', + }, // Initially, start by allowing fake products so that test // receipts can validate. The checkbox in initApi() will // toggle this setting. diff --git a/lib/fxpay/index.js b/lib/fxpay/index.js index ef85dfe..8bb7d45 100644 --- a/lib/fxpay/index.js +++ b/lib/fxpay/index.js @@ -8,6 +8,7 @@ var settings = require('fxpay/settings'); var products = require('fxpay/products'); var receipts = require('fxpay/receipts'); + var utils = require('fxpay/utils'); // // publicly exported functions: @@ -15,7 +16,7 @@ exports.configure = function() { - settings.configure.apply(settings, arguments); + return settings.configure.apply(settings, arguments); }; @@ -129,6 +130,16 @@ log.debug('starting purchase for product', productId); + var paymentWindow; + if (!settings.mozPay) { + // Open a blank payment window on the same event loop tick + // as the click handler. This avoids popup blockers. + // TODO: maybe we should inject some HTML/CSS to indicate + // loading/progress. We probably also need to display any + // errors that might occur from the Ajax request to get a JWT. + paymentWindow = utils.openWindow({url: ''}); + } + api.post(settings.prepareJwtApiUrl, {inapp: productId}, function(err, productData) { if (err) { @@ -155,7 +166,7 @@ pollIntervalMs: opt.pollIntervalMs } ); - }); + }, {paymentWindow: paymentWindow}); }); } diff --git a/lib/fxpay/jwt.js b/lib/fxpay/jwt.js new file mode 100644 index 0000000..af3692c --- /dev/null +++ b/lib/fxpay/jwt.js @@ -0,0 +1,64 @@ +(function() { + "use strict"; + // This is a very minimal JWT utility. It does not validate signatures. + + var exports = window.fxpay.utils.namespace('fxpay.jwt'); + var settings = require('fxpay/settings'); + + + exports.decode = function jwt_decode(jwt, callback) { + var parts = jwt.split('.'); + if (parts.length !== 3) { + settings.log.error('JWT does not have 3 segments:', jwt); + return callback('WRONG_JWT_SEGMENT_COUNT'); + } + + var jwtData = parts[1]; + // Normalize URL safe base64 into regular base64. + jwtData = jwtData.replace("-", "+", "g").replace("_", "/", "g"); + var jwtString; + try { + jwtString = atob(jwtData); + } catch (error) { + settings.log.error('atob() error:', error.toString(), + 'when decoding JWT', jwtData); + return callback('INVALID_JWT_DATA'); + } + var data; + + try { + data = JSON.parse(jwtString); + } catch (error) { + settings.log.error('JSON.parse() error:', error.toString(), + 'when parsing', jwtString); + return callback('INVALID_JWT_DATA'); + } + callback(null, data); + }; + + + exports.getPayUrl = function jwt_getPayUrl(encodedJwt, callback) { + exports.decode(encodedJwt, function(err, jwtData) { + if (err) { + return callback(err); + } + + var payUrl = settings.payProviderUrls[jwtData.typ]; + if (!payUrl) { + settings.log.error('JWT type', jwtData.typ, + 'does not map to any known payment providers'); + return callback('UNEXPECTED_JWT_TYPE'); + } + if (payUrl.indexOf('{jwt}') === -1) { + settings.log.error('JWT type', jwtData.typ, + 'pay URL is formatted incorrectly:', payUrl); + return callback('INVALID_PAY_PROVIDER_URL'); + } + + payUrl = payUrl.replace('{jwt}', encodedJwt); + settings.log.info('JWT', jwtData.typ, 'resulted in pay URL:', payUrl); + callback(null, payUrl); + }); + }; + +})(); diff --git a/lib/fxpay/pay.js b/lib/fxpay/pay.js index db1df40..a62a117 100644 --- a/lib/fxpay/pay.js +++ b/lib/fxpay/pay.js @@ -2,10 +2,12 @@ "use strict"; var exports = window.fxpay.utils.namespace('fxpay.pay'); + var jwt = require('fxpay/jwt'); var settings = require('fxpay/settings'); - exports.processPayment = function pay_processPayment(jwts, callback) { + exports.processPayment = function pay_processPayment(jwts, callback, opt) { + opt = opt || {}; if (settings.mozPay) { settings.log.info('processing payment with mozPay'); @@ -22,14 +24,30 @@ }; } else { + if (!opt.paymentWindow) { + throw new Error('Cannot start a web payment without a ' + + 'reference to the payment window'); + } settings.log.info('processing payment with web flow'); - return processWebPayment(jwts, callback); + return processWebPayment(opt.paymentWindow, jwts[0], callback); } }; - function processWebPayment(jwts, callback) { - return callback('WEB_FLOW_NOT_IMPLEMENTED'); + function processWebPayment(paymentWindow, payJwt, callback) { + jwt.getPayUrl(payJwt, function(err, payUrl) { + if (err) { + return callback(err); + } + // Now that we've extracted a payment URL from the JWT, + // load it into the freshly created popup window. + paymentWindow.location = payUrl; + + // TODO: wait for postMessage result. + // https://bugzilla.mozilla.org/show_bug.cgi?id=1101995 + // Until then, pretend we have immediate success. + callback(); + }); } })(); diff --git a/lib/fxpay/products.js b/lib/fxpay/products.js index 0aca710..04c25bf 100644 --- a/lib/fxpay/products.js +++ b/lib/fxpay/products.js @@ -53,7 +53,7 @@ if (!opt.api) { opt.api = new API(settings.apiUrlBase); } - var origin = encodeURIComponent(settings.appSelf.origin); + var origin = encodeURIComponent(utils.getSelfOrigin()); var url; if (opt.fetchStubs) { diff --git a/lib/fxpay/settings.js b/lib/fxpay/settings.js index fb9bac5..8254b1a 100644 --- a/lib/fxpay/settings.js +++ b/lib/fxpay/settings.js @@ -38,6 +38,14 @@ appSelf: null, // Boolean flag to tell if we have addReceipt() or not. hasAddReceipt: null, + // Map of JWT types to payment provider URLs. + payProviderUrls: { + 'mozilla/payments/pay/v1': + 'https://marketplace.firefox.com/mozpay/?req={jwt}' + }, + openWindow: function() { + return window.open.apply(window, arguments); + }, // Relative API URL that accepts a product ID and returns a JWT. prepareJwtApiUrl: '/webpay/inapp/prepare/', onerror: function(err) { diff --git a/lib/fxpay/utils.js b/lib/fxpay/utils.js index 3b1ea2f..e7ffa34 100644 --- a/lib/fxpay/utils.js +++ b/lib/fxpay/utils.js @@ -47,6 +47,7 @@ }, openWindow: function(options) { + var settings = require('fxpay/settings'); var defaults = { url: 'about:blank', title: 'FxPay', @@ -64,7 +65,12 @@ 'width=' + options.w + ',height=' + options.h + ',top=' + top_ + ',left=' + left; - return window.open(options.url, options.title, winOptString); + var windowRef = settings.openWindow(options.url, options.title, + winOptString); + if (!windowRef) { + settings.log.error('window.open() failed. URL:', options.url); + } + return windowRef; } }; })(); diff --git a/package.json b/package.json index 811d433..84ff06c 100644 --- a/package.json +++ b/package.json @@ -17,13 +17,13 @@ "grunt-contrib-jshint": "0.6.5", "grunt-contrib-uglify": "0.6.0", "grunt-karma": "0.9.0", - "karma": "0.12.23", + "karma": "0.12.28", "karma-chai": "0.1.0", "karma-chrome-launcher": "0.1.4", "karma-coffee-preprocessor": "0.2.1", "karma-firefox-launcher": "0.1.3", "karma-html2js-preprocessor": "0.1.0", - "karma-mocha": "0.1.9", + "karma-mocha": "0.1.10", "karma-mocha-reporter": "0.3.1", "karma-script-launcher": "0.1.0", "karma-sinon": "1.0.3", diff --git a/tests/helper.js b/tests/helper.js index 4bbc882..051a775 100644 --- a/tests/helper.js +++ b/tests/helper.js @@ -65,6 +65,74 @@ }; + exports.finishPurchaseOk = function(receipt, opt) { + var helper = exports; + opt = fxpay.utils.defaults(opt, { + mozPay: null, + productData: null + }); + opt.fetchProductsPattern = (opt.fetchProductsPattern || + new RegExp('.*/payments/.*/in-app/.*')); + + // Respond to fetching the JWT. + helper.server.respondWith('POST', /.*\/webpay\/inapp\/prepare/, + helper.productData(opt.productData)); + helper.server.respond(); + + if (opt.mozPay) { + opt.mozPay.returnValues[0].onsuccess(); + } + + // Respond to validating the transaction. + helper.server.respondWith('GET', /.*\/transaction\/XYZ/, + helper.transactionData({receipt: receipt})); + helper.server.respond(); + + // Respond to getting product info. + helper.server.respondWith('GET', opt.fetchProductsPattern, + [200, {"Content-Type": "application/json"}, + JSON.stringify(helper.apiProduct)]); + + helper.receiptAdd.onsuccess(); + helper.server.respond(); + }; + + + exports.productData = function(overrides, status) { + // Create a JSON helper.server response to a request for product data. + overrides = overrides || {}; + var data = { + webpayJWT: '', + contribStatusURL: '/transaction/XYZ', + }; + for (var k in data) { + if (overrides[k]) { + data[k] = overrides[k]; + } + } + return [status || 200, {"Content-Type": "application/json"}, + JSON.stringify(data)]; + }; + + + exports.transactionData = function(overrides, status) { + // Create a JSON helper.server response to a request for transaction data. + overrides = overrides || {}; + var data = { + status: 'complete', + // Pretend this is a real Marketplace receipt. + receipt: '~' + }; + for (var k in data) { + if (overrides[k]) { + data[k] = overrides[k]; + } + } + return [status || 200, {"Content-Type": "application/json"}, + JSON.stringify(data)]; + }; + + exports.receiptAdd = { error: null, _receipt: null, diff --git a/tests/test-jwt.js b/tests/test-jwt.js new file mode 100644 index 0000000..09da095 --- /dev/null +++ b/tests/test-jwt.js @@ -0,0 +1,120 @@ +describe('fxpay.jwt.decode()', function() { + + it('should decode JWTs', function(done) { + var fakeJwt = { + aud: 'payments-alt.allizom.org', + request: {simulate: {result: 'postback'}, + pricePoint: '10'} + }; + var encJwt = '.' + btoa(JSON.stringify(fakeJwt)) + '.'; + + fxpay.jwt.decode(encJwt, function(err, data) { + // Do a quick sanity check that this was decoded. + + assert.deepPropertyVal(data, 'aud', fakeJwt.aud); + assert.deepPropertyVal(data, 'request.simulate.result', + fakeJwt.request.simulate.result); + assert.deepPropertyVal(data, 'request.pricePoint', + fakeJwt.request.pricePoint); + done(err); + }); + }); + + it('should decode URL safe JWTs', function(done) { + // This JWT has `-` and `_` chars which need to be converted. + var encJwt = + "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJhdWQiOiAibW9j" + + "a3BheXByb3ZpZGVyLnBocGZvZ2FwcC5jb20iLCAiaXNzIjogIkVudGVyI" + + "HlvdSBhcHAga2V5IGhlcmUhIiwgInJlcXVlc3QiOiB7Im5hbWUiOiAiUG" + + "llY2Ugb2YgQ2FrZSIsICJwcmljZSI6ICIxMC41MCIsICJwcmljZVRpZXI" + + "iOiAxLCAicHJvZHVjdGRhdGEiOiAidHJhbnNhY3Rpb25faWQ9ODYiLCAi" + + "Y3VycmVuY3lDb2RlIjogIlVTRCIsICJkZXNjcmlwdGlvbiI6ICJWaXJ0d" + + "WFsIGNob2NvbGF0ZSBjYWtlIHRvIGZpbGwgeW91ciB2aXJ0dWFsIHR1bW" + + "15In0sICJleHAiOiAxMzUyMjMyNzkyLCAiaWF0IjogMTM1MjIyOTE5Miw" + + "gInR5cCI6ICJtb2NrL3BheW1lbnRzL2luYXBwL3YxIn0.QZxc62USCy4U" + + "IyKIC1TKelVhNklvk-Ou1l_daKntaFI"; + + fxpay.jwt.decode(encJwt, function(err, data) { + assert.deepPropertyVal(data, 'request.name', 'Piece of Cake'); + assert.deepPropertyVal( + data, 'request.description', + 'Virtual chocolate cake to fill your virtual tummy'); + done(err); + }); + }); + + it('should error on missing segments', function(done) { + fxpay.jwt.decode('one.two', function(err) { + assert.equal(err, 'WRONG_JWT_SEGMENT_COUNT'); + done(); + }); + }); + + it('should error on invalid binary data', function(done) { + fxpay.jwt.decode('.this{}IS not*base64 encoded.', function(err) { + assert.equal(err, 'INVALID_JWT_DATA'); + done(); + }); + }); + + it('should error on non JSON data within JWT', function(done) { + var encJwt = '.' + btoa('(not / valid JSON}') + '.'; + fxpay.jwt.decode(encJwt, function(err) { + assert.equal(err, 'INVALID_JWT_DATA'); + done(); + }); + }); + +}); + + +describe('fxpay.jwt.getPayUrl()', function() { + + it('should pass through parse errors', function(done) { + var encJwt = '.' + btoa('(not / valid JSON}') + '.'; + fxpay.jwt.getPayUrl(encJwt, function(err) { + assert.equal(err, 'INVALID_JWT_DATA'); + done(); + }); + }); + + it('should error on unknown JWT types', function(done) { + var payRequest = {typ: 'unknown-type'}; + var encJwt = '.' + btoa(JSON.stringify(payRequest)) + '.'; + fxpay.jwt.getPayUrl(encJwt, function(err) { + assert.equal(err, 'UNEXPECTED_JWT_TYPE'); + done(); + }); + }); + + it('should error on invalid URL templates', function(done) { + fxpay.configure({ + payProviderUrls: { + someType: 'https://pay/start?req={nope}' // missing {req} + } + }); + var payRequest = {typ: 'someType'}; + var encJwt = '.' + btoa(JSON.stringify(payRequest)) + '.'; + fxpay.jwt.getPayUrl(encJwt, function(err) { + assert.equal(err, 'INVALID_PAY_PROVIDER_URL'); + done(); + }); + }); + + it('should return a JWT formatted URL', function(done) { + fxpay.configure({ + payProviderUrls: { + someType: 'https://pay/start?req={jwt}' + } + }); + + var payRequest = {typ: 'someType'}; + var encJwt = '.' + btoa(JSON.stringify(payRequest)) + '.'; + + fxpay.jwt.getPayUrl(encJwt, function(err, payUrl) { + assert.equal(payUrl, 'https://pay/start?req=' + encJwt); + done(err); + }); + }); + +}); diff --git a/tests/test-purchase.js b/tests/test-purchase.js index 3fc85b6..e9043cc 100644 --- a/tests/test-purchase.js +++ b/tests/test-purchase.js @@ -1,4 +1,4 @@ -describe('fxpay.purchase()', function () { +describe('fxpay.purchase() on B2G', function () { var mozPay; beforeEach(function() { @@ -6,7 +6,7 @@ describe('fxpay.purchase()', function () { mozPay = sinon.spy(mozPayStub); fxpay.configure({ appSelf: helper.appSelf, - mozPay: mozPay + mozPay: mozPay, }); }); @@ -57,13 +57,13 @@ describe('fxpay.purchase()', function () { 'POST', cfg.apiUrlBase + cfg.apiVersionPrefix + '/webpay/inapp/prepare/', // TODO: assert somehow that productId is part of post data. - productData({webpayJWT: webpayJWT})); + helper.productData({webpayJWT: webpayJWT})); helper.server.respond(); mozPay.returnValues[0].onsuccess(); helper.server.respondWith('GET', cfg.apiUrlBase + '/transaction/XYZ', - transactionData()); + helper.transactionData()); helper.server.respond(); helper.server.respondWith('GET', new RegExp('.*/payments/.*/in-app/.*'), @@ -74,32 +74,6 @@ describe('fxpay.purchase()', function () { helper.server.respond(); }); - it('should open a payment window on the web', function (done) { - var webpayJWT = ''; - var productId = 'some-uuid'; - var cfg = { - apiUrlBase: 'https://not-the-real-marketplace', - apiVersionPrefix: '/api/v1', - appSelf: null, - mozPay: null - }; - fxpay.configure(cfg); - - fxpay.purchase(productId, function(err) { - // TODO: replace this with a window opener test! - assert.equal(err, 'WEB_FLOW_NOT_IMPLEMENTED'); - done(); - }); - - // Respond to fetching the JWT. - helper.server.respondWith( - 'POST', - cfg.apiUrlBase + cfg.apiVersionPrefix + '/webpay/inapp/prepare/', - productData({webpayJWT: webpayJWT})); - - helper.server.respond(); - }); - it('should timeout polling the transaction', function (done) { var productId = 'some-guid'; @@ -114,14 +88,14 @@ describe('fxpay.purchase()', function () { // Respond to fetching the JWT. helper.server.respondWith('POST', /http.*\/webpay\/inapp\/prepare/, - productData()); + helper.productData()); helper.server.respond(); mozPay.returnValues[0].onsuccess(); helper.server.autoRespond = true; helper.server.respondWith('GET', /http.*\/transaction\/XYZ/, - transactionData({status: 'incomplete'})); + helper.transactionData({status: 'incomplete'})); helper.server.respond(); }); @@ -136,7 +110,7 @@ describe('fxpay.purchase()', function () { // Respond to fetching the JWT. helper.server.respondWith('POST', /.*webpay\/inapp\/prepare/, - productData()); + helper.productData()); helper.server.respond(); var domReq = mozPay.returnValues[0]; @@ -153,7 +127,7 @@ describe('fxpay.purchase()', function () { // Respond to fetching the JWT. helper.server.respondWith('POST', /http.*\/webpay\/inapp\/prepare/, - productData()); + helper.productData()); helper.server.respond(); mozPay.returnValues[0].onsuccess(); @@ -161,7 +135,7 @@ describe('fxpay.purchase()', function () { // Respond to polling the transaction. helper.server.respondWith( 'GET', /http.*\/transaction\/XYZ/, - transactionData({status: 'THIS_IS_NOT_A_VALID_STATE'})); + helper.transactionData({status: 'THIS_IS_NOT_A_VALID_STATE'})); helper.server.respond(); helper.receiptAdd.onsuccess(); @@ -175,7 +149,7 @@ describe('fxpay.purchase()', function () { done(err); }); - finishPurchaseOk(receipt); + helper.finishPurchaseOk(receipt, {mozPay: mozPay}); }); it('should call back with complete product info', function (done) { @@ -189,7 +163,7 @@ describe('fxpay.purchase()', function () { done(err); }); - finishPurchaseOk(''); + helper.finishPurchaseOk('', {mozPay: mozPay}); }); it('should fetch stub products when using fake products', function (done) { @@ -204,8 +178,9 @@ describe('fxpay.purchase()', function () { done(err); }); - finishPurchaseOk('', { - fetchProductsPattern: /.*\/stub-in-app-products\/.*/ + helper.finishPurchaseOk('', { + fetchProductsPattern: /.*\/stub-in-app-products\/.*/, + mozPay: mozPay }); }); @@ -226,7 +201,7 @@ describe('fxpay.purchase()', function () { done(err); }); - finishPurchaseOk(receipt); + helper.finishPurchaseOk(receipt, {mozPay: mozPay}); }); it('should not add dupes to localStorage', function (done) { @@ -248,7 +223,7 @@ describe('fxpay.purchase()', function () { done(err); }); - finishPurchaseOk(receipt); + helper.finishPurchaseOk(receipt, {mozPay: mozPay}); }); it('should pass through receipt errors', function (done) { @@ -260,12 +235,13 @@ describe('fxpay.purchase()', function () { // Respond to fetching the JWT. helper.server.respondWith('POST', /.*\/webpay\/inapp\/prepare/, - productData()); + helper.productData()); helper.server.respond(); mozPay.returnValues[0].onsuccess(); - helper.server.respondWith('GET', /.*\/transaction\/XYZ/, transactionData()); + helper.server.respondWith('GET', /.*\/transaction\/XYZ/, + helper.transactionData()); helper.server.respond(); // Simulate a receipt installation error. @@ -290,33 +266,6 @@ describe('fxpay.purchase()', function () { } - function finishPurchaseOk(receipt, opt) { - opt = opt || {}; - opt.fetchProductsPattern = (opt.fetchProductsPattern || - new RegExp('.*/payments/.*/in-app/.*')); - - // Respond to fetching the JWT. - helper.server.respondWith('POST', /.*\/webpay\/inapp\/prepare/, - productData()); - helper.server.respond(); - - mozPay.returnValues[0].onsuccess(); - - // Respond to validating the transaction. - helper.server.respondWith('GET', /.*\/transaction\/XYZ/, - transactionData({receipt: receipt})); - helper.server.respond(); - - // Respond to getting product info. - helper.server.respondWith('GET', opt.fetchProductsPattern, - [200, {"Content-Type": "application/json"}, - JSON.stringify(helper.apiProduct)]); - - helper.receiptAdd.onsuccess(); - helper.server.respond(); - } - - function mozPayStub() { // https://developer.mozilla.org/en-US/docs/Web/API/Navigator.mozPay return { @@ -325,39 +274,4 @@ describe('fxpay.purchase()', function () { }; } - - function productData(overrides, status) { - // Create a JSON helper.server response to a request for product data. - overrides = overrides || {}; - var data = { - webpayJWT: '', - contribStatusURL: '/transaction/XYZ', - }; - for (var k in data) { - if (overrides[k]) { - data[k] = overrides[k]; - } - } - return [status || 200, {"Content-Type": "application/json"}, - JSON.stringify(data)]; - } - - - function transactionData(overrides, status) { - // Create a JSON helper.server response to a request for transaction data. - overrides = overrides || {}; - var data = { - status: 'complete', - // Pretend this is a real Marketplace receipt. - receipt: '~' - }; - for (var k in data) { - if (overrides[k]) { - data[k] = overrides[k]; - } - } - return [status || 200, {"Content-Type": "application/json"}, - JSON.stringify(data)]; - } - }); diff --git a/tests/test-utils.js b/tests/test-utils.js index 4dc77d2..e0c5108 100644 --- a/tests/test-utils.js +++ b/tests/test-utils.js @@ -90,12 +90,8 @@ describe('fxpay.utils.defaults()', function() { describe('fxpay.utils.openWindow()', function() { beforeEach(function(){ - this._oldOpen = window.open; - window.open = sinon.spy(); - }); - - afterEach(function(){ - window.open = this._oldOpen; + this.openWindowSpy = sinon.spy(); + fxpay.configure({openWindow: this.openWindowSpy}); }); it('should be called with props', function() { @@ -105,21 +101,21 @@ describe('fxpay.utils.openWindow()', function() { w: 200, h: 400 }); - assert(window.open.calledWithMatch('http://blah.com', 'whatever')); - assert.include(window.open.args[0][2], 'width=200'); - assert.include(window.open.args[0][2], 'height=400'); + assert(this.openWindowSpy.calledWithMatch('http://blah.com', 'whatever')); + assert.include(this.openWindowSpy.args[0][2], 'width=200'); + assert.include(this.openWindowSpy.args[0][2], 'height=400'); }); it('should be called with defaults', function() { fxpay.utils.openWindow(); - assert(window.open.calledWithMatch('about:blank', 'FxPay')); - assert.include(window.open.args[0][2], 'width=276'); - assert.include(window.open.args[0][2], 'height=384'); + assert(this.openWindowSpy.calledWithMatch('about:blank', 'FxPay')); + assert.include(this.openWindowSpy.args[0][2], 'width=276'); + assert.include(this.openWindowSpy.args[0][2], 'height=384'); }); it('should be passed a features string with no whitespace', function() { fxpay.utils.openWindow(); - assert.notInclude(window.open.args[0][2], ' '); + assert.notInclude(this.openWindowSpy.args[0][2], ' '); }); }); diff --git a/tests/test-web-purchase.js b/tests/test-web-purchase.js new file mode 100644 index 0000000..a90121a --- /dev/null +++ b/tests/test-web-purchase.js @@ -0,0 +1,51 @@ +describe('fxpay.purchase() on the web', function() { + + beforeEach(function(done) { + helper.setUp(); + fxpay.configure({ + appSelf: null, + mozApps: null, + mozPay: null, + apiUrlBase: 'https://not-the-real-marketplace', + apiVersionPrefix: '/api/v1', + }); + fxpay.init({ + oninit: function() { + done(); + }, + onerror: function(err) { + done(err); + } + }); + }); + + afterEach(function() { + helper.tearDown(); + helper.receiptAdd.reset(); + }); + + it('should open a payment window on the web', function (done) { + var payReq = {typ: 'mozilla/payments/pay/v1'}; + var fakeJwt = '.' + btoa(JSON.stringify(payReq)) + '.'; + var productId = 'some-uuid'; + var fakeWindow = {}; + + fxpay.configure({ + openWindow: function() { + return fakeWindow; + } + }); + + fxpay.purchase(productId, function(err) { + assert.equal( + fakeWindow.location, + helper.settings.payProviderUrls[payReq.typ].replace('{jwt}', fakeJwt)); + // TODO: check for success/fail codes. + // https://bugzilla.mozilla.org/show_bug.cgi?id=1101995 + done(err); + }); + + helper.finishPurchaseOk('', + {productData: {webpayJWT: fakeJwt}}); + }); +});