@infomaker/http-test-server
Version:
IMID HTTP Test Server
247 lines (226 loc) • 8.55 kB
JavaScript
/*eslint-env node, jest */
'use strict'
const os = require('os')
const http = require('http')
const networkInterfaces = os.networkInterfaces()
const bodyParser = require('body-parser')
const stoppable = require('stoppable')
/** @module HttpTestServer */
/**
* Http server to help the systemtest visualize http calls.
*/
class HttpTestServer {
/**
* Create a HttpTestServer
* @param {Object} params - The params object containing properties passed to the constructor
* @param {number} params.port - Port number to start the Http server on
* @param {string} params.enableHealthEndpointAt - Url string with the endpoint to the healthcheck
*/
constructor(params) {
this.port = params.port
this.server = stoppable(http.createServer(this.onHttpCall.bind(this)),0) // 0 means no grace, when it's time to kill the server, do it!
this.enableHealthEndpointAt = params.enableHealthEndpointAt
this.callBuffer = new Map()
this.endpoints = new Map()
this.parsers = {
text: bodyParser.text(),
json: bodyParser.json(),
urlencoded: bodyParser.urlencoded({extended: true}),
raw: bodyParser.raw()
}
}
/**
* Start the http server.
*
* Listening on specified port.
*/
start() {
return new Promise(resolve => {
this.server.listen(this.port, resolve)
})
}
/**
* Stop the Http server.
*
* Checks if there are any http calls left to respond to or endpoints waiting for calls, responds to these endpoints and then stops the server.
*
* Throws errror if there are leftover calls not handled before shutdown.
*/
stop() {
return this._assertNoLeftovers().catch(err => err).then(leftoverError => {
return new Promise((resolve, reject) => {
this.server.stop(() => { leftoverError ? reject(leftoverError) : resolve() })
})
})
}
/**
* Gets the server host.
*
* @returns {string} networkInterfaceAddress - A string with the address for the eth0 network
*/
getServerHost() {
let networkInterfaceAddress
Object.keys(networkInterfaces).forEach(networkInterfaceName => {
networkInterfaces[networkInterfaceName].forEach(networkInterface => {
if (networkInterface.family !== 'IPv4' || networkInterface.internal !== false) {
// skip over internal (i.e. 127.0.0.1) and non-ipv4 addresses
return
}
/* istanbul ignore next */
if (networkInterfaceName === 'eth0') {
networkInterfaceAddress = networkInterface.address
}
})
})
return networkInterfaceAddress
}
/**
* Gets an endpoint to handle.
* Checks if an endpoint is waiting for an answer and resolves an answer to it.
*
* Or puts the endpoint on a queue to be handled when a request for that endpoints comes in.
* @param {string} endpoint - Url string with the endpoint to either resolve or put on a queue
* @param {Object} [params={}] - Object with params to the request. Example: treatContentTypeAs: 'application/json'
* @returns {Promise<Object>} Promise object represents the object with resolve, reject and params to resolve the incoming http call
*/
waitForCall(endpoint, params={}) {
return new Promise((resolve, reject) => {
// Do we have any buffered calls or should we wait?
const bufferedEndpointCalls = this.callBuffer.get(endpoint)
if (Array.isArray(bufferedEndpointCalls) && bufferedEndpointCalls.length > 0) {
resolve(bufferedEndpointCalls.pop())
} else {
const promise = {
resolve: resolve,
reject: reject,
params: params
}
if (this.endpoints.has(endpoint)) {
this.endpoints.get(endpoint).push(promise) // add last
} else {
this.endpoints.set(endpoint, [promise]) // add first (new)
}
}
})
}
/**
* Handles all incoming Http calls
*
* Either puts the request on queue to be handled or responds with data.
* @param {Object} request - Request object with the incoming http call
* @param {Object} response - Response object to respond to the incoming http call
* @returns {(Promise<Object>|void)} Promise object represents an object with the data from the request or, returns void and puts the request in queue.
*/
onHttpCall(request, response) {
const originalContentType = request.headers['content-type']
const endpoint = request.url.split('?')[0]
// Respons with 200 and end if the endpoint matches configured health endpoint
if (this.enableHealthEndpointAt === endpoint) {
response.statusCode = 200
return response.end('ok')
}
const waitingEndpointPromises = this.endpoints.get(endpoint)
if (Array.isArray(waitingEndpointPromises) && waitingEndpointPromises.length > 0) {
const tempContentType = waitingEndpointPromises[0].params.treatContentTypeAs
// Temporarily set content-type header to be able to parse body correctly
if (tempContentType) {
request.headers['content-type'] = tempContentType
}
}
switch (request.method) {
case 'POST':
case 'PUT':
case 'DELETE':
case 'PATCH':
return this._parseRequestBody(request, response).then(() => {
// Restore original content-type
request.headers['content-type'] = originalContentType
this._handleCompleteIncomingHttpCall(request, response)
})
default:
return this._handleCompleteIncomingHttpCall(request, response)
}
}
/**
* Handles the complete incoming http call.
*
* Either resolves an object with response and request.
*
* Or puts the request in queue.
* @private
* @param {Object} request - Request object with the incoming http call
* @param {Object} response - Response object to respond to the incoming http call
* @returns {Object|void} Returns an object with response and request objects or, return void and puts the request in queue.
*/
_handleCompleteIncomingHttpCall(request, response) {
const endpoint = request.url.split('?')[0] //.replace(`${this.getServerUrl()}\/`, '')
const data = {
request: request,
response: response
}
// Is someone waiting for this call, or should it be buffered?
const waitingEndpointPromises = this.endpoints.get(endpoint)
if (Array.isArray(waitingEndpointPromises) && waitingEndpointPromises.length > 0) {
waitingEndpointPromises.shift().resolve(data)
} else {
// No one currently waiting for call, place in the buffer
if (this.callBuffer.has(endpoint)) {
this.callBuffer.get(endpoint).push(data)
} else {
this.callBuffer.set(endpoint, [data])
}
}
}
/**
* Parses the request with bodyparsers: json, text, urlencoded and raw.
* @private
* @param {Object} request - Request object with the incoming http call
* @param {Object} response - Response object to respond to the incoming http call
* @returns {Promise<Object>} Promise object with the parsed request
*/
_parseRequestBody(request, response) {
return Promise.all(
Object.keys(this.parsers).map(parserName => {
return new Promise((resolve) => {
this.parsers[parserName](request, response, resolve)
})
})
)
}
/**
* Checks if there are either requests on buffer queue waiting to be handled.
*
* Or if there are endpoints waiting for incoming http calls.
* @private
* @returns {Promise.resolve|Promise.reject} Returns either a reject with an object with the buffered http call and uncalled endpoints. Or resolves.
*/
_assertNoLeftovers() {
const res = {
bufferedHttpCalls: [],
uncalledEndpoints: []
}
this.callBuffer.forEach((requests, url) => {
requests.forEach(req => {
res.bufferedHttpCalls.push({ url: url, data: req })
req.response.statusCode = 400
req.response.statusMessage = 'This HTTP call was not expect by any entity in the test environment'
req.response.end()
req.request.connection.destroy()
})
})
this.endpoints.forEach((callbacks, endpoint) => {
if (callbacks.length > 0) {
res.uncalledEndpoints.push({
endpoint: endpoint,
callbacksCount: callbacks.length
})
}
})
if (res.bufferedHttpCalls.length > 0 || res.uncalledEndpoints.length > 0) {
return Promise.reject(res)
} else {
return Promise.resolve()
}
}
}
module.exports = HttpTestServer