diff --git a/lib/fxpay/pay.js b/lib/fxpay/pay.js index a62a117..cedcf47 100644 --- a/lib/fxpay/pay.js +++ b/lib/fxpay/pay.js @@ -4,6 +4,7 @@ var exports = window.fxpay.utils.namespace('fxpay.pay'); var jwt = require('fxpay/jwt'); var settings = require('fxpay/settings'); + var utils = require('fxpay/utils'); exports.processPayment = function pay_processPayment(jwts, callback, opt) { @@ -34,6 +35,35 @@ }; + exports.acceptPayMessage = function pay_acceptPayMessage(event, + allowedOrigin, + callback) { + settings.log.debug('received', event.data, 'from', event.origin); + if (event.origin !== allowedOrigin) { + settings.log.debug('ignoring message from foreign window at', + event.origin); + return callback('UNKNOWN_MESSAGE_ORIGIN'); + } + var eventData = event.data || {}; + + if (eventData.status === 'ok') { + settings.log.info('received pay success message from window at', + event.origin); + return callback(); + } else if (eventData.status === 'failed') { + settings.log.info('received pay fail message with status', + eventData.status, 'code', eventData.errorCode, + 'from window at', event.origin); + return callback(eventData.errorCode || 'PAY_WINDOW_FAIL_MESSAGE'); + } else { + settings.log.info('received pay message with unknown status', + eventData.status, 'from window at', + event.origin); + return callback('UNKNOWN_MESSAGE_STATUS'); + } + }; + + function processWebPayment(paymentWindow, payJwt, callback) { jwt.getPayUrl(payJwt, function(err, payUrl) { if (err) { @@ -43,10 +73,24 @@ // 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(); + function receivePaymentMessage(event) { + exports.acceptPayMessage(event, utils.getUrlOrigin(payUrl), + function(err) { + if (err === 'UNKNOWN_MESSAGE_ORIGIN') { + // These could come from anywhere so ignore them. + return; + } + settings.removeEventListener('message', receivePaymentMessage); + paymentWindow.close(); + if (err) { + return callback(err); + } + callback(); + }); + } + + settings.addEventListener('message', receivePaymentMessage); + }); } diff --git a/lib/fxpay/settings.js b/lib/fxpay/settings.js index 8254b1a..98b9903 100644 --- a/lib/fxpay/settings.js +++ b/lib/fxpay/settings.js @@ -46,6 +46,12 @@ openWindow: function() { return window.open.apply(window, arguments); }, + addEventListener: function() { + return window.addEventListener.apply(window, arguments); + }, + removeEventListener: function() { + return window.removeEventListener.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 e7ffa34..3287054 100644 --- a/lib/fxpay/utils.js +++ b/lib/fxpay/utils.js @@ -46,6 +46,12 @@ } }, + getUrlOrigin: function(url) { + var a = document.createElement('a'); + a.href = url; + return a.origin || (a.protocol + '//' + a.host); + }, + openWindow: function(options) { var settings = require('fxpay/settings'); var defaults = { diff --git a/tests/helper.js b/tests/helper.js index 051a775..5236ac5 100644 --- a/tests/helper.js +++ b/tests/helper.js @@ -69,7 +69,8 @@ var helper = exports; opt = fxpay.utils.defaults(opt, { mozPay: null, - productData: null + productData: null, + payCompleter: null, }); opt.fetchProductsPattern = (opt.fetchProductsPattern || new RegExp('.*/payments/.*/in-app/.*')); @@ -80,7 +81,11 @@ helper.server.respond(); if (opt.mozPay) { + console.log('Simulate a payment completion with mozPay'); opt.mozPay.returnValues[0].onsuccess(); + } else if (opt.payCompleter) { + console.log('Simulate a payment completion with custom function'); + opt.payCompleter(); } // Respond to validating the transaction. diff --git a/tests/test-utils.js b/tests/test-utils.js index e0c5108..46a3391 100644 --- a/tests/test-utils.js +++ b/tests/test-utils.js @@ -143,3 +143,18 @@ describe('fxpay.utils.getSelfOrigin', function() { 'http://foo.com:3000'); }); }); + + +describe('fxpay.utils.getUrlOrigin()', function() { + + it('returns location from URL', function() { + assert.equal(fxpay.utils.getUrlOrigin('http://foo.com/somewhere.html'), + 'http://foo.com'); + }); + + it('returns location with port', function() { + assert.equal(fxpay.utils.getUrlOrigin('http://foo.com:3000/somewhere.html'), + 'http://foo.com:3000'); + }); + +}); diff --git a/tests/test-web-purchase.js b/tests/test-web-purchase.js index a90121a..856a873 100644 --- a/tests/test-web-purchase.js +++ b/tests/test-web-purchase.js @@ -1,21 +1,50 @@ describe('fxpay.purchase() on the web', function() { + var utils = require('fxpay/utils'); + + var payReq = {typ: 'mozilla/payments/pay/v1'}; + var fakeJwt = '.' + btoa(JSON.stringify(payReq)) + '.'; + var productId = 'some-uuid'; + + var providerUrlTemplate; + var fakeWindow; + var windowSpy; + var handlers; beforeEach(function(done) { helper.setUp(); + handlers = {}; + fakeWindow = { + location: '', + close: function() { + }, + }; + windowSpy = { + close: sinon.spy(fakeWindow, 'close'), + }; + providerUrlTemplate = helper.settings.payProviderUrls[payReq.typ]; + fxpay.configure({ appSelf: null, mozApps: null, mozPay: null, apiUrlBase: 'https://not-the-real-marketplace', apiVersionPrefix: '/api/v1', + openWindow: function() { + return fakeWindow; + }, + addEventListener: function(type, handler) { + handlers[type] = handler; + }, + removeEventListener: function() {}, }); + fxpay.init({ oninit: function() { done(); }, onerror: function(err) { done(err); - } + }, }); }); @@ -24,28 +53,121 @@ describe('fxpay.purchase() on the web', function() { 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 = {}; + it('should open a payment window and call back', function (done) { - fxpay.configure({ - openWindow: function() { - return fakeWindow; + fxpay.purchase(productId, function(err) { + assert.equal( + fakeWindow.location, providerUrlTemplate.replace('{jwt}', fakeJwt)); + assert(windowSpy.close.called); + done(err); + }); + + helper.finishPurchaseOk('', { + productData: {webpayJWT: fakeJwt}, + payCompleter: function() { + simulatePostMessage({status: 'ok'}); } }); + }); + + it('should call back with payment errors', function (done) { 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 + assert.equal(err, 'DIALOG_CLOSED_BY_USER'); + assert(windowSpy.close.called); + done(); + }); + + helper.finishPurchaseOk('', { + productData: {webpayJWT: fakeJwt}, + payCompleter: function() { + simulatePostMessage({status: 'failed', + errorCode: 'DIALOG_CLOSED_BY_USER'}); + } + }); + }); + + + function simulatePostMessage(data) { + handlers.message({data: data, + origin: utils.getUrlOrigin(providerUrlTemplate)}); + } + +}); + + +describe('fxpay.pay.acceptPayMessage()', function() { + var utils = require('fxpay/utils'); + var defaultOrigin = 'http://marketplace.firefox.com'; + + it('calls back on success', function(done) { + fxpay.pay.acceptPayMessage(makeEvent(), defaultOrigin, function(err) { done(err); }); + }); + + it('calls back with error code on failure', function(done) { + fxpay.pay.acceptPayMessage( + makeEvent({status: 'failed', errorCode: 'EXTERNAL_CODE'}), + defaultOrigin, function(err) { + assert.equal(err, 'EXTERNAL_CODE'); + done(); + } + ); + }); + + it('calls back with generic error code', function(done) { + fxpay.pay.acceptPayMessage( + makeEvent({status: 'failed', errorCode: null}), + defaultOrigin, function(err) { + assert.equal(err, 'PAY_WINDOW_FAIL_MESSAGE'); + done(); + } + ); + }); - helper.finishPurchaseOk('', - {productData: {webpayJWT: fakeJwt}}); + it('rejects unknown statuses', function(done) { + fxpay.pay.acceptPayMessage( + makeEvent({status: 'cheezborger'}), + defaultOrigin, function(err) { + assert.equal(err, 'UNKNOWN_MESSAGE_STATUS'); + done(); + } + ); + }); + + it('rejects undefined data', function(done) { + fxpay.pay.acceptPayMessage( + makeEvent({data: null}), defaultOrigin, + function(err) { + assert.equal(err, 'UNKNOWN_MESSAGE_STATUS'); + done(); + } + ); + }); + + it('rejects foreign messages', function(done) { + fxpay.pay.acceptPayMessage( + makeEvent({origin: 'http://bar.com'}), + 'http://foo.com', function(err) { + assert.equal(err, 'UNKNOWN_MESSAGE_ORIGIN'); + done(); + } + ); }); + + + function makeEvent(param) { + param = utils.defaults(param, { + status: 'ok', + data: undefined, + errorCode: undefined, + origin: defaultOrigin, + }); + if (typeof param.data === 'undefined') { + param.data = {status: param.status, errorCode: param.errorCode}; + } + return {origin: param.origin, data: param.data}; + } + });