diff --git a/src/modules/request-recorder.js b/src/modules/request-recorder.js index 310b256d..7ebc554f 100644 --- a/src/modules/request-recorder.js +++ b/src/modules/request-recorder.js @@ -22,6 +22,7 @@ import { convertHeaders, rewriteHeaders } from './request-recorder/util.js'; +import healBody from './request-recorder/heal-body.js'; const nockBack = nock.back; const nockRecorder = nock.recorder; @@ -180,7 +181,7 @@ export default (opts) => { const idx = pendingMocks.findIndex((m) => m.idx === scopeIdx); const requestBody = nullAsString(tryParseJson(body)); if (!isEqual(scope.body, requestBody)) { - pendingMocks[idx].record.body = requestBody; + pendingMocks[idx].record.body = healBody(pendingMocks[idx].record.body, scope.body, requestBody); } return scope.body; } diff --git a/src/modules/request-recorder/heal-body.js b/src/modules/request-recorder/heal-body.js new file mode 100644 index 00000000..9d2c3116 --- /dev/null +++ b/src/modules/request-recorder/heal-body.js @@ -0,0 +1,44 @@ +import objectScan from 'object-scan'; +import cloneDeep from 'lodash.clonedeep'; + +const last = (arr) => arr[arr.length - 1]; + +const healer = objectScan(['**.*|*'], { + breakFn: ({ + isMatch, depth, property, context + }) => { + if (property === undefined) { + return false; + } + context.expected[depth] = last(context.expected)?.[property]; + context.actual[depth] = last(context.actual)?.[property]; + return isMatch; + }, + filterFn: ({ + context, depth, property, value + }) => { + const k = property.split('|')[0]; + const parentExpected = context.expected[depth - 1]; + const parentActual = context.actual[depth - 1]; + const childExpected = parentExpected?.[k]; + const childActual = parentActual?.[k]; + if ( + parentActual !== undefined + && !(childExpected instanceof Object) + && !(childActual instanceof Object) + && childExpected === childActual + ) { + delete parentActual[k]; + parentActual[property] = value; + } + }, + afterFn: ({ context }) => context.actual[0] +}); + +export default (original, expected, actual) => healer( + original, + { + expected: [expected], + actual: [cloneDeep(actual)] + } +); diff --git a/test/modules/request-recorder.spec.js b/test/modules/request-recorder.spec.js index 7daf4c38..e476fc9e 100644 --- a/test/modules/request-recorder.spec.js +++ b/test/modules/request-recorder.spec.js @@ -37,7 +37,8 @@ describe('Testing RequestRecorder', { useTmpDir: true, timestamp: 0 }, () => { }, stripHeaders = undefined, strict = undefined, - heal = undefined + heal = undefined, + method = 'GET' } = {}) => nockRecord( async () => { for (let idx = 0; idx < qs.length; idx += 1) { @@ -45,12 +46,20 @@ describe('Testing RequestRecorder', { useTmpDir: true, timestamp: 0 }, () => { const { data } = await axios({ url: `${server.uri}?q=${qs[idx]}`, data: body, - responseType: 'json' + responseType: 'json', + method }); expect(data).to.deep.equal({ data: String(qs[idx]) }); } }, - { stripHeaders, strict, heal } + { + stripHeaders, + strict, + heal, + modifiers: { + 'JSON.stringify': JSON.stringify + } + } ); it('Testing headers captured', async () => { @@ -267,7 +276,7 @@ describe('Testing RequestRecorder', { useTmpDir: true, timestamp: 0 }, () => { cassetteContent === null ? [{ scope: server.uri, - method, + method: 'GET', path: '/?q=1', body: method === 'GET' ? '' : { id: 123, @@ -281,19 +290,19 @@ describe('Testing RequestRecorder', { useTmpDir: true, timestamp: 0 }, () => { ); if (raises) { const e = await capture(() => runTest({ - heal, qs, body, stripHeaders + heal, qs, body, stripHeaders, method })); expect(e.message).to.match(/^Nock: No match for request/); } else { await runTest({ - heal, qs, body, stripHeaders + heal, qs, body, stripHeaders, method }); } const content = fs.smartRead(cassettePath); if (heals) { expect(content[0].body.payload).to.not.equal(null); expect(content[0].path).to.equal(`/?q=${qs[0]}`); - await runTest({ qs, body }); + await runTest({ qs, body, method }); } else { expect(get(content, [0, 'body', 'payload'], null)).to.equal(null); } @@ -313,6 +322,41 @@ describe('Testing RequestRecorder', { useTmpDir: true, timestamp: 0 }, () => { await runner('body'); }); + it('Testing body healing with modifiers', async () => { + const cassette = { + ...makeCassetteEntry(1), + method: 'POST', + reqheaders: { + accept: 'application/json, text/plain, */*', + 'content-type': 'application/json', + 'user-agent': 'axios/1.6.7', + 'content-length': '^\\d+$', + 'accept-encoding': 'gzip, compress, deflate, br' + }, + body: { + 'data|JSON.stringify': { a: 1 }, + 'other|JSON.stringify': { a: 1 } + } + }; + await runner('body', { + raises: false, + heals: true, + body: { + data: '{"a":1}', + other: '{"a":2}' + }, + method: 'POST', + qs: [1], + cassetteContent: [cassette] + }); + const cassettePath = path.join(tmpDir, cassetteFile); + const content = fs.smartRead(cassettePath); + expect(content[0].body).to.deep.equal({ + 'data|JSON.stringify': { a: 1 }, + other: '{"a":2}' + }); + }); + it('Testing body healing with null body', async () => { await runner('body', { body: null }); });