Skip to content
This repository was archived by the owner on Mar 15, 2018. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 48 additions & 4 deletions lib/fxpay/pay.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -34,6 +35,35 @@
};


exports.acceptPayMessage = function pay_acceptPayMessage(event,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this ship has probably sailed and this is personal preference, but I don't much like the exports.foo = function whatever style. It makes for long lines, plus when you want to refer to the func defined elsewhere you have to refer to it off of exports.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My original reasoning for it was so one doesn't have to repeat the function name down below in the returned dictionary. However, I've noticed it's always helpful to use a named function so one has to repeat the name anyway. Thus, I could come around to always using named functions and exporting them at the bottom. Whichever style we go with, it just needs to be consistent throughout.

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) {
Expand All @@ -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);

});
}

Expand Down
6 changes: 6 additions & 0 deletions lib/fxpay/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions lib/fxpay/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
7 changes: 6 additions & 1 deletion tests/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/.*'));
Expand All @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions tests/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});

});
154 changes: 138 additions & 16 deletions tests/test-web-purchase.js
Original file line number Diff line number Diff line change
@@ -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 = '<algo>.' + btoa(JSON.stringify(payReq)) + '.<sig>';
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);
}
},
});
});

Expand All @@ -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 = '<algo>.' + btoa(JSON.stringify(payReq)) + '.<sig>';
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('<receipt>', {
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('<receipt>', {
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('<receipt>',
{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};
}

});