UNPKG

@infomaker/http-test-server

Version:

IMID HTTP Test Server

247 lines (226 loc) 8.55 kB
/*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