diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..302cfc427 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_size = 4 +indent_style = tab + +[*.{md,yml,yaml}] +indent_size = 2 +indent_style = space diff --git a/internal/mode/static/nginx/modules/src/httpmatches.js b/internal/mode/static/nginx/modules/src/httpmatches.js index 0fcf5ef21..6478b1299 100644 --- a/internal/mode/static/nginx/modules/src/httpmatches.js +++ b/internal/mode/static/nginx/modules/src/httpmatches.js @@ -2,208 +2,208 @@ import qs from 'querystring'; const MATCHES_VARIABLE = 'http_matches'; const HTTP_CODES = { - notFound: 404, - internalServerError: 500, + notFound: 404, + internalServerError: 500, }; function redirect(r) { - let matches; - - try { - matches = extractMatchesFromRequest(r); - } catch (e) { - r.error(e.message); - r.return(HTTP_CODES.internalServerError); - return; - } - - // Matches is a list of http matches in order of precedence. - // We will accept the first match that the request satisfies. - // If there's a match, redirect request to internal location block. - // If an exception occurs, return 500. - // If no matches are found, return 404. - let match; - try { - match = findWinningMatch(r, matches); - } catch (e) { - r.error(e.message); - r.return(HTTP_CODES.internalServerError); - return; - } - - if (!match) { - r.return(HTTP_CODES.notFound); - return; - } - - if (!match.redirectPath) { - r.error( - `cannot redirect the request; the match ${JSON.stringify( - match, - )} does not have a redirectPath set`, - ); - r.return(HTTP_CODES.internalServerError); - return; - } - - r.internalRedirect(match.redirectPath); + let matches; + + try { + matches = extractMatchesFromRequest(r); + } catch (e) { + r.error(e.message); + r.return(HTTP_CODES.internalServerError); + return; + } + + // Matches is a list of http matches in order of precedence. + // We will accept the first match that the request satisfies. + // If there's a match, redirect request to internal location block. + // If an exception occurs, return 500. + // If no matches are found, return 404. + let match; + try { + match = findWinningMatch(r, matches); + } catch (e) { + r.error(e.message); + r.return(HTTP_CODES.internalServerError); + return; + } + + if (!match) { + r.return(HTTP_CODES.notFound); + return; + } + + if (!match.redirectPath) { + r.error( + `cannot redirect the request; the match ${JSON.stringify( + match, + )} does not have a redirectPath set`, + ); + r.return(HTTP_CODES.internalServerError); + return; + } + + r.internalRedirect(match.redirectPath); } function extractMatchesFromRequest(r) { - if (!r.variables[MATCHES_VARIABLE]) { - throw Error( - `cannot redirect the request; the variable ${MATCHES_VARIABLE} is not defined on the request object`, - ); - } - - let matches; - - try { - matches = JSON.parse(r.variables[MATCHES_VARIABLE]); - } catch (e) { - throw Error( - `cannot redirect the request; error parsing ${r.variables[MATCHES_VARIABLE]} into a JSON object: ${e}`, - ); - } - - if (!Array.isArray(matches)) { - throw Error(`cannot redirect the request; expected a list of matches, got ${matches}`); - } - - if (matches.length === 0) { - throw Error(`cannot redirect the request; matches is an empty list`); - } - - return matches; + if (!r.variables[MATCHES_VARIABLE]) { + throw Error( + `cannot redirect the request; the variable ${MATCHES_VARIABLE} is not defined on the request object`, + ); + } + + let matches; + + try { + matches = JSON.parse(r.variables[MATCHES_VARIABLE]); + } catch (e) { + throw Error( + `cannot redirect the request; error parsing ${r.variables[MATCHES_VARIABLE]} into a JSON object: ${e}`, + ); + } + + if (!Array.isArray(matches)) { + throw Error(`cannot redirect the request; expected a list of matches, got ${matches}`); + } + + if (matches.length === 0) { + throw Error(`cannot redirect the request; matches is an empty list`); + } + + return matches; } function findWinningMatch(r, matches) { - for (let i = 0; i < matches.length; i++) { - try { - let found = testMatch(r, matches[i]); - if (found) { - return matches[i]; - } - } catch (e) { - throw e; - } - } - - return null; + for (let i = 0; i < matches.length; i++) { + try { + let found = testMatch(r, matches[i]); + if (found) { + return matches[i]; + } + } catch (e) { + throw e; + } + } + + return null; } function testMatch(r, match) { - // check for any - if (match.any) { - return true; - } - - // check method - if (match.method && r.method !== match.method) { - return false; - } - - // check headers - if (match.headers) { - try { - let found = headersMatch(r.headersIn, match.headers); - if (!found) { - return false; - } - } catch (e) { - throw e; - } - } - - // check params - if (match.params) { - try { - let found = paramsMatch(r.args, match.params); - if (!found) { - return false; - } - } catch (e) { - throw e; - } - } - - // all match conditions are satisfied so return true - return true; + // check for any + if (match.any) { + return true; + } + + // check method + if (match.method && r.method !== match.method) { + return false; + } + + // check headers + if (match.headers) { + try { + let found = headersMatch(r.headersIn, match.headers); + if (!found) { + return false; + } + } catch (e) { + throw e; + } + } + + // check params + if (match.params) { + try { + let found = paramsMatch(r.args, match.params); + if (!found) { + return false; + } + } catch (e) { + throw e; + } + } + + // all match conditions are satisfied so return true + return true; } function headersMatch(requestHeaders, headers) { - for (let i = 0; i < headers.length; i++) { - const h = headers[i]; - const kv = h.split(':'); - - if (kv.length !== 2) { - throw Error(`invalid header match: ${h}`); - } - // Header names are compared in a case-insensitive manner, meaning header name "FOO" is equivalent to "foo". - // The NGINX request's headersIn object lookup is case-insensitive as well. - // This means that requestHeaders['FOO'] is equivalent to requestHeaders['foo']. - let val = requestHeaders[kv[0]]; - - if (!val) { - return false; - } - - // split on comma because nginx uses commas to delimit multiple header values - const values = val.split(','); - if (!values.includes(kv[1])) { - return false; - } - } - - return true; + for (let i = 0; i < headers.length; i++) { + const h = headers[i]; + const kv = h.split(':'); + + if (kv.length !== 2) { + throw Error(`invalid header match: ${h}`); + } + // Header names are compared in a case-insensitive manner, meaning header name "FOO" is equivalent to "foo". + // The NGINX request's headersIn object lookup is case-insensitive as well. + // This means that requestHeaders['FOO'] is equivalent to requestHeaders['foo']. + let val = requestHeaders[kv[0]]; + + if (!val) { + return false; + } + + // split on comma because nginx uses commas to delimit multiple header values + const values = val.split(','); + if (!values.includes(kv[1])) { + return false; + } + } + + return true; } function paramsMatch(requestParams, params) { - for (let i = 0; i < params.length; i++) { - let p = params[i]; - // We store query parameter matches as strings with the format "key=value"; however, there may be more than one - // instance of "=" in the string. - // To recover the key and value, we need to find the first occurrence of "=" in the string. - const idx = params[i].indexOf('='); - // Check for an improperly constructed query parameter match. There are three possible error cases: - // (1) if the index is -1, then there are no "=" in the string (e.g. "keyvalue") - // (2) if the index is 0, then there is no value in the string (e.g. "key="). - // (3) if the index is equal to length -1, then there is no key in the string (e.g. "=value"). - if (idx === -1 || (idx === 0) | (idx === p.length - 1)) { - throw Error(`invalid query parameter: ${p}`); - } - - // Divide string into key value using the index. - let kv = [p.slice(0, idx), p.slice(idx + 1)]; - - // val can either be a string or an array of strings. - // Also, the NGINX request's args object lookup is case-sensitive. - // For example, 'a=1&b=2&A=3&b=4' will be parsed into {a: "1", b: ["2", "4"], A: "3"} - let val = requestParams[kv[0]]; - if (!val) { - return false; - } - - // If val is an array, we will match against the first element in the array according to the Gateway API spec. - if (Array.isArray(val)) { - val = val[0]; - } - - if (val !== kv[1]) { - return false; - } - } - - return true; + for (let i = 0; i < params.length; i++) { + let p = params[i]; + // We store query parameter matches as strings with the format "key=value"; however, there may be more than one + // instance of "=" in the string. + // To recover the key and value, we need to find the first occurrence of "=" in the string. + const idx = params[i].indexOf('='); + // Check for an improperly constructed query parameter match. There are three possible error cases: + // (1) if the index is -1, then there are no "=" in the string (e.g. "keyvalue") + // (2) if the index is 0, then there is no value in the string (e.g. "key="). + // (3) if the index is equal to length -1, then there is no key in the string (e.g. "=value"). + if (idx === -1 || (idx === 0) | (idx === p.length - 1)) { + throw Error(`invalid query parameter: ${p}`); + } + + // Divide string into key value using the index. + let kv = [p.slice(0, idx), p.slice(idx + 1)]; + + // val can either be a string or an array of strings. + // Also, the NGINX request's args object lookup is case-sensitive. + // For example, 'a=1&b=2&A=3&b=4' will be parsed into {a: "1", b: ["2", "4"], A: "3"} + let val = requestParams[kv[0]]; + if (!val) { + return false; + } + + // If val is an array, we will match against the first element in the array according to the Gateway API spec. + if (Array.isArray(val)) { + val = val[0]; + } + + if (val !== kv[1]) { + return false; + } + } + + return true; } export default { - redirect, - testMatch, - findWinningMatch, - headersMatch, - paramsMatch, - extractMatchesFromRequest, - HTTP_CODES, - MATCHES_VARIABLE, + redirect, + testMatch, + findWinningMatch, + headersMatch, + paramsMatch, + extractMatchesFromRequest, + HTTP_CODES, + MATCHES_VARIABLE, }; diff --git a/internal/mode/static/nginx/modules/test/httpmatches.test.js b/internal/mode/static/nginx/modules/test/httpmatches.test.js index 7360b4f9b..0a1ca26ab 100644 --- a/internal/mode/static/nginx/modules/test/httpmatches.test.js +++ b/internal/mode/static/nginx/modules/test/httpmatches.test.js @@ -4,460 +4,464 @@ import { assert, describe, expect, it } from 'vitest'; // Creates a NGINX HTTP Request Object for testing. // See documentation for all properties available: http://nginx.org/en/docs/njs/reference.html function createRequest({ method = '', headers = {}, params = {}, matches = '' } = {}) { - let r = { - // Test mocks - return(statusCode) { - r.testReturned = statusCode; - }, - internalRedirect(redirectPath) { - r.testRedirectedTo = redirectPath; - }, - error(msg) { - console.log('\tngx_error:', msg); - }, - variables: {}, - }; + let r = { + // Test mocks + return(statusCode) { + r.testReturned = statusCode; + }, + internalRedirect(redirectPath) { + r.testRedirectedTo = redirectPath; + }, + error(msg) { + console.log('\tngx_error:', msg); + }, + variables: {}, + }; - if (method) { - r.method = method; - } + if (method) { + r.method = method; + } - if (headers) { - r.headersIn = headers; - } + if (headers) { + r.headersIn = headers; + } - if (params) { - r.args = params; - } + if (params) { + r.args = params; + } - if (matches) { - r.variables[hm.MATCHES_VARIABLE] = matches; - } + if (matches) { + r.variables[hm.MATCHES_VARIABLE] = matches; + } - return r; + return r; } describe('extractMatchesFromRequest', () => { - const tests = [ - { - name: 'throws if matches variable does not exist on request', - request: createRequest(), - expectThrow: true, - errSubstring: 'http_matches is not defined', - }, - { - name: 'throws if matches variable is not JSON', - request: createRequest({ matches: 'not-JSON' }), - expectThrow: true, - errSubstring: 'error parsing', - }, - { - name: 'throws if matches variable is not an array', - request: createRequest({ matches: '{}' }), - expectThrow: true, - errSubstring: 'expected a list of matches', - }, - { - name: 'throws if the length of the matches variable is zero', - request: createRequest({ matches: '[]' }), - expectThrow: true, - errSubstring: 'matches is an empty list', - }, - { - name: 'does not throw if matches variable is expected list of matches', - request: createRequest({ matches: '[{"any":true}]' }), - expectThrow: false, - }, - ]; - tests.forEach((test) => { - it(test.name, () => { - if (test.expectThrow) { - expect(() => hm.extractMatchesFromRequest(test.request)).to.throw(test.errSubstring); - } else { - expect(() => hm.extractMatchesFromRequest(test.request).to.not.throw()); - } - }); - }); + const tests = [ + { + name: 'throws if matches variable does not exist on request', + request: createRequest(), + expectThrow: true, + errSubstring: 'http_matches is not defined', + }, + { + name: 'throws if matches variable is not JSON', + request: createRequest({ matches: 'not-JSON' }), + expectThrow: true, + errSubstring: 'error parsing', + }, + { + name: 'throws if matches variable is not an array', + request: createRequest({ matches: '{}' }), + expectThrow: true, + errSubstring: 'expected a list of matches', + }, + { + name: 'throws if the length of the matches variable is zero', + request: createRequest({ matches: '[]' }), + expectThrow: true, + errSubstring: 'matches is an empty list', + }, + { + name: 'does not throw if matches variable is expected list of matches', + request: createRequest({ matches: '[{"any":true}]' }), + expectThrow: false, + }, + ]; + tests.forEach((test) => { + it(test.name, () => { + if (test.expectThrow) { + expect(() => hm.extractMatchesFromRequest(test.request)).to.throw( + test.errSubstring, + ); + } else { + expect(() => hm.extractMatchesFromRequest(test.request).to.not.throw()); + } + }); + }); }); describe('testMatch', () => { - const tests = [ - { - name: 'returns true if any is set to true', - match: { any: true }, - request: createRequest(), - expected: true, - }, - { - name: 'returns true if method matches and no other conditions are set', - match: { method: 'GET' }, - request: createRequest({ method: 'GET' }), - expected: true, - }, - { - name: 'returns true if headers match and no other conditions are set', - match: { headers: ['header:value'] }, - request: createRequest({ headers: { header: 'value' } }), - expected: true, - }, - { - name: 'returns true if query parameters match and no other conditions are set', - match: { params: ['key=value'] }, - request: createRequest({ params: { key: 'value' } }), - expected: true, - }, - { - name: 'returns true if multiple conditions match', - match: { method: 'GET', headers: ['header:value'], params: ['key=value'] }, - request: createRequest({ - method: 'GET', - headers: { header: 'value' }, - params: { key: 'value' }, - }), - expected: true, - }, - { - name: 'returns false if method does not match', - match: { method: 'POST' }, - request: createRequest({ method: 'GET' }), - expected: false, - }, - { - name: 'returns false if headers do not match', - match: { method: 'GET', headers: ['header:value'] }, - request: createRequest({ method: 'GET' }), // no headers are set on request - expected: false, - }, - { - name: 'returns false if query parameters do not match', - match: { method: 'GET', headers: ['header:value'], params: ['key=value'] }, - request: createRequest({ method: 'GET', headers: { header: 'value' } }), // no params set on request - expected: false, - }, - { - name: 'throws if headers are malformed', - match: { headers: ['malformedheader'] }, - request: createRequest(), - expectThrow: true, - errSubstring: 'invalid header match', - }, - { - name: 'throws if params are malformed', - match: { params: ['keyvalue'] }, - request: createRequest(), - expectThrow: true, - errSubstring: 'invalid query parameter', - }, - ]; + const tests = [ + { + name: 'returns true if any is set to true', + match: { any: true }, + request: createRequest(), + expected: true, + }, + { + name: 'returns true if method matches and no other conditions are set', + match: { method: 'GET' }, + request: createRequest({ method: 'GET' }), + expected: true, + }, + { + name: 'returns true if headers match and no other conditions are set', + match: { headers: ['header:value'] }, + request: createRequest({ headers: { header: 'value' } }), + expected: true, + }, + { + name: 'returns true if query parameters match and no other conditions are set', + match: { params: ['key=value'] }, + request: createRequest({ params: { key: 'value' } }), + expected: true, + }, + { + name: 'returns true if multiple conditions match', + match: { method: 'GET', headers: ['header:value'], params: ['key=value'] }, + request: createRequest({ + method: 'GET', + headers: { header: 'value' }, + params: { key: 'value' }, + }), + expected: true, + }, + { + name: 'returns false if method does not match', + match: { method: 'POST' }, + request: createRequest({ method: 'GET' }), + expected: false, + }, + { + name: 'returns false if headers do not match', + match: { method: 'GET', headers: ['header:value'] }, + request: createRequest({ method: 'GET' }), // no headers are set on request + expected: false, + }, + { + name: 'returns false if query parameters do not match', + match: { method: 'GET', headers: ['header:value'], params: ['key=value'] }, + request: createRequest({ method: 'GET', headers: { header: 'value' } }), // no params set on request + expected: false, + }, + { + name: 'throws if headers are malformed', + match: { headers: ['malformedheader'] }, + request: createRequest(), + expectThrow: true, + errSubstring: 'invalid header match', + }, + { + name: 'throws if params are malformed', + match: { params: ['keyvalue'] }, + request: createRequest(), + expectThrow: true, + errSubstring: 'invalid query parameter', + }, + ]; - tests.forEach((test) => { - it(test.name, () => { - if (test.expectThrow) { - expect(() => hm.testMatch(test.request, test.match)).to.throw(test.errSubstring); - } else { - const result = hm.testMatch(test.request, test.match); - expect(result).to.equal(test.expected); - } - }); - }); + tests.forEach((test) => { + it(test.name, () => { + if (test.expectThrow) { + expect(() => hm.testMatch(test.request, test.match)).to.throw(test.errSubstring); + } else { + const result = hm.testMatch(test.request, test.match); + expect(result).to.equal(test.expected); + } + }); + }); }); describe('findWinningMatch', () => { - const headerMatch = { headers: ['header:value'] }; - const queryParamMatch = { params: ['key=value'] }; - const methodMatch = { method: 'POST' }; - const anyMatch = { any: true }; - const malformedMatch = { headers: ['malformed'] }; + const headerMatch = { headers: ['header:value'] }; + const queryParamMatch = { params: ['key=value'] }; + const methodMatch = { method: 'POST' }; + const anyMatch = { any: true }; + const malformedMatch = { headers: ['malformed'] }; - const tests = [ - { - name: 'returns first match that the request satisfies', - matches: [headerMatch, queryParamMatch, methodMatch, anyMatch], // second match should be returned - request: createRequest({ method: 'POST', params: { key: 'value' } }), - expected: queryParamMatch, - }, - { - name: 'returns null when no match exists', - matches: [headerMatch, queryParamMatch, methodMatch], - request: createRequest({ method: 'GET' }), - expected: null, - }, - { - name: 'throws if an exception occurs while finding a match', - matches: [headerMatch, queryParamMatch, malformedMatch], - request: createRequest({ method: 'GET' }), - expectThrow: true, - errSubstring: 'invalid header match', - }, - ]; + const tests = [ + { + name: 'returns first match that the request satisfies', + matches: [headerMatch, queryParamMatch, methodMatch, anyMatch], // second match should be returned + request: createRequest({ method: 'POST', params: { key: 'value' } }), + expected: queryParamMatch, + }, + { + name: 'returns null when no match exists', + matches: [headerMatch, queryParamMatch, methodMatch], + request: createRequest({ method: 'GET' }), + expected: null, + }, + { + name: 'throws if an exception occurs while finding a match', + matches: [headerMatch, queryParamMatch, malformedMatch], + request: createRequest({ method: 'GET' }), + expectThrow: true, + errSubstring: 'invalid header match', + }, + ]; - tests.forEach((test) => { - it(test.name, () => { - test.request.variables = { - http_matches: JSON.stringify(test.matches), - }; + tests.forEach((test) => { + it(test.name, () => { + test.request.variables = { + http_matches: JSON.stringify(test.matches), + }; - if (test.expectThrow) { - expect(() => hm.findWinningMatch(test.request, test.matches)).to.throw(test.errSubstring); - } else { - const result = hm.findWinningMatch(test.request, test.matches); - expect(result).to.equal(test.expected); - } - }); - }); + if (test.expectThrow) { + expect(() => hm.findWinningMatch(test.request, test.matches)).to.throw( + test.errSubstring, + ); + } else { + const result = hm.findWinningMatch(test.request, test.matches); + expect(result).to.equal(test.expected); + } + }); + }); }); describe('headersMatch', () => { - const multipleHeaders = ['header1:VALUE1', 'header2:value2', 'header3:value3']; // case matters for header values + const multipleHeaders = ['header1:VALUE1', 'header2:value2', 'header3:value3']; // case matters for header values - const tests = [ - { - name: 'throws an error if a header has multiple colons', - headers: ['too:many:colons'], - expectThrow: true, - }, - { - name: 'throws an error if a header has no colon', - headers: ['wrong=delimiter'], - requestHeaders: {}, - expectThrow: true, - }, - { - name: 'returns false if one of the header values does not match', - headers: multipleHeaders, - requestHeaders: { - header1: 'VALUE1', - header2: 'value2', - header3: 'wrong-value', // this value does not match - }, - expected: false, - }, - { - name: 'returns false if one of the header values case does not match', - headers: multipleHeaders, - requestHeaders: { - header1: 'value1', // this value is not the correct case - header2: 'value2', - header3: 'value3', - }, - expected: false, - }, - { - name: 'returns true if all headers match', - headers: multipleHeaders, - requestHeaders: { - header1: 'VALUE1', // this value is not the correct case - header2: 'value2', - header3: 'value3', - }, - expected: true, - }, - { - name: 'returns true if request has multiple values for a header name and one value matches ', - headers: ['multiValueHeader:val3'], - requestHeaders: { - multiValueHeader: 'val1,val2,val3,val4,val5', - }, - expected: true, - }, - ]; + const tests = [ + { + name: 'throws an error if a header has multiple colons', + headers: ['too:many:colons'], + expectThrow: true, + }, + { + name: 'throws an error if a header has no colon', + headers: ['wrong=delimiter'], + requestHeaders: {}, + expectThrow: true, + }, + { + name: 'returns false if one of the header values does not match', + headers: multipleHeaders, + requestHeaders: { + header1: 'VALUE1', + header2: 'value2', + header3: 'wrong-value', // this value does not match + }, + expected: false, + }, + { + name: 'returns false if one of the header values case does not match', + headers: multipleHeaders, + requestHeaders: { + header1: 'value1', // this value is not the correct case + header2: 'value2', + header3: 'value3', + }, + expected: false, + }, + { + name: 'returns true if all headers match', + headers: multipleHeaders, + requestHeaders: { + header1: 'VALUE1', // this value is not the correct case + header2: 'value2', + header3: 'value3', + }, + expected: true, + }, + { + name: 'returns true if request has multiple values for a header name and one value matches ', + headers: ['multiValueHeader:val3'], + requestHeaders: { + multiValueHeader: 'val1,val2,val3,val4,val5', + }, + expected: true, + }, + ]; - tests.forEach((test) => { - it(test.name, () => { - if (test.expectThrow) { - expect(() => hm.headersMatch(test.requestHeaders, test.headers)).to.throw( - 'invalid header match', - ); - } else { - expect(hm.headersMatch(test.requestHeaders, test.headers)).to.equal(test.expected); - } - }); - }); + tests.forEach((test) => { + it(test.name, () => { + if (test.expectThrow) { + expect(() => hm.headersMatch(test.requestHeaders, test.headers)).to.throw( + 'invalid header match', + ); + } else { + expect(hm.headersMatch(test.requestHeaders, test.headers)).to.equal(test.expected); + } + }); + }); }); describe('paramsMatch', () => { - const params = ['Arg1=value1', 'arg2=value2=SOME=other=value', 'arg3===value3&*1(*+']; // case matters for header values + const params = ['Arg1=value1', 'arg2=value2=SOME=other=value', 'arg3===value3&*1(*+']; // case matters for header values - const tests = [ - { - name: 'throws an error a param has no key', - params: ['=nokey'], - expectThrow: true, - }, - { - name: 'throws an error if a param has no value', - params: ['novalue='], - expectThrow: true, - }, - { - name: 'throws an error a param has no equal sign delimiter', - params: ['keyval'], - expectThrow: true, - }, - { - name: 'returns false if one of the params is missing from request', - params: params, - requestParams: { - // Arg1 is missing, - arg2: 'value2=SOME=other=value', - arg3: '==value3&*1(*+', - }, - expected: false, - }, - { - name: 'returns false if one of the params has an empty value', - params: params, - requestParams: { - Arg1: 'value1', - arg2: 'value2=SOME=other=value', - arg3: '', // empty value - }, - expected: false, - }, - { - name: 'returns false if one of the param values does not match', - params: params, - requestParams: { - Arg1: 'Arg1=not-value-1', // this value does not match - arg2: 'value2=SOME=other=value', - arg3: '==value3&*1(*+', - }, - expected: false, - }, - { - name: 'returns false if the case of one param values does not match', - params: params, - requestParams: { - Arg1: 'VALUE1', // this value is not the correct case - arg2: 'value2=SOME=other=value', - arg3: '==value3&*1(*+', - }, - expected: false, - }, - { - name: 'returns false if the case of one param name does not match', - params: params, - requestParams: { - Arg1: 'value1', - arg2: 'value2=SOME=other=value', - ARG3: '==value3&*1(*+', // this param name is not the correct case - }, - expected: false, - }, - { - name: 'returns true if all params match', - params: params, - requestParams: { - Arg1: 'value1', - arg2: 'value2=SOME=other=value', - arg3: '==value3&*1(*+', - }, - expected: true, - }, - { - name: 'returns true if all params match with one param having multiple values', - params: params, - requestParams: { - Arg1: ['value1', 'value2'], // 'value1' wins - arg2: 'value2=SOME=other=value', - arg3: '==value3&*1(*+', - }, - expected: true, - }, - { - name: 'returns false if one param does not match because of multiple values', - params: params, - requestParams: { - Arg1: ['value2', 'value1'], // 'value2' wins but it does not match - arg2: 'value2=SOME=other=value', - arg3: '==value3&*1(*+', - }, - expected: false, - }, - ]; + const tests = [ + { + name: 'throws an error a param has no key', + params: ['=nokey'], + expectThrow: true, + }, + { + name: 'throws an error if a param has no value', + params: ['novalue='], + expectThrow: true, + }, + { + name: 'throws an error a param has no equal sign delimiter', + params: ['keyval'], + expectThrow: true, + }, + { + name: 'returns false if one of the params is missing from request', + params: params, + requestParams: { + // Arg1 is missing, + arg2: 'value2=SOME=other=value', + arg3: '==value3&*1(*+', + }, + expected: false, + }, + { + name: 'returns false if one of the params has an empty value', + params: params, + requestParams: { + Arg1: 'value1', + arg2: 'value2=SOME=other=value', + arg3: '', // empty value + }, + expected: false, + }, + { + name: 'returns false if one of the param values does not match', + params: params, + requestParams: { + Arg1: 'Arg1=not-value-1', // this value does not match + arg2: 'value2=SOME=other=value', + arg3: '==value3&*1(*+', + }, + expected: false, + }, + { + name: 'returns false if the case of one param values does not match', + params: params, + requestParams: { + Arg1: 'VALUE1', // this value is not the correct case + arg2: 'value2=SOME=other=value', + arg3: '==value3&*1(*+', + }, + expected: false, + }, + { + name: 'returns false if the case of one param name does not match', + params: params, + requestParams: { + Arg1: 'value1', + arg2: 'value2=SOME=other=value', + ARG3: '==value3&*1(*+', // this param name is not the correct case + }, + expected: false, + }, + { + name: 'returns true if all params match', + params: params, + requestParams: { + Arg1: 'value1', + arg2: 'value2=SOME=other=value', + arg3: '==value3&*1(*+', + }, + expected: true, + }, + { + name: 'returns true if all params match with one param having multiple values', + params: params, + requestParams: { + Arg1: ['value1', 'value2'], // 'value1' wins + arg2: 'value2=SOME=other=value', + arg3: '==value3&*1(*+', + }, + expected: true, + }, + { + name: 'returns false if one param does not match because of multiple values', + params: params, + requestParams: { + Arg1: ['value2', 'value1'], // 'value2' wins but it does not match + arg2: 'value2=SOME=other=value', + arg3: '==value3&*1(*+', + }, + expected: false, + }, + ]; - tests.forEach((test) => { - it(test.name, () => { - if (test.expectThrow) { - expect(() => hm.paramsMatch(test.requestParams, test.params)).to.throw( - 'invalid query parameter', - ); - } else { - expect(hm.paramsMatch(test.requestParams, test.params)).to.equal(test.expected); - } - }); - }); + tests.forEach((test) => { + it(test.name, () => { + if (test.expectThrow) { + expect(() => hm.paramsMatch(test.requestParams, test.params)).to.throw( + 'invalid query parameter', + ); + } else { + expect(hm.paramsMatch(test.requestParams, test.params)).to.equal(test.expected); + } + }); + }); }); describe('redirect', () => { - const testAnyMatch = { any: true, redirectPath: '/any' }; - const testHeaderMatches = { - headers: ['header1:VALUE1', 'header2:value2', 'header3:value3'], - redirectPath: '/headers', - }; - const testQueryParamMatches = { - params: ['Arg1=value1', 'arg2=value2=SOME=other=value', 'arg3===value3&*1(*+'], - redirectPath: '/params', - }; - const testAllMatchTypes = { - method: 'GET', - headers: ['header1:value1', 'header2:value2'], - params: ['Arg1=value1', 'arg2=value2=SOME=other=value'], - redirectPath: '/a-match', - }; + const testAnyMatch = { any: true, redirectPath: '/any' }; + const testHeaderMatches = { + headers: ['header1:VALUE1', 'header2:value2', 'header3:value3'], + redirectPath: '/headers', + }; + const testQueryParamMatches = { + params: ['Arg1=value1', 'arg2=value2=SOME=other=value', 'arg3===value3&*1(*+'], + redirectPath: '/params', + }; + const testAllMatchTypes = { + method: 'GET', + headers: ['header1:value1', 'header2:value2'], + params: ['Arg1=value1', 'arg2=value2=SOME=other=value'], + redirectPath: '/a-match', + }; - const tests = [ - { - name: 'returns Internal Server Error status code if http_matches variable is not set', - request: createRequest(), - matches: null, - expectedReturn: hm.HTTP_CODES.internalServerError, - }, - { - name: 'returns Internal Server Error status code if http_matches contains malformed match', - request: createRequest(), - matches: [{ headers: ['malformedheader'] }], - expectedReturn: hm.HTTP_CODES.internalServerError, - }, - { - name: 'returns Not Found status code if request does not satisfy any match', - request: createRequest({ method: 'GET' }), - matches: [{ method: 'POST' }], - expectedReturn: hm.HTTP_CODES.notFound, - }, - { - name: 'returns Internal Server Error status code if request satisfies match, but the redirectPath is missing', - request: createRequest({ method: 'GET' }), - matches: [{ method: 'GET' }], - expectedReturn: hm.HTTP_CODES.internalServerError, - }, - { - name: 'redirects to the redirectPath of the first match the request satisfies', - request: createRequest({ - method: 'GET', - headers: { header1: 'value1', header2: 'value2' }, - params: { Arg1: 'value1', arg2: 'value2=SOME=other=value' }, - }), - matches: [testHeaderMatches, testQueryParamMatches, testAllMatchTypes, testAnyMatch], // request matches testAllMatchTypes and testAnyMatch. But first match should win. - expectedRedirect: '/a-match', - }, - ]; + const tests = [ + { + name: 'returns Internal Server Error status code if http_matches variable is not set', + request: createRequest(), + matches: null, + expectedReturn: hm.HTTP_CODES.internalServerError, + }, + { + name: 'returns Internal Server Error status code if http_matches contains malformed match', + request: createRequest(), + matches: [{ headers: ['malformedheader'] }], + expectedReturn: hm.HTTP_CODES.internalServerError, + }, + { + name: 'returns Not Found status code if request does not satisfy any match', + request: createRequest({ method: 'GET' }), + matches: [{ method: 'POST' }], + expectedReturn: hm.HTTP_CODES.notFound, + }, + { + name: 'returns Internal Server Error status code if request satisfies match, but the redirectPath is missing', + request: createRequest({ method: 'GET' }), + matches: [{ method: 'GET' }], + expectedReturn: hm.HTTP_CODES.internalServerError, + }, + { + name: 'redirects to the redirectPath of the first match the request satisfies', + request: createRequest({ + method: 'GET', + headers: { header1: 'value1', header2: 'value2' }, + params: { Arg1: 'value1', arg2: 'value2=SOME=other=value' }, + }), + matches: [testHeaderMatches, testQueryParamMatches, testAllMatchTypes, testAnyMatch], // request matches testAllMatchTypes and testAnyMatch. But first match should win. + expectedRedirect: '/a-match', + }, + ]; - tests.forEach((test) => { - it(test.name, () => { - if (test.matches) { - // set http_matches variable - test.request.variables = { - http_matches: JSON.stringify(test.matches), - }; - } + tests.forEach((test) => { + it(test.name, () => { + if (test.matches) { + // set http_matches variable + test.request.variables = { + http_matches: JSON.stringify(test.matches), + }; + } - hm.redirect(test.request); - if (test.expectedReturn) { - expect(test.request.testReturned).to.equal(test.expectedReturn); - } else if (test.expectedRedirect) { - expect(test.request.testRedirectedTo).to.equal(test.expectedRedirect); - } - }); - }); + hm.redirect(test.request); + if (test.expectedReturn) { + expect(test.request.testReturned).to.equal(test.expectedReturn); + } else if (test.expectedRedirect) { + expect(test.request.testRedirectedTo).to.equal(test.expectedRedirect); + } + }); + }); });