fastify / light-my-request

Fake HTTP injection library
Other
317 stars 47 forks source link
fastify-library http injection nodejs

Light my Request

CI NPM version js-standard-style

Injects a fake HTTP request/response into a node HTTP server for simulating server logic, writing tests, or debugging. Does not use a socket connection so can be run against an inactive server (server not in listen mode).

Example

const http = require('node:http')
const inject = require('light-my-request')

const dispatch = function (req, res) {
  const reply = 'Hello World'
  res.writeHead(200, { 'Content-Type': 'text/plain', 'Content-Length': reply.length })
  res.end(reply)
}

const server = http.createServer(dispatch)

inject(dispatch, { method: 'get', url: '/' }, (err, res) => {
  console.log(res.payload)
})

Note how server.listen is never called.

Async await and promises are supported as well!

// promises
inject(dispatch, { method: 'get', url: '/' })
  .then(res => console.log(res.payload))
  .catch(console.log)

// async-await
try {
  const res = await inject(dispatch, { method: 'get', url: '/' })
  console.log(res.payload)
} catch (err) {
  console.log(err)
}

You can also use chaining methods if you do not pass the callback function. Check here for details.

// chaining methods
inject(dispatch)
  .get('/')                   // set the request method to GET, and request URL to '/'
  .headers({ foo: 'bar' })    // set the request headers
  .query({ foo: 'bar' })      // set the query parameters
  .end((err, res) => {
    console.log(res.payload)
  })

inject(dispatch)
  .post('/')                  // set the request method to POST, and request URL to '/'
  .payload('request payload') // set the request payload
  .body('request body')       // alias for payload
  .end((err, res) => {
    console.log(res.payload)
  })

// async-await is also supported
try {
  const chain = inject(dispatch).get('/')
  const res = await chain.end()
  console.log(res.payload)
} catch (err) {
  console.log(err)
}

File uploads (multipart/form-data) or form submit (x-www-form-urlencoded) can be achieved by using form-auto-content package as shown below:

const formAutoContent = require('form-auto-content')
const fs = require('node:fs')

try {
  const form = formAutoContent({
    myField: 'hello',
    myFile: fs.createReadStream(`./path/to/file`)
  })

  const res = await inject(dispatch, {
    method: 'post',
    url: '/upload',
    ...form
  })
  console.log(res.payload)
} catch (err) {
  console.log(err)
}

This module ships with a handwritten TypeScript declaration file for TS support. The declaration exports a single namespace LightMyRequest. You can import it one of two ways:

import * as LightMyRequest from 'light-my-request'

const dispatch: LightMyRequest.DispatchFunc = function (req, res) {
  const reply = 'Hello World'
  res.writeHead(200, { 'Content-Type': 'text/plain', 'Content-Length': reply.length })
  res.end(reply)
}

LightMyRequest.inject(dispatch, { method: 'get', url: '/' }, (err, res) => {
  console.log(res.payload)
})

// or
import { inject, DispatchFunc } from 'light-my-request'

const dispatch: DispatchFunc = function (req, res) {
  const reply = 'Hello World'
  res.writeHead(200, { 'Content-Type': 'text/plain', 'Content-Length': reply.length })
  res.end(reply)
}

inject(dispatch, { method: 'get', url: '/' }, (err, res) => {
  console.log(res.payload)
})

The declaration file exports types for the following parts of the API:

API

inject(dispatchFunc[, options, callback])

Injects a fake request into an HTTP server.

Notes:

Request x 155,018 ops/sec ±0.47% (94 runs sampled)
Custom Request x 30,373 ops/sec ±0.64% (90 runs sampled)
Request With Cookies x 125,696 ops/sec ±0.29% (96 runs sampled)
Request With Cookies n payload x 114,391 ops/sec ±0.33% (97 runs sampled)
ParseUrl x 255,790 ops/sec ±0.23% (99 runs sampled)
ParseUrl and query x 194,479 ops/sec ±0.16% (99 runs sampled)

inject.isInjection(obj)

Checks if given object obj is a light-my-request Request object.

Method chaining

There are following methods you can used as chaining:

And finally you need to call end. It has the signature function (callback). If you invoke end without a callback function, the method will return a promise, thus you can:

const chain = inject(dispatch).get('/')

try {
  const res = await chain.end()
  console.log(res.payload)
} catch (err) {
  // handle error
}

// or
chain.end()
  .then(res => {
    console.log(res.payload)
  })
  .catch(err => {
    // handle error
  })

By the way, you can also use promises without calling end!

inject(dispatch)
  .get('/')
  .then(res => {
    console.log(res.payload)
  })
  .catch(err => {
    // handle error
  })

Note: The application would not respond multiple times. If you try to invoking any method after the application has responded, the application would throw an error.

Acknowledgements

This project has been forked from hapi/shot because we wanted to support Node ≥ v4 and not only Node ≥ v8. All the credits before the commit 00a2a82 goes to the hapi/shot project contributors. Since the commit db8bced the project will be maintained by the Fastify team.

License

Licensed under BSD-3-Clause.