Repository: nock/nock (js)

Issue:
`replyWithFile` prevents deleting folder when `persist` is used or interceptor is not hit
### Please avoid duplicates

- [X] I checked [all open bugs](https://github.com/nock/nock.js/issues?q=is%3Aissue+is%3Aopen+label%3Abug) and none of them matched my problem.

### Reproducible test case

See description below with repro snippet.

### Nock Version

13.0.4

### Node Version

16.13.1

### TypeScript Version

_No response_

### What happened?

When setting up a route with `replyWithFile`, it eagerly opens a ReadStream on the file. If the interceptor was not hit _or_ if the interceptor is in a scope with `.persist()`, it retains a lock on the file and prevents deleting the folder containing it. 

The following snippet showcases the issue. Note that manually removing the interceptor does _not_ close the file stream. I included a call to `nock.removeInterceptor` in the repro to prove it. (Hitting the route with a request does close the ReadStream, unless `persist()` was used.)

```
const assert = require('assert')
const fs = require('fs')
const path = require('path')

const nock = require('nock')

fs.mkdirSync('./temp')
fs.writeFileSync('./temp/file.txt', 'dummy file')
const interceptor = nock('http://example.org').get('/')
interceptor.replyWithFile(200, './temp/file.txt')

// Note: setImmediate below is necessary because Nock fails to open the ReadStream on a subsequent tick; 
// that's another problem caused by opening the stream eagerly rather than on-demand
setImmediate(() => fs.rmSync('./temp', { recursive: true }))

nock.removeInterceptor(interceptor) // won't have any effect

assert(!fs.existsSync('./temp'), 'temp folder was not actually deleted')
```

I would expect the stream to be opened when the interceptor is hit only and/or the ReadStream to be closed when the interceptor is removed.

### Would you be interested in contributing a fix?

- [X] yes

Pull request:
fix: call `fs.createReadStream` lazily
When using `replyWithFile`, a ReadStream is immediately created wih the file path. When the mock is hit, another ReadStream is created immediately if the interceptor is configured to be set again or if its scope has `_persist` set. When calling `nock.removeInterceptor`, the ReadStream isn't closed. 

As a result, it's not possible to delete the folder where the file exists on Windows unless implementing a workaround like multiple `fs.rm` calls.

This PR removes the eager `fs.createReadStream` call when setting up the interceptor and instead configures the route to create 
 the stream lazily, in a callback invoked when intercepting a call. There is no longer any need for `replyWithFile`-related logic in `Interceptor#markConsumed()`.

I also added an extra test for `replyWithFile` + `persist()`, which I think makes coverage a bit more complete.

Fixes #2354 

Patch:
diff --git a/lib/interceptor.js b/lib/interceptor.js
index aebbdad..00c4522 100644
--- a/lib/interceptor.js
+++ b/lib/interceptor.js
@@ -198,10 +198,16 @@ module.exports = class Interceptor {
     if (!fs) {
       throw new Error('No fs')
     }
-    const readStream = fs.createReadStream(filePath)
-    readStream.pause()
     this.filePath = filePath
-    return this.reply(statusCode, readStream, headers)
+    return this.reply(
+      statusCode,
+      () => {
+        const readStream = fs.createReadStream(filePath)
+        readStream.pause()
+        return readStream
+      },
+      headers,
+    )
   }
 
   // Also match request headers
@@ -453,15 +459,6 @@ module.exports = class Interceptor {
   markConsumed() {
     this.interceptionCounter++
 
-    if (
-      (this.scope.shouldPersist() || this.counter > 0) &&
-      this.interceptionCounter > 1 &&
-      this.filePath
-    ) {
-      this.body = fs.createReadStream(this.filePath)
-      this.body.pause()
-    }
-
     remove(this)
 
     if (!this.scope.shouldPersist() && this.counter < 1) {


Test patch:
diff --git a/tests/got/test_reply_with_file.js b/tests/got/test_reply_with_file.js
index 473743e..9c2de20 100644
--- a/tests/got/test_reply_with_file.js
+++ b/tests/got/test_reply_with_file.js
@@ -64,6 +64,29 @@ describe('`replyWithFile()`', () => {
     scope.done()
   })
 
+  it('reply with file with persist', async () => {
+    sinon.spy(fs)
+
+    const scope = nock('http://example.test')
+      .persist()
+      .get('/')
+      .replyWithFile(200, binaryFilePath, {
+        'content-encoding': 'gzip',
+      })
+
+    const response1 = await got('http://example.test/')
+    expect(response1.statusCode).to.equal(200)
+    expect(response1.body).to.have.lengthOf(20)
+
+    const response2 = await got('http://example.test/')
+    expect(response2.statusCode).to.equal(200)
+    expect(response2.body).to.have.lengthOf(20)
+
+    expect(fs.createReadStream.callCount).to.equal(2)
+
+    scope.done()
+  })
+
   describe('with no fs', () => {
     const { Scope } = proxyquire('../../lib/scope', {
       './interceptor': proxyquire('../../lib/interceptor', {
@@ -79,4 +102,14 @@ describe('`replyWithFile()`', () => {
       ).to.throw(Error, 'No fs')
     })
   })
+
+  it('does not create ReadStream eagerly', async () => {
+    sinon.spy(fs)
+
+    nock('http://example.test').get('/').replyWithFile(200, binaryFilePath, {
+      'content-encoding': 'gzip',
+    })
+
+    expect(fs.createReadStream.callCount).to.equal(0)
+  })
 })