Repository: nock/nock
Pull request #2737 (2024-04-12 17:26:59)

fix: mapValues should not update the object in place
The original lodash implementation of `mapValues` is a pure function that doesn't modify the object in place.

The new implementation is causing issues in test cases where the same object is used for the nock response and for validation. 

Here's an example of a test that broke when attempting to use the beta

```typescript
  describe('#patch', () => {
    it('can patch data locally', async () => {
      const patchBody = {name: 'test', id: 1234};

      nock(baseUrl)
        .patch('/', patchBody)
        .reply(200, patchBody);

	  // client#patch uses URL encoding
      const response = await client.patch('/', patchBody);
      should(response.body).eql(patchBody); // breaks
      should(response.statusCode).eql(200);

      should(nock.isDone()).eql(true);
    });
  });
```

pranaygp: cc @mikicho - Not sure if you're the best person to assign for review on this :)
mikicho: @pranaygp Thanks for opening this PR!

What's `client` in your example? `axios`? `http.get`?
pranaygp: It's a custom wrapper around [needle](https://github.com/tomas/needle)

for PATCH requests, we send the data as formdata instead of JSON. That's the triggered the codepath leading to this  `mapValues` implementation

Patch:
diff --git a/lib/match_body.js b/lib/match_body.js
index 436b7b5..3d568ec 100644
--- a/lib/match_body.js
+++ b/lib/match_body.js
@@ -63,10 +63,11 @@ module.exports = function matchBody(options, spec, body) {
 
 function mapValues(object, cb) {
   const keys = Object.keys(object)
+  const clonedObject = { ...object }
   for (const key of keys) {
-    object[key] = cb(object[key], key, object)
+    clonedObject[key] = cb(clonedObject[key], key, clonedObject)
   }
-  return object
+  return clonedObject
 }
 
 /**


Test patch:
diff --git a/tests/got/test_reply_body.js b/tests/got/test_reply_body.js
index 71f44ff..ebedb07 100644
--- a/tests/got/test_reply_body.js
+++ b/tests/got/test_reply_body.js
@@ -115,6 +115,23 @@ describe('`reply()` body', () => {
     expect(body).to.be.a('string').and.equal('')
     scope.done()
   })
+
+  it('does not modify the object used for response', async () => {
+    const patchBody = { number: 1234 }
+    const form = new FormData()
+    form.append('number', 1234)
+
+    const scope = nock('http://example.test')
+      .patch('/', patchBody)
+      .reply(200, patchBody)
+
+    await got.patch('http://example.test/', {
+      form,
+    })
+
+    expect(patchBody.number).to.be.a('number').and.equal(1234)
+    scope.done()
+  })
 })
 
 describe('`reply()` status code', () => {
diff --git a/tests/test_common.js b/tests/test_common.js
index f91ddb4..68d8922 100644
--- a/tests/test_common.js
+++ b/tests/test_common.js
@@ -71,6 +71,18 @@ describe('Body Match', () => {
       const result = matchBody({}, { number: 1 }, '{"number": "1"}')
       expect(result).to.equal(false)
     })
+
+    it('should not modify the original spec object', () => {
+      const spec = { number: 1 }
+      matchBody(
+        {
+          headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+        },
+        spec,
+        '',
+      )
+      expect(spec).to.deep.equal({ number: 1 })
+    })
   })
 })