Skip to content

Commit

Permalink
Merge pull request #21 from jordanwalsh23/feat/match_postman_mocking_…
Browse files Browse the repository at this point in the history
…algorithm

Feat/match postman mocking algorithm
  • Loading branch information
jordanwalsh23 authored Jul 2, 2024
2 parents 8d5c638 + f5b39fe commit c1051f8
Show file tree
Hide file tree
Showing 15 changed files with 1,106 additions and 440 deletions.
7 changes: 4 additions & 3 deletions bin/postman-local-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,13 @@ async function main() {

} catch (e) {
if (e.code == 'ENOENT') {
console.log(`Error starting local mock server. Collection file not found: "${options.collection}"`)
console.log("Error starting local mock server. Collection file not found: ", cliOptions.collection)
} else if (e.code == 'ERR_SOCKET_BAD_PORT') {
console.log("Error starting local mock server. Could not bind server to port:", options.port)
console.log("Error starting local mock server. Could not bind server to port:", cliOptions.port)
} else if (e.code == 'ERR_NON_JSON_RESPONSE') {
console.log("Error starting local mock server. The URL provided was not a valid collection file. Check your URL and try again:", options.collection)
console.log("Error starting local mock server. The URL provided was not a valid collection file. Check your URL and try again:", cliOptions.collection)
} else {
console.log(e)
console.log(e.code)
}
}
Expand Down
7 changes: 6 additions & 1 deletion lib/replacements.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ function getReplacementValue (replacementPath, request) {

if(defaultValue) {
defaultValue = defaultValue.replaceAll("'","").toString();
if(defaultValue.indexOf("{") > -1) {
//The default value needs a replacement
defaultValue = defaultValue.replace('{{', '').replace('}}', '').toString();
defaultValue = getReplacementValue(defaultValue);
}
}

switch (id) {
Expand All @@ -20,7 +25,7 @@ function getReplacementValue (replacementPath, request) {
return request.headers[field] || defaultValue || null;
case '$pathSegments':
let index = parseInt(field) + 1;
return request.path.split("/")[index];
return request.path.split("/")[index] || defaultValue || null;
case '$body':
if(!field && !defaultValue) {
return JSON.stringify(request.body);
Expand Down
314 changes: 314 additions & 0 deletions lib/responses.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,314 @@
const _ = require('lodash')

function getMatchedResponse(req, responses, debug) {
var bestMatchedResponse = null;
var bestMatchedResponseScore = 0;

//1. Filter on METHOD
let method = req.method;

responses = responses.filter(response => {
return response.originalRequest.method == method;
})

if (responses.length == 0) {
return;
}

//2. Filter on Custom Mock Server Headers
//Check and see if any of the mock response selectors were used
let postmanHeaders = [
'x-mock-response-name',
'x-mock-response-id',
'x-mock-response-code'
]

let selectorUsed = false

postmanHeaders.forEach(header => {
if (req.headers[header]) {
selectorUsed = true
}
})

if (selectorUsed) {
debug && console.log('Scoring on x-mock selector')
//we've used a selector, so if no responses match we have to move on.
//Look through the responses on this particular request and see if we can get a match.
let matchedPostmanHeadersResponse = responses.find(response => {
//Case 1: x-response-mock-name header is set
if (req.headers['x-mock-response-name']) {
return response.name == req.headers['x-mock-response-name']
}

//Case 2: x-response-mock-id header is set
if (req.headers['x-mock-response-id']) {
return response.id == req.headers['x-mock-response-id']
}

//Case 3: x-response-mock-code header is set
if (req.headers['x-mock-response-code']) {
return response.code == req.headers['x-mock-response-code']
}
})

if (matchedPostmanHeadersResponse) {
//We've found a matching header so send the response immediately.
//TODO: there could be multiple responses with the same name/code - we need to handle this.
debug && console.log('matched a header - returning immediately');
return matchedPostmanHeadersResponse;
}
}

//3. Filter by URL
debug && console.log('scoring responses by URL match...')

let results = {};

debug && console.log('Scoring path structure match')
responses.every(response => {
results[response.id] = {
score: 100
};

//get the path for this response.
let thisResponsePath = `${response.originalRequest.url.path.join(
'/'
)}`

//3.1.1 Perfect Score
debug && console.log(`${req.path} eq /${thisResponsePath}`, _.isEqual(req.path, `/${thisResponsePath}`))

if (_.isEqual(req.path, `/${thisResponsePath}`)) {
return true;
}

//3.1.2 Compare case insensitive
if (_.isEqual(req.path.toLowerCase(), `/${thisResponsePath.toLowerCase()}`)) {
results[response.id].score -= 10;
return true;
}

//3.1.3 Remove trailing slashes
let newReqPath = req.path.replace(/\/?\s*$/g, '');
let newResPath = thisResponsePath.replace(/\/?\s*$/g, '');

if (_.isEqual(newReqPath, `/${newResPath}`)) {
results[response.id].score -= 5;
return true;
}

//3.1.4 Remove trailing slashes and case insensitive
if (_.isEqual(newReqPath.toLowerCase(), `/${newResPath.toLowerCase()}`)) {
results[response.id].score -= 15;
return true;
}

//3.1.5 Remove wildcards and variables and test match
if (thisResponsePath.indexOf(':') > -1) {
//There are parameters in this response path that we need to accommodate.
thisResponsePath = thisResponsePath.replace(/:[^\/]+/g, '.+')
}

let thisResponsePathParts = `/${thisResponsePath}`.split("/");
let thisRequestPathParts = req.path.split("/");

let matches = false;

//Test if we replace the response path parts, will this request match
if (thisRequestPathParts.length == thisResponsePathParts.length) {

thisRequestPathParts.every((requestpart, idx) => {

if (thisResponsePathParts[idx].indexOf("{{") > -1) {
//Let's temporarily set this to the request value
thisResponsePathParts[idx] = requestpart;
}

matches = requestpart == thisResponsePathParts[idx];
return matches;

})

if (matches) {
results[response.id].score -= 20;
return true;
}

}

//no match - we need to delete this ID
delete results[response.id];
return true;

});

debug && console.log('Scoring query parameters')

//3.1.6 Match Query Parameters
if (Object.keys(req.query).length > 0) {

//iterate through the results array as these are the only potential matches.
Object.keys(results).forEach((id) => {

let response = responses.find(response => response.id == id);
debug && console.log('Scoring query parameters')
let fullMatches = 0;
let partialMatches = 0;
let missingMatches = 0;
let totalQueryParams = Object.keys(req.query).length;

Object.keys(req.query).every((key) => {

if (
response.originalRequest &&
response.originalRequest.url &&
response.originalRequest.url.query &&
response.originalRequest.url.query.members &&
Array.isArray(response.originalRequest.url.query.members)
) {
let matchKeyAndValue =
response.originalRequest.url.query.find(param => {
return key == param.key && req.query[key] == param.value
})

if (matchKeyAndValue) {
//We found a matched query and value so increase the score.
fullMatches++;
return true;
}

let match = response.originalRequest.url.query.find(param => {
debug && console.log("key:" + key.toLowerCase(), "param key: ", param.key.toLowerCase())
return key.toLowerCase() == param.key.toLowerCase()
})

if (match) {
//We found a matched query key so increase the score.
partialMatches++;
return true;
} else {
missingMatches++;
return true;
}

} else {
debug && console.log('No query parameters on this response example.')
}
});

let matchingPct = parseFloat(fullMatches / (fullMatches + partialMatches + missingMatches));
debug && console.log(`matchingpct: ${matchingPct}.`, fullMatches, partialMatches, missingMatches, totalQueryParams);
debug && console.log(fullMatches == totalQueryParams)

debug && console.log("results score before: ", results[id].score)

if (fullMatches == totalQueryParams) {
results[id].score += 10;
} else if (partialMatches > 0) {
results[id].score += (10 * matchingPct);
} else {
results[id].score -= missingMatches > 10 ? 10 : missingMatches;
}

debug && console.log("results score after: ", results[id].score)
});
}


debug && console.log('Scoring headers and body match')
//3.1.7 Match Headers
if (req.headers['x-mock-match-request-headers'] || req.headers['x-mock-match-request-body']) {

//Iterate through the headers on the actual request object and see if we can find a match

//Headers first
let headersToFind = req.headers['x-mock-match-request-headers'];

if (headersToFind && headersToFind != "") {
headersToFind = headersToFind.split(",");
if (!Array.isArray(headersToFind)) {
headersToFind = [headersToFind];
}
}

//Iterate through the results array as these are the only potential matches
Object.keys(results).forEach((id) => {

let response = responses.find(response => response.id == id);

if (headersToFind) {
let headersFound = true;

headersToFind.every(searchHeader => {

let match = response.originalRequest.headers.find(header => {
return header.key == searchHeader
})

if (!match) {
headersFound = false;
return false;
} else {
return true;
}
});

debug && console.log("headers found:", headersFound)
if (!headersFound) {
delete results[id];
}
}

debug && console.log('Matching body')
//check if the body matches
if (
req.method.toLowerCase() == 'post' ||
req.method.toLowerCase() == 'put' ||
req.method.toLowerCase() == 'patch'
) {

if (
response.originalRequest &&
response.originalRequest.body &&
response.originalRequest.body.raw &&
_.isEqual(
req.body,
JSON.parse(response.originalRequest.body.raw))
) {
debug && console.log("body matches increase by 5");
results[id].score += 5;
} else {

if (req.headers['x-mock-match-request-body'] == 'true') {
delete results[id];
}

}
}

});
}

bestMatchedResponseScore = 0;
Object.keys(results).forEach(id => {
if (results[id].score > bestMatchedResponseScore) {
bestMatchedResponseScore = results[id].score;
bestMatchedResponse = responses.find((response) => response.id == id);
}
})

if (!bestMatchedResponse) {
debug && console.log("Couldn't find a response")
} else {
debug && console.log("Best matched:", bestMatchedResponse.name, bestMatchedResponse.id)
bestMatchedResponse['score'] = bestMatchedResponseScore;
}

return bestMatchedResponse;

}

module.exports = {
getMatchedResponse: getMatchedResponse
}
Loading

0 comments on commit c1051f8

Please sign in to comment.