unit/controller/authorization.js
import chai from 'chai';
import { ACCESS_TOKEN_RESPONSE, DEVICE_CODE_RESPONSE, DEVICE_CODE_POLL_RESPONSE, DEVICE_CODE_DENIED_RESPONSE, DEVICE_CODE_EXPIRED_RESPONSE } from '../../test-data';
import mockery from 'mockery';
import fetchMock from 'fetch-mock';
import Authorization from './../../../src/controller/authorization';
import NprOne from './../../../src/index';
import { testConfig } from '../../test';
const should = chai.should();
/** @test {Authorization} */
describe('Authorization', () => {
let authorization;
let refreshTokenUrl;
let logoutUrl;
let newDeviceCodeUrl;
let deviceCodePollUrl;
beforeEach(() => {
authorization = new Authorization();
NprOne.config = testConfig;
refreshTokenUrl = `^${testConfig.authProxyBaseUrl}${NprOne.config.refreshTokenPath}`;
logoutUrl = `^${testConfig.authProxyBaseUrl}${NprOne.config.logoutPath}`;
newDeviceCodeUrl = `${NprOne.config.authProxyBaseUrl}${NprOne.config.newDeviceCodePath}`;
deviceCodePollUrl = `${NprOne.config.authProxyBaseUrl}${NprOne.config.pollDeviceCodePath}`;
});
afterEach(() => {
fetchMock.restore();
mockery.deregisterMock('fetch');
});
/** @test {Authorization.refreshExistingAccessToken} */
describe('refreshExistingAccessToken', function () { // intentionally can't use arrow function here
// see: https://github.com/mochajs/mocha/issues/1763
this.timeout(16000);
// @TODO this should not be necessary - quick-fix to deal with implicit dependency on Listening controller - see #8
const adsWizzWwwUrl = 'https://adswizz.com';
const adsWizzCdnUrl = '^https://delivery-s3.adswizz.com';
it('should call the refresh token endpoint in the auth proxy', (done) => {
mockery.registerMock('fetch', fetchMock
.mock(refreshTokenUrl, 'POST', ACCESS_TOKEN_RESPONSE)
.mock(adsWizzWwwUrl, 'GET', 200) // @TODO remove as part of fix for #8
.mock(adsWizzCdnUrl, 'GET', 200) // @TODO remove as part of fix for #8
.getMock());
Authorization.refreshExistingAccessToken()
.then(() => {
fetchMock.called(refreshTokenUrl).should.be.true;
fetchMock.calls().unmatched.length.should.equal(0);
NprOne.accessToken.should.equal(ACCESS_TOKEN_RESPONSE.access_token);
done();
})
.catch(done);
});
it('should retry the call to the refresh token endpoint in the auth proxy if it receives a bad response the first time', (done) => {
let numTries = 0;
const responses = () => {
numTries += 1;
if (numTries === 1) {
return 500;
} else {
return ACCESS_TOKEN_RESPONSE;
}
};
mockery.registerMock('fetch', fetchMock
.mock(refreshTokenUrl, 'POST', responses)
.mock(adsWizzWwwUrl, 'GET', 200) // @TODO remove as part of fix for #8
.mock(adsWizzCdnUrl, 'GET', 200) // @TODO remove as part of fix for #8
.getMock());
Authorization.refreshExistingAccessToken()
.then(() => {
fetchMock.called(refreshTokenUrl).should.be.true;
fetchMock.calls().matched.length.should.be.greaterThan(1);
fetchMock.calls().unmatched.length.should.equal(0);
NprOne.accessToken.should.equal(ACCESS_TOKEN_RESPONSE.access_token);
done();
})
.catch(done);
});
it('should retry the call to the refresh token endpoint in the auth proxy up to 3 times, but then error out', (done) => {
let numTries = 0;
const responses = () => {
numTries += 1;
if (numTries < 5) {
return 500;
} else {
return ACCESS_TOKEN_RESPONSE;
}
};
mockery.registerMock('fetch', fetchMock
.mock(refreshTokenUrl, 'POST', responses)
.mock(adsWizzWwwUrl, 'GET', 200) // @TODO remove as part of fix for #8
.mock(adsWizzCdnUrl, 'GET', 200) // @TODO remove as part of fix for #8
.getMock());
Authorization.refreshExistingAccessToken()
.then(() => {
done('Should not be here, call should have ended on a throw');
})
.catch(() => {
fetchMock.called(refreshTokenUrl).should.be.true;
fetchMock.calls().matched.length.should.be.greaterThan(1);
fetchMock.calls().unmatched.length.should.equal(0);
done();
});
});
describe('if no auth proxy URL is set', () => {
beforeEach(() => {
NprOne.config = { authProxyBaseUrl: '' };
});
it('should throw a TypeError', () => {
chai.expect(() => {
Authorization.refreshExistingAccessToken();
}).to.throw('OAuth proxy not configured. Unable to refresh the access token.');
});
});
describe('if no access token is set', () => {
beforeEach(() => {
NprOne.config = { accessToken: '' };
});
it('should throw a TypeError', () => {
chai.expect(() => {
Authorization.refreshExistingAccessToken();
}).to.throw('An access token must be set in order to attempt a refresh.');
});
});
});
/** @test {Authorization#logout} */
describe('logout', () => {
it('should call the logout endpoint in the auth proxy', (done) => {
const oldAccessToken = NprOne.accessToken;
mockery.registerMock('fetch', fetchMock
.mock(logoutUrl, 'POST', JSON.stringify(''))
.getMock());
authorization.logout()
.then(() => {
fetchMock.called(logoutUrl).should.be.true;
fetchMock.calls().unmatched.length.should.equal(0);
const options = fetchMock.lastOptions(logoutUrl);
options.body.should.equal(`token=${oldAccessToken}`);
NprOne.accessToken.should.equal('');
done();
})
.catch(done);
});
it('should throw an error if the response is not \'ok\'', (done) => {
mockery.registerMock('fetch', fetchMock
.mock(logoutUrl, 'POST', 400)
.getMock());
authorization.logout().should.be.rejected.notify(() => {
fetchMock.called(logoutUrl).should.be.true;
fetchMock.calls().unmatched.length.should.equal(0);
done();
});
});
describe('if no access token is set', () => {
beforeEach(() => {
NprOne.config = { accessToken: '' };
});
it('should throw a TypeError', () => {
chai.expect(() => {
authorization.logout();
}).to.throw('An access token must be set in order to attempt a logout.');
});
});
describe('if no auth proxy URL is set', () => {
beforeEach(() => {
NprOne.config = { authProxyBaseUrl: '' };
});
it('should throw a TypeError', () => {
chai.expect(() => {
authorization.logout();
}).to.throw('OAuth proxy not configured. Unable to securely log out the user.');
});
});
});
/** @test {Authorization#getDeviceCode} */
describe('getDeviceCode', () => {
it('should throw an error if not configured properly', () => {
NprOne.config = { authProxyBaseUrl: '' };
chai.expect(() => {
authorization.getDeviceCode();
}).to.throw('OAuth proxy not configured. Unable to use the device code.');
});
it('should make a request to authProxyUrl when called', (done) => {
mockery.registerMock('fetch', fetchMock
.mock(newDeviceCodeUrl, 'POST', DEVICE_CODE_RESPONSE)
.getMock());
authorization.getDeviceCode()
.then((deviceCode) => {
fetchMock.called(newDeviceCodeUrl).should.be.true;
fetchMock.calls().unmatched.length.should.equal(0);
deviceCode.userCode.should.equal(DEVICE_CODE_RESPONSE.user_code);
done();
})
.catch(done);
});
it('should make a request to authProxyUrl with additional scopes when scopes are passed in', (done) => {
mockery.registerMock('fetch', fetchMock
.mock(newDeviceCodeUrl, 'POST', DEVICE_CODE_RESPONSE)
.getMock());
authorization.getDeviceCode(['listening.readonly', 'identity.readonly'])
.then(() => {
fetchMock.called(newDeviceCodeUrl).should.be.true;
const options = fetchMock.lastOptions(newDeviceCodeUrl);
options.body.indexOf('listening.readonly').should.not.equal(-1);
options.body.indexOf('identity.readonly').should.not.equal(-1);
done();
})
.catch(done);
});
});
/** @test {Authorization#pollDeviceCode} */
describe('pollDeviceCode', function () { // intentionally can't use arrow function here
// see: https://github.com/mochajs/mocha/issues/1763
this.timeout(16000);
it('should throw an error if not configured properly', () => {
NprOne.config = { authProxyBaseUrl: '' };
chai.expect(() => {
authorization.pollDeviceCode();
}).to.throw('OAuth proxy not configured. Unable to use the device code.');
});
it('should throw an error if no active device code exists', () => {
chai.expect(() => {
authorization.pollDeviceCode();
}).to.throw('No active device code set. Please call getDeviceCode() before calling this function.');
});
it('should return a Promise that rejects if the active device code is expired', (done) => {
const deviceCodeClone = JSON.parse(JSON.stringify(DEVICE_CODE_RESPONSE));
deviceCodeClone.expires_in = 0;
mockery.registerMock('fetch', fetchMock
.mock(newDeviceCodeUrl, 'POST', deviceCodeClone)
.getMock());
authorization.getDeviceCode()
.then(() => {
authorization.pollDeviceCode().should.be.rejectedWith('The device code has expired. Please generate a new one before continuing.').notify(done);
});
});
it('should return a Promise that resolves to a valid access token if device code is valid', (done) => {
mockery.registerMock('fetch', fetchMock
.mock(newDeviceCodeUrl, 'POST', DEVICE_CODE_RESPONSE)
.mock(deviceCodePollUrl, 'POST', ACCESS_TOKEN_RESPONSE)
.getMock());
authorization.getDeviceCode()
.then(() => {
return authorization.pollDeviceCode();
})
.then((accessToken) => {
NprOne.accessToken.should.equal(ACCESS_TOKEN_RESPONSE.access_token);
accessToken.toString().should.equal(ACCESS_TOKEN_RESPONSE.access_token);
})
.then(done)
.catch(done);
});
it('should return a Promise that rejects if the active device code is invalid (or possibly expired)', (done) => {
mockery.registerMock('fetch', fetchMock
.mock(newDeviceCodeUrl, 'POST', DEVICE_CODE_RESPONSE)
.mock(deviceCodePollUrl, 'POST', {
status: 400,
body: DEVICE_CODE_EXPIRED_RESPONSE,
})
.getMock());
authorization.getDeviceCode()
.then(() => {
authorization.pollDeviceCode().should.be.rejectedWith('Response status: 400 Bad Request').notify(done);
});
});
it('should return a Promise that rejects if the active device code is valid but the user went to log in and denied the app access', (done) => {
mockery.registerMock('fetch', fetchMock
.mock(newDeviceCodeUrl, 'POST', DEVICE_CODE_RESPONSE)
.mock(deviceCodePollUrl, 'POST', {
status: 401,
body: DEVICE_CODE_DENIED_RESPONSE,
})
.getMock());
authorization.getDeviceCode()
.then(() => {
authorization.pollDeviceCode().should.be.rejectedWith('Response status: 401 Unauthorized').notify(done);
});
});
it('should return a Promise that resolves to valid access token after polling a number of times if the user logs in', (done) => {
let numTries = 0;
const responses = () => {
numTries += 1;
if (numTries < 3) {
return {
status: 401,
body: DEVICE_CODE_POLL_RESPONSE,
};
} else {
return ACCESS_TOKEN_RESPONSE; // a.k.a. the user logged in
}
};
mockery.registerMock('fetch', fetchMock
.mock(newDeviceCodeUrl, 'POST', DEVICE_CODE_RESPONSE)
.mock(deviceCodePollUrl, 'POST', responses)
.getMock());
authorization.getDeviceCode()
.then(() => {
return authorization.pollDeviceCode();
})
.then((accessToken) => {
fetchMock.called(deviceCodePollUrl).should.be.true;
fetchMock.calls().matched.length.should.be.greaterThan(1);
fetchMock.calls().unmatched.length.should.equal(0);
NprOne.accessToken.should.equal(ACCESS_TOKEN_RESPONSE.access_token);
accessToken.toString().should.equal(ACCESS_TOKEN_RESPONSE.access_token);
})
.then(done)
.catch(done);
});
it('should return a Promise that rejects if the returned access token is invalid', (done) => {
const accessTokenClone = JSON.parse(JSON.stringify(ACCESS_TOKEN_RESPONSE));
delete accessTokenClone.expires_in;
mockery.registerMock('fetch', fetchMock
.mock(newDeviceCodeUrl, 'POST', DEVICE_CODE_RESPONSE)
.mock(deviceCodePollUrl, 'POST', accessTokenClone)
.getMock());
authorization.getDeviceCode()
.then(() => {
authorization.pollDeviceCode().should.be.rejectedWith(`TypeError: 'expires_in' is missing and is required. :${JSON.stringify(accessTokenClone)}`).notify(done);
});
});
});
});