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:
parent
e9dd0fe10a
commit
abd0508c30
|
@ -1,11 +1,11 @@
|
|||
import 'isomorphic-fetch';
|
||||
import log from 'loglevel';
|
||||
import Http from './util/http';
|
||||
|
||||
log.setLevel('INFO');
|
||||
|
||||
export default class Keystone {
|
||||
|
||||
constructor(cloudConfig) {
|
||||
constructor (cloudConfig) {
|
||||
// Sanity checks.
|
||||
if (!cloudConfig) {
|
||||
throw new Error('A configuration is required.');
|
||||
|
@ -13,12 +13,10 @@ export default class Keystone {
|
|||
// Clone the config, so that this instance is immutable
|
||||
// at runtime (no modifying the config after the fact).
|
||||
this.cloudConfig = Object.assign({}, cloudConfig);
|
||||
this.http = new Http();
|
||||
}
|
||||
|
||||
authenticate() {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
const body = {
|
||||
auth: {
|
||||
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) => {
|
||||
this.token = res.headers.get('X-Subject-Token');
|
||||
return res.json(); // This returns a promise...
|
||||
|
|
|
@ -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',
|
||||
password: 'pass',
|
||||
project_name: 'js-openstack-lib',
|
||||
auth_url: 'http://keystone'
|
||||
auth_url: 'http://keystone/'
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue