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

Allow wildcards for query string parameters #81

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ Response-Delay: 5000

The delay value is expected in milliseconds, if not set for a given file there will be no delay.

## Query string parameters and POST body
## Query string parameters

In order to support query string parameters in the mocked files, replace all occurrences of `?` with `--`, then
append the entire string to the end of the file.
Expand All @@ -188,6 +188,29 @@ test/GET--a=b&c=d--.mock

(This has been introduced to overcome issues in file naming on windows)

Query parameters can be passed in any order. For example,
```
GET /hello?a=b&c=d
GET /hello?c=d&a=b
```
both match the file `hello/GET--a=b&c=d`.

You can specify a wildcard for a query param by including `__` in place of a value in the file name.
```
GET /hello?a=b&c=d

matches
hello/GET--a=b&c=__
```

In the event that there are multiple files that match the provided query params pattern, mockserver selects files in the following order:
* Files with params in the same order (no wildcards)
* Files with params in any order (no wildcards)
* Files with params in any order and wild cards


## Query string parameters and POST body

To combine custom headers and query parameters, simply add the headers _then_ add the parameters:

```
Expand Down Expand Up @@ -295,7 +318,7 @@ Content-Type: application/json; charset=utf-8
Access-Control-Allow-Origin: *

{
"Random": "Content"
"Random": "Content"
}
```

