Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/post with dynamic body #84

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ node_modules
test.js
.nyc_output
*.code-workspace
.vscode/
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ In the same way, if your POST body is a json like `{"json": "yesPlease"}`,
mockserver will look for a file called `POST--{"json": "yesPlease"}.mock`.
_Warning! This feature is_ **NOT compatible with Windows**_. This is because Windows doesn't accept curly brackets as filenames._

To overcome this limitation or to keep the file names tidy, you can put the body in [a json file](#body-in-json-file).

If no parametrized mock file is found, mockserver will default to the
nearest headers based .mock file

Expand All @@ -217,6 +219,24 @@ Authorization: 12345

if there's no `hello/GET_Authorization=12345--a=b.mock`, we'll default to `hello/GET_Authorization=12345.mock` or to `hello/GET.mock`

## Body in json file

To support Windows and tidier file naming, the expected body of the request can be saved in a separate `.json` file. If the request contains a body in json format, mockserver will look for that body in json files in the same `$REQUEST-PATH` directory.

For example, if a POST body is `{"json": "yesPlease"}`, and a file in the path called `payload.json` has the same content (order is important, but spacing between keys/values is not), mockserver will look for a file called `[email protected]`.

The general naming convention is:

```
$REQUEST-PATH/$HTTP-METHOD@$JSON-FILENAME.mock
```

The precedence for matching requests containing a json body is:

1) Contained within mock file name
2) Contained within .json file
3) No match - nearest headers based .mock file

## Wildcard slugs

