Skip to content

Commit

Permalink
Merge e4b01b8 into 151a5e7
Browse files Browse the repository at this point in the history
  • Loading branch information
alexjeffburke committed Aug 11, 2019
2 parents 151a5e7 + e4b01b8 commit f693404
Show file tree
Hide file tree
Showing 4 changed files with 310 additions and 70 deletions.
10 changes: 9 additions & 1 deletion .eslintrc
Expand Up @@ -16,5 +16,13 @@
"ignoreReadBeforeAssign": false
}
]
}
},
"overrides": [
{
"files": ["test/**/*.js"],
"rules": {
"no-new": "off"
}
}
]
}
152 changes: 83 additions & 69 deletions lib/UnexpectedMitmMocker.js
Expand Up @@ -13,8 +13,8 @@ const createMessyResponse = require('./createMessyResponse');
const createSerializedRequestHandler = require('./createSerializedRequestHandler');
const errors = require('./errors');
const isBodyJson = require('./isBodyJson');
const OrderedMockStrategy = require('./mockstrategies/OrderedMockStrategy');
const resolveExpectedRequestProperties = require('./resolveExpectedRequestProperties');
const trimMessyHeaders = require('./trimMessyHeaders');

const expect = unexpected.clone().use(unexpectedMessy);

Expand Down Expand Up @@ -188,19 +188,27 @@ function trimMockResponse(mockResponse) {

class UnexpectedMitmMocker {
constructor(options) {
this.requestDescriptions = options.requestDescriptions || [];
options = options || {};

this.strategy = null;
this.timeline = null;
this.fulfilmentValue = null;

if (options.strategy) {
this.strategy = options.strategy;
} else if (Array.isArray(options.requestDescriptions)) {
this.strategy = new OrderedMockStrategy(options.requestDescriptions);
} else {
throw new Error(
'UnexpectedMitmMocker: missing strategy or request descriptions'
);
}
}

mock(consumptionFunction) {
const that = this;
const requestDescriptions = this.requestDescriptions;
const mitm = createMitm();

// Keep track of the current requestDescription
let nextRequestDescriptionIndex = 0;

// Keep track of the http/https agents that we have seen
// during the test so we can clean up afterwards:
const seenAgents = [];
Expand All @@ -219,7 +227,7 @@ class UnexpectedMitmMocker {
mitm.disable();
}

function handleRequest(req, metadata, spec) {
function handleRequest(req, metadata) {
return consumeReadableStream(req, { skipConcat: true }).then(result => {
const properties = {
method: req.method,
Expand All @@ -237,7 +245,7 @@ class UnexpectedMitmMocker {
error: result.error,
chunks: result.body,
properties,
spec
spec: undefined
};
});
}
Expand Down Expand Up @@ -320,39 +328,41 @@ class UnexpectedMitmMocker {
)
);

const requestDescription =
requestDescriptions[nextRequestDescriptionIndex];
nextRequestDescriptionIndex += 1;
const hasRequestDescription = !!requestDescription;

let requestStruct;
let expectedRequestProperties;
let responseProperties;
let responseStruct;

const responseProperties = hasRequestDescription
? requestDescription.response
: undefined;
let __earlyExit = null;

Promise.resolve()
.then(() => {
if (!hasRequestDescription) {
return;
}

expectedRequestProperties = resolveExpectedRequestProperties(
requestDescription && requestDescription.request
);
})
.then(() => handleRequest(req, metadata, expectedRequestProperties))
.then(() => handleRequest(req, metadata))
.then(result => {
// make available for use further down the promise chain
requestStruct = result;

return this.strategy
.nextDescriptionForIncomingRequest(requestStruct)
.catch(err => {
if (err.name === 'EarlyExitError') {
__earlyExit = err;
const requestDescription = err.data;
// update the request with the spec it needs to satisfy
requestStruct.spec = resolveExpectedRequestProperties(
requestDescription && requestDescription.request
);
return requestDescription;
}

throw err;
});
})
.then(requestDescription => {
if (requestStruct.error) {
// TODO: Consider adding support for recording this (the request erroring out while we're consuming it)
throw requestStruct.error;
}

if (!hasRequestDescription) {
if (!requestDescription) {
// there was no mock so arrange "<no response>"
assertExchange(requestStruct, null);

Expand All @@ -368,12 +378,23 @@ class UnexpectedMitmMocker {
// is effectively ignored and we proceed with
// our output.

if (__earlyExit) {
throw __earlyExit;
}

// cancel the delegated assertion
throw new errors.SawUnexpectedRequestsError(
'unexpected-mitm: Saw unexpected requests.'
);
}

// update the request with the spec it needs to satisfy
requestStruct.spec = resolveExpectedRequestProperties(
requestDescription && requestDescription.request
);
// set the response to be constructed based on the strategy
responseProperties = requestDescription.response;

if (typeof responseProperties === 'function') {
// reset the readable req stream state
stream.Readable.call(req);
Expand Down Expand Up @@ -402,32 +423,19 @@ class UnexpectedMitmMocker {
return getMockResponse(responseProperties);
}
})
.then(responseStruct => {
.then(result => {
responseStruct = result;

if (!(responseStruct.response || responseStruct.error)) {
return;
}

return Promise.resolve()
.then(() => {
const assertionMockRequest = new messy.HttpRequest(
requestStruct.properties
);
trimMessyHeaders(assertionMockRequest.headers);

expect.errorMode = 'default';
return expect(
assertionMockRequest,
'to satisfy',
requestStruct.spec
);
})
.then(() => deliverMockResponse(responseStruct))
.catch(e => {
assertExchange(requestStruct, responseStruct);
throw new errors.EarlyExitError(
'Seen request did not match the expected request.'
);
});
if (__earlyExit) {
assertExchange(requestStruct, responseStruct);
throw __earlyExit;
}

return deliverMockResponse(responseStruct);
})
.catch(e => {
// Given an error occurs, the deferred assertion
Expand Down Expand Up @@ -512,24 +520,31 @@ class UnexpectedMitmMocker {
// Where the driving assertion resolves we must check
// if any mocks still exist. If so, we add them to the
// set of expected exchanges and resolve the promise.
const hasRemainingRequestDescriptions =
nextRequestDescriptionIndex < requestDescriptions.length;
if (hasRemainingRequestDescriptions) {
// exhaust remaining mocks using a promises chain
return (function nextItem() {
const remainingDescription =
requestDescriptions[nextRequestDescriptionIndex];
nextRequestDescriptionIndex += 1;
if (remainingDescription) {
return this.strategy
.firstDescriptionRemaining()
.then(firstUnexercisedDescription => {
if (!firstUnexercisedDescription) {
resolve(fulfilmentValue);
}

// exhaust remaining mocks using a promises chain
const exhaustDescription = remainingDescription => {
if (!remainingDescription) {
throw new errors.UnexercisedMocksError();
}

let expectedRequestProperties;
let responseProperties;

return Promise.resolve()
.then(() => {
expectedRequestProperties = resolveExpectedRequestProperties(
remainingDescription.request
remainingDescription && remainingDescription.request
);
// set the response to be constructed based on the strategy
responseProperties = remainingDescription.response;
})
.then(() => getMockResponse(remainingDescription.response))
.then(() => getMockResponse(responseProperties))
.then(result => {
const spec = {
request: expectedRequestProperties,
Expand All @@ -539,15 +554,14 @@ class UnexpectedMitmMocker {

timeline.push({ spec });

return nextItem();
return this.strategy
.firstDescriptionRemaining()
.then(exhaustDescription);
});
} else {
throw new errors.UnexercisedMocksError();
}
})();
} else {
resolve(fulfilmentValue);
}
};

return exhaustDescription(firstUnexercisedDescription);
});
})
.catch(e => {
timeline.push(e);
Expand Down
62 changes: 62 additions & 0 deletions lib/mockstrategies/OrderedMockStrategy.js
@@ -0,0 +1,62 @@
const expect = require('unexpected')
.clone()
.use(require('unexpected-messy'));
const messy = require('messy');

const errors = require('../errors');
const resolveExpectedRequestProperties = require('../resolveExpectedRequestProperties');
const trimMessyHeaders = require('../trimMessyHeaders');

module.exports = class OrderedMockStrategy {
constructor(requestDescriptions) {
this.requestDescriptions = requestDescriptions;
this.nextRequestDescriptionIndex = 0;
}

get isEmpty() {
return this.nextRequestDescriptionIndex >= this.requestDescriptions.length;
}

firstDescriptionRemaining() {
return this.nextDescriptionForIncomingRequest();
}

nextDescriptionForIncomingRequest(requestStruct) {
if (this.isEmpty) {
return Promise.resolve(null);
}

const description = this.requestDescriptions[
this.nextRequestDescriptionIndex
];
this.nextRequestDescriptionIndex += 1;

if (!requestStruct) {
// skip early exit when exhausting requests
return Promise.resolve(description);
}

return Promise.resolve()
.then(() => {
const assertionMockRequest = new messy.HttpRequest(
requestStruct.properties
);
trimMessyHeaders(assertionMockRequest.headers);

// update the request with the spec it needs to satisfy
const assertionSeenSpec = resolveExpectedRequestProperties(
description.request
);

expect.errorMode = 'default';
return expect(assertionMockRequest, 'to satisfy', assertionSeenSpec);
})
.then(() => description)
.catch(e => {
throw new errors.EarlyExitError({
message: 'Seen request did not match the expected request.',
data: description
});
});
}
};

0 comments on commit f693404

Please sign in to comment.