Skip to content
This repository has been archived by the owner on Feb 23, 2019. It is now read-only.

Commit

Permalink
Automatically retry logins (#113)
Browse files Browse the repository at this point in the history
  • Loading branch information
rhoggSugarcrm authored and khigakisugar committed Feb 7, 2017
1 parent ed9af67 commit 2f14f26
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 12 deletions.
36 changes: 30 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,9 @@ class UserAgent {
this.version = version;

this._cacheMe();

this._maxSessionAttempts = 3;
this._setState('sessionAttempt', 0);
}

/**
Expand All @@ -575,19 +578,40 @@ class UserAgent {

/**
* Log this user agent in.
* After calling this function,
* `cachedAgents[this.username]._state.loginPromise` is set to a promise
* that, when resolved, will verify that the OAuth token is available.
* If the login is unsuccessful, it is retried a maximum of two additional
* times, after which it throws an error.
*
* @return {ChakramPromise} A promise resolving to the result of the login
* request.
*
* @private
*/
_login = () => {
this._setState('loginPromise', utils.login({
let loginPromise = this._getState('loginPromise');
if (loginPromise) {
return loginPromise;
}

let sessionAttempt = this._getState('sessionAttempt') + 1;
this._setState('sessionAttempt', sessionAttempt);
if (sessionAttempt > this._maxSessionAttempts) {
throw new Error('Max number of login attempts exceeded for user: ' + this.username);
}

loginPromise = utils.login({
username: this.username,
password: this.password,
version: this.version,
xthorn: 'Agent',
}).then(this._updateAuthState));
}).then((response) => {
this._updateAuthState(response);
this._setState('sessionAttempt', 0);
}).catch(() => {
this._setState('loginPromise', null);
return this._login();
});
this._setState('loginPromise', loginPromise);
return loginPromise;
};

/**
Expand All @@ -604,7 +628,7 @@ class UserAgent {
_requestSkeleton = (chakramMethod, args) => {
args[0] = utils.constructUrl(this.version, args[0]);

return this._getState('loginPromise').then(() => {
return this._login().then(() => {
// must wait for login promise to resolve or else OAuth-Token may not be available
let paramIndex = args.length - 1;
// FIXME: eventually will want to support multiple types of headers
Expand Down
79 changes: 73 additions & 6 deletions tests/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -641,21 +641,23 @@ describe('Thorn', () => {
});

describe('Agent', () => {
beforeEach(() => {
nock(process.env.THORN_SERVER_URL)
.post(isTokenReq)
.reply(200, ACCESS);
});

describe('as', () => {
it('should return an Agent with cached username and password', () => {
nock(process.env.THORN_SERVER_URL)
.post(isTokenReq)
.reply(200, ACCESS);

let myAgent = Agent.as(process.env.THORN_ADMIN_USERNAME);

expect(myAgent.username).to.equal(process.env.THORN_ADMIN_USERNAME);
expect(myAgent.password).to.equal(process.env.THORN_ADMIN_PASSWORD);
});

it('should return the same agent if called twice with the same username', () => {
nock(process.env.THORN_SERVER_URL)
.post(isTokenReq)
.reply(200, ACCESS);

let agent1 = Agent.as(process.env.THORN_ADMIN_USERNAME);
let agent2 = Agent.as(process.env.THORN_ADMIN_USERNAME);

Expand All @@ -669,12 +671,73 @@ describe('Thorn', () => {
it('should throw an error if given username is not found', () => {
expect(() => Agent.as('nonexistent')).to.throw('No credentials available for user: nonexistent');
});

it('should retry login 2 times', function*() {
let server = nock(process.env.THORN_SERVER_URL)
.post(isTokenReq)
.reply(401)
.post(isTokenReq)
.reply(401)
.post(isTokenReq)
.reply(200, ACCESS)
.get(/not\/real\/endpoint/)
.reply(200, { fake: 'data' });

let myAgent = Agent.as(process.env.THORN_ADMIN_USERNAME);

// note: Agent.as does not expose a Promise, so have to wait on an arbitrary request
yield myAgent.get('not/real/endpoint');

expect(server.isDone()).to.be.true;
});

it('should give up after 3 login attempts', function*() {
nock(process.env.THORN_SERVER_URL)
.post(isTokenReq)
.reply(401)
.post(isTokenReq)
.reply(401)
.post(isTokenReq)
.reply(401);

let myAgent = Agent.as(process.env.THORN_ADMIN_USERNAME);

// note: Agent.as does not expose a Promise, so have to wait on an arbitrary request
yield myAgent.get('not/real/endpoint').catch((e) => {
let msg = 'Max number of login attempts exceeded for user: ' + process.env.THORN_ADMIN_USERNAME;
expect(e.message).to.equal(msg);
});
});

// Apache sometimes sends 200 responses with empty bodies on thread death,
// which is interpreted by Chakram as ECONNRESET and results in an empty
// response.response object. Ensure this scenario is handled appropriately.
it('should retry login on empty response body', function*() {
let server = nock(process.env.THORN_SERVER_URL)
.post(isTokenReq)
.reply(200)
.post(isTokenReq)
.reply(200, ACCESS)
.get(/not\/real\/endpoint/)
.reply(200, { fake: 'data' });

let myAgent = Agent.as(process.env.THORN_ADMIN_USERNAME);

// note: Agent.as does not expose a Promise, so have to wait on an arbitrary request
yield myAgent.get('not/real/endpoint');

expect(server.isDone()).to.be.true;
});
});

describe('on', () => {
let myAgent;

beforeEach(() => {
nock(process.env.THORN_SERVER_URL)
.post(isTokenReq)
.reply(200, ACCESS);

myAgent = Agent.as(process.env.THORN_ADMIN_USERNAME);
});

Expand Down Expand Up @@ -702,6 +765,10 @@ describe('Thorn', () => {
}

beforeEach(() => {
nock(process.env.THORN_SERVER_URL)
.post(isTokenReq)
.reply(200, ACCESS);

myAgent = Agent.as(process.env.THORN_ADMIN_USERNAME);
endpoint = 'not/real/endpoint';
});
Expand Down

0 comments on commit 2f14f26

Please sign in to comment.