If you want to match against a route with a wildcard - say in the case of an ID or other parameter in the URL, you can
Expand Down
147 changes: 87 additions & 60 deletions mockserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,14 @@ function parseStatus(header) {
* Parses an HTTP header, splitting
* by colon.
*/
const parseHeader = function (header, context, request) {
const parseHeader = function(header, context, request) {
header = header.split(': ');

return { key: normalizeHeader(header[0]), value: parseValue(header[1], context, request) };
};

const parseValue = function(value, context, request) {
return Monad
.of(value)
return Monad.of(value)
.map((value) => importHandler(value, context, request))
.map((value) => headerHandler(value, request))
.map((value) => evalHandler(value, request))
Expand All @@ -45,8 +44,7 @@ const parseValue = function(value, context, request) {
* Priority exports over ENV definition.
*/
const prepareWatchedHeaders = function() {
const exportHeaders =
module.exports.headers && module.exports.headers.toString();
const exportHeaders = module.exports.headers && module.exports.headers.toString();
const headers = (exportHeaders || process.env.MOCK_HEADERS || '').split(',');

return headers.filter(function(item, pos, self) {
Expand All @@ -65,7 +63,7 @@ const addHeader = function(headers, line) {
} else {
headers[key] = value;
}
}
};

/**
* Parser the content of a mockfile
Expand All @@ -78,14 +76,12 @@ const parse = function(content, file, request) {
let body;
const bodyContent = [];
content = content.split(/\r?\n/);
const status = Monad
.of(content[0])
const status = Monad.of(content[0])
.map((value) => importHandler(value, context, request))
.map((value) => evalHandler(value, context, request))
.map(parseStatus)
.join();


let headerEnd = false;
delete content[0];

Expand All @@ -103,9 +99,7 @@ const parse = function(content, file, request) {
}
});


body = Monad
.of(bodyContent.join('\n'))
body = Monad.of(bodyContent.join('\n'))
.map((value) => importHandler(value, context, request))
.map((value) => evalHandler(value, context, request))
.join();
Expand All @@ -119,7 +113,6 @@ function removeBlanks(array) {
});
}


/**
* This method will look for a header named Response-Delay. When set it
* delay the response in that number of milliseconds simulating latency
Expand Down Expand Up @@ -157,7 +150,7 @@ function getWildcardPath(dir) {
}

const res = getDirectoriesRecursive(mockserver.directory)
.filter(dir => {
.filter((dir) => {
const directories = dir.split(path.sep);
return directories.includes('__');
})
Expand All @@ -170,7 +163,7 @@ function getWildcardPath(dir) {
// Order from longest file path to shortest.
return aLength > bLength ? -1 : 1;
})
.map(dir => {
.map((dir) => {
const steps = dir.split(path.sep);
const baseDir = mockserver.directory.split(path.sep);
steps.splice(0, baseDir.length);
Expand Down Expand Up @@ -211,31 +204,28 @@ function matchWildcardPath(steps, dirSteps) {

function flattenDeep(directories) {
return directories.reduce(
(acc, val) =>
Array.isArray(val) ? acc.concat(flattenDeep(val)) : acc.concat(val),
(acc, val) => (Array.isArray(val) ? acc.concat(flattenDeep(val)) : acc.concat(val)),
[]
);
}

function getDirectories(srcpath) {
return fs
.readdirSync(srcpath)
.map(file => path.join(srcpath, file))
.filter(path => fs.statSync(path).isDirectory());
.map((file) => path.join(srcpath, file))
.filter((path) => fs.statSync(path).isDirectory());
}

function getDirectoriesRecursive(srcpath) {
const nestedDirectories = getDirectories(srcpath).map(
getDirectoriesRecursive
);
const nestedDirectories = getDirectories(srcpath).map(getDirectoriesRecursive);
const directories = flattenDeep(nestedDirectories);
directories.push(srcpath);
return directories;
}

/**
* Returns the body or query string to be used in
* the mock name.
* the mock fileName.
*
* In any case we will prepend the value with a double
* dash so that the mock files will look like:
Expand Down Expand Up @@ -282,28 +272,84 @@ function getBody(req, callback) {
});
}

function isJsonString(str) {
if (typeof str !== 'string') {
return false;
}
try {
JSON.parse(str);
} catch (err) {
return false;
}
return true;
}

function getMatchingJsonFile(files, fullPath, jsonBody) {
for (var file of files) {
if (file.endsWith('.json')) {
var data = fs.readFileSync(join(fullPath, file), { encoding: 'utf8' });

try {
if (jsonBody === JSON.stringify(JSON.parse(data))) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you could change this check to if (JSON.stringify(JSON.parse(jsonBody)) === JSON.stringify(JSON.parse(data))) { to avoid a falsy result due to an extra white space or some other formatting detail.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with that because without it, it couldn't work correctly (if some space character exist for example)

return file;
}
} catch (err) {
if (mockserver.verbose) {
console.log(
'Tried to match json body with ' + file.yellow + '. File has invalid JSON'.red
);
}
}
}
}
return null;
}

function getMockedContent(path, prefix, body, query) {
const mockName = prefix + (getBodyOrQueryString(body, query) || '') + '.mock';
const mockFile = join(mockserver.directory, path, mockName);
let content;
var fullPath = join(mockserver.directory, path);
var mockName = prefix + (getBodyOrQueryString(body, query) || '') + '.mock';
var prefixFallback = prefix + '.mock';

try {
content = fs.readFileSync(mockFile, { encoding: 'utf8' });
if (mockserver.verbose) {
console.log(
'Reading from ' + mockFile.yellow + ' file: ' + 'Matched'.green
);
var files = fs.readdirSync(fullPath);

// 1st try to match on body or query within file name
if (files.indexOf(mockName) !== -1) {
if (mockserver.verbose) {
console.log('Reading from ' + mockName.yellow + ' file: ' + 'Matched'.green);
}
return fs.readFileSync(join(fullPath, mockName), { encoding: 'utf8' });
}

// 2nd (for json body only) try to match on json body within file contents
if (body && isJsonString(body)) {
var matchingJsonFile = getMatchingJsonFile(files, fullPath, body);

if (matchingJsonFile) {
var fileWithoutExtension = matchingJsonFile.replace('.json', '');
var mockNameFromJson = prefix + '@' + fileWithoutExtension + '.mock';
if (mockserver.verbose) {
console.log('Reading from ' + mockNameFromJson.yellow + ' file: ' + 'Matched'.green);
}
return fs.readFileSync(join(fullPath, mockNameFromJson), { encoding: 'utf8' });
}
}

// 3rd try fallback with only prefix
if (files.indexOf(prefixFallback) !== -1) {
if (mockserver.verbose) {
console.log('Reading from ' + mockName.yellow + ' file: ' + 'Not matched'.red);
}
return fs.readFileSync(join(fullPath, prefixFallback), { encoding: 'utf8' });
}
} catch (err) {
if (mockserver.verbose) {
console.log(
'Reading from ' + mockFile.yellow + ' file: ' + 'Not matched'.red
);
console.log('Reading from ' + mockName.yellow + ' file: ' + 'Not matched'.red);
}
content = (body || query) && getMockedContent(path, prefix);
}

return content;
return null;
}

function getContentFromPermutations(path, method, body, query, permutations) {
Expand Down Expand Up @@ -333,8 +379,7 @@ const mockserver = {
let path = url;

const queryIndex = url.indexOf('?'),
query =
queryIndex >= 0 ? url.substring(queryIndex).replace(/\?/g, '') : '',
query = queryIndex >= 0 ? url.substring(queryIndex).replace(/\?/g, '') : '',
method = req.method.toUpperCase(),
headers = [];

Expand All @@ -346,9 +391,7 @@ const mockserver = {
mockserver.headers.forEach(function(header) {
header = header.toLowerCase();
if (req.headers[header]) {
headers.push(
'_' + normalizeHeader(header) + '=' + req.headers[header]
);
headers.push('_' + normalizeHeader(header) + '=' + req.headers[header]);
}
});
}
Expand All @@ -367,30 +410,14 @@ const mockserver = {
permutations.push([]);
}

matched = getContentFromPermutations(
path,
method,
body,
query,
permutations.slice(0)
);
matched = getContentFromPermutations(path, method, body, query, permutations.slice(0));

if (!matched.content && (path = getWildcardPath(path))) {
matched = getContentFromPermutations(
path,
method,
body,
query,
permutations.slice(0)
);
matched = getContentFromPermutations(path, method, body, query, permutations.slice(0));
}

if (matched.content) {
const mock = parse(
matched.content,
join(mockserver.directory, path, matched.prefix),
req
);
const mock = parse(matched.content, join(mockserver.directory, path, matched.prefix), req);
const delay = getResponseDelay(mock.headers);
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delay);
res.writeHead(mock.status, mock.headers);
Expand All @@ -400,7 +427,7 @@ const mockserver = {
res.end('Not Mocked');
}
});
},
}
};

module.exports = function(directory, silent) {
Expand Down
File renamed without changes.
4 changes: 4 additions & 0 deletions test/mocks/query-params/GET.mock
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
HTTP/1.1 200 OK
Content-Type: text

Passed!
6 changes: 6 additions & 0 deletions test/mocks/request-json/POST.mock
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
HTTP/1.1 404 OK
Content-Type: application/json; charset=utf-8

{
"error": "User not found"
}
6 changes: 6 additions & 0 deletions test/mocks/request-json/[email protected]
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{
"token": "longJWT"
}
6 changes: 6 additions & 0 deletions test/mocks/request-json/not-payload.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"user": {
"username": "notTheUser",
"password": "123456"
}
}
6 changes: 6 additions & 0 deletions test/mocks/request-json/payload.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"user": {
"username": "theUser",
"password": "123456"
}
}
File renamed without changes.
Loading