unexpected-mitm
Advanced tools
Comparing version 1.4.1 to 2.0.0
@@ -1,18 +0,313 @@ | ||
/* global setImmediate */ | ||
/* global setImmediate, after */ | ||
var messy = require('messy'), | ||
createMitm = require('mitm-papandreou'), | ||
_ = require('underscore'), | ||
http = require('http'), | ||
urlModule = require('url'); | ||
https = require('https'), | ||
fs = require('fs'), | ||
urlModule = require('url'), | ||
memoizeSync = require('memoizesync'), | ||
callsite = require('callsite'), | ||
detectIndent = require('detect-indent'), | ||
passError = require('passerror'); | ||
function formatHeaderObj(headerObj) { | ||
var result = {}; | ||
Object.keys(headerObj).forEach(function (headerName) { | ||
result[messy.formatHeaderName(headerName)] = headerObj[headerName]; | ||
}); | ||
return result; | ||
} | ||
function isTextualContentType(contentType) { | ||
if (typeof contentType === 'string') { | ||
contentType = contentType.toLowerCase().trim().replace(/\s*;.*$/, ''); | ||
return ( | ||
/^text\//.test(contentType) || | ||
/^application\/(json|javascript)$/.test(contentType) || | ||
/^application\/xml/.test(contentType) || | ||
/^application\/x-www-form-urlencoded\b/.test(contentType) || | ||
/\+xml$/.test(contentType) | ||
); | ||
} | ||
return false; | ||
} | ||
function bufferCanBeInterpretedAsUtf8(buffer) { | ||
// Hack: Since Buffer.prototype.toString('utf-8') is very forgiving, convert the buffer to a string | ||
// with percent-encoded octets, then see if decodeURIComponent accepts it. | ||
try { | ||
decodeURIComponent(Array.prototype.map.call(buffer, function (octet) { | ||
return '%' + (octet < 16 ? '0' : '') + octet.toString(16); | ||
}).join('')); | ||
} catch (e) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
function trimMessage(message) { | ||
if (typeof message.body !== 'undefined') { | ||
if (message.body.length === 0) { | ||
delete message.body; | ||
} else if (isTextualContentType(message.headers['Content-Type']) && bufferCanBeInterpretedAsUtf8(message.body)) { | ||
message.body = message.body.toString('utf-8'); | ||
} | ||
if (/^application\/json(?:;|$)/.test(message.headers['Content-Type']) && /^\s*[[{]/.test(message.body)) { | ||
try { | ||
message.body = JSON.parse(message.body); | ||
if (message.headers['Content-Type'] === 'application/json') { | ||
delete message.headers['Content-Type']; | ||
} | ||
} catch (e) {} | ||
} | ||
} | ||
if (message.headers) { | ||
delete message.headers['Content-Length']; | ||
delete message.headers['Transfer-Encoding']; | ||
delete message.headers['Connection']; | ||
delete message.headers['Date']; | ||
if (Object.keys(message.headers).length === 0) { | ||
delete message.headers; | ||
} | ||
} | ||
if (message.url && message.method) { | ||
message.url = message.method + ' ' + message.url; | ||
delete message.method; | ||
} | ||
if (Object.keys(message).length === 1) { | ||
if (typeof message.url === 'string') { | ||
return message.url; | ||
} | ||
if (typeof message.statusCode === 'number') { | ||
return message.statusCode; | ||
} | ||
} | ||
return message; | ||
} | ||
function bufferStream(buffer, cb) { | ||
var chunks = []; | ||
buffer.on('data', function (chunk) { | ||
chunks.push(chunk); | ||
}).on('end', function () { | ||
cb(null, Buffer.concat(chunks)); | ||
}).on('error', cb); | ||
} | ||
function trimRecordedExchange(recordedExchange) { | ||
return { | ||
request: trimMessage(recordedExchange.request), | ||
response: trimMessage(recordedExchange.response) | ||
}; | ||
} | ||
function createSerializedRequestHandler(onRequest) { | ||
var activeRequest = false, | ||
requestQueue = []; | ||
function processNextRequest() { | ||
while (requestQueue.length > 0 && !activeRequest) { | ||
activeRequest = true; | ||
var reqAndRes = requestQueue.shift(), | ||
req = reqAndRes[0], | ||
res = reqAndRes[1], | ||
resEnd = res.end; | ||
res.end = function () { | ||
resEnd.apply(this, arguments); | ||
activeRequest = false; | ||
setImmediate(processNextRequest); | ||
}; | ||
onRequest(req, res); | ||
} | ||
} | ||
return function (req, res) { | ||
requestQueue.push([req, res]); | ||
processNextRequest(); | ||
}; | ||
} | ||
module.exports = { | ||
name: 'unexpected-mitm', | ||
installInto: function (expect) { | ||
var expectForRendering = expect.clone(); | ||
expectForRendering.addType({ | ||
name: 'Buffer', | ||
base: 'Buffer', | ||
hexDumpWidth: Infinity // Prevents Buffer instances > 16 bytes from being truncated | ||
}); | ||
function stringify(obj, indentationWidth) { | ||
expectForRendering.output.indentationWidth = indentationWidth; | ||
return expectForRendering.inspect(obj, Infinity).toString('text'); | ||
} | ||
var injectionsBySourceFileName = {}, | ||
getSourceText = memoizeSync(function (sourceFileName) { | ||
return fs.readFileSync(sourceFileName, 'utf-8'); | ||
}); | ||
function injectRecordedExchanges(sourceFileName, recordedExchanges, pos) { | ||
var sourceText = getSourceText(sourceFileName), | ||
// FIXME: Does not support tabs: | ||
indentationWidth = 4, | ||
detectedIndent = detectIndent(sourceText); | ||
if (detectedIndent) { | ||
indentationWidth = detectedIndent.amount; | ||
} | ||
while (pos > 0 && sourceText.charAt(pos - 1) !== '\n') { | ||
pos -= 1; | ||
} | ||
var searchRegExp = /([ ]*)(.*)(['"])with http recorded\3,/g; | ||
searchRegExp.lastIndex = pos; | ||
// NB: Return value of replace not used: | ||
var matchSearchRegExp = searchRegExp.exec(sourceText); | ||
if (matchSearchRegExp) { | ||
var lineIndentation = matchSearchRegExp[1], | ||
before = matchSearchRegExp[2], | ||
quote = matchSearchRegExp[3]; | ||
(injectionsBySourceFileName[sourceFileName] = injectionsBySourceFileName[sourceFileName] || []).push({ | ||
pos: matchSearchRegExp.index, | ||
length: matchSearchRegExp[0].length, | ||
replacement: lineIndentation + before + quote + 'with http mocked out' + quote + ', ' + stringify(recordedExchanges, indentationWidth).replace(/\n^/mg, '\n' + lineIndentation) + ',' | ||
}); | ||
} | ||
} | ||
function applyInjections() { | ||
Object.keys(injectionsBySourceFileName).forEach(function (sourceFileName) { | ||
var injections = injectionsBySourceFileName[sourceFileName], | ||
sourceText = getSourceText(sourceFileName), | ||
offset = 0; | ||
injections.sort(function (a, b) { | ||
return a.pos - b.pos; | ||
}).forEach(function (injection) { | ||
var pos = injection.pos + offset; | ||
sourceText = sourceText.substr(0, pos) + injection.replacement + sourceText.substr(pos + injection.length); | ||
offset += injection.replacement.length - injection.length; | ||
}); | ||
fs.writeFileSync(sourceFileName, sourceText, 'utf-8'); | ||
}); | ||
} | ||
var afterBlockRegistered = false; | ||
expect | ||
.addAssertion('with http recorded', function (expect, subject) { | ||
var stack = callsite(), | ||
cb = this.args.pop(), | ||
mitm = createMitm(), | ||
callbackCalled = false, | ||
recordedExchanges = []; | ||
if (!afterBlockRegistered) { | ||
after(applyInjections); | ||
afterBlockRegistered = true; | ||
} | ||
function cleanUp() { | ||
mitm.disable(); | ||
} | ||
function handleError(err) { | ||
if (!callbackCalled) { | ||
callbackCalled = true; | ||
cleanUp(); | ||
cb(err); | ||
} | ||
} | ||
var bypassNextConnect = false; | ||
mitm.on('connect', function (socket, opts) { | ||
if (bypassNextConnect) { | ||
socket.bypass(); | ||
bypassNextConnect = false; | ||
} | ||
}).on('request', createSerializedRequestHandler(function (req, res) { | ||
var recordedExchange = { | ||
request: { | ||
url: req.method + ' ' + req.url, | ||
headers: formatHeaderObj(req.headers) | ||
}, | ||
response: {} | ||
}; | ||
recordedExchanges.push(recordedExchange); | ||
bufferStream(req, passError(handleError, function (body) { | ||
recordedExchange.request.body = body; | ||
bypassNextConnect = true; | ||
var matchHostHeader = req.headers.host && req.headers.host.match(/^([^:]*)(?::(\d+))?/), | ||
host, | ||
port; | ||
// https://github.com/moll/node-mitm/issues/14 | ||
if (matchHostHeader) { | ||
if (matchHostHeader[1]) { | ||
host = matchHostHeader[1]; | ||
} | ||
if (matchHostHeader[2]) { | ||
port = parseInt(matchHostHeader[2], 10); | ||
} | ||
} | ||
if (!host) { | ||
return handleError(new Error('unexpected-mitm recording mode: Could not determine the host name from Host header: ' + req.headers.host)); | ||
} | ||
(req.socket.encrypted ? https : http).request({ | ||
method: req.method, | ||
host: host, | ||
port: port || (req.socket.encrypted ? 443 : 80), | ||
headers: req.headers, | ||
path: req.url | ||
}).on('response', function (response) { | ||
recordedExchange.response.headers = formatHeaderObj(response.headers); | ||
bufferStream(response, passError(handleError, function (body) { | ||
recordedExchange.response.body = body; | ||
setImmediate(function () { | ||
Object.keys(response.headers).forEach(function (headerName) { | ||
res.setHeader(headerName, response.headers[headerName]); | ||
}); | ||
res.end(recordedExchange.response.body); | ||
}); | ||
})); | ||
}).on('error', function (err) { | ||
recordedExchange.response = err; | ||
}).end(recordedExchange.request.body); | ||
})); | ||
})); | ||
this.args.push(function (err) { | ||
var args = arguments; | ||
if (!callbackCalled) { | ||
callbackCalled = true; | ||
cleanUp(); | ||
setImmediate(function () { | ||
recordedExchanges = recordedExchanges.map(trimRecordedExchange); | ||
if (recordedExchanges.length === 1) { | ||
recordedExchanges = recordedExchanges[0]; | ||
} | ||
// Find the first call site that has mocha's "test" property: | ||
var containingCallsite = stack.filter(function (parentCallsite) { | ||
return parentCallsite.receiver.test; | ||
}).shift(); | ||
var fileName = containingCallsite && containingCallsite.getFileName(); | ||
if (fileName) { | ||
injectRecordedExchanges(fileName, recordedExchanges, containingCallsite.pos); | ||
} | ||
return cb.apply(this, args); | ||
}); | ||
} | ||
}); | ||
try { | ||
this.shift(expect, subject, 0); | ||
} finally { | ||
this.args.pop(); // Prevent the wrapped callback from being inspected when the assertion fails. | ||
} | ||
}) | ||
.addAssertion('with http mocked out', function (expect, subject, requestDescriptions) { // ... | ||
var cb = this.args.pop(); | ||
var that = this, | ||
cb = this.args.pop(); | ||
this.errorMode = 'nested'; | ||
expect(cb, 'to be a function'); // We need a cb | ||
this.errorMode = 'default'; | ||
var mitm = require('mitm-papandreou')(), | ||
var mitm = createMitm(), | ||
callbackCalled = false; | ||
@@ -44,3 +339,6 @@ | ||
mitm.on('request', function (req, res) { | ||
mitm.on('request', createSerializedRequestHandler(function (req, res) { | ||
if (callbackCalled) { | ||
return; | ||
} | ||
@@ -80,8 +378,5 @@ var noMoreMockedOutRequests = nextRequestDescriptionIndex >= requestDescriptions.length, | ||
mockResponseBodyIsReady = false; | ||
var mockResponseBodyChunks = []; | ||
mockResponse.body.on('data', function (chunk) { | ||
mockResponseBodyChunks.push(chunk); | ||
}).on('end', function () { | ||
mockResponse.body = Buffer.concat(mockResponseBodyChunks); | ||
}).on('error', handleError); | ||
bufferStream(mockResponse.body, passError(handleError, function (body) { | ||
mockResponse.body = body; | ||
})); | ||
} | ||
@@ -135,10 +430,14 @@ } | ||
httpConversation.exchanges.push(httpExchange); | ||
var httpConversationSnapshot = new messy.HttpConversation({ | ||
exchanges: [].concat(httpConversation.exchanges) | ||
}); | ||
if (expectedRequestProperties) { | ||
httpConversationSatisfySpec.exchanges.push({request: expectedRequestProperties || {}}); | ||
} | ||
var requestBodyChunks = []; | ||
req.on('data', function (chunk) { | ||
requestBodyChunks.push(chunk); | ||
}).on('end', function () { | ||
actualRequest.body = Buffer.concat(requestBodyChunks); | ||
var httpConversationSatisfySpecSnapshot = { exchanges: [].concat(httpConversationSatisfySpec.exchanges) }; | ||
bufferStream(req, passError(handleError, function (body) { | ||
if (callbackCalled) { | ||
return; | ||
} | ||
actualRequest.body = body; | ||
function assertAndDeliverMockResponse() { | ||
@@ -153,20 +452,25 @@ if (mockResponse) { | ||
} | ||
try { | ||
expect(httpConversation, 'to satisfy', httpConversationSatisfySpec); | ||
} catch (e) { | ||
return handleError(e); | ||
} | ||
setImmediate(function () { | ||
try { | ||
that.errorMode = 'default'; | ||
expect(httpConversationSnapshot, 'to satisfy', httpConversationSatisfySpecSnapshot); | ||
} catch (e) { | ||
that.errorMode = 'nested'; | ||
return handleError(e); | ||
} | ||
that.errorMode = 'nested'; | ||
if (mockResponse) { | ||
res.statusCode = mockResponse.statusCode; | ||
mockResponse.headers.getNames().forEach(function (headerName) { | ||
mockResponse.headers.getAll(headerName).forEach(function (value) { | ||
res.setHeader(headerName, value); | ||
if (mockResponse) { | ||
res.statusCode = mockResponse.statusCode; | ||
mockResponse.headers.getNames().forEach(function (headerName) { | ||
mockResponse.headers.getAll(headerName).forEach(function (value) { | ||
res.setHeader(headerName, value); | ||
}); | ||
}); | ||
}); | ||
if (typeof mockResponse.body !== 'undefined') { | ||
res.write(mockResponse.body); | ||
if (typeof mockResponse.body !== 'undefined') { | ||
res.write(mockResponse.body); | ||
} | ||
} | ||
} | ||
res.end(); | ||
res.end(); | ||
}); | ||
} | ||
@@ -181,4 +485,4 @@ | ||
} | ||
}).on('error', handleError); | ||
}); | ||
})); | ||
})); | ||
@@ -185,0 +489,0 @@ this.args.push(function (err) { |
{ | ||
"name": "unexpected-mitm", | ||
"version": "1.4.1", | ||
"version": "2.0.0", | ||
"description": "Unexpected plugin for the mitm library", | ||
@@ -22,7 +22,11 @@ "main": "lib/unexpectedMitm.js", | ||
"dependencies": { | ||
"callsite": "1.0.0", | ||
"detect-indent": "3.0.0", | ||
"memoizesync": "0.5.0", | ||
"messy": "4.1.1", | ||
"mitm-papandreou": "1.0.3-patch1", | ||
"mocha": "2.1.0", | ||
"passerror": "1.1.0", | ||
"underscore": "1.7.0" | ||
} | ||
} |
@@ -297,3 +297,3 @@ /*global describe, it, __dirname*/ | ||
it('should produce an error if the test issues more requests than has been mocked', function (done) { | ||
it('should produce an error if the test issues more requests than have been mocked', function (done) { | ||
expect('http://www.google.com/foo', 'with http mocked out', [], 'to yield response', 200, function (err) { | ||
@@ -332,2 +332,22 @@ expect(err, 'to be an', Error); | ||
}); | ||
it('should record', function (done) { | ||
expect({ | ||
url: 'POST http://www.google.com/', | ||
headers: { | ||
'Content-Type': 'application/x-www-form-urlencoded' | ||
}, | ||
body: 'foo=bar' | ||
}, 'with http recorded', 'to yield response', 200, done); | ||
}); | ||
it('should record some more', function (done) { | ||
expect({ | ||
url: 'DELETE http://www.google.com/', | ||
headers: { | ||
'Content-Type': 'application/x-www-form-urlencoded' | ||
}, | ||
body: 'foo=bar' | ||
}, 'with http recorded', 'to yield response', 200, done); | ||
}); | ||
}); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
101308
800
8
3
2
+ Addedcallsite@1.0.0
+ Addeddetect-indent@3.0.0
+ Addedmemoizesync@0.5.0
+ Addedpasserror@1.1.0
+ Addedcallsite@1.0.0(transitive)
+ Addeddetect-indent@3.0.0(transitive)
+ Addedget-stdin@3.0.2(transitive)
+ Addedis-finite@1.1.0(transitive)
+ Addedmemoizesync@0.5.0(transitive)
+ Addedminimist@1.2.8(transitive)
+ Addedpasserror@1.1.0(transitive)
+ Addedrepeating@1.1.3(transitive)
- Removedlru-cache@2.7.3(transitive)