Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for custom request types #190

Merged
merged 10 commits into from
May 4, 2022
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,9 @@ The declaration file exports types for the following parts of the API:
- `InjectPayload` - a union type for valid payload types
- `isInjection` - standard light-my-request `isInjection` method
- `InjectOptions` - options object for `inject` method
- `Request` - custom light-my-request `request` object interface. Extends Node.js `stream.Readable` type
- `Request` - custom light-my-request `request` object interface. Extends Node.js
`stream.Readable` type by default. This behavior can be changed by setting
the `customRequestType` option in the `inject` method's options
- `Response` - custom light-my-request `response` object interface. Extends Node.js `http.ServerResponse` type

## API
Expand All @@ -140,7 +142,9 @@ The declaration file exports types for the following parts of the API:
Injects a fake request into an HTTP server.

- `dispatchFunc` - listener function. The same as you would pass to `Http.createServer` when making a node HTTP server. Has the signature `function (req, res)` where:
- `req` - a simulated request object. Inherits from `Stream.Readable`.
- `req` - a simulated request object. Inherits from `Stream.Readable` by
default. Optionally inherits from another class, set in
`options.customRequestType`
- `res` - a simulated response object. Inherits from node's `Http.ServerResponse`.
- `options` - request options object where:
- `url` | `path` - a string specifying the request URL.
Expand All @@ -162,6 +166,8 @@ Injects a fake request into an HTTP server.
- `server` - Optional http server. It is used for binding the `dispatchFunc`.
- `autoStart` - Automatically start the request as soon as the method
is called. It is only valid when not passing a callback. Defaults to `true`.
- `customRequestType` - Optional type from which the `request` object should
inherit instead of `stream.Readable`
- `callback` - the callback function using the signature `function (err, res)` where:
- `err` - error object
- `res` - a response object where:
Expand All @@ -178,7 +184,21 @@ Injects a fake request into an HTTP server.
- `json` - a function that parses the `application/json` response payload and returns an object. Throws if the content type does not contain `application/json`.
- `cookies` - a getter that parses the `set-cookie` response header and returns an array with all the cookies and their metadata.

Note: You can also pass a string in place of the `options` object as a shorthand for `{url: string, method: 'GET'}`.
Notes:

- You can also pass a string in place of the `options` object as a shorthand
for `{url: string, method: 'GET'}`.
- Beware when using `customRequestType`. That might make _light-my-request_
slower. Sample benchmark result run on an i5-8600K CPU:

```
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)`

