Skip to content

Commit

Permalink
Add support for JSON patch transforms on response bodies
Browse files Browse the repository at this point in the history
  • Loading branch information
pimterry committed Jul 3, 2024
1 parent 3b3e30c commit b7e003a
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 15 deletions.
22 changes: 20 additions & 2 deletions src/rules/requests/request-handler-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<JsonPatchOperation>;

/**
* Perform a series of string match & replace operations on the response body.
*
Expand Down Expand Up @@ -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;
}
}
Expand Down
33 changes: 21 additions & 12 deletions src/rules/requests/request-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,7 @@ export class PassThroughHandler extends PassThroughHandlerDefinition {
replaceBody,
replaceBodyFromFile,
updateJsonBody,
patchJsonBody,
matchReplaceBody
} = this.transformResponse;

Expand All @@ -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);
Expand Down
35 changes: 34 additions & 1 deletion test/integration/proxying/proxy-transforms.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
});
});

});
});
});

0 comments on commit b7e003a

Please sign in to comment.