dave-dredd
Version:
HTTP API Testing Framework
149 lines (148 loc) • 5.64 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const request_1 = __importDefault(require("request"));
const caseless_1 = __importDefault(require("caseless"));
const logger_1 = __importDefault(require("./logger"));
/**
* Performs the HTTP request as described in the 'transaction.request' object
*
* In future we should introduce a 'real' request object as well so user has
* access to the modifications made on the way.
*
* @param {string} uri
* @param {Object} transactionReq
* @param {Object} [options]
* @param {Object} [options.logger] Custom logger
* @param {Object} [options.request] Custom 'request' library implementation
* @param {Object} [options.http] Custom default 'request' library options
* @param {Function} callback
*/
function performRequest(uri, transactionReq, options, callback) {
if (typeof options === 'function') {
[options, callback] = [{}, options];
}
const logger = options.logger || logger_1.default;
const request = options.request || request_1.default;
const httpOptions = Object.assign({}, options.http || {});
httpOptions.proxy = false;
httpOptions.followRedirect = false;
httpOptions.encoding = null;
httpOptions.method = transactionReq.method;
httpOptions.uri = uri;
try {
httpOptions.body = getBodyAsBuffer(transactionReq.body, transactionReq.bodyEncoding);
httpOptions.headers = normalizeContentLengthHeader(transactionReq.headers, httpOptions.body);
const protocol = httpOptions.uri.split(':')[0].toUpperCase();
logger.debug(`Performing ${protocol} request to the server under test: ` +
`${httpOptions.method} ${httpOptions.uri}`);
request(httpOptions, (error, response, responseBody) => {
logger.debug(`Handling ${protocol} response from the server under test`);
if (error) {
callback(error);
}
else {
callback(null, createTransactionResponse(response, responseBody));
}
});
}
catch (error) {
process.nextTick(() => callback(error));
}
}
/**
* Coerces the HTTP request body to a Buffer
*
* @param {string|Buffer} body
* @param {*} encoding
*/
function getBodyAsBuffer(body, encoding) {
return body instanceof Buffer
? body
: Buffer.from(`${body || ''}`, normalizeBodyEncoding(encoding));
}
exports.getBodyAsBuffer = getBodyAsBuffer;
/**
* Returns the encoding as either 'utf-8' or 'base64'. Throws
* an error in case any other encoding is provided.
*
* @param {string} encoding
*/
function normalizeBodyEncoding(encoding) {
if (!encoding) {
return 'utf-8';
}
switch (encoding.toLowerCase()) {
case 'utf-8':
case 'utf8':
return 'utf-8';
case 'base64':
return 'base64';
default:
throw new Error(`Unsupported encoding: '${encoding}' (only UTF-8 and ` +
'Base64 are supported)');
}
}
exports.normalizeBodyEncoding = normalizeBodyEncoding;
/**
* Detects an existing Content-Length header and overrides the user-provided
* header value in case it's out of sync with the real length of the body.
*
* @param {Object} headers HTTP request headers
* @param {Buffer} body HTTP request body
* @param {Object} [options]
* @param {Object} [options.logger] Custom logger
*/
function normalizeContentLengthHeader(headers, body, options = {}) {
const logger = options.logger || logger_1.default;
const modifiedHeaders = Object.assign({}, headers);
const calculatedValue = Buffer.byteLength(body);
const name = caseless_1.default(modifiedHeaders).has('Content-Length');
if (name) {
const value = parseInt(modifiedHeaders[name], 10);
if (value !== calculatedValue) {
modifiedHeaders[name] = `${calculatedValue}`;
logger.warn(`Specified Content-Length header is ${value}, but the real ` +
`body length is ${calculatedValue}. Using ${calculatedValue} instead.`);
}
}
else {
modifiedHeaders['Content-Length'] = `${calculatedValue}`;
}
return modifiedHeaders;
}
exports.normalizeContentLengthHeader = normalizeContentLengthHeader;
/**
* Real transaction response object factory. Serializes binary responses
* to string using Base64 encoding.
*
* @param {Object} response Node.js HTTP response
* @param {Buffer} body HTTP response body as Buffer
*/
function createTransactionResponse(response, body) {
const transactionRes = {
statusCode: response.statusCode,
headers: Object.assign({}, response.headers),
};
if (Buffer.byteLength(body || '')) {
transactionRes.bodyEncoding = detectBodyEncoding(body);
transactionRes.body = body.toString(transactionRes.bodyEncoding);
}
return transactionRes;
}
exports.createTransactionResponse = createTransactionResponse;
/**
* @param {Buffer} body
*/
function detectBodyEncoding(body) {
// U+FFFD is a replacement character in UTF-8 and indicates there
// are some bytes which could not been translated as UTF-8. Therefore
// let's assume the body is in binary format. Dredd encodes binary as
// Base64 to be able to transfer it wrapped in JSON over the TCP to non-JS
// hooks implementations.
return body.toString().includes('\ufffd') ? 'base64' : 'utf-8';
}
exports.detectBodyEncoding = detectBodyEncoding;
exports.default = performRequest;