diff --git a/README.md b/README.md index 3b5ef9e..e6e79d6 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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: ``` @@ -295,7 +318,7 @@ Content-Type: application/json; charset=utf-8 Access-Control-Allow-Origin: * { - "Random": "Content" + "Random": "Content" } ``` diff --git a/mockserver.js b/mockserver.js index 2b780b0..a7e0e28 100644 --- a/mockserver.js +++ b/mockserver.js @@ -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) }; @@ -247,6 +247,10 @@ function getDirectoriesRecursive(srcpath) { * GET--query=string&hello=hella.mock */ function getBodyOrQueryString(body, query) { + if (body && query) { + return '_'+ body + '--' + query; + } + if (query) { return '--' + query; } @@ -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) { diff --git a/test/mocks/return-200/POST_Hello=123--a=b.mock b/test/mocks/return-200/POST_Hello=123--a=b.mock new file mode 100644 index 0000000..0878d04 --- /dev/null +++ b/test/mocks/return-200/POST_Hello=123--a=b.mock @@ -0,0 +1,4 @@ +HTTP/1.1 200 OK +Content-Type: text/xml; charset=utf-8 + +Hella \ No newline at end of file diff --git a/test/mocks/return-200/POST_Hello=456--c=__.mock b/test/mocks/return-200/POST_Hello=456--c=__.mock new file mode 100644 index 0000000..3a3133b --- /dev/null +++ b/test/mocks/return-200/POST_Hello=456--c=__.mock @@ -0,0 +1,4 @@ +HTTP/1.1 200 OK +Content-Type: text/xml; charset=utf-8 + +Hello!!! \ No newline at end of file diff --git a/test/mocks/wildcard-params/GET--foo=bar&buz=__.mock b/test/mocks/wildcard-params/GET--foo=bar&buz=__.mock new file mode 100644 index 0000000..de9a291 --- /dev/null +++ b/test/mocks/wildcard-params/GET--foo=bar&buz=__.mock @@ -0,0 +1,3 @@ +HTTP/1.1 200 OK + +wildcard-params diff --git a/test/mocks/wildcard-params/GET--foo=bar&buz=bak.mock b/test/mocks/wildcard-params/GET--foo=bar&buz=bak.mock new file mode 100644 index 0000000..fa016a6 --- /dev/null +++ b/test/mocks/wildcard-params/GET--foo=bar&buz=bak.mock @@ -0,0 +1,3 @@ +HTTP/1.1 200 OK + +exact match \ No newline at end of file diff --git a/test/mockserver.js b/test/mockserver.js index 4ff6870..531932c 100644 --- a/test/mockserver.js +++ b/test/mockserver.js @@ -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); }) @@ -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']; @@ -431,6 +419,86 @@ 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 prefer exact matches over wildcard matches', function () { + processRequest('/wildcard-params?foo=bar&buz=bak', 'GET'); + + assert.equal(res.status, 200); + assert.equal(res.body, 'exact match'); + }) + + 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 = [