index.js

const assert = require('assert')
const debug = require('debug')('node-vcr')
const fse = require('fs-extra')
const messageHash = require('incoming-message-hash')
const path = require('path')
const buffer = require('./buffer')
const curl = require('./curl')
const record = require('./record')
const proxy = require('./proxy')

const defaultOpts = {
  dirname: './tapes/',
  noRecord: false,
  maxRedirects: 5,
  tapeRequestBody: false,
  ignoreHeaders: [],
  hash: messageHash.sync
}

const RecordingDisabledError = new Error('Recording Disabled')
RecordingDisabledError.status = 404

/**
 * Returns a new node-vcr proxy middleware.
 *
 * @module node-vcr
 * @param { string } host                               The hostname to proxy to
 * @param { object } options
 * @param { string } options.dirname                    The tapes directory
 * @param { boolean } [options.noRecord=false]          If true, requests will return a 404 error if the tape doesn't exist
 * @param { boolean } [options.maxRedirects=5]          If set to 0 redirects will be disabled
 * @param { boolean } [options.tapeRequestBody=false]   If true, the request body will be stored to tape
 * @param { array } [options.ignoreHeaders=[]]          A list of headers which must not be written down to tape
 * @param { function } [options.hash=messageHash.sync]  Provide your own IncomingMessage hash function of the signature `function (req, body)`
 * @param { boolean } [options.reload=false]            If true, node-vcr will reload (delete and record again) required tape
 * @param { boolean } [options.refresh=false]           If true, node-vcr will refresh (reload only in require.cache) required tape
 * @returns { function } A function of the signature `function (req, res)` that you can give to an `http.Server` as its handler
 */
module.exports = (host, usrOpts) => {
  assert(host)

  const opts = Object.assign({}, defaultOpts, usrOpts)
  debug('opts', opts)

  return (req, res) => {
    return fse.ensureDir(opts.dirname)
      .then(() => buffer(req))
      .then((body) => {
        const filename = path.join(opts.dirname, `${opts.hash(req, Buffer.concat(body))}.js`)

        let exists = fse.existsSync(filename)
        if (opts.reload && exists) {
          exists = false
          delete require.cache[require.resolve(filename)]
          fse.removeSync(filename)
        }

        if (exists) {
          return filename
        } else if (opts.noRecord) {
          throw RecordingDisabledError
        } else {
          return proxy(req, body, host, opts.maxRedirects)
            .then((pRes) => {
              let reqBody
              if (opts.tapeRequestBody) {
                reqBody = body.toString()
              }
              return record(pRes.req, pRes, filename, opts.ignoreHeaders, reqBody)
            })
        }
      })
      .then((file) => {
        if (opts.refresh) {
          delete require.cache[require.resolve(file)]
        }
        return require(file)
      })
      .then((tape) => tape(req, res))
      .catch((err) => {
        if (err.message && err.message === 'Recording Disabled') {
          /* eslint-disable no-console */
          console.log('An HTTP request has been made that node-vcr does not know how to handle')
          console.log(curl.request(req))
          /* eslint-enable no-console */
          res.statusCode = err.status
          res.end(err.message)
        } else {
          console.error(err)
        }
      })
  }
}