Expand Down
102 changes: 86 additions & 16 deletions mockserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ 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) };
Expand Down Expand Up @@ -247,6 +247,10 @@ function getDirectoriesRecursive(srcpath) {
* GET--query=string&hello=hella.mock
*/
function getBodyOrQueryString(body, query) {
if (body && query) {
return '_'+ body + '--' + query;
}

Comment on lines +250 to +253
Copy link
Author

@KyleW KyleW Nov 8, 2019

Choose a reason for hiding this comment

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

I'd appreciate another set of eyes on this.

This is the behavior described in the docs, but I don't think it was actually implemented before now. I added what I think is the right implementation, but I don't want to inadvertently introduce a breaking change.

If it's in doubt, I'm glad to remove it since it's not directly tied to the core purpose of the pr.

if (query) {
return '--' + query;
}
Expand Down Expand Up @@ -283,27 +287,93 @@ function getBody(req, callback) {
}

function getMockedContent(path, prefix, body, query) {
const mockName = prefix + (getBodyOrQueryString(body, query) || '') + '.mock';
const mockFile = join(mockserver.directory, path, mockName);
let content;
// Check for an exact match
const exactName = prefix + (getBodyOrQueryString(body, query) || '') + '.mock';
let content = handleMatch(path, exactName, fs.existsSync);

try {
content = fs.readFileSync(mockFile, { encoding: 'utf8' });
if (mockserver.verbose) {
console.log(
'Reading from ' + mockFile.yellow + ' file: ' + 'Matched'.green
);
// Compare params without regard to order
if (!content && query) {
content = testForQuery(path, prefix, body, query, false);

// Compare params without regard to order and allow wildcards
if (!content) {
content = testForQuery(path, prefix, body, query, true);
}
} catch (err) {
if (mockserver.verbose) {
console.log(
'Reading from ' + mockFile.yellow + ' file: ' + 'Not matched'.red
}

// fallback option (e.g. GET.mock). ignores body and query
if (!content) {
const fallbackName = prefix + '.mock';
content = handleMatch(path, fallbackName, fs.existsSync);
}

return content;
}

function testForQuery(path, prefix, body, query, allowWildcards) {
// Find all files in the directory
return fs
.readdirSync(join(mockserver.directory, path))
.filter(possibleFile => {
if (body) {
return possibleFile.startsWith(prefix + '_' + body) && possibleFile.endsWith('.mock');
}

return possibleFile.startsWith(prefix) && possibleFile.endsWith('.mock');
})
.filter(possibleFile => possibleFile.match(/--[\s\S]*__/))
.reduce((prev, possibleFile) => {
if (prev) {
return prev;
}

let isMatch = true;
//get params from file
const paramMap = queryStringToMap(query);
const possibleFileParamMap = queryStringToMap(
possibleFile.replace('.mock', '').split('--')[1]
);

for (const key in paramMap) {
if (!isMatch) {
continue;
}
isMatch =
possibleFileParamMap[key] === paramMap[key] ||
(allowWildcards && possibleFileParamMap[key] === '__');
}

return handleMatch(path, possibleFile, isMatch);
}, undefined);
}

function queryStringToMap(query) {
const result = {};
query.split('&').forEach(param => {
const [key, val] = param.split('=');
result[key] = val;
});
return result;
}

function handleMatch(path, fileName, isMatchOrTest) {
const mockFile = join(mockserver.directory, path, fileName);

let isMatch = isMatchOrTest;
if (typeof isMatchOrTest === 'function') {
isMatch = isMatchOrTest(mockFile);
}

if (isMatch) {
if (mockserver.verbose) {
console.log('Reading from ' + mockFile.yellow + ' file: ' + 'Matched'.green);
}
content = (body || query) && getMockedContent(path, prefix);
return fs.readFileSync(mockFile, { encoding: 'utf8' });
}

return content;
if (mockserver.verbose) {
console.log('Reading from ' + mockFile.yellow + ' file: ' + 'Not matched'.red);
}
}

function getContentFromPermutations(path, method, body, query, permutations) {
Expand Down
4 changes: 4 additions & 0 deletions test/mocks/return-200/POST_Hello=123--a=b.mock
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
HTTP/1.1 200 OK
Content-Type: text/xml; charset=utf-8

Hella
4 changes: 4 additions & 0 deletions test/mocks/return-200/POST_Hello=456--c=__.mock
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
HTTP/1.1 200 OK
Content-Type: text/xml; charset=utf-8

Hello!!!
3 changes: 3 additions & 0 deletions test/mocks/wildcard-params/GET--foo=bar&buz=__.mock
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
HTTP/1.1 200 OK

wildcard-params
87 changes: 74 additions & 13 deletions test/mockserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ describe('mockserver', function() {

it('should combine the identical headers names', function() {
processRequest('/multiple-headers-same-name/', 'GET');

assert.equal(res.headers['Set-Cookie'].length, 3);
})

Expand Down Expand Up @@ -143,18 +143,6 @@ describe('mockserver', function() {
assert.equal(res.body, 'multi-level url');
});

it('should be able to handle GET parameters', function() {
processRequest('/test?a=b', 'GET');

assert.equal(res.status, 200);
});

it('should default to GET.mock if no matching parameter file is found', function() {
processRequest('/test?a=c', 'GET');

assert.equal(res.status, 200);
});

it('should be able track custom headers', function() {
mockserver.headers = ['authorization'];

Expand Down Expand Up @@ -431,6 +419,79 @@ describe('mockserver', function() {
assert.equal(res.status, 404);
});
});

describe('query string parameters', function() {
it('should be able to handle GET parameters', function() {
processRequest('/test?a=b', 'GET');

assert.equal(res.status, 200);
});

it('should handle a file with wildcards as query params', function() {
processRequest('/wildcard-params?foo=bar&buz=baz', 'GET');

assert.equal(res.status, 200);
});

it('should handle a request regardless of the order of the params in the query string', function() {
processRequest('/wildcard-params?buz=baz&foo=bar', 'GET');

assert.equal(res.status, 200);
});

it('should not handle requests with extra params in the query string', function() {
processRequest('/wildcard-params?buz=baz&foo=bar&biz=bak', 'GET');

assert.equal(res.status, 404);
});

it('should default to GET.mock if no matching parameter file is found', function() {
processRequest('/test?a=c', 'GET');

assert.equal(res.status, 200);
});

it('should be able to include POST bodies and query params', function(done) {
const req = new MockReq({
method: 'POST',
url: '/return-200?a=b',
headers: {
Accept: 'text/plain'
}
});
req.write('Hello=123');
req.end();

mockserver(mocksDirectory, verbose)(req, res);

req.on('end', function() {
assert.equal(res.body, 'Hella');
assert.equal(res.status, 200);
done();
});
});

it('should be able to include POST bodies and query params with wildcards', function(done) {
const req = new MockReq({
method: 'POST',
url: '/return-200?c=d',
headers: {
Accept: 'text/plain'
}
});
req.write('Hello=456');
req.end();

mockserver(mocksDirectory, verbose)(req, res);

req.on('end', function() {
assert.equal(res.body, 'Hello!!!');
assert.equal(res.status, 200);
done();
});
});
});

describe('.getResponseDelay', function() {
it('should return a value greater than zero when valid', function() {
const ownValueHeaders = [
Expand Down