Expand Down
14 changes: 11 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,18 @@ function doInject (dispatchFunc, options, callback) {

const server = options.server || {}

const RequestConstructor = options.customRequestType
? Request.CustomRequest
: Request

if (typeof callback === 'function') {
const req = new Request(options)
const req = new RequestConstructor(options)
const res = new Response(req, callback)

return makeRequest(dispatchFunc, server, req, res)
} else {
return new Promise((resolve, reject) => {
const req = new Request(options)
const req = new RequestConstructor(options)
const res = new Response(req, resolve, reject)

makeRequest(dispatchFunc, server, req, res)
Expand Down Expand Up @@ -203,7 +207,11 @@ Object.getOwnPropertyNames(Promise.prototype).forEach(method => {
})

function isInjection (obj) {
return (obj instanceof Request || obj instanceof Response)
return (
obj instanceof Request ||
obj instanceof Response ||
obj?.constructor?.name === '_CustomLMRRequest'
wilkmaia marked this conversation as resolved.
Show resolved Hide resolved
)
}

function toLowerCase (m) { return m.toLowerCase() }
Expand Down
40 changes: 40 additions & 0 deletions lib/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,43 @@ class MockSocket extends EventEmitter {
}
}

/**
* CustomRequest
*
* @constructor
* @param {Object} options
* @param {(Object|String)} options.url || options.path
* @param {String} [options.method='GET']
* @param {String} [options.remoteAddress]
* @param {Object} [options.cookies]
* @param {Object} [options.headers]
* @param {Object} [options.query]
* @param {Object} [options.customRequestType]
* @param {any} [options.payload]
*/
function CustomRequest (options) {
assert(
options.customRequestType?.prototype !== undefined,
'options.customRequestType must have a prototype'
)

return new _CustomLMRRequest()

function _CustomLMRRequest () {
Request.call(this, {
...options,
customRequestType: undefined
})

for (const fn of Object.keys(Request.prototype)) {
this.constructor.prototype[fn] = Request.prototype[fn]
}

util.inherits(this.constructor, options.customRequestType)
return this
}
}

/**
* Request
*
Expand Down Expand Up @@ -125,6 +162,7 @@ function Request (options) {
}

util.inherits(Request, Readable)
util.inherits(CustomRequest, Request)

Request.prototype.prepare = function (next) {
const payload = this._lightMyRequest.payload
Expand Down Expand Up @@ -197,3 +235,5 @@ Request.prototype.destroy = function (error) {
}

module.exports = Request
module.exports.Request = Request
module.exports.CustomRequest = CustomRequest
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"tsd": "^0.20.0"
},
"scripts": {
"benchmark": "node test/benchmark.js",
"coverage": "npm run unit -- --cov --coverage-report=html",
"lint": "standard",
"unit": "tap test/test.js test/*.test.js --100",
Expand Down
16 changes: 16 additions & 0 deletions test/benchmark.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const http = require('http')

const Benchmark = require('benchmark')
const suite = new Benchmark.Suite()
const Request = require('../lib/request')
Expand All @@ -13,6 +15,17 @@ const mockReq = {
authorization: 'granted'
}
}
const mockCustomReq = {
url: 'http://localhost',
method: 'GET',
headers: {
foo: 'bar',
'content-type': 'html',
accepts: 'json',
authorization: 'granted'
},
customRequestType: http.IncomingMessage
}
const mockReqCookies = {
url: 'http://localhost',
method: 'GET',
Expand Down Expand Up @@ -42,6 +55,9 @@ const mockReqCookiesPayload = {
suite.add('Request', function () {
new Request(mockReq)
})
.add('Custom Request', function () {
new Request.CustomRequest(mockCustomReq)
})
.add('Request With Cookies', function () {
new Request(mockReqCookies)
})
Expand Down
37 changes: 30 additions & 7 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,19 @@ test('request has rawHeaders', (t) => {
})
})

test('request inherits from custom class', (t) => {
t.plan(2)
const dispatch = function (req, res) {
t.ok(req instanceof http.IncomingMessage)
res.writeHead(200)
res.end()
}

inject(dispatch, { method: 'GET', url: 'http://example.com:8080/hello', customRequestType: http.IncomingMessage }, (err, res) => {
t.error(err)
})
})

test('passes remote address', (t) => {
t.plan(2)
const dispatch = function (req, res) {
Expand Down Expand Up @@ -424,18 +437,28 @@ test('allows ending twice', (t) => {
})

test('identifies injection object', (t) => {
t.plan(3)
const dispatch = function (req, res) {
t.plan(6)
const dispatchRequest = function (req, res) {
t.equal(inject.isInjection(req), true)
t.equal(inject.isInjection(res), true)

res.writeHead(200, { 'Content-Length': 0 })
res.end()
}

inject(dispatch, { method: 'GET', url: '/' }, (err, res) => {
t.error(err)
})
const dispatchCustomRequest = function (req, res) {
t.equal(inject.isInjection(req), true)
t.equal(inject.isInjection(res), true)

res.writeHead(200, { 'Content-Length': 0 })
res.end()
}

const options = { method: 'GET', url: '/' }
const cb = (err, res) => { t.error(err) }

inject(dispatchRequest, options, cb)
inject(dispatchCustomRequest, { ...options, customRequestType: http.IncomingMessage }, cb)
})

test('pipes response', (t) => {
Expand Down Expand Up @@ -877,7 +900,7 @@ test('this should be the server instance', t => {
res.end('hello')
}

inject(dispatch, { method: 'GET', url: 'http://example.com:8080/hello', server: server })
inject(dispatch, { method: 'GET', url: 'http://example.com:8080/hello', server })
.then(res => t.equal(res.statusCode, 200))
.catch(err => t.fail(err))
})
Expand Down Expand Up @@ -1751,7 +1774,7 @@ test('value of request url when using inject should not differ', (t) => {
res.end(req.url)
}

inject(dispatch, { method: 'GET', url: 'http://example.com:8080//hello', server: server })
inject(dispatch, { method: 'GET', url: 'http://example.com:8080//hello', server })
.then(res => { t.equal(res.body, '//hello') })
.catch(err => t.error(err))
})
Expand Down