Created HTTP wrapper
This utility class provides an abstraction layer for HTTP calls via fetch(). Its purpose is to provide common, SDK-wide behavior for all HTTP requests. Included are: - Providing a common extension point for request and response manipulation. - Access to default headers. In the future, this class chould also be extended to provide: - Some form of progress() support for large uploads and downloads (perhaps via introduction of Q) - Convenience decoding of the response body, depending on Content-Type. - Internal error handling (At this time, HTTP errors are passed to then() rather than catch()). - Other features. Change-Id: I8173554b1b7ef052e2a74504b98f4ec574ce6136
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
import 'isomorphic-fetch';
|
|
||||||
import log from 'loglevel';
|
import log from 'loglevel';
|
||||||
|
import Http from './util/http';
|
||||||
|
|
||||||
log.setLevel('INFO');
|
log.setLevel('INFO');
|
||||||
|
|
||||||
export default class Keystone {
|
export default class Keystone {
|
||||||
|
|
||||||
constructor(cloudConfig) {
|
constructor (cloudConfig) {
|
||||||
// Sanity checks.
|
// Sanity checks.
|
||||||
if (!cloudConfig) {
|
if (!cloudConfig) {
|
||||||
throw new Error('A configuration is required.');
|
throw new Error('A configuration is required.');
|
||||||
@@ -13,12 +13,10 @@ export default class Keystone {
|
|||||||
// Clone the config, so that this instance is immutable
|
// Clone the config, so that this instance is immutable
|
||||||
// at runtime (no modifying the config after the fact).
|
// at runtime (no modifying the config after the fact).
|
||||||
this.cloudConfig = Object.assign({}, cloudConfig);
|
this.cloudConfig = Object.assign({}, cloudConfig);
|
||||||
|
this.http = new Http();
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticate() {
|
authenticate() {
|
||||||
const headers = {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
};
|
|
||||||
const body = {
|
const body = {
|
||||||
auth: {
|
auth: {
|
||||||
identity: {
|
identity: {
|
||||||
@@ -32,13 +30,8 @@ export default class Keystone {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const init = {
|
|
||||||
method: 'POST',
|
|
||||||
headers: headers,
|
|
||||||
body: JSON.stringify(body)
|
|
||||||
};
|
|
||||||
|
|
||||||
return fetch(this.cloudConfig.auth.auth_url, init)
|
return this.http.httpPost(this.cloudConfig.auth.auth_url, body)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
this.token = res.headers.get('X-Subject-Token');
|
this.token = res.headers.get('X-Subject-Token');
|
||||||
return res.json(); // This returns a promise...
|
return res.json(); // This returns a promise...
|
||||||
|
|||||||
174
src/util/http.js
Normal file
174
src/util/http.js
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2016 Hewlett Packard Enterprise Development L.P.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy
|
||||||
|
* of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See
|
||||||
|
* the License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'isomorphic-fetch';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This utility class provides an abstraction layer for HTTP calls via fetch(). Its purpose is
|
||||||
|
* to provide common, SDK-wide behavior for all HTTP requests. Included are:
|
||||||
|
*
|
||||||
|
* - Providing a common extension point for request and response manipulation.
|
||||||
|
* - Access to default headers.
|
||||||
|
*
|
||||||
|
* In the future, this class chould also be extended to provide:
|
||||||
|
*
|
||||||
|
* - Some form of progress() support for large uploads and downloads (perhaps via introduction of Q)
|
||||||
|
* - Convenience decoding of the response body, depending on Content-Type.
|
||||||
|
* - Internal error handling (At this time, HTTP errors are passed to then() rather than catch()).
|
||||||
|
* - Other features.
|
||||||
|
*/
|
||||||
|
export default class Http {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of active request interceptors for this instance. You may modify this list to
|
||||||
|
* adjust how your responses are processed. Each interceptor will be passed the Request
|
||||||
|
* instance, which must be returned from the interceptor either directly, or via a promise.
|
||||||
|
*
|
||||||
|
* @returns {Array} An array of all request interceptors.
|
||||||
|
*/
|
||||||
|
get requestInterceptors () {
|
||||||
|
return this._requestInterceptors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of active response interceptors for this instance. Each interceptor will be passed
|
||||||
|
* the raw (read-only) Response instance, which should be returned from the interceptor either
|
||||||
|
* directly, or via a promise.
|
||||||
|
*
|
||||||
|
* @returns {Array} An array of all response interceptors.
|
||||||
|
*/
|
||||||
|
get responseInterceptors () {
|
||||||
|
return this._responseInterceptors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default headers which will be sent with every request. A copy of these headers will be
|
||||||
|
* added to the Request instance passed through the interceptor chain, and may be
|
||||||
|
* modified there.
|
||||||
|
*
|
||||||
|
* @returns {{string: string}} A mapping of 'headerName': 'headerValue'
|
||||||
|
*/
|
||||||
|
get defaultHeaders () {
|
||||||
|
return this._defaultHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new HTTP handler.
|
||||||
|
*/
|
||||||
|
constructor () {
|
||||||
|
this._requestInterceptors = [];
|
||||||
|
this._responseInterceptors = [];
|
||||||
|
|
||||||
|
// Add default response interceptors.
|
||||||
|
this._defaultHeaders = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a decorated HTTP request.
|
||||||
|
*
|
||||||
|
* @param {String} method The HTTP method.
|
||||||
|
* @param {String} url The request URL.
|
||||||
|
* @param {{}} headers A map of HTTP headers.
|
||||||
|
* @param {{}} body The body. It will be JSON-Encoded by the handler.
|
||||||
|
* @returns {Promise} A promise which will resolve with the processed request response.
|
||||||
|
*/
|
||||||
|
httpRequest (method, url, headers = {}, body) {
|
||||||
|
// Sanitize the headers...
|
||||||
|
headers = Object.assign({}, headers, this.defaultHeaders);
|
||||||
|
|
||||||
|
// Build the request
|
||||||
|
const init = {method, headers};
|
||||||
|
|
||||||
|
// The Request() constructor will throw an error if the method is GET/HEAD, and there's a body.
|
||||||
|
if (['GET', 'HEAD'].indexOf(method) === -1 && body) {
|
||||||
|
init.body = JSON.stringify(body);
|
||||||
|
}
|
||||||
|
const request = new Request(url, init);
|
||||||
|
|
||||||
|
let promise = Promise.resolve(request);
|
||||||
|
|
||||||
|
// Loop through the request interceptors, constructing a promise chain.
|
||||||
|
for (let interceptor of this.requestInterceptors) {
|
||||||
|
promise = promise.then(interceptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the actual request...
|
||||||
|
promise = promise
|
||||||
|
.then((request) => {
|
||||||
|
// Deconstruct the request, since fetch-mock doesn't actually support fetch(Request);
|
||||||
|
const init = {
|
||||||
|
method: request.method,
|
||||||
|
headers: request.headers
|
||||||
|
};
|
||||||
|
if (['GET', 'HEAD'].indexOf(request.method) === -1 && request.body) {
|
||||||
|
init.body = request.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(request.url, init);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pass the response content through the response interceptors...
|
||||||
|
for (let interceptor of this.responseInterceptors) {
|
||||||
|
promise = promise.then(interceptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a raw GET request against a particular URL.
|
||||||
|
*
|
||||||
|
* @param {String} url The request URL.
|
||||||
|
* @returns {Promise} A promise which will resolve with the processed request response.
|
||||||
|
*/
|
||||||
|
httpGet (url) {
|
||||||
|
return this.httpRequest('GET', url, {}, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a raw PUT request against a particular URL.
|
||||||
|
*
|
||||||
|
* @param {String} url The request URL.
|
||||||
|
* @param {{}} body The body. It will be JSON-Encoded by the handler.
|
||||||
|
* @returns {Promise} A promise which will resolve with the processed request response.
|
||||||
|
*/
|
||||||
|
httpPut (url, body) {
|
||||||
|
return this.httpRequest('PUT', url, {}, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a raw POST request against a particular URL.
|
||||||
|
*
|
||||||
|
* @param {String} url The request URL.
|
||||||
|
* @param {{}} body The body. It will be JSON-Encoded by the handler.
|
||||||
|
* @returns {Promise} A promise which will resolve with the processed request response.
|
||||||
|
*/
|
||||||
|
httpPost (url, body) {
|
||||||
|
return this.httpRequest('POST', url, {}, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a raw DELETE request against a particular URL.
|
||||||
|
*
|
||||||
|
* @param {String} url The request URL.
|
||||||
|
* @returns {Promise} A promise which will resolve with the processed request response.
|
||||||
|
*/
|
||||||
|
httpDelete (url) {
|
||||||
|
return this.httpRequest('DELETE', url, {}, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -60,7 +60,7 @@ describe('Openstack connection test', () => {
|
|||||||
username: 'user',
|
username: 'user',
|
||||||
password: 'pass',
|
password: 'pass',
|
||||||
project_name: 'js-openstack-lib',
|
project_name: 'js-openstack-lib',
|
||||||
auth_url: 'http://keystone'
|
auth_url: 'http://keystone/'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
210
test/unit/util/httpTest.js
Normal file
210
test/unit/util/httpTest.js
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2016 Hewlett Packard Enterprise Development L.P.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy
|
||||||
|
* of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See
|
||||||
|
* the License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Http from '../../../src/util/http.js';
|
||||||
|
import fetchMock from 'fetch-mock';
|
||||||
|
|
||||||
|
describe('Http', () => {
|
||||||
|
let http;
|
||||||
|
const testUrl = 'https://example.com/';
|
||||||
|
const testRequest = {lol: 'cat'};
|
||||||
|
const testResponse = {foo: 'bar'};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
http = new Http();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(fetchMock.restore);
|
||||||
|
|
||||||
|
it("should permit manually constructing requests", (done) => {
|
||||||
|
fetchMock.get(testUrl, testResponse);
|
||||||
|
|
||||||
|
http.httpRequest('GET', testUrl)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((body) => {
|
||||||
|
expect(fetchMock.called(testUrl)).toBe(true);
|
||||||
|
expect(body).toEqual(testResponse);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
expect(error).toBeNull();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should make GET requests", (done) => {
|
||||||
|
fetchMock.get(testUrl, testResponse);
|
||||||
|
|
||||||
|
http.httpGet(testUrl)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((body) => {
|
||||||
|
expect(fetchMock.called(testUrl)).toBe(true);
|
||||||
|
expect(body).toEqual(testResponse);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
expect(error).toBeNull();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should make PUT requests", (done) => {
|
||||||
|
fetchMock.put(testUrl, testResponse, testRequest);
|
||||||
|
|
||||||
|
http.httpPut(testUrl, testRequest)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((body) => {
|
||||||
|
expect(fetchMock.called(testUrl)).toEqual(true);
|
||||||
|
expect(body).toEqual(testResponse);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
expect(error).toBeNull();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should make POST requests", (done) => {
|
||||||
|
fetchMock.post(testUrl, testResponse, testRequest);
|
||||||
|
|
||||||
|
http.httpPost(testUrl, testRequest)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((body) => {
|
||||||
|
expect(fetchMock.called(testUrl)).toEqual(true);
|
||||||
|
expect(body).toEqual(testResponse);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
expect(error).toBeNull();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should make DELETE requests", (done) => {
|
||||||
|
fetchMock.delete(testUrl, testRequest);
|
||||||
|
|
||||||
|
http.httpDelete(testUrl, testRequest)
|
||||||
|
.then(() => {
|
||||||
|
expect(fetchMock.called(testUrl)).toEqual(true);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
expect(error).toBeNull();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should permit setting default headers", (done) => {
|
||||||
|
http.defaultHeaders['Custom-Header'] = 'Custom-Value';
|
||||||
|
fetchMock.get(testUrl, testResponse);
|
||||||
|
|
||||||
|
http.httpGet(testUrl)
|
||||||
|
.then(() => {
|
||||||
|
let headers = fetchMock.lastOptions().headers;
|
||||||
|
expect(headers.get('custom-header')).toEqual('Custom-Value');
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
expect(error).toBeNull();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should permit request interception", (done) => {
|
||||||
|
fetchMock.get(testUrl, testResponse);
|
||||||
|
|
||||||
|
http.requestInterceptors.push((request) => {
|
||||||
|
request.headers.direct = true;
|
||||||
|
return request;
|
||||||
|
});
|
||||||
|
http.requestInterceptors.push((request) => {
|
||||||
|
request.headers.promise = true;
|
||||||
|
return Promise.resolve(request);
|
||||||
|
});
|
||||||
|
|
||||||
|
http.httpGet(testUrl)
|
||||||
|
.then(() => {
|
||||||
|
let options = fetchMock.lastOptions();
|
||||||
|
expect(options.headers.direct).toEqual(true);
|
||||||
|
expect(options.headers.promise).toEqual(true);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
expect(error).toBeNull();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should permit response interception", (done) => {
|
||||||
|
fetchMock.get(testUrl, testResponse);
|
||||||
|
|
||||||
|
http.responseInterceptors.push((response) => {
|
||||||
|
response.headers.direct = true;
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
http.responseInterceptors.push((response) => {
|
||||||
|
response.headers.promise = true;
|
||||||
|
return Promise.resolve(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
http.httpGet(testUrl)
|
||||||
|
.then((response) => {
|
||||||
|
expect(response.headers.direct).toEqual(true);
|
||||||
|
expect(response.headers.promise).toEqual(true);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
expect(error).toBeNull();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should pass exceptions back to the invoker", (done) => {
|
||||||
|
fetchMock.get(testUrl, () => {
|
||||||
|
throw new TypeError(); // Example- net::ERR_NAME_NOT_RESOLVED
|
||||||
|
});
|
||||||
|
|
||||||
|
http.httpGet(testUrl)
|
||||||
|
.then((response) => {
|
||||||
|
// We shouldn't reach this point.
|
||||||
|
expect(response).toBeNull();
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
expect(error.stack).toBeDefined();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should pass failed requests back to the invoker", (done) => {
|
||||||
|
fetchMock.get(testUrl, {status: 500, body: testResponse});
|
||||||
|
|
||||||
|
http.httpGet(testUrl)
|
||||||
|
.then((response) => {
|
||||||
|
// The HTTP request 'succeeded' with a failing state.
|
||||||
|
expect(response.status).toEqual(500);
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((body) => {
|
||||||
|
expect(body).toEqual(testResponse);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
expect(error).toBeNull();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user