Skip to content

Commit

Permalink
feat: add support for custom request types (#190)
Browse files Browse the repository at this point in the history
* feat: add support for custom request types

* chore: update missing documentation item

* feat: make CustomRequest independent from Request

* chore: fix linter warnings

* feat: export CustomRequest for clients to instantiate directly

* chore: add custom request benchmark

* chore: add info on performance when using CustomRequest to readme

* fix: fix isInjection for node@10/12

Co-authored-by: KaKa <climba03003@gmail.com>

* chore: rename customRequestType options to Request

* feat: add option.Request schema validation

Co-authored-by: KaKa <climba03003@gmail.com>
  • Loading branch information
wilkmaia and climba03003 committed May 4, 2022
1 parent b52ec7f commit 2e4dcd8
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 14 deletions.
27 changes: 24 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 `Request` 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.Request`
- `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`.
- `Request` - 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,22 @@ 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 the `Request` option. That might make _light-my-request_
slower. Sample benchmark result run on an i5-8600K CPU with `Request` set to
`http.IncomingMessage`:

```
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
24 changes: 20 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ const urlSchema = {
}

const ajv = new Ajv()

ajv.addKeyword({
keyword: 'prototypedType',
validate: (_, data) =>
data && data.prototype && typeof data.prototype === 'object'
})

const schema = {
type: 'object',
properties: {
Expand Down Expand Up @@ -55,7 +62,8 @@ const schema = {
authority: { type: 'string' },
remoteAddress: { type: 'string' },
method: { type: 'string', enum: http.METHODS.concat(http.METHODS.map(toLowerCase)) },
validate: { type: 'boolean' }
validate: { type: 'boolean' },
Request: { prototypedType: true }
// payload type => any
},
additionalProperties: true,
Expand Down Expand Up @@ -100,14 +108,18 @@ function doInject (dispatchFunc, options, callback) {

const server = options.server || {}

const RequestConstructor = options.Request
? 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 +215,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 && obj.constructor && obj.constructor.name === '_CustomLMRRequest')
)
}

function toLowerCase (m) { return m.toLowerCase() }
Expand Down
35 changes: 35 additions & 0 deletions lib/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,38 @@ 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.Request]
* @param {any} [options.payload]
*/
function CustomRequest (options) {
return new _CustomLMRRequest()

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

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

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

/**
* Request
*
Expand Down Expand Up @@ -125,6 +157,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 +230,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'
},
Request: 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
56 changes: 49 additions & 7 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,38 @@ 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', Request: http.IncomingMessage }, (err, res) => {
t.error(err)
})
})

test('assert Request option has a valid prototype', (t) => {
t.plan(2)
const dispatch = function (req, res) {
t.error('should not get here')
res.writeHead(500)
res.end()
}

const MyInvalidRequest = {}

t.throws(() => {
inject(dispatch, { method: 'GET', url: 'http://example.com:8080/hello', Request: MyInvalidRequest }, () => {})
}, {})

t.throws(() => {
inject(dispatch, { method: 'GET', url: 'http://example.com:8080/hello', Request: 'InvalidRequest' }, () => {})
}, {})
})

test('passes remote address', (t) => {
t.plan(2)
const dispatch = function (req, res) {
Expand Down Expand Up @@ -424,18 +456,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, Request: http.IncomingMessage }, cb)
})

test('pipes response', (t) => {
Expand Down Expand Up @@ -877,7 +919,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 +1793,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

0 comments on commit 2e4dcd8

Please sign in to comment.