Skip to content

Commit

Permalink
Added event emitter to send updated tokens.
Browse files Browse the repository at this point in the history
There were two options here:

1. create the emitter in the application and allow the consumer to use it, or
2. allow the consumer to create the emitter and inject it as a dependency

I've built it as option 1, but I'm open to discussion on this.

The creation for partner apps would look something like:

```
xeroClient = new xero.PartnerApplication(config);
    eventReceiver = xeroClient.eventEmitter;
    eventReceiver.on('xeroTokenUpdate', function(data) {
        //Store the data that was received from the xeroTokenRefresh event
        console.log("Received xero token refresh: ", data);
    });
```

This is only for partner applications, but it will be required to refresh the emitter every time a new application object is created to ensure the listening stack is shared.
  • Loading branch information
Jordan Walsh committed Mar 14, 2017
1 parent 804e36e commit c17877b
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 19 deletions.
33 changes: 24 additions & 9 deletions lib/application.js
Expand Up @@ -8,7 +8,8 @@ var _ = require('lodash'),
Core = require('./core'),
qs = require('querystring'),
Payroll = require('./payroll'),
xml2js = require('xml2js')
xml2js = require('xml2js'),
events = new require('events');

function Batch(application) {
logger.debug('Batch::constructor');
Expand Down Expand Up @@ -45,6 +46,7 @@ function Application(options) {

var core = new Core(this);
var payroll = new Payroll(this);

Object.defineProperties(this, {
core: {
get: function() {
Expand Down Expand Up @@ -81,6 +83,8 @@ Object.assign(Application.prototype, {
if (this.options["runscopeBucketId"] && this.options["runscopeBucketId"] !== "") {
this.options.baseUrl = "https://api-xero-com-" + this.options["runscopeBucketId"] + ".runscope.net";
}

this.eventEmitter = new events.EventEmitter();
},
post: function(path, body, options, callback) {
return this.putOrPost('post', path, body, options, callback);
Expand Down Expand Up @@ -474,9 +478,10 @@ Object.assign(Application.prototype, {
*/

var expiry = new Date(this.options.tokenExpiry),
checkTime = addMinutes(new Date(), 3);
checkTime = addMinutes(new Date(), 29);

if (checkTime >= expiry) {
logger.debug("Refreshing Access Token");
return this.refreshAccessToken();
} else {
return Promise.resolve();
Expand Down Expand Up @@ -554,7 +559,7 @@ var RequireAuthorizationApplication = Application.extend({
accessSecret: results.oauth_token_secret,
sessionHandle: results.oauth_session_handle,
tokenExpiry: exp.toString()
});
}, false);
resolve({ results: results });
}
callback && callback.apply(callback, arguments);
Expand All @@ -576,7 +581,7 @@ var RequireAuthorizationApplication = Application.extend({
accessSecret: results.oauth_token_secret,
sessionHandle: results.oauth_session_handle,
tokenExpiry: exp.toString()
});
}, true);
resolve({ results: results });
}

Expand All @@ -588,11 +593,21 @@ var RequireAuthorizationApplication = Application.extend({
var q = Object.assign({}, { oauth_token: requestToken }, other);
return this.options.baseUrl + this.options.authorizeUrl + '?' + querystring.stringify(q);
},
setOptions: function(options) {
this.options.accessToken = options.accessToken;
this.options.accessSecret = options.accessSecret;
this.options.sessionHandle = options.sessionHandle;
this.options.tokenExpiry = options.tokenExpiry;
setOptions: function(options, emitThisEvent) {
logger.debug("Setting options");
options.accessToken ? this.options.accessToken = options.accessToken : false;
options.accessSecret ? this.options.accessSecret = options.accessSecret : false;
options.sessionHandle ? this.options.sessionHandle = options.sessionHandle : false;
options.tokenExpiry ? this.options.tokenExpiry = options.tokenExpiry : false;

if (emitThisEvent && this.eventEmitter) {
logger.debug("Emitting event");
try {
this.eventEmitter.emit('xeroTokenUpdate', options);
} catch (e) {
logger.error(e);
}
}
}
});

Expand Down
2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -21,6 +21,7 @@
"license": "MIT",
"dependencies": {
"dateformat": "~1.0.7-1.2.3",
"events": "^1.1.1",
"lodash": "~2.4.1",
"log4js": "~0.6.9",
"lru-cache": "^4.0.2",
Expand All @@ -39,6 +40,7 @@
"mustache": "^2.3.0",
"nodemailer": "",
"nyc": "^10.1.2",
"sinon": "^1.17.7",
"zombie": "^5.0.5",
"zombie-phantom": "0.0.6"
}
Expand Down
7 changes: 6 additions & 1 deletion sample_app/index.js
Expand Up @@ -7,6 +7,7 @@ var express = require('express'),
metaConfig = require('./config/config.json');

var xeroClient;
var eventReceiver;

function getXeroClient(session) {

Expand All @@ -26,10 +27,14 @@ function getXeroClient(session) {
switch (APPTYPE) {
case "PUBLIC":
xeroClient = new xero.PublicApplication(config);
return xeroClient;
break;
case "PARTNER":
xeroClient = new xero.PartnerApplication(config);
eventReceiver = xeroClient.eventEmitter;
eventReceiver.on('xeroTokenUpdate', function(data) {
//Store the data that was received from the xeroTokenRefresh event
console.log("Received xero token refresh: ", data);
});
break;
default:
throw "No App Type Set!!"
Expand Down
78 changes: 69 additions & 9 deletions test/accountingtests.js
@@ -1,6 +1,7 @@
var chai = require('chai'),
should = chai.should(),
expect = chai.expect,
sinon = require('sinon'),
_ = require('lodash'),
xero = require('..'),
util = require('util'),
Expand All @@ -14,6 +15,7 @@ process.on('uncaughtException', function(err) {
})

var currentApp;
var eventReceiver;
var organisationCountry = '';

var APPTYPE = metaConfig.APPTYPE;
Expand All @@ -36,6 +38,8 @@ before('init instance and set options', function(done) {
throw "No App Type Set!!"
}

eventReceiver = currentApp.eventEmitter;

done();
})

Expand All @@ -47,7 +51,6 @@ describe('get access for public or partner application', function() {
});

describe('Get tokens', function() {

var authoriseUrl = "";
var requestToken = "";
var requestSecret = "";
Expand All @@ -56,8 +59,29 @@ describe('get access for public or partner application', function() {
var accessToken = "";
var accessSecret = "";

//This function is used by the event emitter to receive the event when the token
//is automatically refreshed. We use the 'spy' function so that we can include
//some checks within the tests.
var spy = sinon.spy(function() {
console.log("Event Received. Creating new Partner App");

//Create a new application object when we receive new tokens
currentApp = new xero.PartnerApplication(config);
currentApp.setOptions(arguments[0]);
//Reset the event receiver so the listener stack is shared correctly.
eventReceiver = currentApp.eventEmitter;
eventReceiver.on('xeroTokenUpdate', function(data) { console.log("Event Received: ", data); });

console.log("Partner app recreated");
});

it('adds the event listener', function(done) {
eventReceiver.on('xeroTokenUpdate', spy);
done();
});

it('user gets a token and builds the url', function(done) {

currentApp.getRequestToken(function(err, token, secret) {
if (!err) {
authoriseUrl = currentApp.buildAuthorizeUrl(token);
Expand Down Expand Up @@ -162,15 +186,24 @@ describe('get access for public or partner application', function() {
expect(currentApp.options.accessToken).to.not.equal("");
expect(currentApp.options.accessSecret).to.not.equal(undefined);
expect(currentApp.options.accessSecret).to.not.equal("");
expect(currentApp.options.sessionHandle).to.not.equal(undefined);
expect(currentApp.options.sessionHandle).to.not.equal("");

if (APPTYPE === "PARTNER") {
expect(currentApp.options.sessionHandle).to.not.equal(undefined);
expect(currentApp.options.sessionHandle).to.not.equal("");
}

done();
}).catch(function(err) {
done(wrapError(err));
});
});

it('refreshes the token', function(done) {
if (APPTYPE !== "PARTNER") {
this.skip();
}

//Only supported for Partner integrations
currentApp.refreshAccessToken()
.then(function() {
expect(currentApp.options.accessToken).to.not.equal(undefined);
Expand All @@ -179,19 +212,18 @@ describe('get access for public or partner application', function() {
expect(currentApp.options.accessSecret).to.not.equal("");
expect(currentApp.options.sessionHandle).to.not.equal(undefined);
expect(currentApp.options.sessionHandle).to.not.equal("");

expect(spy.called).to.equal(true);
done();
}).catch(function(err) {
done(wrapError(err));
});
});
});


});
});

describe('regression tests', function() {

var InvoiceID = "";
var PaymentID = "";

Expand Down Expand Up @@ -591,7 +623,37 @@ describe('regression tests', function() {
.catch(function(err) {
done(wrapError(err));
})
})
});

it('saves multiple invoices', function(done) {
var invoices = [];

for (var i = 0; i < 10; i++) {
invoices.push(currentApp.core.invoices.newInvoice({
Type: 'ACCREC',
Contact: {
Name: 'Department of Testing'
},
DueDate: new Date().toISOString().split("T")[0],
LineItems: [{
Description: 'Services',
Quantity: 2,
UnitAmount: 230,
AccountCode: '400'
}]
}));
}

currentApp.core.invoices.saveInvoices(invoices)
.then(function(response) {
expect(response.entities).to.have.length.greaterThan(9);
done();
})
.catch(function(err) {
done(wrapError(err));
})

});
});

describe('payments', function() {
Expand Down Expand Up @@ -1449,6 +1511,4 @@ function wrapError(err) {
}
return new Error(err.statusCode + ': ' + msg);
}


}

0 comments on commit c17877b

Please sign in to comment.