diff --git a/package.json b/package.json index dac6096..dbfe35b 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "dependencies": { "babel-runtime": "^6.11.6", "isomorphic-fetch": "^2.2.1", - "loglevel": "^1.4.1" + "loglevel": "^1.4.1", + "url-parse": "^1.1.3" }, "devDependencies": { "babel-cli": "^6.10.1", diff --git a/src/util/abstract_service.js b/src/util/abstract_service.js index 8a8e425..aba087f 100644 --- a/src/util/abstract_service.js +++ b/src/util/abstract_service.js @@ -15,6 +15,7 @@ */ import Http from './http'; +import URL from 'url-parse'; export default class AbstractService { @@ -57,10 +58,26 @@ export default class AbstractService { * @returns {Promise.} A promise that will resolve with the list of API versions. */ versions () { - return this.http - .httpGet(this._endpointUrl) - .then((response) => response.json()) - .then((body) => body.versions); + return new Promise((resolve, reject) => { + let promise = this.http + .httpGet(this._endpointUrl) + .catch((response) => { + if (response.status === 401) { + let rootUrl = new URL(this._endpointUrl); + rootUrl.set('pathname', '/'); + rootUrl.set('query', ''); + rootUrl.set('hash', ''); + + return this.http.httpGet(rootUrl.href); + } else { + return reject(response); + } + }); + + promise + .then((response) => response.json()) + .then((body) => resolve(body.versions)); + }); } /** diff --git a/test/unit/helpers/data/versions.js b/test/unit/helpers/data/versions.js index 47e8d97..4e160cf 100644 --- a/test/unit/helpers/data/versions.js +++ b/test/unit/helpers/data/versions.js @@ -23,6 +23,7 @@ * URLs to match the test data below. */ const rootUrl = "http://example.com/"; +const subUrl = `${rootUrl}/v1`; /** * A mock list of supported versions for the below requests. @@ -36,7 +37,7 @@ const versions = [ /** * Build a new FetchMock configuration for the versions (root) endpoint. * - * @returns {{}} A full FetchMock configuration for Glance's Root Resource. + * @returns {{}} A full FetchMock configuration for a versions resource. */ function rootResponse() { return { @@ -109,8 +110,26 @@ function rootResponse() { }; } +/** + * FetchMock configuration for a 401 response against the versioned API url. + * + * @param {int} httpStatus The HTTP status for the response. + * @returns {{}} A full FetchMock configuration a failing request.. + */ +function subResponse(httpStatus = 401) { + return { + method: 'GET', + matcher: subUrl, + response: { + status: httpStatus + } + }; +} + export { rootUrl, + subUrl, versions, - rootResponse + rootResponse, + subResponse }; diff --git a/test/unit/util/abstract_serviceTest.js b/test/unit/util/abstract_serviceTest.js index 7c274a8..8dd88b1 100644 --- a/test/unit/util/abstract_serviceTest.js +++ b/test/unit/util/abstract_serviceTest.js @@ -73,6 +73,36 @@ describe('AbstractService', () => { .catch((error) => done.fail(error)); }); + // This test catches the case when the service catalog already points + // at an API version. + it("Should retry at the root URL if a 401 is encountered", (done) => { + const service = new AbstractService(mockData.subUrl, mockData.versions); + + fetchMock.mock(mockData.subResponse()); + fetchMock.mock(mockData.rootResponse()); + + service.versions() + .then((versions) => { + // Quick sanity check. + expect(versions.length).toBe(6); + done(); + }) + .catch((error) => done.fail(error)); + }); + + it("Should not retry at the root URL if a different status is encountered", (done) => { + const service = new AbstractService(mockData.subUrl, mockData.versions); + + fetchMock.mock(mockData.subResponse(500)); + + service.versions() + .then((response) => done.fail(response)) + .catch((error) => { + expect(error).not.toBeNull(); + done(); + }); + }); + it("Should NOT cache its results", (done) => { const service = new AbstractService(mockData.rootUrl, mockData.versions); const mockOptions = mockData.rootResponse();