Home Reference Source Test Repository

unit/controller/authorization.js

  1. import chai from 'chai';
  2. import { ACCESS_TOKEN_RESPONSE, DEVICE_CODE_RESPONSE, DEVICE_CODE_POLL_RESPONSE, DEVICE_CODE_DENIED_RESPONSE, DEVICE_CODE_EXPIRED_RESPONSE } from '../../test-data';
  3. import mockery from 'mockery';
  4. import fetchMock from 'fetch-mock';
  5. import Authorization from './../../../src/controller/authorization';
  6. import NprOne from './../../../src/index';
  7. import { testConfig } from '../../test';
  8.  
  9. const should = chai.should();
  10.  
  11.  
  12. /** @test {Authorization} */
  13. describe('Authorization', () => {
  14. let authorization;
  15. let refreshTokenUrl;
  16. let logoutUrl;
  17. let newDeviceCodeUrl;
  18. let deviceCodePollUrl;
  19.  
  20.  
  21. beforeEach(() => {
  22. authorization = new Authorization();
  23. NprOne.config = testConfig;
  24.  
  25. refreshTokenUrl = `^${testConfig.authProxyBaseUrl}${NprOne.config.refreshTokenPath}`;
  26. logoutUrl = `^${testConfig.authProxyBaseUrl}${NprOne.config.logoutPath}`;
  27. newDeviceCodeUrl = `${NprOne.config.authProxyBaseUrl}${NprOne.config.newDeviceCodePath}`;
  28. deviceCodePollUrl = `${NprOne.config.authProxyBaseUrl}${NprOne.config.pollDeviceCodePath}`;
  29. });
  30.  
  31. afterEach(() => {
  32. fetchMock.restore();
  33. mockery.deregisterMock('fetch');
  34. });
  35.  
  36.  
  37. /** @test {Authorization.refreshExistingAccessToken} */
  38. describe('refreshExistingAccessToken', function () { // intentionally can't use arrow function here
  39. // see: https://github.com/mochajs/mocha/issues/1763
  40. this.timeout(16000);
  41.  
  42. // @TODO this should not be necessary - quick-fix to deal with implicit dependency on Listening controller - see #8
  43. const adsWizzWwwUrl = 'https://adswizz.com';
  44. const adsWizzCdnUrl = '^https://delivery-s3.adswizz.com';
  45.  
  46. it('should call the refresh token endpoint in the auth proxy', (done) => {
  47. mockery.registerMock('fetch', fetchMock
  48. .mock(refreshTokenUrl, 'POST', ACCESS_TOKEN_RESPONSE)
  49. .mock(adsWizzWwwUrl, 'GET', 200) // @TODO remove as part of fix for #8
  50. .mock(adsWizzCdnUrl, 'GET', 200) // @TODO remove as part of fix for #8
  51. .getMock());
  52.  
  53. Authorization.refreshExistingAccessToken()
  54. .then(() => {
  55. fetchMock.called(refreshTokenUrl).should.be.true;
  56. fetchMock.calls().unmatched.length.should.equal(0);
  57. NprOne.accessToken.should.equal(ACCESS_TOKEN_RESPONSE.access_token);
  58. done();
  59. })
  60. .catch(done);
  61. });
  62.  
  63. it('should retry the call to the refresh token endpoint in the auth proxy if it receives a bad response the first time', (done) => {
  64. let numTries = 0;
  65.  
  66. const responses = () => {
  67. numTries += 1;
  68.  
  69. if (numTries === 1) {
  70. return 500;
  71. } else {
  72. return ACCESS_TOKEN_RESPONSE;
  73. }
  74. };
  75.  
  76. mockery.registerMock('fetch', fetchMock
  77. .mock(refreshTokenUrl, 'POST', responses)
  78. .mock(adsWizzWwwUrl, 'GET', 200) // @TODO remove as part of fix for #8
  79. .mock(adsWizzCdnUrl, 'GET', 200) // @TODO remove as part of fix for #8
  80. .getMock());
  81.  
  82. Authorization.refreshExistingAccessToken()
  83. .then(() => {
  84. fetchMock.called(refreshTokenUrl).should.be.true;
  85. fetchMock.calls().matched.length.should.be.greaterThan(1);
  86. fetchMock.calls().unmatched.length.should.equal(0);
  87. NprOne.accessToken.should.equal(ACCESS_TOKEN_RESPONSE.access_token);
  88. done();
  89. })
  90. .catch(done);
  91. });
  92.  
  93. it('should retry the call to the refresh token endpoint in the auth proxy up to 3 times, but then error out', (done) => {
  94. let numTries = 0;
  95.  
  96. const responses = () => {
  97. numTries += 1;
  98.  
  99. if (numTries < 5) {
  100. return 500;
  101. } else {
  102. return ACCESS_TOKEN_RESPONSE;
  103. }
  104. };
  105.  
  106. mockery.registerMock('fetch', fetchMock
  107. .mock(refreshTokenUrl, 'POST', responses)
  108. .mock(adsWizzWwwUrl, 'GET', 200) // @TODO remove as part of fix for #8
  109. .mock(adsWizzCdnUrl, 'GET', 200) // @TODO remove as part of fix for #8
  110. .getMock());
  111.  
  112. Authorization.refreshExistingAccessToken()
  113. .then(() => {
  114. done('Should not be here, call should have ended on a throw');
  115. })
  116. .catch(() => {
  117. fetchMock.called(refreshTokenUrl).should.be.true;
  118. fetchMock.calls().matched.length.should.be.greaterThan(1);
  119. fetchMock.calls().unmatched.length.should.equal(0);
  120. done();
  121. });
  122. });
  123.  
  124. describe('if no auth proxy URL is set', () => {
  125. beforeEach(() => {
  126. NprOne.config = { authProxyBaseUrl: '' };
  127. });
  128.  
  129. it('should throw a TypeError', () => {
  130. chai.expect(() => {
  131. Authorization.refreshExistingAccessToken();
  132. }).to.throw('OAuth proxy not configured. Unable to refresh the access token.');
  133. });
  134. });
  135.  
  136. describe('if no access token is set', () => {
  137. beforeEach(() => {
  138. NprOne.config = { accessToken: '' };
  139. });
  140.  
  141. it('should throw a TypeError', () => {
  142. chai.expect(() => {
  143. Authorization.refreshExistingAccessToken();
  144. }).to.throw('An access token must be set in order to attempt a refresh.');
  145. });
  146. });
  147. });
  148.  
  149.  
  150. /** @test {Authorization#logout} */
  151. describe('logout', () => {
  152. it('should call the logout endpoint in the auth proxy', (done) => {
  153. const oldAccessToken = NprOne.accessToken;
  154.  
  155. mockery.registerMock('fetch', fetchMock
  156. .mock(logoutUrl, 'POST', JSON.stringify(''))
  157. .getMock());
  158.  
  159. authorization.logout()
  160. .then(() => {
  161. fetchMock.called(logoutUrl).should.be.true;
  162. fetchMock.calls().unmatched.length.should.equal(0);
  163. const options = fetchMock.lastOptions(logoutUrl);
  164. options.body.should.equal(`token=${oldAccessToken}`);
  165. NprOne.accessToken.should.equal('');
  166. done();
  167. })
  168. .catch(done);
  169. });
  170.  
  171. it('should throw an error if the response is not \'ok\'', (done) => {
  172. mockery.registerMock('fetch', fetchMock
  173. .mock(logoutUrl, 'POST', 400)
  174. .getMock());
  175.  
  176. authorization.logout().should.be.rejected.notify(() => {
  177. fetchMock.called(logoutUrl).should.be.true;
  178. fetchMock.calls().unmatched.length.should.equal(0);
  179. done();
  180. });
  181. });
  182.  
  183. describe('if no access token is set', () => {
  184. beforeEach(() => {
  185. NprOne.config = { accessToken: '' };
  186. });
  187.  
  188. it('should throw a TypeError', () => {
  189. chai.expect(() => {
  190. authorization.logout();
  191. }).to.throw('An access token must be set in order to attempt a logout.');
  192. });
  193. });
  194.  
  195. describe('if no auth proxy URL is set', () => {
  196. beforeEach(() => {
  197. NprOne.config = { authProxyBaseUrl: '' };
  198. });
  199.  
  200. it('should throw a TypeError', () => {
  201. chai.expect(() => {
  202. authorization.logout();
  203. }).to.throw('OAuth proxy not configured. Unable to securely log out the user.');
  204. });
  205. });
  206. });
  207.  
  208.  
  209. /** @test {Authorization#getDeviceCode} */
  210. describe('getDeviceCode', () => {
  211. it('should throw an error if not configured properly', () => {
  212. NprOne.config = { authProxyBaseUrl: '' };
  213. chai.expect(() => {
  214. authorization.getDeviceCode();
  215. }).to.throw('OAuth proxy not configured. Unable to use the device code.');
  216. });
  217.  
  218. it('should make a request to authProxyUrl when called', (done) => {
  219. mockery.registerMock('fetch', fetchMock
  220. .mock(newDeviceCodeUrl, 'POST', DEVICE_CODE_RESPONSE)
  221. .getMock());
  222.  
  223. authorization.getDeviceCode()
  224. .then((deviceCode) => {
  225. fetchMock.called(newDeviceCodeUrl).should.be.true;
  226. fetchMock.calls().unmatched.length.should.equal(0);
  227. deviceCode.userCode.should.equal(DEVICE_CODE_RESPONSE.user_code);
  228. done();
  229. })
  230. .catch(done);
  231. });
  232.  
  233. it('should make a request to authProxyUrl with additional scopes when scopes are passed in', (done) => {
  234. mockery.registerMock('fetch', fetchMock
  235. .mock(newDeviceCodeUrl, 'POST', DEVICE_CODE_RESPONSE)
  236. .getMock());
  237.  
  238. authorization.getDeviceCode(['listening.readonly', 'identity.readonly'])
  239. .then(() => {
  240. fetchMock.called(newDeviceCodeUrl).should.be.true;
  241. const options = fetchMock.lastOptions(newDeviceCodeUrl);
  242. options.body.indexOf('listening.readonly').should.not.equal(-1);
  243. options.body.indexOf('identity.readonly').should.not.equal(-1);
  244. done();
  245. })
  246. .catch(done);
  247. });
  248. });
  249.  
  250.  
  251. /** @test {Authorization#pollDeviceCode} */
  252. describe('pollDeviceCode', function () { // intentionally can't use arrow function here
  253. // see: https://github.com/mochajs/mocha/issues/1763
  254. this.timeout(16000);
  255.  
  256. it('should throw an error if not configured properly', () => {
  257. NprOne.config = { authProxyBaseUrl: '' };
  258. chai.expect(() => {
  259. authorization.pollDeviceCode();
  260. }).to.throw('OAuth proxy not configured. Unable to use the device code.');
  261. });
  262.  
  263. it('should throw an error if no active device code exists', () => {
  264. chai.expect(() => {
  265. authorization.pollDeviceCode();
  266. }).to.throw('No active device code set. Please call getDeviceCode() before calling this function.');
  267. });
  268.  
  269. it('should return a Promise that rejects if the active device code is expired', (done) => {
  270. const deviceCodeClone = JSON.parse(JSON.stringify(DEVICE_CODE_RESPONSE));
  271. deviceCodeClone.expires_in = 0;
  272.  
  273. mockery.registerMock('fetch', fetchMock
  274. .mock(newDeviceCodeUrl, 'POST', deviceCodeClone)
  275. .getMock());
  276.  
  277. authorization.getDeviceCode()
  278. .then(() => {
  279. authorization.pollDeviceCode().should.be.rejectedWith('The device code has expired. Please generate a new one before continuing.').notify(done);
  280. });
  281. });
  282.  
  283. it('should return a Promise that resolves to a valid access token if device code is valid', (done) => {
  284. mockery.registerMock('fetch', fetchMock
  285. .mock(newDeviceCodeUrl, 'POST', DEVICE_CODE_RESPONSE)
  286. .mock(deviceCodePollUrl, 'POST', ACCESS_TOKEN_RESPONSE)
  287. .getMock());
  288.  
  289. authorization.getDeviceCode()
  290. .then(() => {
  291. return authorization.pollDeviceCode();
  292. })
  293. .then((accessToken) => {
  294. NprOne.accessToken.should.equal(ACCESS_TOKEN_RESPONSE.access_token);
  295. accessToken.toString().should.equal(ACCESS_TOKEN_RESPONSE.access_token);
  296. })
  297. .then(done)
  298. .catch(done);
  299. });
  300.  
  301. it('should return a Promise that rejects if the active device code is invalid (or possibly expired)', (done) => {
  302. mockery.registerMock('fetch', fetchMock
  303. .mock(newDeviceCodeUrl, 'POST', DEVICE_CODE_RESPONSE)
  304. .mock(deviceCodePollUrl, 'POST', {
  305. status: 400,
  306. body: DEVICE_CODE_EXPIRED_RESPONSE,
  307. })
  308. .getMock());
  309.  
  310. authorization.getDeviceCode()
  311. .then(() => {
  312. authorization.pollDeviceCode().should.be.rejectedWith('Response status: 400 Bad Request').notify(done);
  313. });
  314. });
  315.  
  316. 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) => {
  317. mockery.registerMock('fetch', fetchMock
  318. .mock(newDeviceCodeUrl, 'POST', DEVICE_CODE_RESPONSE)
  319. .mock(deviceCodePollUrl, 'POST', {
  320. status: 401,
  321. body: DEVICE_CODE_DENIED_RESPONSE,
  322. })
  323. .getMock());
  324.  
  325. authorization.getDeviceCode()
  326. .then(() => {
  327. authorization.pollDeviceCode().should.be.rejectedWith('Response status: 401 Unauthorized').notify(done);
  328. });
  329. });
  330.  
  331. it('should return a Promise that resolves to valid access token after polling a number of times if the user logs in', (done) => {
  332. let numTries = 0;
  333.  
  334. const responses = () => {
  335. numTries += 1;
  336.  
  337. if (numTries < 3) {
  338. return {
  339. status: 401,
  340. body: DEVICE_CODE_POLL_RESPONSE,
  341. };
  342. } else {
  343. return ACCESS_TOKEN_RESPONSE; // a.k.a. the user logged in
  344. }
  345. };
  346.  
  347. mockery.registerMock('fetch', fetchMock
  348. .mock(newDeviceCodeUrl, 'POST', DEVICE_CODE_RESPONSE)
  349. .mock(deviceCodePollUrl, 'POST', responses)
  350. .getMock());
  351.  
  352. authorization.getDeviceCode()
  353. .then(() => {
  354. return authorization.pollDeviceCode();
  355. })
  356. .then((accessToken) => {
  357. fetchMock.called(deviceCodePollUrl).should.be.true;
  358. fetchMock.calls().matched.length.should.be.greaterThan(1);
  359. fetchMock.calls().unmatched.length.should.equal(0);
  360. NprOne.accessToken.should.equal(ACCESS_TOKEN_RESPONSE.access_token);
  361. accessToken.toString().should.equal(ACCESS_TOKEN_RESPONSE.access_token);
  362. })
  363. .then(done)
  364. .catch(done);
  365. });
  366.  
  367. it('should return a Promise that rejects if the returned access token is invalid', (done) => {
  368. const accessTokenClone = JSON.parse(JSON.stringify(ACCESS_TOKEN_RESPONSE));
  369. delete accessTokenClone.expires_in;
  370.  
  371. mockery.registerMock('fetch', fetchMock
  372. .mock(newDeviceCodeUrl, 'POST', DEVICE_CODE_RESPONSE)
  373. .mock(deviceCodePollUrl, 'POST', accessTokenClone)
  374. .getMock());
  375.  
  376. authorization.getDeviceCode()
  377. .then(() => {
  378. authorization.pollDeviceCode().should.be.rejectedWith(`TypeError: 'expires_in' is missing and is required. :${JSON.stringify(accessTokenClone)}`).notify(done);
  379. });
  380. });
  381. });
  382. });