From b7e003ad4bb17122a2c2ce32a341c31ce05196eb Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Wed, 3 Jul 2024 13:24:38 +0200 Subject: [PATCH] Add support for JSON patch transforms on response bodies --- .../requests/request-handler-definitions.ts | 22 ++++++++++-- src/rules/requests/request-handlers.ts | 33 ++++++++++------- .../proxying/proxy-transforms.spec.ts | 35 ++++++++++++++++++- 3 files changed, 75 insertions(+), 15 deletions(-) diff --git a/src/rules/requests/request-handler-definitions.ts b/src/rules/requests/request-handler-definitions.ts index a217dc2a4..c8e0fccba 100644 --- a/src/rules/requests/request-handler-definitions.ts +++ b/src/rules/requests/request-handler-definitions.ts @@ -672,13 +672,25 @@ export interface ResponseTransform { /** * A JSON object which will be merged with the real response body. Undefined values - * will be removed. Any responses which are received with an invalid JSON body that - * match this rule will fail. + * will be removed, and other values will be merged directly with the target value + * recursively. + * + * Any responses which are received with an invalid JSON body that match this rule + * will fail. */ updateJsonBody?: { [key: string]: any; }; + /** + * A series of operations to apply to the response body in JSON Patch format (RFC + * 6902). + * + * Any responses which are received with an invalid JSON body that match this rule + * will fail. + */ + patchJsonBody?: Array; + /** * Perform a series of string match & replace operations on the response body. * @@ -855,11 +867,17 @@ export class PassThroughHandlerDefinition extends Serializable implements Reques options.transformResponse.replaceBody, options.transformResponse.replaceBodyFromFile, options.transformResponse.updateJsonBody, + options.transformResponse.patchJsonBody, options.transformResponse.matchReplaceBody ].filter(o => !!o).length > 1) { throw new Error("Only one response body transform can be specified at a time"); } + if (options.transformResponse.patchJsonBody) { + const validationError = validateJsonPatch(options.transformResponse.patchJsonBody); + if (validationError) throw validationError; + } + this.transformResponse = options.transformResponse; } } diff --git a/src/rules/requests/request-handlers.ts b/src/rules/requests/request-handlers.ts index 3b2d19310..56c186480 100644 --- a/src/rules/requests/request-handlers.ts +++ b/src/rules/requests/request-handlers.ts @@ -836,6 +836,7 @@ export class PassThroughHandler extends PassThroughHandlerDefinition { replaceBody, replaceBodyFromFile, updateJsonBody, + patchJsonBody, matchReplaceBody } = this.transformResponse; @@ -862,23 +863,31 @@ export class PassThroughHandler extends PassThroughHandlerDefinition { } else if (updateJsonBody) { originalBody = await streamToBuffer(serverRes); const realBody = buildBodyReader(originalBody, serverRes.headers); + const jsonBody = await realBody.getJson(); - if (await realBody.getJson() === undefined) { - throw new Error("Can't transform non-JSON response body"); + if (jsonBody === undefined) { + throw new Error("Can't update JSON in non-JSON response body"); } - const updatedBody = _.mergeWith( - await realBody.getJson(), - updateJsonBody, - (_oldValue, newValue) => { - // We want to remove values with undefines, but Lodash ignores - // undefined return values here. Fortunately, JSON.stringify - // ignores Symbols, omitting them from the result. - if (newValue === undefined) return OMIT_SYMBOL; - } - ); + const updatedBody = _.mergeWith(jsonBody, updateJsonBody, (_oldValue, newValue) => { + // We want to remove values with undefines, but Lodash ignores + // undefined return values here. Fortunately, JSON.stringify + // ignores Symbols, omitting them from the result. + if (newValue === undefined) return OMIT_SYMBOL; + }); resBodyOverride = asBuffer(JSON.stringify(updatedBody)); + } else if (patchJsonBody) { + originalBody = await streamToBuffer(serverRes); + const realBody = buildBodyReader(originalBody, serverRes.headers); + const jsonBody = await realBody.getJson(); + + if (jsonBody === undefined) { + throw new Error("Can't patch JSON in non-JSON response body"); + } + + applyJsonPatch(jsonBody, patchJsonBody, true); // Mutates the JSON body returned above + resBodyOverride = asBuffer(JSON.stringify(jsonBody)); } else if (matchReplaceBody) { originalBody = await streamToBuffer(serverRes); const realBody = buildBodyReader(originalBody, serverRes.headers); diff --git a/test/integration/proxying/proxy-transforms.spec.ts b/test/integration/proxying/proxy-transforms.spec.ts index 3b68a1973..f85711833 100644 --- a/test/integration/proxying/proxy-transforms.spec.ts +++ b/test/integration/proxying/proxy-transforms.spec.ts @@ -784,7 +784,8 @@ nodeOnly(() => { it("can update a JSON body with new fields", async () => { await server.forAnyRequest().thenPassThrough({ transformResponse: { - updateJsonBody:{ + // Same update as the JSON Patch below, in simpler merge form: + updateJsonBody: { 'body-value': false, // Update 'another-body-value': undefined, // Remove 'new-value': 123 // Add @@ -817,6 +818,7 @@ nodeOnly(() => { updateHeaders: { 'content-encoding': 'br' }, + // Same update as the JSON Patch below, in simpler merge form: updateJsonBody:{ 'body-value': false, // Update 'another-body-value': undefined, // Remove @@ -853,6 +855,37 @@ nodeOnly(() => { }); }); + it("can update a JSON body with a JSON patch", async () => { + await server.forAnyRequest().thenPassThrough({ + transformResponse: { + patchJsonBody: [ + // Same logic as the update above, in JSON Patch form: + { op: 'replace', path: '/body-value', value: false }, + { op: 'remove', path: '/another-body-value' }, + { op: 'add', path: '/new-value', value: 123 } + ] + } + }); + + let response = await request.post(remoteServer.url, { + resolveWithFullResponse: true, + simple: false + }); + + expect(response.statusCode).to.equal(200); + expect(response.statusMessage).to.equal('OK'); + expect(response.headers).to.deep.equal({ + 'content-type': 'application/json', + 'content-length': '36', + 'connection': 'keep-alive', + 'custom-response-header': 'custom-value' + }); + expect(JSON.parse(response.body)).to.deep.equal({ + 'body-value': false, + 'new-value': 123 + }); + }); + }); }); }); \ No newline at end of file