From b64f4bb999af3758d014b2f48009e2b9c9f9cd44 Mon Sep 17 00:00:00 2001 From: Olof Dahlbom Date: Thu, 19 Sep 2024 23:44:46 +0200 Subject: [PATCH] Initial commit --- .gitignore | 1 + package.json | 7 +- src/mocks/mock-request.js | 119 +++++-- src/mocks/mock-resource.js | 28 ++ src/mocks/mock-utils.js | 7 + src/test/__snapshots__/index.spec.js.snap | 401 ++++++++++++++++++++++ src/test/data/random-json-data.json | 37 ++ src/test/enhanced-debugging.ts | 286 +++++++++++++++ src/test/index.d.ts | 9 +- src/test/index.js | 13 +- src/test/index.spec.js | 229 ++++++++++++ yarn.lock | 241 ++++++++++++- 12 files changed, 1348 insertions(+), 30 deletions(-) create mode 100644 src/test/__snapshots__/index.spec.js.snap create mode 100644 src/test/data/random-json-data.json create mode 100644 src/test/enhanced-debugging.ts create mode 100644 src/test/index.spec.js diff --git a/.gitignore b/.gitignore index 5da7d798..a21399a0 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ tsconfig.tsbuildinfo tmp/ dist/ .vscode/ +.yarn/cache diff --git a/package.json b/package.json index 96faabc6..bbdd5fa5 100644 --- a/package.json +++ b/package.json @@ -217,6 +217,7 @@ "@babel/register": "^7.24.6", "@changesets/cli": "^2.27.8", "@chiragrupani/karma-chromium-edge-launcher": "^2.4.1", + "@types/diff": "^5.2.2", "@types/jest": "^29.5.12", "@types/karma": "^6.3.8", "@types/node": "^22.5.4", @@ -267,5 +268,9 @@ "webpack": "^5.94.0", "whatwg-fetch": "^3.6.20" }, - "packageManager": "yarn@4.4.1" + "packageManager": "yarn@4.4.1", + "dependencies": { + "diff": "^7.0.0", + "tty-table": "^4.2.3" + } } diff --git a/src/mocks/mock-request.js b/src/mocks/mock-request.js index e74470d4..373af0af 100644 --- a/src/mocks/mock-request.js +++ b/src/mocks/mock-request.js @@ -2,7 +2,8 @@ import MockAssert from './mock-assert' import Response from '../response' import { isPlainObject } from '../utils/index' import { clone } from '../utils/clone' -import { sortedUrl, toSortedQueryString, isSubset } from './mock-utils' +import { sortedUrl, toSortedQueryString, filterKeys, diffString } from './mock-utils' +import { mock } from 'node:test' /** * @param {number} id @@ -10,14 +11,21 @@ import { sortedUrl, toSortedQueryString, isSubset } from './mock-utils' * @param {string} props.method * @param {string|function} props.url * @param {string|function} props.body - request body + * @param {string} props.mockName * @param {object} props.response * @param {string} props.response.body * @param {object} props.response.headers * @param {integer} props.response.status */ + +const MATCHED_AS_UNDEFINED_IN_MOCK = 'MATCHED_AS_UNDEFINED_IN_MOCK' +const MATCHED_BY_FUNCTION = 'MATCHED_BY_FUNCTION' +const MISMATCH_BY_FUNCTION = 'MISMATCHED_BY_FUNCTION' + function MockRequest(id, props) { this.id = id + this.mockName = props.mockName ? props.mockName : this.id this.method = props.method || 'get' this.urlFunction = typeof props.url === 'function' this.url = props.url @@ -80,33 +88,100 @@ MockRequest.prototype = { return new MockAssert(this.calls) }, - /** - * Checks if the request matches with the mock HTTP method, URL, headers and body - * - * @return {boolean} - */ - isExactMatch(request) { - const bodyMatch = () => { - if (this.body === undefined) { - return true + bodyMatchRequest(request) { + if (this.body === undefined) { + return { + match: true, + mockValue: MATCHED_AS_UNDEFINED_IN_MOCK, + value: MATCHED_AS_UNDEFINED_IN_MOCK, } + } + if (this.bodyFunction) { + const match = this.body(request.body()) + const value = match ? MATCHED_BY_FUNCTION : MISMATCH_BY_FUNCTION + return { match, mockValue: value, requestValue: value } + } + const requestBodyAsString = toSortedQueryString(request.body()) + const match = this.body === requestBodyAsString + return { + match, + mockValue: decodeURIComponent(this.body), + requestValue: decodeURIComponent(requestBodyAsString), + } + }, - return this.bodyFunction - ? this.body(request.body()) - : this.body === toSortedQueryString(request.body()) + urlMatchRequest(request) { + if (this.urlFunction) { + const match = Boolean(this.url(request.url(), request.params())) + const value = match ? MATCHED_BY_FUNCTION : MISMATCH_BY_FUNCTION + return { match, mockValue: value, requestValue: value } + } + const requestUrlAsSortedString = sortedUrl(request.url()) + const mockRequestUrlAsSortedString = sortedUrl(this.url) + const match = mockRequestUrlAsSortedString === requestUrlAsSortedString + return { + match, + mockValue: decodeURIComponent(mockRequestUrlAsSortedString), + requestValue: decodeURIComponent(requestUrlAsSortedString), } + }, - const urlMatch = this.urlFunction - ? this.url(request.url(), request.params()) - : sortedUrl(this.url) === sortedUrl(request.url()) + headersMatchRequest(request) { + if (!this.headers) + return { + match: true, + mockValue: MATCHED_AS_UNDEFINED_IN_MOCK, + value: MATCHED_AS_UNDEFINED_IN_MOCK, + } + if (this.headersFunction) { + const match = this.headers(request.headers()) + const value = match ? MATCHED_BY_FUNCTION : MISMATCH_BY_FUNCTION + return { match, mockValue: value, requestValue: value } + } + const filteredRequestHeaders = filterKeys(this.headersObject, request.headers()) + const requestHeadersAsSortedString = toSortedQueryString(filteredRequestHeaders) + const mockRequestHeadersAsSortedString = toSortedQueryString(this.headersObject) + const match = requestHeadersAsSortedString === mockRequestHeadersAsSortedString + + return { + match, + mockValue: mockRequestHeadersAsSortedString, + requestValue: requestHeadersAsSortedString, + } + }, - const headerMatch = - !this.headers || - (this.headersFunction - ? this.headers(request.headers()) - : isSubset(this.headersObject, request.headers())) + methodMatchRequest(request) { + const requestMethod = request.method() + const match = this.method === requestMethod + return { + match, + mockValue: this.method, + requestValue: requestMethod, + } + }, - return this.method === request.method() && urlMatch && bodyMatch() && headerMatch + getRequestMatching(request) { + const method = this.methodMatchRequest(request) + const url = this.urlMatchRequest(request) + const body = this.bodyMatchRequest(request) + const headers = this.headersMatchRequest(request) + return { + mockName: this.mockName, + isExactMatch: method.match && url.match && body.match && headers.match, + isPartialMatch: this.isPartialMatch(request), + method, + url, + body, + headers, + } + }, + /** + * Checks if the request matches with the mock HTTP method, URL, headers and body + * + * @return {boolean} + */ + isExactMatch(request) { + return this.getRequestMatching(request).isExactMatch }, /** diff --git a/src/mocks/mock-resource.js b/src/mocks/mock-resource.js index e7a2421a..ad423030 100644 --- a/src/mocks/mock-resource.js +++ b/src/mocks/mock-resource.js @@ -16,6 +16,7 @@ function MockResource(id, client) { this.id = id this.manifest = client._manifest this.resourceName = null + this.mockName = null this.methodName = null this.requestParams = {} this.responseData = null @@ -37,6 +38,32 @@ MockResource.prototype = { return this }, + /** + * Names you mock instance for debugging purposes. + * @return {MockResource} + */ + named(mockName) { + this.mockName = mockName + return this + }, + + /** + * Creates a name for the mock based on the client id and the resource name. + * @returns {String} + */ + getName() { + const { mockName, manifest, resourceName, id } = this + const { clientId } = manifest || {} + if (mockName) return mockName + + const resourcePart = resourceName || id + if (clientId) { + return `${clientId} - ${resourcePart}` + } + + return resourceName ? `${resourceName} - ${id}` : id + }, + /** * @return {MockResource} */ @@ -115,6 +142,7 @@ MockResource.prototype = { if (!this.mockRequest) { this.mockRequest = new MockRequest(this.id, { + mockName: this.getName(), method: finalRequest.method(), url: this.generateUrlMatcher(finalRequest), body: finalRequest.body(), diff --git a/src/mocks/mock-utils.js b/src/mocks/mock-utils.js index 63d2e1b4..f8c6d4b5 100644 --- a/src/mocks/mock-utils.js +++ b/src/mocks/mock-utils.js @@ -51,6 +51,13 @@ export function isSubset(A, B) { return toSortedQueryString(A) === toSortedQueryString(filteredB) } +export function filterKeys(A, B) { + // Make B only contain the non-nullish keys it has in in common with A + const keysFromA = validKeys(A) + const filteredB = filterByPredicate(B, (keyFromB) => keysFromA.includes(keyFromB)) + return filteredB +} + /** * Sort the query params on a URL based on the 'key=value' string value. * E.g. /example?b=2&a=1 will become /example?a=1&b=2 diff --git a/src/test/__snapshots__/index.spec.js.snap b/src/test/__snapshots__/index.spec.js.snap new file mode 100644 index 00000000..7c5d3469 --- /dev/null +++ b/src/test/__snapshots__/index.spec.js.snap @@ -0,0 +1,401 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`mappersmith/test/lookupResponse should correctly handle m.anything() displaying that it matches ANYTHING 1`] = ` +" + + +------------------------------------------------------------------------------------------------------------------ +Mappersmith matches a mock to an outgoing request by comparing the request's URL, METHOD, BODY and HEADERS. + +URL: The URL is sorted and normalized before comparison. +BODY: The BODY is sorted and normalized in the form of a query-string before comparison. +HEADERS: The headers of the outgoing request is stripped of any headers that are not present in the + mock definition headers and then sorted and normalized before comparison. +METHOD: The method of the outgoing request is compared as is. +------------------------------------------------------------------------------------------------------------------ + +Request: + + ┌────────────────────┬──────────────────────────────────────────────────────────────────────┐ + │ Outbound Request │ Value │ + ├────────────────────┼──────────────────────────────────────────────────────────────────────┤ + │ Url │ http://example.org/path │ + ├────────────────────┼──────────────────────────────────────────────────────────────────────┤ + │ Method │ get │ + ├────────────────────┼──────────────────────────────────────────────────────────────────────┤ + │ Body │ id=2 │ + ├────────────────────┼──────────────────────────────────────────────────────────────────────┤ + │ Headers │ x-test=VALUE2 │ + └────────────────────┴──────────────────────────────────────────────────────────────────────┘ + +Mock definitions installed: + + + ┌───────────────────────────────────────────────┬───────────────────────────────────────────────┐ + │ Colors Explanation │ Diff Explanation │ + ├───────────────────────────────────────────────┼───────────────────────────────────────────────┤ + │ │ │ + │ ● Exact match │ Mock value matches the request value │ + │ ● Partial match (When URL and METHOD match) │ Present in mock but not in request │ + │ ● Not matching │ Present in request but not in mock │ + │ │ │ + └───────────────────────────────────────────────┴───────────────────────────────────────────────┘ + + + ┌────────────────┬────────────┬─────────────────────────────────────────────────────────────────┐ + │ users - 4 │ Match │ Value/Diff │ + ├────────────────┼────────────┼─────────────────────────────────────────────────────────────────┤ + │ Url │ Yes │ http://example.org/path │ + ├────────────────┼────────────┼─────────────────────────────────────────────────────────────────┤ + │ Method │ Yes │ get │ + ├────────────────┼────────────┼─────────────────────────────────────────────────────────────────┤ + │ Body │ Yes │ MATCHED_BY_FUNCTION │ + ├────────────────┼────────────┼─────────────────────────────────────────────────────────────────┤ + │ Headers │ No │ x-test=VALUE-2 │ + └────────────────┴────────────┴─────────────────────────────────────────────────────────────────┘ + + +[Mappersmith Test] No exact match found but a partial match was found, check your mock definitions, debug information available above. +" +`; + +exports[`mappersmith/test/lookupResponse should handle multiple mocks with large bodies and long URLs, including a partial match 1`] = ` +" + + +------------------------------------------------------------------------------------------------------------------ +Mappersmith matches a mock to an outgoing request by comparing the request's URL, METHOD, BODY and HEADERS. + +URL: The URL is sorted and normalized before comparison. +BODY: The BODY is sorted and normalized in the form of a query-string before comparison. +HEADERS: The headers of the outgoing request is stripped of any headers that are not present in the + mock definition headers and then sorted and normalized before comparison. +METHOD: The method of the outgoing request is compared as is. +------------------------------------------------------------------------------------------------------------------ + +Request: + + ┌────────────────────┬──────────────────────────────────────────────────────────────────────┐ + │ Outbound Request │ Value │ + ├────────────────────┼──────────────────────────────────────────────────────────────────────┤ + │ Url │ http://example.org/path?query[id]=2 │ + ├────────────────────┼──────────────────────────────────────────────────────────────────────┤ + │ Method │ get │ + ├────────────────────┼──────────────────────────────────────────────────────────────────────┤ + │ Body │ _id=66ec092d7189db5c15ea3f04&about=Cupidatat+excepteur+do+excepteur+ │ + │ │ quis+deserunt+nisi+aliqua+nulla+ut+esse.+Non+ea+tempor+occaecat+sunt │ + │ │ +commodo.+Nisi+cillum+non+anim+aute+laborum+pariatur+irure+occaecat+ │ + │ │ minim. │ + │ │ &address=377+Conduit+Boulevard,+Franklin,+Alabama,+7940&age=31&balan │ + │ │ ce=$1,349.22&company=WATERBABY&email=deborahcantu@waterbaby.com&eyeC │ + │ │ olor=green&favoriteFruit=banana&friends[][id]=0&friends[][name]=Marq │ + │ │ uez+Langley&friends[][id]=1&friends[][name]=Blackburn+Oconnor&friend │ + │ │ s[][id]=2&friends[][name]=Tameka+Mcdonald&gender=female&greeting=Hel │ + │ │ lo,+Deborah+Cantu!+You+have+1+unread+messages.&guid=3f6091f3-a629-46 │ + │ │ 53-913f-eb797bede6e9&index=6&isActive=true&latitude=26.758942&longit │ + │ │ ude=29.872733&name=Deborah+Cantu&phone=+1+(951)+590-3102&picture=htt │ + │ │ p://placehold.it/32x32®istered=2014-10-31T02:57:12+-01:00&tags[]= │ + │ │ pariatur&tags[]=Lorem&tags[]=tempor&tags[]=ut&tags[]=veniam&tags[]=t │ + │ │ empor&tags[]=amet │ + ├────────────────────┼──────────────────────────────────────────────────────────────────────┤ + │ Headers │ x-test=DIFFERENT-VALUE │ + └────────────────────┴──────────────────────────────────────────────────────────────────────┘ + +Mock definitions installed: + + + ┌───────────────────────────────────────────────┬───────────────────────────────────────────────┐ + │ Colors Explanation │ Diff Explanation │ + ├───────────────────────────────────────────────┼───────────────────────────────────────────────┤ + │ │ │ + │ ● Exact match │ Mock value matches the request value │ + │ ● Partial match (When URL and METHOD match) │ Present in mock but not in request │ + │ ● Not matching │ Present in request but not in mock │ + │ │ │ + └───────────────────────────────────────────────┴───────────────────────────────────────────────┘ + + + ┌────────────────┬────────────┬─────────────────────────────────────────────────────────────────┐ + │ My Client - │ Match │ Value/Diff │ + │ posts │ │ │ + ├────────────────┼────────────┼─────────────────────────────────────────────────────────────────┤ + │ Url │ No │ http://example.org/long/path/to/resource/with/many/segments?que │ + │ │ │ ry[ipostId]=12345 │ + ├────────────────┼────────────┼─────────────────────────────────────────────────────────────────┤ + │ Method │ No │ gepost │ + ├────────────────┼────────────┼─────────────────────────────────────────────────────────────────┤ + │ Body │ No │ _id=66ec092d7189db5c15ea3f04&abount=Cupidatat+excepteur+do+exce │ + │ │ │ pteur+quis+deserunt+n=Thisi+aliqua+nulla+ut+esse.+Non+ea+tvempo │ + │ │ │ ry+occaecat+sunt+commodo.+Nisi+cillum+non+anim+autrge+laborum+p │ + │ │ │ ariatur+irure+occaecat+minim. │ + │ │ │ &address=377y+Conduwith+Boulevard,+Franklin,+Alabama,+7940&age= │ + │ │ │ 31&balance=$1,349.22&company=WATERBABY&email=deborahcantu@water │ + │ │ │ baby.com&eyeColor=green&favoriteFruit=banana&friends[][id]=0&fr │ + │ │ │ iends[][name]=Marquez+Langley&friends[][id]=1&friends[][name]=B │ + │ │ │ lackburn+Oconnor&friends[][id]=2&friends[][name]=Tameka+Mcdonal │ + │ │ │ d&gender=female&greeting=Hello,+Deborah+Cantu!+You+have+1+unrea │ + │ │ │ d+messages.&guid=3f6091f3-a629-4653-913f-eb797bede6e9&index=6&i │ + │ │ │ sActive=true&latitude=26.758942&longitude=29.872733&name=Debora │ + │ │ │ h+Cantu&phone=+1+(951)+590-3102&picture=http://placehold.it/32x │ + │ │ │ 32®istered=2014-10-31T02:57:12+-01:00&tags[]=pariatur&tags[] │ + │ │ │ =Lorem&tags[]=tempor&tags[]=ut&tags[]=veniam&tags[]=tempor&tags │ + │ │ │ []=amet │ + ├────────────────┼────────────┼─────────────────────────────────────────────────────────────────┤ + │ Headers │ No │ content-type=application%2Fjson │ + └────────────────┴────────────┴─────────────────────────────────────────────────────────────────┘ + + ┌────────────────┬────────────┬─────────────────────────────────────────────────────────────────┐ + │ My Client - │ Match │ Value/Diff │ + │ comments │ │ │ + ├────────────────┼────────────┼─────────────────────────────────────────────────────────────────┤ + │ Url │ No │ http://example.org/another/very/long/path/to/resource?query[ico │ + │ │ │ mmentId]=267890 │ + ├────────────────┼────────────┼─────────────────────────────────────────────────────────────────┤ + │ Method │ No │ geput │ + ├────────────────┼────────────┼─────────────────────────────────────────────────────────────────┤ + │ Body │ No │ _id=66ec092d7189db5c15ea3f045&about=Cupidatat+excepteur+do+exce │ + │ │ │ pteur+quis+deserunt+nisi+aliqua+nulla+ut+esse.+Non+ea+tempor+oc │ + │ │ │ caecat+sunt+commodo.+Nisi+cillum+non+anim+aute+laborum+pariatur │ + │ │ │ +irure+occaecat+minim. │ + │ │ │ &address=377+Conduit+Boulevard,+Franklin,+Alabama,+7940&age=31& │ + │ │ │ balance=$1,349.22&company=WATERBABY&email=deborahcantu@waterbab │ + │ │ │ y.com&eyeColor=green&favoriteFruit=banana&friends[][id]=0&frien │ + │ │ │ ds[][name]=Marquez+Langley&friends[][id]=1&friends[][name]=Blac │ + │ │ │ kburn+Oconnor&friends[][id]=2&friends[][name]=Tameka+Mcdonald&g │ + │ │ │ ender=female&greeting=Hello,+Deborah+Cantu!+You+have+1+unread+m │ + │ │ │ essages.&guid=3f6091f3-a629-4653-913f-eb797bede6e9&index=6&isAc │ + │ │ │ tive=true&latitude=26.758942&longitude=29.872733&name=Deborah+C │ + │ │ │ antu&phone=+1+(951)+590-3102&picture=http://placehold.it/32x32& │ + │ │ │ registered=2014-10-31T02:57:12+-01:00&tags[]=pariatur&tags[]=Lo │ + │ │ │ rem&tags[]=tempor&tags[]=ut&tags[]=veniam&tags[]=tempor&tags[]= │ + │ │ │ amet │ + ├────────────────┼────────────┼─────────────────────────────────────────────────────────────────┤ + │ Headers │ No │ content-type=application%2Fjson │ + └────────────────┴────────────┴─────────────────────────────────────────────────────────────────┘ + + ┌────────────────┬────────────┬─────────────────────────────────────────────────────────────────┐ + │ My Client - │ Match │ Value/Diff │ + │ users │ │ │ + ├────────────────┼────────────┼─────────────────────────────────────────────────────────────────┤ + │ Url │ No │ http://example.org/path?query[id]=21 │ + ├────────────────┼────────────┼─────────────────────────────────────────────────────────────────┤ + │ Method │ Yes │ get │ + ├────────────────┼────────────┼─────────────────────────────────────────────────────────────────┤ + │ Body │ No │ _id=66ec092d7189db5c15ea3f04&about=Cupidatat+excepteur+do+excep │ + │ │ │ teur+quis+deserunt+nisi+aliqua+nulla+ut+esse.+Non+ea+tempor+occ │ + │ │ │ aecat+sunt+commodo.+Nisi+cillum+non+anim+aute+laborum+pariatur+ │ + │ │ │ irure+occaecat+minim. │ + │ │ │ &address=377+Conduit+Boulevard,+Franklin,+Alabama,+7940&age=31& │ + │ │ │ balance=$1,349.22&company=WATERBABY&email=deborahcantu@waterbab │ + │ │ │ y.com&eyeColor=green&favoriteFruit=banana&friends[][id]=0&frien │ + │ │ │ ds[][name]=Marquez+Langley&friends[][id]=1&friends[][name]=Blac │ + │ │ │ kburn+Oconnor&friends[][id]=2&friends[][name]=Tameka+Mcdonald&g │ + │ │ │ ender=female&greeting=Hello,+Deborah+Cantu!+You+have+1+unread+m │ + │ │ │ essages.&guid=3f6091f3-a629-4653-913f-eb797bede6e9&index=6&isAc │ + │ │ │ tive=true&latitude=26.758942&longitude=29.872733&name=Deborah+C │ + │ │ │ antu&phone=+1+(951)+590-3102&picture=http://placehold.it/32x32& │ + │ │ │ registered=2014-10-31T02:57:12+-01:00&tags[]=pariatur&tags[]=Lo │ + │ │ │ rem&tags[]=tempor&tags[]=ut&tags[]=veniam&tags[]=tempor&tags[]= │ + │ │ │ amet │ + ├────────────────┼────────────┼─────────────────────────────────────────────────────────────────┤ + │ Headers │ No │ x-test=DIFFERENT-VALUE │ + └────────────────┴────────────┴─────────────────────────────────────────────────────────────────┘ + + ┌────────────────┬────────────┬─────────────────────────────────────────────────────────────────┐ + │ My Named Mock │ Match │ Value/Diff │ + ├────────────────┼────────────┼─────────────────────────────────────────────────────────────────┤ + │ Url │ No │ http://example.org/path?query[id]=2 │ + ├────────────────┼────────────┼─────────────────────────────────────────────────────────────────┤ + │ Method │ Yes │ get │ + ├────────────────┼────────────┼─────────────────────────────────────────────────────────────────┤ + │ Body │ Yes │ _id=66ec092d7189db5c15ea3f04&about=Cupidatat+excepteur+do+excep │ + │ │ │ teur+quis+deserunt+nisi+aliqua+nulla+ut+esse.+Non+ea+tempor+occ │ + │ │ │ aecat+sunt+commodo.+Nisi+cillum+non+anim+aute+laborum+pariatur+ │ + │ │ │ irure+occaecat+minim. │ + │ │ │ &address=377+Conduit+Boulevard,+Franklin,+Alabama,+7940&age=31& │ + │ │ │ balance=$1,349.22&company=WATERBABY&email=deborahcantu@waterbab │ + │ │ │ y.com&eyeColor=green&favoriteFruit=banana&friends[][id]=0&frien │ + │ │ │ ds[][name]=Marquez+Langley&friends[][id]=1&friends[][name]=Blac │ + │ │ │ kburn+Oconnor&friends[][id]=2&friends[][name]=Tameka+Mcdonald&g │ + │ │ │ ender=female&greeting=Hello,+Deborah+Cantu!+You+have+1+unread+m │ + │ │ │ essages.&guid=3f6091f3-a629-4653-913f-eb797bede6e9&index=6&isAc │ + │ │ │ tive=true&latitude=26.758942&longitude=29.872733&name=Deborah+C │ + │ │ │ antu&phone=+1+(951)+590-3102&picture=http://placehold.it/32x32& │ + │ │ │ registered=2014-10-31T02:57:12+-01:00&tags[]=pariatur&tags[]=Lo │ + │ │ │ rem&tags[]=tempor&tags[]=ut&tags[]=veniam&tags[]=tempor&tags[]= │ + │ │ │ amet │ + ├────────────────┼────────────┼─────────────────────────────────────────────────────────────────┤ + │ Headers │ Yes │ x-test=DIFFERENT-VALUE │ + └────────────────┴────────────┴─────────────────────────────────────────────────────────────────┘ + + +[Mappersmith Test] No exact match found but a partial match was found, check your mock definitions, debug information available above. +" +`; + +exports[`mappersmith/test/lookupResponse should log all existing mocks when there is a partial match 1`] = ` +" + + +------------------------------------------------------------------------------------------------------------------ +Mappersmith matches a mock to an outgoing request by comparing the request's URL, METHOD, BODY and HEADERS. + +URL: The URL is sorted and normalized before comparison. +BODY: The BODY is sorted and normalized in the form of a query-string before comparison. +HEADERS: The headers of the outgoing request is stripped of any headers that are not present in the + mock definition headers and then sorted and normalized before comparison. +METHOD: The method of the outgoing request is compared as is. +------------------------------------------------------------------------------------------------------------------ + +Request: + + ┌────────────────────┬──────────────────────────────────────────────────────────────────────┐ + │ Outbound Request │ Value │ + ├────────────────────┼──────────────────────────────────────────────────────────────────────┤ + │ Url │ http://example.org/path │ + ├────────────────────┼──────────────────────────────────────────────────────────────────────┤ + │ Method │ get │ + ├────────────────────┼──────────────────────────────────────────────────────────────────────┤ + │ Body │ id=2 │ + ├────────────────────┼──────────────────────────────────────────────────────────────────────┤ + │ Headers │ x-test=VALUE2 │ + └────────────────────┴──────────────────────────────────────────────────────────────────────┘ + +Mock definitions installed: + + + ┌───────────────────────────────────────────────┬───────────────────────────────────────────────┐ + │ Colors Explanation │ Diff Explanation │ + ├───────────────────────────────────────────────┼───────────────────────────────────────────────┤ + │ │ │ + │ ● Exact match │ Mock value matches the request value │ + │ ● Partial match (When URL and METHOD match) │ Present in mock but not in request │ + │ ● Not matching │ Present in request but not in mock │ + │ │ │ + └───────────────────────────────────────────────┴───────────────────────────────────────────────┘ + + + ┌────────────────┬────────────┬─────────────────────────────────────────────────────────────────┐ + │ users - 3 │ Match │ Value/Diff │ + ├────────────────┼────────────┼─────────────────────────────────────────────────────────────────┤ + │ Url │ Yes │ http://example.org/path │ + ├────────────────┼────────────┼─────────────────────────────────────────────────────────────────┤ + │ Method │ Yes │ get │ + ├────────────────┼────────────┼─────────────────────────────────────────────────────────────────┤ + │ Body │ No │ id=21 │ + ├────────────────┼────────────┼─────────────────────────────────────────────────────────────────┤ + │ Headers │ No │ x-test=VALUE2 │ + └────────────────┴────────────┴─────────────────────────────────────────────────────────────────┘ + + +[Mappersmith Test] No exact match found but a partial match was found, check your mock definitions, debug information available above. +" +`; + +exports[`mappersmith/test/lookupResponse should log all existing mocks when there is no match 1`] = ` +" + + +------------------------------------------------------------------------------------------------------------------ +Mappersmith matches a mock to an outgoing request by comparing the request's URL, METHOD, BODY and HEADERS. + +URL: The URL is sorted and normalized before comparison. +BODY: The BODY is sorted and normalized in the form of a query-string before comparison. +HEADERS: The headers of the outgoing request is stripped of any headers that are not present in the + mock definition headers and then sorted and normalized before comparison. +METHOD: The method of the outgoing request is compared as is. +------------------------------------------------------------------------------------------------------------------ + +Request: + + ┌────────────────────┬──────────────────────────────────────────────────────────────────────┐ + │ Outbound Request │ Value │ + ├────────────────────┼──────────────────────────────────────────────────────────────────────┤ + │ Url │ http://example.org/path?query[parameter]=1&query[parameter2]=2 │ + ├────────────────────┼──────────────────────────────────────────────────────────────────────┤ + │ Method │ get │ + ├────────────────────┼──────────────────────────────────────────────────────────────────────┤ + │ Body │ maybe_gql=query+{+user+{+id+}+}&id=1¶meter=1 │ + ├────────────────────┼──────────────────────────────────────────────────────────────────────┤ + │ Headers │ header1=value&header2=value2 │ + └────────────────────┴──────────────────────────────────────────────────────────────────────┘ + +Mock definitions installed: + + + ┌───────────────────────────────────────────────┬───────────────────────────────────────────────┐ + │ Colors Explanation │ Diff Explanation │ + ├───────────────────────────────────────────────┼───────────────────────────────────────────────┤ + │ │ │ + │ ● Exact match │ Mock value matches the request value │ + │ ● Partial match (When URL and METHOD match) │ Present in mock but not in request │ + │ ● Not matching │ Present in request but not in mock │ + │ │ │ + └───────────────────────────────────────────────┴───────────────────────────────────────────────┘ + + + ┌────────────────┬────────────┬─────────────────────────────────────────────────────────────────┐ + │ users - 2 │ Match │ Value/Diff │ + ├────────────────┼────────────┼─────────────────────────────────────────────────────────────────┤ + │ Url │ No │ http://example-1.org/no-path?query[parameter]=1&query[parameter │ + │ │ │ 2]=2 │ + ├────────────────┼────────────┼─────────────────────────────────────────────────────────────────┤ + │ Method │ No │ gepost │ + ├────────────────┼────────────┼─────────────────────────────────────────────────────────────────┤ + │ Body │ Yes │ maybe_gql=query+{+user+{+id+}+}&id=1¶meter=1 │ + ├────────────────┼────────────┼─────────────────────────────────────────────────────────────────┤ + │ Headers │ Yes │ header1=value&header2=value2 │ + └────────────────┴────────────┴─────────────────────────────────────────────────────────────────┘ + + +[Mappersmith Test] No match found, check your mock definitions, debug information available above. +" +`; + +exports[`mappersmith/test/lookupResponse should output a clear message when there are no mocks installed 1`] = ` +" + + +------------------------------------------------------------------------------------------------------------------ +Mappersmith matches a mock to an outgoing request by comparing the request's URL, METHOD, BODY and HEADERS. + +URL: The URL is sorted and normalized before comparison. +BODY: The BODY is sorted and normalized in the form of a query-string before comparison. +HEADERS: The headers of the outgoing request is stripped of any headers that are not present in the + mock definition headers and then sorted and normalized before comparison. +METHOD: The method of the outgoing request is compared as is. +------------------------------------------------------------------------------------------------------------------ + +Request: + + ┌────────────────────┬──────────────────────────────────────────────────────────────────────┐ + │ Outbound Request │ Value │ + ├────────────────────┼──────────────────────────────────────────────────────────────────────┤ + │ Url │ http://example.org/path?query[parameter]=1&query[parameter2]=2 │ + ├────────────────────┼──────────────────────────────────────────────────────────────────────┤ + │ Method │ get │ + ├────────────────────┼──────────────────────────────────────────────────────────────────────┤ + │ Body │ maybe_gql=query+{+user+{+id+}+}&id=1¶meter=1 │ + ├────────────────────┼──────────────────────────────────────────────────────────────────────┤ + │ Headers │ header1=value&header2=value2 │ + └────────────────────┴──────────────────────────────────────────────────────────────────────┘ + +Mock definitions installed: + + + ┌───────────────────────────────────────────────┬───────────────────────────────────────────────┐ + │ Colors Explanation │ Diff Explanation │ + ├───────────────────────────────────────────────┼───────────────────────────────────────────────┤ + │ │ │ + │ ● Exact match │ Mock value matches the request value │ + │ ● Partial match (When URL and METHOD match) │ Present in mock but not in request │ + │ ● Not matching │ Present in request but not in mock │ + │ │ │ + └───────────────────────────────────────────────┴───────────────────────────────────────────────┘ + +NO MOCKS INSTALLED + + +[Mappersmith Test] There are no mocks installed, please refer to the documentation: https://github.com/tulios/mappersmith?tab=readme-ov-file#-testing-mappersmith +" +`; diff --git a/src/test/data/random-json-data.json b/src/test/data/random-json-data.json new file mode 100644 index 00000000..5080b569 --- /dev/null +++ b/src/test/data/random-json-data.json @@ -0,0 +1,37 @@ +{ + "_id": "66ec092d7189db5c15ea3f04", + "index": 6, + "guid": "3f6091f3-a629-4653-913f-eb797bede6e9", + "isActive": true, + "balance": "$1,349.22", + "picture": "http://placehold.it/32x32", + "age": 31, + "eyeColor": "green", + "name": "Deborah Cantu", + "gender": "female", + "company": "WATERBABY", + "email": "deborahcantu@waterbaby.com", + "phone": "+1 (951) 590-3102", + "address": "377 Conduit Boulevard, Franklin, Alabama, 7940", + "about": "Cupidatat excepteur do excepteur quis deserunt nisi aliqua nulla ut esse. Non ea tempor occaecat sunt commodo. Nisi cillum non anim aute laborum pariatur irure occaecat minim.\r\n", + "registered": "2014-10-31T02:57:12 -01:00", + "latitude": 26.758942, + "longitude": 29.872733, + "tags": ["pariatur", "Lorem", "tempor", "ut", "veniam", "tempor", "amet"], + "friends": [ + { + "id": 0, + "name": "Marquez Langley" + }, + { + "id": 1, + "name": "Blackburn Oconnor" + }, + { + "id": 2, + "name": "Tameka Mcdonald" + } + ], + "greeting": "Hello, Deborah Cantu! You have 1 unread messages.", + "favoriteFruit": "banana" +} diff --git a/src/test/enhanced-debugging.ts b/src/test/enhanced-debugging.ts new file mode 100644 index 00000000..e1f791fb --- /dev/null +++ b/src/test/enhanced-debugging.ts @@ -0,0 +1,286 @@ +import { sortedUrl, toSortedQueryString } from '../mocks/mock-utils' +import { Request } from '../request' +import colors from '@colors/colors' +import MockResource from '../mocks/mock-resource' +import MockRequest from '../mocks/mock-request' +import ttyTable from 'tty-table' +import * as Diff from 'diff' + +/** + * + * @param {string} stringOne + * @param {string} stringTwo + * @returns {string[]} + */ +export const diffString = (stringOne: string, stringTwo: string) => { + const diff = Diff.diffChars(stringOne, stringTwo) + return diff + .map(function (part) { + return part.added ? part.value.bgGreen : part.removed ? part.value.bgRed : part.value.green + }) + .join('') +} + +type MockMatchResult = ReturnType +type ColumnValue = + | MockMatchResult['body'] + | MockMatchResult['headers'] + | MockMatchResult['method'] + | MockMatchResult['url'] + +const getMatchScore = (match: MockMatchResult): number => { + return ( + (match.isExactMatch ? 10 : match.isPartialMatch ? 5 : 0) + + (match.url.match ? 4 : 0) + + (match.method.match ? 3 : 0) + + (match.body.match ? 2 : 0) + + (match.headers.match ? 1 : 0) + ) +} +/** + * Sorts the matches based on how well they match. + * The most matching match will be the last one in the array. + * @param {MockMatchResult[]} matches - Array of mock match results. + * @return {MockMatchResult[]} The sorted array of mock match results. + */ +const sortMatches = (matches: MockMatchResult[]): MockMatchResult[] => { + return matches.sort((a, b) => getMatchScore(a) - getMatchScore(b)) +} + +/** + * Creates a tty-table for the given mock matches. + * @param {MockMatchResult[]} matches - Array of mock match results. + * @return {string} The rendered table as a string. + */ +const createTableForMocks = (matches: MockMatchResult[]) => { + const tables = sortMatches(matches).map((match) => { + const header = [ + { + value: bold(`${match.mockName}`), + width: 20, + color: 'white', + headerColor: 'white', + align: 'left', + }, + { value: bold('Match'), width: 15, color: 'white', headerColor: 'white', align: 'left' }, + { value: bold('Value/Diff'), width: 80, color: 'white', headerColor: 'white', align: 'left' }, + ] + + const getColumnValuesFromMatch = ({ match, mockValue, requestValue }: ColumnValue) => { + console.log(mockValue, requestValue) + const value = mockValue !== requestValue ? diffString(requestValue, mockValue) : mockValue + return [ + match ? colors.green('Yes') : colors.red('No'), + match ? colors.green(value) : colors.red(value), + ] + } + + const rows = [ + [bold('Url'), ...getColumnValuesFromMatch(match.url)], + [bold('Method'), ...getColumnValuesFromMatch(match.method)], + [bold('Body'), ...getColumnValuesFromMatch(match.body)], + [bold('Headers'), ...getColumnValuesFromMatch(match.headers)], + ] + + const table = ttyTable(header, rows, { + borderStyle: 'solid', + borderColor: match.isExactMatch ? 'green' : match.isPartialMatch ? 'yellow' : 'red', + color: 'white', + }) + + return table.render() + }) + + return tables.join('\n') +} + +/** + * Makes the given text bold. + * @param {string} text - The text to be made bold. + * @return {string} The bolded text. + */ +const bold = (text: string) => `\x1b[1m${text}\x1b[22m` + +/** + * Creates a tty-table for the given request. + * @param {Request} request - The request object. + * @return {string} The rendered table as a string. + */ +const createTableForRequest = (request: Request) => { + const header = [ + { value: 'Outbound Request', width: 20, color: 'white', align: 'left' }, + { value: 'Value', width: 70, color: 'white', align: 'left' }, + ] + + const rows = [ + [bold('Url'), decodeURIComponent(sortedUrl(request.url()))], + ['Method', request.method()], + ['Body', decodeURIComponent(toSortedQueryString(request.body()))], + ['Headers', decodeURIComponent(toSortedQueryString(request.headers()))], + ] + + const table = ttyTable(header, rows, { + borderStyle: 'solid', + borderColor: 'blue', + color: 'white', + }) + + return table.render() +} + +/** + * Creates a horizontal table with one row and two columns using tty-table. + * @return {string} The rendered table as a string. + */ +const createColorExplanationTable = () => { + const header = [ + { value: 'Colors Explanation', width: 50, headerColor: 'white', align: 'left' }, + { value: 'Diff Explanation', width: 50, headerColor: 'white', align: 'left' }, + ] + + const text1 = ` +${colors.green('●')} Exact match +${colors.yellow('●')} Partial match (When URL and METHOD match) +${colors.red('●')} Not matching +` + + const text2 = ` +${colors.green('Mock value matches the request value')} +${colors.bgGreen('Present in mock but not in request')} +${colors.bgRed('Present in request but not in mock')} +` + + const rows = [[text1, text2]] + + const table = ttyTable(header, rows, { + borderStyle: 'solid', + borderColor: 'blue', + color: 'white', + }) + + return table.render() +} + +/** + * Generates a debug print message for the given request and mock matches. + * @param {Request} request - The request object. + * @param {MockMatchResult[]} matches - Array of mock match results. + * @return {string} The debug print message. + */ +export const getDebugPrintMessage = (request: Request, matches: MockMatchResult[]) => { + const requestTable = createTableForRequest(request) + const message = ` +------------------------------------------------------------------------------------------------------------------ +Mappersmith matches a mock to an outgoing request by comparing the request's URL, METHOD, BODY and HEADERS. + +URL: The URL is sorted and normalized before comparison. +BODY: The BODY is sorted and normalized in the form of a query-string before comparison. +HEADERS: The headers of the outgoing request is stripped of any headers that are not present in the + mock definition headers and then sorted and normalized before comparison. +METHOD: The method of the outgoing request is compared as is. +------------------------------------------------------------------------------------------------------------------ + +Request: +${requestTable} + +Mock definitions installed: + +${createColorExplanationTable()} + +${matches.length ? createTableForMocks(matches) : 'NO MOCKS INSTALLED'} +` + return message +} + +/** + * Generates a message indicating no mocks are installed. + * @param {Request} request - The request object. + * @return {string} The no mocks installed message. + */ +const getNoMocksInstalledMessage = (request: Request) => { + const debugInfo = getDebugPrintMessage(request, []) + return ` + +${debugInfo} + +${colors.red('[Mappersmith Test] There are no mocks installed, please refer to the documentation: https://github.com/tulios/mappersmith?tab=readme-ov-file#-testing-mappersmith')} +` +} + +/** + * Generates a message indicating no match was found for the given request and mock matches. + * @param {Request} request - The request object. + * @param {MockMatchResult[]} matches - Array of mock match results. + * @return {string} The no match found message. + */ +export const noMatchFoundMessage = (request: Request, matches: MockMatchResult[]) => { + const debugInfo = getDebugPrintMessage(request, matches) + return ` + +${debugInfo} + +${colors.red('[Mappersmith Test] No match found, check your mock definitions, debug information available above.')} +` +} + +/** + * Generates a message indicating a partial match was found for the given request and mock matches. + * @param {Request} request - The request object. + * @param {MockMatchResult[]} matches - Array of mock match results. + * @return {string} The partial match message. + */ +export const partialMatchMessage = (request: Request, matches: MockMatchResult[]) => { + const debugInfo = getDebugPrintMessage(request, matches) + return ` + +${debugInfo} + +${colors.yellow(`[Mappersmith Test] No exact match found but a partial match was found, check your mock definitions, debug information available above.`)} +` +} + +/** + * Looks up a response for the given request in the mock store. + * @param {MockResource[]} store - Array of mock resources. + * @param {Request} request - The request object. + * @return {Response} The matched response. + * @throws Will throw an error if it doesn't find a mock to match the given request. + */ +export const lookupResponse = (store: MockResource[]) => (request: Request) => { + if (!store.length) { + throw new Error(getNoMocksInstalledMessage(request)) + } + + const mocks = store.map((mockResource) => { + const mockRequest = mockResource.toMockRequest() + const requestMatching = mockRequest.getRequestMatching(request) + return { + requestMatching, + mockRequest, + mockResource, + } + }) + + const exactMatch = mocks.filter(({ requestMatching }) => requestMatching.isExactMatch).pop() + if (exactMatch) { + return exactMatch.mockRequest.call(request) + } + + const partialMatch = mocks.filter(({ requestMatching }) => requestMatching.isPartialMatch).pop() + + if (partialMatch) { + throw new Error( + partialMatchMessage( + request, + mocks.map(({ requestMatching }) => requestMatching) + ) + ) + } + + throw new Error( + noMatchFoundMessage( + request, + mocks.map(({ requestMatching }) => requestMatching) + ) + ) +} diff --git a/src/test/index.d.ts b/src/test/index.d.ts index b18beb86..4270c044 100644 --- a/src/test/index.d.ts +++ b/src/test/index.d.ts @@ -27,10 +27,17 @@ export interface MockClient } +export interface InstallOptions { + /** + * Whether to enable enhanced debugging. + */ + enhancedDebugging?: boolean +} + // eslint-disable-next-line @typescript-eslint/no-explicit-any export function lookupResponseAsync(req: any): Promise export function clear(): void -export function install(): void +export function install(installOptions?: InstallOptions): void export function uninstall(): void export function unusedMocks(): number export function mockClient< diff --git a/src/test/index.js b/src/test/index.js index fc179c34..c02e4a43 100644 --- a/src/test/index.js +++ b/src/test/index.js @@ -3,12 +3,16 @@ import MockResource from '../mocks/mock-resource' import { Mock as MockGateway } from '../gateway/mock' import { configs } from '../index' import { toQueryString } from '../utils/index' +import { lookupResponse as lookupResponseWithEnhancedDebugging } from './enhanced-debugging' export { requestFactory } from './request-factory' export { responseFactory } from './response-factory' let store = [] let ids = 1 let originalGateway = null +const options = { + enhancedDebugging: false, +} /** * High level abstraction, it works directly on your client mocking @@ -45,7 +49,12 @@ export const mockRequest = (props) => { /** * Setup the test library */ -export const install = () => { +export const install = ( + { enhancedDebugging } = { + enhancedDebugging: false, + } +) => { + options.enhancedDebugging = enhancedDebugging originalGateway = configs.gateway configs.gateway = MockGateway } @@ -102,8 +111,8 @@ export const lookupResponseAsync = (request) => { * @throws Will throw an error if it doesn't find a mock to match the given request */ export const lookupResponse = (request) => { + if (options.enhancedDebugging) return lookupResponseWithEnhancedDebugging(store, options)(request) const mocks = store.map((mock) => mock.toMockRequest()) - const exactMatch = mocks.filter((mock) => mock.isExactMatch(request)).pop() if (exactMatch) { diff --git a/src/test/index.spec.js b/src/test/index.spec.js new file mode 100644 index 00000000..d035c016 --- /dev/null +++ b/src/test/index.spec.js @@ -0,0 +1,229 @@ +import { lookupResponse, mockClient, clear, install, m } from './index' +import { MethodDescriptor } from '../method-descriptor' +import { Request } from '../request' +import randomJsonData from './data/random-json-data.json' +import { forge } from '../index' + +const stripAnsi = (str) => { + // Regular expression to match ANSI escape codes + // This removes all the color codes and other special codes + // eslint-disable-next-line no-control-regex + const ansiRegex = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g + return str.replace(ansiRegex, '') +} + +const expectToThrowErrorMatchingSnapshotWithoutAnsi = (fn, logToConsole) => { + try { + fn() + throw new Error('Expected function to throw an error, but it did not.') + } catch (error) { + if (logToConsole) console.log(error) + const cleanedError = stripAnsi(error.message) + expect(cleanedError).toMatchSnapshot() + } +} + +describe('mappersmith/test/lookupResponse', () => { + beforeEach(() => { + clear() + // Set this to a fixed value so that the tables are always the same width + process.stdout.columns = 100 + install({ enhancedDebugging: true }) + }) + + it('should match when request is exact match', () => { + const method = 'GET' + const host = 'http://example.org' + const path = '/path' + const body = { id: 1 } + const responseBody = { id: 'my-id' } + const headers = { 'X-TEST': 'VALUE' } + const query = { id: 1 } + + mockClient(forge({ host, resources: { users: { get: { path, method } } } })) + .resource('users') + .method('get') + .with({ body, headers, query }) + .response(responseBody) + .status(200) + + const request = new Request(new MethodDescriptor({ method, host, path }), { + body, + headers, + query, + }) + + expect(() => lookupResponse(request)).not.toThrow() + + const response = lookupResponse(request) + expect(response.responseData).toEqual(JSON.stringify(responseBody)) + }) + + it('should output a clear message when there are no mocks installed', () => { + const method = 'GET' + const host = 'http://example.org' + const path = '/path' + const body = { + parameter: 1, + deep: { + maybe_gql: 'query { user { id } }', + variables: { id: 1 }, + }, + } + const headers = { header1: 'value', header2: 'value2' } + const query = { parameter: 1, parameter2: 2 } + + const request = new Request(new MethodDescriptor({ method, host, path }), { + body, + headers, + query, + }) + + expectToThrowErrorMatchingSnapshotWithoutAnsi(() => lookupResponse(request)) + }) + + it('should log all existing mocks when there is no match', () => { + const body = { + parameter: 1, + deep: { + maybe_gql: 'query { user { id } }', + variables: { id: 1 }, + }, + } + const headers = { header1: 'value', header2: 'value2' } + const query = { parameter: 1, parameter2: 2 } + + mockClient( + forge({ + host: 'http://example-1.org', + resources: { users: { get: { path: '/no-path', method: 'POST' } } }, + }) + ) + .resource('users') + .method('get') + .with({ body, headers, query }) + + const request = new Request( + new MethodDescriptor({ host: 'http://example.org', method: 'GET', path: '/path' }), + { body, headers, query } + ) + + expectToThrowErrorMatchingSnapshotWithoutAnsi(() => lookupResponse(request)) + }) + + it('should log all existing mocks when there is a partial match', () => { + mockClient( + forge({ + host: 'http://example.org', + resources: { + users: { get: { path: '/path', method: 'GET' } }, + }, + }) + ) + .resource('users') + .method('get') + .with({ body: { id: 1 }, headers: { 'X-TEST': 'VALUE' } }) + + const request = new Request( + new MethodDescriptor({ host: 'http://example.org', method: 'GET', path: '/path' }), + { + body: { id: 2 }, + headers: { 'X-TEST': 'VALUE2' }, + } + ) + + expectToThrowErrorMatchingSnapshotWithoutAnsi(() => lookupResponse(request)) + }) + + it('should correctly handle m.anything() displaying that it matches ANYTHING', () => { + mockClient( + forge({ + host: 'http://example.org', + resources: { + users: { get: { path: '/path', method: 'GET' } }, + }, + }) + ) + .resource('users') + .method('get') + .with({ body: m.anything(), headers: { 'X-TEST': 'VALUE-2' } }) + + const request = new Request( + new MethodDescriptor({ host: 'http://example.org', method: 'GET', path: '/path' }), + { + body: { id: 2 }, + headers: { 'X-TEST': 'VALUE2' }, + } + ) + + expectToThrowErrorMatchingSnapshotWithoutAnsi(() => lookupResponse(request)) + }) + + it('should handle multiple mocks with large bodies and long URLs, including a partial match', () => { + const client = forge({ + clientId: 'My Client', + host: 'http://example.org', + resources: { + users: { get: { path: '/path', method: 'GET' } }, + posts: { post: { path: '/long/path/to/resource/with/many/segments', method: 'POST' } }, + comments: { put: { path: '/another/very/long/path/to/resource', method: 'PUT' } }, + }, + }) + + mockClient(client) + .resource('users') + .method('get') + .with({ + body: { id: 1 }, + headers: { 'X-TEST': 'VALUE' }, + query: { id: 1 }, + }) + .response({ id: 'user-id' }) + .status(200) + + mockClient(client) + .resource('posts') + .method('post') + .with({ + body: { content: 'This is a very large body with lots of content...' }, + headers: { 'Content-Type': 'application/json' }, + query: { postId: 12345 }, + }) + .response({ id: 'post-id' }) + .status(201) + + // Partial match: same URL and method, but different body and headers + mockClient(client) + .named('My Named Mock') + .resource('users') + .method('get') + .with({ + body: randomJsonData, + headers: { 'X-TEST': 'DIFFERENT-VALUE' }, + }) + .response({ id: 'partial-match-id' }) + .status(200) + + mockClient(client) + .resource('comments') + .method('put') + .with({ + body: { ...randomJsonData, _id: '66ec092d7189db5c15ea3f05' }, + headers: { 'Content-Type': 'application/json' }, + query: { commentId: 67890 }, + }) + .response({ id: 'comment-id' }) + .status(200) + + const request = new Request( + new MethodDescriptor({ host: 'http://example.org', method: 'GET', path: '/path' }), + { + body: randomJsonData, + headers: { 'X-TEST': 'DIFFERENT-VALUE' }, + query: { id: 2 }, + } + ) + + expectToThrowErrorMatchingSnapshotWithoutAnsi(() => lookupResponse(request)) + }) +}) diff --git a/yarn.lock b/yarn.lock index ae47a131..04323fe1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2917,6 +2917,13 @@ __metadata: languageName: node linkType: hard +"@types/diff@npm:^5.2.2": + version: 5.2.2 + resolution: "@types/diff@npm:5.2.2" + checksum: 10/e682ef27160192ad7986544f7cc059cbc99ddcc138866f03d2cdfe8579d35dbb376f2152fb7dfa034ad089f3a164148ed998dcc2d27c619d307a94c35d0d4ae2 + languageName: node + linkType: hard + "@types/eslint-scope@npm:^3.7.3": version: 3.7.7 resolution: "@types/eslint-scope@npm:3.7.7" @@ -3881,7 +3888,7 @@ __metadata: languageName: node linkType: hard -"array.prototype.flat@npm:^1.3.2": +"array.prototype.flat@npm:^1.2.3, array.prototype.flat@npm:^1.3.2": version: 1.3.2 resolution: "array.prototype.flat@npm:1.3.2" dependencies: @@ -4279,6 +4286,15 @@ __metadata: languageName: node linkType: hard +"breakword@npm:^1.0.5": + version: 1.0.6 + resolution: "breakword@npm:1.0.6" + dependencies: + wcwidth: "npm:^1.0.1" + checksum: 10/e8a3f308c0214986e1b768ca4460a798ffe4bbe08c375576de526431a01a9738318710cc05e309486ac5809d77d9f33d957f80939a890e07be5e89baad9816f8 + languageName: node + linkType: hard + "browser-resolve@npm:^2.0.0": version: 2.0.0 resolution: "browser-resolve@npm:2.0.0" @@ -4457,7 +4473,7 @@ __metadata: languageName: node linkType: hard -"camelcase@npm:^5.3.1": +"camelcase@npm:^5.0.0, camelcase@npm:^5.3.1": version: 5.3.1 resolution: "camelcase@npm:5.3.1" checksum: 10/e6effce26b9404e3c0f301498184f243811c30dfe6d0b9051863bd8e4034d09c8c2923794f280d6827e5aa055f6c434115ff97864a16a963366fb35fd673024b @@ -4613,6 +4629,17 @@ __metadata: languageName: node linkType: hard +"cliui@npm:^6.0.0": + version: 6.0.0 + resolution: "cliui@npm:6.0.0" + dependencies: + string-width: "npm:^4.2.0" + strip-ansi: "npm:^6.0.0" + wrap-ansi: "npm:^6.2.0" + checksum: 10/44afbcc29df0899e87595590792a871cd8c4bc7d6ce92832d9ae268d141a77022adafca1aeaeccff618b62a613b8354e57fe22a275c199ec04baf00d381ef6ab + languageName: node + linkType: hard + "cliui@npm:^7.0.2": version: 7.0.4 resolution: "cliui@npm:7.0.4" @@ -4646,6 +4673,13 @@ __metadata: languageName: node linkType: hard +"clone@npm:^1.0.2": + version: 1.0.4 + resolution: "clone@npm:1.0.4" + checksum: 10/d06418b7335897209e77bdd430d04f882189582e67bd1f75a04565f3f07f5b3f119a9d670c943b6697d0afb100f03b866b3b8a1f91d4d02d72c4ecf2bb64b5dd + languageName: node + linkType: hard + "co@npm:^4.6.0": version: 4.6.0 resolution: "co@npm:4.6.0" @@ -5002,6 +5036,39 @@ __metadata: languageName: node linkType: hard +"csv-generate@npm:^3.4.3": + version: 3.4.3 + resolution: "csv-generate@npm:3.4.3" + checksum: 10/93f18eb1897a886ca5e45d22e82b6c026ac3e9dc3f04918e797d14b1f8e22234a14c018bbbf55a3cc2cfe4284bfa6b8a45097f902451af738911f0e2b0c6d0ed + languageName: node + linkType: hard + +"csv-parse@npm:^4.16.3": + version: 4.16.3 + resolution: "csv-parse@npm:4.16.3" + checksum: 10/b873dd2d312ac0329200f13788176bae3073862241483b0339a4777c9eddcebd9f2f48f13d02dc0baf4bc02e957f886ea03a9cb22160d70836b0017432f8fa41 + languageName: node + linkType: hard + +"csv-stringify@npm:^5.6.5": + version: 5.6.5 + resolution: "csv-stringify@npm:5.6.5" + checksum: 10/efed94869b8426e6a983f2237bd74eff15953e2e27affee9c1324f66a67dabe948573c4c21a8661a79aa20b58efbcafcf11c34e80bdd532a43f35e9cde5985b9 + languageName: node + linkType: hard + +"csv@npm:^5.5.3": + version: 5.5.3 + resolution: "csv@npm:5.5.3" + dependencies: + csv-generate: "npm:^3.4.3" + csv-parse: "npm:^4.16.3" + csv-stringify: "npm:^5.6.5" + stream-transform: "npm:^2.1.3" + checksum: 10/3928e1d88b98f0c3aa26e078cfca36086e0953afa5e83f45fa769b0f6fb4f79e82b4dfd400e8c61637edf144b2650f6ba8c585ec1aad11a6cda84aa04da5bc38 + languageName: node + linkType: hard + "custom-event@npm:~1.0.0": version: 1.0.1 resolution: "custom-event@npm:1.0.1" @@ -5109,6 +5176,13 @@ __metadata: languageName: node linkType: hard +"decamelize@npm:^1.2.0": + version: 1.2.0 + resolution: "decamelize@npm:1.2.0" + checksum: 10/ad8c51a7e7e0720c70ec2eeb1163b66da03e7616d7b98c9ef43cce2416395e84c1e9548dd94f5f6ffecfee9f8b94251fc57121a8b021f2ff2469b2bae247b8aa + languageName: node + linkType: hard + "decimal.js@npm:^10.4.2": version: 10.4.3 resolution: "decimal.js@npm:10.4.3" @@ -5142,6 +5216,15 @@ __metadata: languageName: node linkType: hard +"defaults@npm:^1.0.3": + version: 1.0.4 + resolution: "defaults@npm:1.0.4" + dependencies: + clone: "npm:^1.0.2" + checksum: 10/3a88b7a587fc076b84e60affad8b85245c01f60f38fc1d259e7ac1d89eb9ce6abb19e27215de46b98568dd5bc48471730b327637e6f20b0f1bc85cf00440c80a + languageName: node + linkType: hard + "define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.1": version: 1.1.1 resolution: "define-data-property@npm:1.1.1" @@ -5249,6 +5332,13 @@ __metadata: languageName: node linkType: hard +"diff@npm:^7.0.0": + version: 7.0.0 + resolution: "diff@npm:7.0.0" + checksum: 10/e9b8e48d054c9c0c093c65ce8e2637af94b35f2427001607b14e5e0589e534ea3413a7f91ebe6d7c5a1494ace49cb7c7c3972f442ddd96a4767ff091999a082e + languageName: node + linkType: hard + "dir-glob@npm:^3.0.1": version: 3.0.1 resolution: "dir-glob@npm:3.0.1" @@ -6606,7 +6696,7 @@ __metadata: languageName: node linkType: hard -"get-caller-file@npm:^2.0.5": +"get-caller-file@npm:^2.0.1, get-caller-file@npm:^2.0.5": version: 2.0.5 resolution: "get-caller-file@npm:2.0.5" checksum: 10/b9769a836d2a98c3ee734a88ba712e62703f1df31b94b784762c433c27a386dd6029ff55c2a920c392e33657d80191edbf18c61487e198844844516f843496b9 @@ -6819,6 +6909,13 @@ __metadata: languageName: node linkType: hard +"grapheme-splitter@npm:^1.0.4": + version: 1.0.4 + resolution: "grapheme-splitter@npm:1.0.4" + checksum: 10/fdb2f51fd430ce881e18e44c4934ad30e59736e46213f7ad35ea5970a9ebdf7d0fe56150d15cc98230d55d2fd48c73dc6781494c38d8cf2405718366c36adb88 + languageName: node + linkType: hard + "graphemer@npm:^1.4.0": version: 1.4.0 resolution: "graphemer@npm:1.4.0" @@ -8393,6 +8490,13 @@ __metadata: languageName: node linkType: hard +"kleur@npm:^4.1.5": + version: 4.1.5 + resolution: "kleur@npm:4.1.5" + checksum: 10/44d84cc4eedd4311099402ef6d4acd9b2d16e08e499d6ef3bb92389bd4692d7ef09e35248c26e27f98acac532122acb12a1bfee645994ae3af4f0a37996da7df + languageName: node + linkType: hard + "leven@npm:^3.1.0": version: 3.1.0 resolution: "leven@npm:3.1.0" @@ -8649,6 +8753,7 @@ __metadata: "@babel/register": "npm:^7.24.6" "@changesets/cli": "npm:^2.27.8" "@chiragrupani/karma-chromium-edge-launcher": "npm:^2.4.1" + "@types/diff": "npm:^5.2.2" "@types/jest": "npm:^29.5.12" "@types/karma": "npm:^6.3.8" "@types/node": "npm:^22.5.4" @@ -8665,6 +8770,7 @@ __metadata: copyfiles: "npm:^2.4.1" core-js: "npm:^3.38.1" cross-env: "npm:^7.0.3" + diff: "npm:^7.0.0" esbuild-plugin-file-path-extensions: "npm:^2.1.2" eslint: "npm:^8.57.0" eslint-config-prettier: "npm:^9.1.0" @@ -8694,6 +8800,7 @@ __metadata: ts-jest: "npm:^29.2.5" ts-node: "npm:^10.9.2" tsup: "npm:^8.2.4" + tty-table: "npm:^4.2.3" typescript: "npm:<5.5.0" wait-on: "npm:^8.0.1" webpack: "npm:^5.94.0" @@ -8940,6 +9047,13 @@ __metadata: languageName: node linkType: hard +"mixme@npm:^0.5.1": + version: 0.5.10 + resolution: "mixme@npm:0.5.10" + checksum: 10/b0834a462f0960eaa6ec161bb2be56d75a13ad3b2ffefa092960c91dd456e335f181316de5206965f160ce33815b2b559c9dccc1042041738c3bcc1075dbfad2 + languageName: node + linkType: hard + "mkdirp@npm:^0.5.4, mkdirp@npm:^0.5.5": version: 0.5.6 resolution: "mkdirp@npm:0.5.6" @@ -10011,6 +10125,13 @@ __metadata: languageName: node linkType: hard +"require-main-filename@npm:^2.0.0": + version: 2.0.0 + resolution: "require-main-filename@npm:2.0.0" + checksum: 10/8604a570c06a69c9d939275becc33a65676529e1c3e5a9f42d58471674df79357872b96d70bb93a0380a62d60dc9031c98b1a9dad98c946ffdd61b7ac0c8cedd + languageName: node + linkType: hard + "requires-port@npm:^1.0.0": version: 1.0.0 resolution: "requires-port@npm:1.0.0" @@ -10405,6 +10526,13 @@ __metadata: languageName: node linkType: hard +"set-blocking@npm:^2.0.0": + version: 2.0.0 + resolution: "set-blocking@npm:2.0.0" + checksum: 10/8980ebf7ae9eb945bb036b6e283c547ee783a1ad557a82babf758a065e2fb6ea337fd82cac30dd565c1e606e423f30024a19fff7afbf4977d784720c4026a8ef + languageName: node + linkType: hard + "set-function-length@npm:^1.1.1": version: 1.1.1 resolution: "set-function-length@npm:1.1.1" @@ -10567,6 +10695,22 @@ __metadata: languageName: node linkType: hard +"smartwrap@npm:^2.0.2": + version: 2.0.2 + resolution: "smartwrap@npm:2.0.2" + dependencies: + array.prototype.flat: "npm:^1.2.3" + breakword: "npm:^1.0.5" + grapheme-splitter: "npm:^1.0.4" + strip-ansi: "npm:^6.0.0" + wcwidth: "npm:^1.0.1" + yargs: "npm:^15.1.0" + bin: + smartwrap: src/terminal-adapter.js + checksum: 10/dcc7b9082b74a0ce0f391fce8a4be72f56d1b6e78fbfed9b4191da89d66d82f62a7b44c727d7e68714f0faf1ed1fce0be498563b0d4ef8aad80e2433983b0603 + languageName: node + linkType: hard + "socket.io-adapter@npm:~2.5.2": version: 2.5.2 resolution: "socket.io-adapter@npm:2.5.2" @@ -10707,6 +10851,15 @@ __metadata: languageName: node linkType: hard +"stream-transform@npm:^2.1.3": + version: 2.1.3 + resolution: "stream-transform@npm:2.1.3" + dependencies: + mixme: "npm:^0.5.1" + checksum: 10/3167bf23a96b3fc7f991d4a8224cd0701c9234be8acd8450f8692c431bed4548d1ef90d1b410fdeff567fa740c3db97b52d5f3c4ad4485fc0e0b8be655800ab7 + languageName: node + linkType: hard + "streamroller@npm:^3.1.5": version: 3.1.5 resolution: "streamroller@npm:3.1.5" @@ -11395,6 +11548,23 @@ __metadata: languageName: node linkType: hard +"tty-table@npm:^4.2.3": + version: 4.2.3 + resolution: "tty-table@npm:4.2.3" + dependencies: + chalk: "npm:^4.1.2" + csv: "npm:^5.5.3" + kleur: "npm:^4.1.5" + smartwrap: "npm:^2.0.2" + strip-ansi: "npm:^6.0.1" + wcwidth: "npm:^1.0.1" + yargs: "npm:^17.7.1" + bin: + tty-table: adapters/terminal-adapter.js + checksum: 10/8532c784027a833bd805de5962be469faaee0ec314cc1c01e77d06ec89d44f18992455969b29ec460abbc7798ea584d805966cbd6480f5a5ffd29865e8de2501 + languageName: node + linkType: hard + "type-check@npm:^0.4.0, type-check@npm:~0.4.0": version: 0.4.0 resolution: "type-check@npm:0.4.0" @@ -11848,6 +12018,15 @@ __metadata: languageName: node linkType: hard +"wcwidth@npm:^1.0.1": + version: 1.0.1 + resolution: "wcwidth@npm:1.0.1" + dependencies: + defaults: "npm:^1.0.3" + checksum: 10/182ebac8ca0b96845fae6ef44afd4619df6987fe5cf552fdee8396d3daa1fb9b8ec5c6c69855acb7b3c1231571393bd1f0a4cdc4028d421575348f64bb0a8817 + languageName: node + linkType: hard + "webidl-conversions@npm:^3.0.0": version: 3.0.1 resolution: "webidl-conversions@npm:3.0.1" @@ -12025,6 +12204,13 @@ __metadata: languageName: node linkType: hard +"which-module@npm:^2.0.0": + version: 2.0.1 + resolution: "which-module@npm:2.0.1" + checksum: 10/1967b7ce17a2485544a4fdd9063599f0f773959cca24176dbe8f405e55472d748b7c549cd7920ff6abb8f1ab7db0b0f1b36de1a21c57a8ff741f4f1e792c52be + languageName: node + linkType: hard + "which-typed-array@npm:^1.1.11, which-typed-array@npm:^1.1.13": version: 1.1.13 resolution: "which-typed-array@npm:1.1.13" @@ -12095,6 +12281,17 @@ __metadata: languageName: node linkType: hard +"wrap-ansi@npm:^6.2.0": + version: 6.2.0 + resolution: "wrap-ansi@npm:6.2.0" + dependencies: + ansi-styles: "npm:^4.0.0" + string-width: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + checksum: 10/0d64f2d438e0b555e693b95aee7b2689a12c3be5ac458192a1ce28f542a6e9e59ddfecc37520910c2c88eb1f82a5411260566dba5064e8f9895e76e169e76187 + languageName: node + linkType: hard + "wrap-ansi@npm:^8.1.0": version: 8.1.0 resolution: "wrap-ansi@npm:8.1.0" @@ -12198,6 +12395,13 @@ __metadata: languageName: node linkType: hard +"y18n@npm:^4.0.0": + version: 4.0.3 + resolution: "y18n@npm:4.0.3" + checksum: 10/392870b2a100bbc643bc035fe3a89cef5591b719c7bdc8721bcdb3d27ab39fa4870acdca67b0ee096e146d769f311d68eda6b8195a6d970f227795061923013f + languageName: node + linkType: hard + "y18n@npm:^5.0.5": version: 5.0.8 resolution: "y18n@npm:5.0.8" @@ -12226,6 +12430,16 @@ __metadata: languageName: node linkType: hard +"yargs-parser@npm:^18.1.2": + version: 18.1.3 + resolution: "yargs-parser@npm:18.1.3" + dependencies: + camelcase: "npm:^5.0.0" + decamelize: "npm:^1.2.0" + checksum: 10/235bcbad5b7ca13e5abc54df61d42f230857c6f83223a38e4ed7b824681875b7f8b6ed52139d88a3ad007050f28dc0324b3c805deac7db22ae3b4815dae0e1bf + languageName: node + linkType: hard + "yargs-parser@npm:^20.2.2": version: 20.2.9 resolution: "yargs-parser@npm:20.2.9" @@ -12240,6 +12454,25 @@ __metadata: languageName: node linkType: hard +"yargs@npm:^15.1.0": + version: 15.4.1 + resolution: "yargs@npm:15.4.1" + dependencies: + cliui: "npm:^6.0.0" + decamelize: "npm:^1.2.0" + find-up: "npm:^4.1.0" + get-caller-file: "npm:^2.0.1" + require-directory: "npm:^2.1.1" + require-main-filename: "npm:^2.0.0" + set-blocking: "npm:^2.0.0" + string-width: "npm:^4.2.0" + which-module: "npm:^2.0.0" + y18n: "npm:^4.0.0" + yargs-parser: "npm:^18.1.2" + checksum: 10/bbcc82222996c0982905b668644ca363eebe6ffd6a572fbb52f0c0e8146661d8ce5af2a7df546968779bb03d1e4186f3ad3d55dfaadd1c4f0d5187c0e3a5ba16 + languageName: node + linkType: hard + "yargs@npm:^16.1.0, yargs@npm:^16.1.1": version: 16.2.0 resolution: "yargs@npm:16.2.0" @@ -12255,7 +12488,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^17.3.1, yargs@npm:^17.7.2": +"yargs@npm:^17.3.1, yargs@npm:^17.7.1, yargs@npm:^17.7.2": version: 17.7.2 resolution: "yargs@npm:17.7.2" dependencies: