diff --git a/environment/config.js b/environment/config.js index a94ef30..b0c8b3f 100644 --- a/environment/config.js +++ b/environment/config.js @@ -36,7 +36,8 @@ define(['/node_modules/@oat-sa/tao-core-libs/dist/pathdefinition.js'], function( 'jquery.mockjax': '/node_modules/jquery-mockjax/dist/jquery.mockjax', 'webcrypto-shim': '/node_modules/webcrypto-shim/webcrypto-shim', - 'idb-wrapper': '/node_modules/idb-wrapper/idbstore' + 'idb-wrapper': '/node_modules/idb-wrapper/idbstore', + 'fetch-mock': '/node_modules/fetch-mock/es5/client-bundle' }, libPathDefinition ), diff --git a/package-lock.json b/package-lock.json index b45eeec..130352e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@oat-sa/tao-core-sdk", - "version": "1.3.0", + "version": "1.4.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2087,6 +2087,31 @@ "pend": "~1.2.0" } }, + "fetch-mock": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-9.4.0.tgz", + "integrity": "sha512-tqnFmcjYheW5Z9zOPRVY+ZXjB/QWCYtPiOrYGEsPgKfpGHco97eaaj7Rv9MjK7PVWG4rWfv6t2IgQAzDQizBZA==", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "core-js": "^3.0.0", + "debug": "^4.1.1", + "glob-to-regexp": "^0.4.0", + "is-subset": "^0.1.1", + "lodash.isequal": "^4.5.0", + "path-to-regexp": "^2.2.1", + "querystring": "^0.2.0", + "whatwg-url": "^6.5.0" + }, + "dependencies": { + "core-js": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz", + "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==", + "dev": true + } + } + }, "figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", @@ -2287,6 +2312,12 @@ "@types/glob": "*" } }, + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -2600,6 +2631,12 @@ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", "dev": true }, + "is-subset": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", + "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=", + "dev": true + }, "is-symbol": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", @@ -2934,6 +2971,18 @@ "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", "dev": true }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", + "dev": true + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "dev": true + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -3532,6 +3581,12 @@ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, + "path-to-regexp": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.4.0.tgz", + "integrity": "sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==", + "dev": true + }, "path-type": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", @@ -3644,6 +3699,12 @@ "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", "dev": true }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, "quick-lru": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", @@ -4486,6 +4547,15 @@ "popper.js": "^1.0.2" } }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, "trim-newlines": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", @@ -4641,6 +4711,23 @@ "resolved": "https://registry.npmjs.org/webcrypto-shim/-/webcrypto-shim-0.1.4.tgz", "integrity": "sha512-I2lnL+K2oPNE9ryVHwo42oDnt8XQ9E1KKMGCmcT7OXaAKPmUeCi/G0nUgLR6M6Ztj05ZCxLMGf5bXNaSo+wURg==" }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "whatwg-url": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", + "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", diff --git a/package.json b/package.json index 9b1fdcb..be6233f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@oat-sa/tao-core-sdk", - "version": "1.3.0", + "version": "1.4.0", "displayName": "TAO Core SDK", "description": "Core libraries of TAO", "homepage": "https://github.com/oat-sa/tao-core-sdk-fe#readme", @@ -50,6 +50,7 @@ "eslint": "^5.16.0", "eslint-plugin-es": "^1.4.0", "eslint-plugin-jsdoc": "^4.8.3", + "fetch-mock": "^9.4.0", "gamp": "0.2.1", "glob": "^7.1.3", "handlebars": "1.3.0", diff --git a/src/core/fetchRequest.js b/src/core/fetchRequest.js new file mode 100644 index 0000000..d03a1af --- /dev/null +++ b/src/core/fetchRequest.js @@ -0,0 +1,113 @@ +/** + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; under version 2 + * of the License (non-upgradable). + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * Copyright (c) 2020 (original work) Open Assessment Technologies SA ; + */ + +/** + * !!! IE11 requires polyfill https://www.npmjs.com/package/whatwg-fetch + * Creates an HTTP request to the url based on the provided parameters + * Request is based on fetch, so behaviour and parameters are the same, except + * - every response where response code is not 2xx will be rejected and + * - every response will be parsed as json. + * @param {string} url - url that should be requested + * @param {object} options - fetch request options that implements RequestInit (https://fetch.spec.whatwg.org/#requestinit) + * @param {integer} [options.timeout] - (default: 5000) if timeout reached, the request will be rejected + * @param {object} [options.jwtTokenHandler] - core/jwt/jwtTokenHandler instance that should be used during request + * @returns {Promise} resolves with http Response object + */ +const requestFactory = (url, options) => { + options = Object.assign( + { + timeout: 5000 + }, + options + ); + + let flow = Promise.resolve(); + + if (options.jwtTokenHandler) { + flow = flow + .then(options.jwtTokenHandler.getToken) + .then(token => ({ + Authorization: `Bearer ${token}` + })) + .then(headers => { + options.headers = Object.assign({}, options.headers, headers); + }); + } + + flow = flow.then(() => Promise.race([ + fetch(url, options), + new Promise((resolve, reject) => { + setTimeout(() => reject(new Error('Timeout')), options.timeout); + }) + ])); + + if (options.jwtTokenHandler) { + flow = flow.then(response => { + if (response.status === 401) { + return options.jwtTokenHandler + .refreshToken() + .then(options.jwtTokenHandler.getToken) + .then(token => { + options.headers.Authorization = `Bearer ${token}`; + return fetch(url, options); + }); + } + + return Promise.resolve(response); + }); + } + + /** + * Stores the original response + */ + let originalResponse; + /** + * Stores the response code + */ + let responseCode; + + flow = flow.then(response => { + originalResponse = response; + responseCode = response.status; + return response.json().catch(() => ({})); + }) + .then(response => { + // successful request + if (responseCode === 200 || response.success === true) { + return response; + } + + if (responseCode === 204) { + return null; + } + + // create error + let err; + if (response.errorCode) { + err = new Error(`${response.errorCode} : ${response.errorMsg || response.errorMessage || response.error}`); + } else { + err = new Error(`${responseCode} : Request error`); + } + err.response = originalResponse; + throw err; + }); + + return flow; +}; + +export default requestFactory; diff --git a/src/core/jwt/jwtTokenHandler.js b/src/core/jwt/jwtTokenHandler.js index 17983b7..2d59d0b 100644 --- a/src/core/jwt/jwtTokenHandler.js +++ b/src/core/jwt/jwtTokenHandler.js @@ -18,12 +18,13 @@ /** * Give and refresh JWT token + * !!! The module uses native fetch request to refresh token. + * !!! IE11 requires polyfill https://www.npmjs.com/package/whatwg-fetch * @module core/jwtTokenHandler * @author Tamas Besenyei */ import jwtTokenStoreFactory from 'core/jwt/jwtTokenStore'; -import coreRequest from 'core/request'; import promiseQueue from 'core/promiseQueue'; /** @@ -54,14 +55,22 @@ const jwtTokenHandlerFactory = function jwtTokenHandlerFactory({serviceName = 't if (!refreshToken) { throw new Error('Refresh token is not available'); } else { - return coreRequest({ - url: refreshTokenUrl, + return fetch(refreshTokenUrl, { method: 'POST', - data: JSON.stringify({ refreshToken }), - dataType: 'json', - contentType: 'application/json', - noToken: true - }).then(({ accessToken }) => tokenStorage.setAccessToken(accessToken).then(() => accessToken)); + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ refreshToken }) + }) + .then(response => { + if (response.status === 200) { + return response.json(); + } + const error = new Error('Unsuccessful token refresh'); + error.response = response; + return Promise.reject(error); + }) + .then(({ accessToken }) => tokenStorage.setAccessToken(accessToken).then(() => accessToken)); } }); diff --git a/test/core/fetchRequest/test.html b/test/core/fetchRequest/test.html new file mode 100644 index 0000000..ece9826 --- /dev/null +++ b/test/core/fetchRequest/test.html @@ -0,0 +1,21 @@ + + + + + Core - Fetch Request + + + + +
+
+ + diff --git a/test/core/fetchRequest/test.js b/test/core/fetchRequest/test.js new file mode 100644 index 0000000..a7ff03b --- /dev/null +++ b/test/core/fetchRequest/test.js @@ -0,0 +1,260 @@ +/** + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; under version 2 + * of the License (non-upgradable). + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * Copyright (c) 2020 (original work) Open Assessment Technologies SA ; + */ + +define(['core/fetchRequest', 'core/jwt/jwtTokenHandler', 'fetch-mock'], ( + request, + jwtTokenHandlerFactory, + fetchMock +) => { + 'use strict'; + + // can mocked url redefined + fetchMock.config.overwriteRoutes = true; + + QUnit.module('Request', { + beforeEach: function () { + this.jwtTokenHandler = jwtTokenHandlerFactory({ refreshTokenUrl: '/refresh-token' }); + }, + afterEach: function (done) { + fetchMock.restore(); + this.jwtTokenHandler.clearStore().then(done); + } + }); + + QUnit.test('request returns with correct response', assert => { + assert.expect(1); + const done = assert.async(); + + const mockResponse = { data: { ok: true } }; + fetchMock.mock('/foo', mockResponse); + + request('/foo').then(response => { + assert.deepEqual(response, mockResponse); + done(); + }); + }); + + QUnit.test('request returns with correct response if success true', assert => { + assert.expect(1); + const done = assert.async(); + + fetchMock.mock('/foo', new Response(JSON.stringify({ success: true, data: { ping: 123 } }), { status: 201 })); + + request('/foo').then(response => { + assert.deepEqual(response.data, { ping: 123 }); + done(); + }); + }); + + QUnit.test('request returns with correct response if 204 empty content', assert => { + assert.expect(1); + const done = assert.async(); + + fetchMock.mock('/foo', new Response(null, { status: 204 })); + + request('/foo').then(response => { + assert.equal(response, null); + done(); + }); + }); + + QUnit.test('request returns with a correct error response', assert => { + assert.expect(2); + const done = assert.async(); + + fetchMock.mock('/foo', 404); + + request('/foo').catch(error => { + assert.equal(error.message, '404 : Request error'); + assert.equal(error.response.status, 404); + done(); + }); + }); + + QUnit.test('request returns with a correct error response if success false', assert => { + assert.expect(2); + const done = assert.async(); + + fetchMock.mock( + '/foo', + new Response(JSON.stringify({ success: false, errorCode: 'ABC123', errorMessage: 'Cannot trigger ABC' }), { + status: 201 + }) + ); + + request('/foo').catch(error => { + assert.equal(error.message, 'ABC123 : Cannot trigger ABC'); + assert.equal(error.response.status, 201); + done(); + }); + }); + + QUnit.test('request sends an auth header', function (assert) { + assert.expect(2); + const done = assert.async(); + + const mockResponse = { foo: 'bar' }; + const accessToken = 'someToken'; + const url = '/bar'; + + fetchMock.mock(url, (uri, opts) => { + assert.deepEqual(opts.headers, { + Authorization: `Bearer ${accessToken}` + }); + return mockResponse; + }); + + this.jwtTokenHandler + .storeAccessToken(accessToken) + .then(() => request(url, { jwtTokenHandler: this.jwtTokenHandler })) + .then(response => { + assert.deepEqual(response, mockResponse); + done(); + }); + }); + + QUnit.test('request refreshes the token when it does not exist', function (assert) { + assert.expect(5); + const done = assert.async(); + + const mockResponse = { response: 2 }; + const refreshToken = 'refreshToken'; + const newAccessToken = 'newAccessToken'; + const url = '/api/request'; + + fetchMock.mock('/refresh-token', (uri, opts) => { + const data = JSON.parse(opts.body); + assert.equal(opts.headers['Content-Type'], 'application/json'); + assert.equal(opts.method, 'POST'); + assert.deepEqual(data, { refreshToken }); + return JSON.stringify({ accessToken: newAccessToken }); + }); + + fetchMock.mock(url, (uri, opts) => { + assert.equal(opts.headers.Authorization, `Bearer ${newAccessToken}`); + return JSON.stringify(mockResponse); + }); + + this.jwtTokenHandler + .storeRefreshToken(refreshToken) + .then(() => request(url, { jwtTokenHandler: this.jwtTokenHandler })) + .then(response => { + assert.deepEqual(response, mockResponse); + done(); + }); + }); + + QUnit.test('request refreshes the token when it is not valid', function (assert) { + assert.expect(6); + const done = assert.async(); + + const mockResponse = { response: 2 }; + const refreshToken = 'refreshToken'; + const accessToken = 'invalidAccessToken'; + const newAccessToken = 'newAccessToken'; + const url = '/api/request'; + + const setupSecondRequest = () => { + fetchMock.mock(url, function (uri, opts) { + assert.equal(opts.headers.Authorization, `Bearer ${newAccessToken}`); + return JSON.stringify(mockResponse); + }); + }; + + fetchMock.mock(url, function (uri, opts) { + assert.equal(opts.headers.Authorization, `Bearer ${accessToken}`); + setupSecondRequest(); + return { status: 401 }; + }); + + fetchMock.mock('/refresh-token', function (uri, opts) { + const data = JSON.parse(opts.body); + assert.equal(opts.method, 'POST'); + assert.equal(opts.headers['Content-Type'], 'application/json'); + assert.deepEqual(data, { refreshToken }); + return JSON.stringify({ accessToken: newAccessToken }); + }); + + this.jwtTokenHandler + .storeRefreshToken(refreshToken) + .then(() => this.jwtTokenHandler.storeAccessToken(accessToken)) + .then(() => request(url, { jwtTokenHandler: this.jwtTokenHandler })) + .then(response => { + assert.deepEqual(response, mockResponse); + done(); + }); + }); + + QUnit.test('request returns with the correct error response if there are no tokens', function (assert) { + assert.expect(1); + assert.rejects( + request('/foo', { jwtTokenHandler: this.jwtTokenHandler }), + /Token not available and cannot be refreshed/ + ); + }); + + QUnit.test( + 'request returns with the correct error response if token is not valid and cannot be refreshed', + function (assert) { + assert.expect(1); + const done = assert.async(); + + const accessToken = 'invalidAccessToken'; + + fetchMock.mock('/', 401); + + this.jwtTokenHandler.storeAccessToken(accessToken).then(() => { + assert.rejects( + request('/', { jwtTokenHandler: this.jwtTokenHandler }), + /Refresh token is not available/ + ); + done(); + }); + } + ); + + QUnit.test('request fails if token is refreshed and is still not valid', function (assert) { + assert.expect(2); + const done = assert.async(); + + const refreshToken = 'refreshToken'; + const accessToken = 'invalidAccessToken'; + const newAccessToken = 'stillInvalidAccessToken'; + const url = '/api/request'; + + fetchMock.mock(url, 401); + fetchMock.mock('/refresh-token', JSON.stringify({ accessToken: newAccessToken })); + + this.jwtTokenHandler + .storeRefreshToken(refreshToken) + .then(() => this.jwtTokenHandler.storeAccessToken(accessToken)) + .then(() => request(url, { jwtTokenHandler: this.jwtTokenHandler })) + .catch(error => { + assert.equal(error.message, '401 : Request error'); + assert.equal(error.response.status, 401); + done(); + }); + }); + + QUnit.test('request rejects if timeout reached', function (assert) { + assert.expect(1); + fetchMock.mock('/', new Promise(resolve => setTimeout(resolve, 2000))); + + assert.rejects(request('/', { timeout: 1000 }), /Timeout/); + }); +}); diff --git a/test/core/jwt/jwtTokenHandler/test.js b/test/core/jwt/jwtTokenHandler/test.js index ee45c1a..5e3f3cf 100644 --- a/test/core/jwt/jwtTokenHandler/test.js +++ b/test/core/jwt/jwtTokenHandler/test.js @@ -20,7 +20,7 @@ * @author Tamas Besenyei */ -define(['jquery', 'core/jwt/jwtTokenHandler', 'jquery.mockjax'], ($, jwtTokenHandlerFactory) => { +define(['jquery', 'core/jwt/jwtTokenHandler', 'fetch-mock'], ($, jwtTokenHandlerFactory, fetchMock) => { 'use strict'; QUnit.module('factory'); @@ -40,17 +40,13 @@ define(['jquery', 'core/jwt/jwtTokenHandler', 'jquery.mockjax'], ($, jwtTokenHan ); }); - // prevent the AJAX mocks to pollute the logs - $.mockjaxSettings.logger = null; - $.mockjaxSettings.responseTime = 1; - QUnit.module('API', { beforeEach: function() { - this.handler = jwtTokenHandlerFactory({ refreshTokenUrl: '//refreshUrl' }); + this.handler = jwtTokenHandlerFactory({ refreshTokenUrl: '/refreshUrl' }); }, afterEach: function(assert) { const done = assert.async(); - $.mockjax.clear(); + fetchMock.restore(); this.handler.clearStore().then(done); } }); @@ -123,17 +119,11 @@ define(['jquery', 'core/jwt/jwtTokenHandler', 'jquery.mockjax'], ($, jwtTokenHan const accessToken = 'some access token'; const refreshToken = 'some refresh token'; - $.mockjax([ - { - url: /^\/\/refreshUrl$/, - status: 200, - response: function(request) { - const data = JSON.parse(request.data); - assert.equal(data.refreshToken, refreshToken, 'refresh token is sent to the api'); - this.responseText = JSON.stringify({ accessToken }); - } - } - ]); + fetchMock.mock('/refreshUrl', function(url, opts) { + const data = JSON.parse(opts.body); + assert.equal(data.refreshToken, refreshToken, 'refresh token is sent to the api'); + return JSON.stringify({ accessToken }); + }); this.handler.storeRefreshToken(refreshToken).then(setTokenResult => { assert.equal(setTokenResult, true, 'refresh token is set'); @@ -149,58 +139,58 @@ define(['jquery', 'core/jwt/jwtTokenHandler', 'jquery.mockjax'], ($, jwtTokenHan }); QUnit.test('unsuccessful refresh token', function(assert) { - assert.expect(3); + assert.expect(5); const done = assert.async(); const error = 'some backend error'; const refreshToken = 'some refresh token'; - $.mockjax([ - { - url: /^\/\/refreshUrl$/, - status: 401, - response: function(request) { - const data = JSON.parse(request.data); - assert.equal(data.refreshToken, refreshToken, 'refresh token is sent to the api'); - this.responseText = JSON.stringify({ error }); - } - } - ]); + fetchMock.mock('/refreshUrl', function(url, opts) { + const data = JSON.parse(opts.body); + assert.equal(data.refreshToken, refreshToken, 'refresh token is sent to the api'); + return new Response(JSON.stringify({error}), { status: 401 }); + }); this.handler.storeRefreshToken(refreshToken).then(setTokenResult => { assert.equal(setTokenResult, true, 'refresh token is set'); - this.handler.refreshToken().catch(errorResponse => { - assert.equal(errorResponse.response.error, error, 'should get back api error message'); + this.handler.refreshToken() + .catch(e => { + assert.equal(e instanceof Error, true, 'rejects with error'); + assert.equal(e.response instanceof Response, true, 'passes response'); + return e.response.json(); + }) + .then(errorResponse => { + assert.equal(errorResponse.error, error, 'should get back api error message'); done(); }); }); }); QUnit.test('unsuccessful get token if refresh fails', function(assert) { - assert.expect(3); + assert.expect(5); const done = assert.async(); const error = 'some backend error'; const refreshToken = 'some refresh token'; - $.mockjax([ - { - url: /^\/\/refreshUrl$/, - status: 401, - response: function(request) { - const data = JSON.parse(request.data); - assert.equal(data.refreshToken, refreshToken, 'refresh token is sent to the api'); - this.responseText = JSON.stringify({ error }); - } - } - ]); + fetchMock.mock('/refreshUrl', function(url, opts) { + const data = JSON.parse(opts.body); + assert.equal(data.refreshToken, refreshToken, 'refresh token is sent to the api'); + return new Response(JSON.stringify({error}), { status: 401 }); + }); this.handler.storeRefreshToken(refreshToken).then(setTokenResult => { assert.equal(setTokenResult, true, 'refresh token is set'); - this.handler.getToken().catch(errorResponse => { - assert.equal(errorResponse.response.error, error, 'should get back api error message'); + this.handler.getToken() + .catch(e => { + assert.equal(e instanceof Error, true, 'rejects with error'); + assert.equal(e.response instanceof Response, true, 'passes response'); + return e.response.json(); + }) + .then(errorResponse => { + assert.equal(errorResponse.error, error, 'should get back api error message'); done(); }); }); @@ -208,11 +198,11 @@ define(['jquery', 'core/jwt/jwtTokenHandler', 'jquery.mockjax'], ($, jwtTokenHan QUnit.module('Concurrency', { beforeEach: function() { - this.handler = jwtTokenHandlerFactory({ refreshTokenUrl: '//refreshUrl' }); + this.handler = jwtTokenHandlerFactory({ refreshTokenUrl: '/refreshUrl' }); }, afterEach: function(assert) { const done = assert.async(); - $.mockjax.clear(); + fetchMock.restore(); this.handler.clearStore().then(done); } }); @@ -225,17 +215,11 @@ define(['jquery', 'core/jwt/jwtTokenHandler', 'jquery.mockjax'], ($, jwtTokenHan const accessToken = 'some access token'; const refreshToken = 'some refresh token'; - $.mockjax([ - { - url: /^\/\/refreshUrl$/, - status: 200, - response: function(request) { - const data = JSON.parse(request.data); - assert.equal(data.refreshToken, refreshToken, 'refresh token is sent to the api'); - this.responseText = JSON.stringify({ accessToken }); - } - } - ]); + fetchMock.mock('/refreshUrl', function(url, opts) { + const data = JSON.parse(opts.body); + assert.equal(data.refreshToken, refreshToken, 'refresh token is sent to the api'); + return JSON.stringify({accessToken}); + }); this.handler.storeRefreshToken(refreshToken).then(setTokenResult => { assert.equal(setTokenResult, true, 'refresh token is set'); @@ -260,17 +244,11 @@ define(['jquery', 'core/jwt/jwtTokenHandler', 'jquery.mockjax'], ($, jwtTokenHan const accessToken = 'some access token'; const refreshToken = 'some refresh token'; - $.mockjax([ - { - url: /^\/\/refreshUrl$/, - status: 200, - response: function(request) { - const data = JSON.parse(request.data); - assert.equal(data.refreshToken, refreshToken, 'refresh token is sent to the api'); - this.responseText = JSON.stringify({ accessToken }); - } - } - ]); + fetchMock.mock('/refreshUrl', function(url, opts) { + const data = JSON.parse(opts.body); + assert.equal(data.refreshToken, refreshToken, 'refresh token is sent to the api'); + return JSON.stringify({accessToken}); + }); this.handler.storeRefreshToken(refreshToken).then(setTokenResult => { assert.equal(setTokenResult, true, 'refresh token is set'); @@ -297,33 +275,20 @@ define(['jquery', 'core/jwt/jwtTokenHandler', 'jquery.mockjax'], ($, jwtTokenHan const refreshToken = 'some refresh token'; const setupSecondRequest = () => { - $.mockjax.clear(); - $.mockjax([ - { - url: /^\/\/refreshUrl$/, - status: 200, - response: function(request) { - const data = JSON.parse(request.data); - assert.equal(data.refreshToken, refreshToken, 'refresh token is sent to the api'); - this.responseText = JSON.stringify({ accessToken: accessToken2 }); - setupSecondRequest(); - } - } - ]); + fetchMock.restore(); + fetchMock.mock('/refreshUrl', function(url, opts) { + const data = JSON.parse(opts.body); + assert.equal(data.refreshToken, refreshToken, 'refresh token is sent to the api'); + return JSON.stringify({accessToken: accessToken2}); + }); }; - $.mockjax([ - { - url: /^\/\/refreshUrl$/, - status: 200, - response: function(request) { - const data = JSON.parse(request.data); - assert.equal(data.refreshToken, refreshToken, 'refresh token is sent to the api'); - this.responseText = JSON.stringify({ accessToken: accessToken1 }); - setupSecondRequest(); - } - } - ]); + fetchMock.mock('/refreshUrl', function(url, opts) { + const data = JSON.parse(opts.body); + assert.equal(data.refreshToken, refreshToken, 'refresh token is sent to the api'); + setupSecondRequest(); + return JSON.stringify({accessToken: accessToken1}); + }); this.handler.storeRefreshToken(refreshToken).then(setTokenResult => { assert.equal(setTokenResult, true, 'refresh token is set'); diff --git a/test/core/request/test.js b/test/core/request/test.js index ddfbbfd..0c721bd 100644 --- a/test/core/request/test.js +++ b/test/core/request/test.js @@ -21,13 +21,14 @@ * * @author Martin Nicholson */ -define(['jquery', 'lodash', 'core/request', 'core/tokenHandler', 'core/jwt/jwtTokenHandler', 'core/logger', 'jquery.mockjax'], function( +define(['jquery', 'lodash', 'core/request', 'core/tokenHandler', 'core/jwt/jwtTokenHandler', 'core/logger', 'fetch-mock', 'jquery.mockjax'], function( $, _, request, tokenHandlerFactory, jwtTokenHandlerFactory, - loggerFactory + loggerFactory, + fetchMock ) { 'use strict'; @@ -616,11 +617,12 @@ define(['jquery', 'lodash', 'core/request', 'core/tokenHandler', 'core/jwt/jwtTo QUnit.module('JWT token', { beforeEach: function() { - this.handler = jwtTokenHandlerFactory({ refreshTokenUrl: '//refreshUrl' }); + this.handler = jwtTokenHandlerFactory({ refreshTokenUrl: '/refreshUrl' }); }, afterEach: function() { this.handler.clearStore(); $.mockjax.clear(); + fetchMock.restore(); } }); @@ -662,6 +664,12 @@ define(['jquery', 'lodash', 'core/request', 'core/tokenHandler', 'core/jwt/jwtTo const accessToken = 'some access token'; const refreshToken = 'some refresh token'; + fetchMock.mock('/refreshUrl', function(uri, opts) { + const data = JSON.parse(opts.body); + assert.equal(data.refreshToken, refreshToken, 'refresh token is sent to the api'); + return JSON.stringify({ accessToken }); + }); + $.mockjax([ { url: /^\/\/200$/, @@ -674,15 +682,6 @@ define(['jquery', 'lodash', 'core/request', 'core/tokenHandler', 'core/jwt/jwtTo ); this.responseText = JSON.stringify({}); } - }, - { - url: /^\/\/refreshUrl$/, - status: 200, - response: function(requestData) { - const data = JSON.parse(requestData.data); - assert.equal(data.refreshToken, refreshToken, 'refresh token is sent to the api'); - this.responseText = JSON.stringify({ accessToken: accessToken }); - } } ]); @@ -703,6 +702,12 @@ define(['jquery', 'lodash', 'core/request', 'core/tokenHandler', 'core/jwt/jwtTo const validAccessToken = 'valid access token'; const refreshToken = 'some refresh token'; + fetchMock.mock('/refreshUrl', function(uri, opts) { + const data = JSON.parse(opts.body); + assert.equal(data.refreshToken, refreshToken, 'refresh token is sent to the api'); + return JSON.stringify({ accessToken: validAccessToken }); + }); + $.mockjax([ { url: /^\/\/endpoint$/, @@ -718,15 +723,6 @@ define(['jquery', 'lodash', 'core/request', 'core/tokenHandler', 'core/jwt/jwtTo this.responseText = JSON.stringify({}); } } - }, - { - url: /^\/\/refreshUrl$/, - status: 200, - response: function(requestData) { - const data = JSON.parse(requestData.data); - assert.equal(data.refreshToken, refreshToken, 'refresh token is sent to the api'); - this.responseText = JSON.stringify({ accessToken: validAccessToken }); - } } ]); @@ -754,6 +750,12 @@ define(['jquery', 'lodash', 'core/request', 'core/tokenHandler', 'core/jwt/jwtTo error: 'some error' }; + fetchMock.mock('/refreshUrl', function(uri, opts) { + const data = JSON.parse(opts.body); + assert.equal(data.refreshToken, refreshToken, 'refresh token is sent to the api'); + return JSON.stringify({ accessToken: expiredAccessToken }); + }); + $.mockjax([ { url: /^\/\/endpoint$/, @@ -766,15 +768,6 @@ define(['jquery', 'lodash', 'core/request', 'core/tokenHandler', 'core/jwt/jwtTo ); this.responseText = JSON.stringify(originalError); } - }, - { - url: /^\/\/refreshUrl$/, - status: 200, - response: function(requestData) { - const data = JSON.parse(requestData.data); - assert.equal(data.refreshToken, refreshToken, 'refresh token is sent to the api'); - this.responseText = JSON.stringify({ accessToken: expiredAccessToken }); - } } ]); @@ -803,6 +796,12 @@ define(['jquery', 'lodash', 'core/request', 'core/tokenHandler', 'core/jwt/jwtTo error: 'some error' }; + fetchMock.mock('/refreshUrl', function(uri, opts) { + const data = JSON.parse(opts.body); + assert.equal(data.refreshToken, refreshToken, 'refresh token is sent to the api'); + return new Response(JSON.stringify({ accessToken: expiredAccessToken }), { status: 401 }); + }); + $.mockjax([ { url: /^\/\/endpoint$/, @@ -815,14 +814,6 @@ define(['jquery', 'lodash', 'core/request', 'core/tokenHandler', 'core/jwt/jwtTo ); this.responseText = JSON.stringify(originalError); } - }, - { - url: /^\/\/refreshUrl$/, - status: 401, - response: function(requestData) { - const data = JSON.parse(requestData.data); - assert.equal(data.refreshToken, refreshToken, 'refresh token is sent to the api'); - } } ]);