Skip to content

Commit

Permalink
test: Add fake-registry, npm-registry-mock replacement
Browse files Browse the repository at this point in the history
  • Loading branch information
iarna committed Jun 8, 2018
1 parent 0d5251f commit 2d08866
Show file tree
Hide file tree
Showing 4 changed files with 353 additions and 12 deletions.
16 changes: 5 additions & 11 deletions test/common-tap.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ var path = require('path')

var port = exports.port = 1337
exports.registry = 'http://localhost:' + port

var fakeRegistry = require('./fake-registry.js')
exports.fakeRegistry = fakeRegistry

const ourenv = {}
ourenv.npm_config_loglevel = 'error'
ourenv.npm_config_progress = 'false'
Expand Down Expand Up @@ -145,18 +149,8 @@ exports.pendIfWindows = function (why) {
process.exit(0)
}

let mr
exports.withServer = cb => {
if (!mr) { mr = Bluebird.promisify(require('npm-registry-mock')) }
return mr({port: port++, throwOnUnmatched: true})
.tap(server => {
server.registry = exports.registry.replace(exports.port, server.port)
return cb(server)
})
.then((server) => {
server.done()
return server.close()
})
return fakeRegistry.compat().tap(cb).then(server => server.close())
}

exports.newEnv = function () {
Expand Down
149 changes: 149 additions & 0 deletions test/fake-registry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
'use strict'
const common = require('./common-tap.js')
const Bluebird = require('bluebird')
const log = require('npmlog')

const http = require('http')
const EventEmitter = require('events')
// See mock-registry.md for details

class FakeRegistry extends EventEmitter {
constructor (opts) {
if (!opts) opts = {}
super(opts)
this.mocks = {}
this.port = opts.port || common.port
this.registry = 'http://localhost:' + this.port
this.server = http.createServer()
if (!opts.keepNodeAlive) this.server.unref()
this.server.on('request', (req, res) => {
if (this.mocks[req.method] && this.mocks[req.method][req.url]) {
this.mocks[req.method][req.url](req, res)
} else {
res.statusCode = 404
res.end(JSON.stringify({error: 'not found'}))
}
log.http('fake-registry', res.statusCode || 'unknown', '→', req.method, req.url)
})
this._error = err => {
log.silly('fake-registry', err)
this.emit('error', err)
}
this._addErrorHandler()
}
reset () {
this.mocks = {}
return this
}
close () {
this.reset()
this._removeErrorHandler()
return new Promise((resolve, reject) => {
this.server.once('error', reject)
this.server.once('close', () => {
this.removeListener('error', reject)
resolve(this)
})
this.server.close()
})
}
_addErrorHandler () {
this.server.on('error', this._error)
}
_removeErrorHandler () {
if (!this._error) return
this.server.removeListener('error', this._error)
}
listen (cb) {
this._removeErrorHandler()
return this._findPort(this.port).then(port => {
this._addErrorHandler()
this.port = port
this.registry = 'http://localhost:' + port
common.port = this.port
common.registry = this.registry
return this
}).asCallback(cb)
}
_findPort (port) {
return new Bluebird((resolve, reject) => {
let onListening
const onError = err => {
this.server.removeListener('listening', onListening)
if (err.code === 'EADDRINUSE') {
return resolve(this._findPort(++port))
} else {
return reject(err)
}
}
onListening = () => {
this.server.removeListener('error', onError)
resolve(port)
}
this.server.once('error', onError)
this.server.once('listening', onListening)
this.server.listen(port)
})
}

mock (method, url, respondWith) {
log.http('fake-registry', 'mock', method, url, respondWith)
if (!this.mocks[method]) this.mocks[method] = {}
if (typeof respondWith === 'function') {
this.mocks[method][url] = respondWith
} else if (Array.isArray(respondWith)) {
const [status, body] = respondWith
this.mocks[method][url] = (req, res) => {
res.statusCode = status
if (typeof body === 'object') {
res.end(JSON.stringify(body))
} else {
res.end(String(body))
}
}
} else {
throw new Error('Invalid args, expected: mr.mock(method, url, [status, body])')
}
return this
}

// compat
done () {
this.reset()
}
filteringRequestBody () {
return this
}
post (url, matchBody) {
return this._createReply('POST', url)
}
get (url) {
return this._createReply('GET', url)
}
put (url, matchBody) {
return this._createReply('PUT', url)
}
delete (url) {
return this._createReply('DELETE', url)
}
_createReply (method, url) {
const mr = this
return {
twice: function () { return this },
reply: function (status, responseBody) {
mr.mock(method, url, [status, responseBody])
return mr
}
}
}
}

module.exports = new FakeRegistry()
module.exports.FakeRegistry = FakeRegistry
module.exports.compat = function (opts, cb) {
if (arguments.length === 1 && typeof opts === 'function') {
cb = opts
opts = {}
}
return new FakeRegistry(Object.assign({keepNodeAlive: true}, opts || {})).listen(cb)
}
198 changes: 198 additions & 0 deletions test/fake-registry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
# FakeRegistry

This is a replacement for npm-registry-mock in times where its fixtures are
not used. (Adding support for its standard fixtures is TODO, but should be
straightforward—tacks-ify them and call `mr.mock…`

# Usage

The intent is for this to be a drop in replacement for npm-registry-mock
(and by extension hock). New scripts will be better served using its native
interface, however.

# Main Interface

## Logging

All requests the mock registry receives are logged at the `http` level. You can
see these by running your tests with:

```
npm --loglevel=http run tap test/tap/name-of-test.js
```

Or directly with:

```
npm_config_loglevel=http node test/tap/name-of-test.js
```

## Construction

Ordinarily there's no reason to construct more than one FakeRegistyr object
at a time, as it can be entirely reset between tests, so best practice
would be to use its singleton.

```
const common = require('../common-tap.js')
const mr = common.mockRegistry
```

If you have need of multiple registries at the same time, you can construct
them by hand:

```
const common = require('../common-tap.js')
const FakeRegistry = common.mockRegistry.FakeRegistry
const mr = new FakeRegistry(opts)
```

## new FakeRegistry(opts)

Valid options are:

* `opts.port` is the first port to try when looking for an available port. If it
is unavialable it will be incremented until one available is found.

The default value of `port` is taken from `common.npm`.

* `opts.keepNodeAlive` will instruct FakeRegistry to not unref the
underlying server.

## mr.reset() → this

Reset all mocks to their default values. Further requests

## mr.listen() → Promise(mr)

Start listening for connections. The promise resolves when the server is
online and ready to accept connections.

`mr.port` and `mr.registry` contain the port that was actually selected.

To ease compatibility, `common` will also have its `port` and `registry`
values updated at this time. Note that this means `common.port` refers
to the port of the most recent listening server. Each server will maintain
its own `mr.port`.

Any errors emitted by the server while it was coming online will result in a
promise rejection.

## mr.mock(method, url, respondWith) → this

Adds a new route that matches `method` (should be all caps) and `url`.

`respondWith` can be:

* A function, that takes `(request, response)` as arguments and calls
[`response` methods](https://nodejs.org/api/http.html#http_class_http_serverresponse)
to do what it wants. Does not have a return value. This function may be
async (the response isn't complete till its stream completes), typically
either because you piped something into it or called `response.end()`.
* An array of `[statusCode, responseBody]`. `responseBody` may be a string or
an object. Objects are serialized with `JSON.stringify`.

## mr.close() → Promise(mr)

Calls `mr.reset()` to clear the mocks.

Calls `.close()` on the http server. The promise resolves when the http
server completes closing. Any errors while the http server was closing will
result in a rejection. If running with `keepNodeAlive` set this call
is required for node to exit the event loop.

# Events

## mr.on('error', err => { … })

Error events from the http server are forwarded to the associated fake
registry instance.

The exception to this is while the `mr.listen()` and `mr.close()` promises
are waiting to complete. Those promises capture any errors during their duration
and turn them into rejections. (Errors during those phases are very rare.)

# Compat Interface

## Differences

### Ports

You aren't guaranteed to get the port you ask for. If the port you asked
for is in use, it will be incremented until an available port is found.

`server.port` and `server.registry` contain the port that was actually selected.

For compatibility reasons:

`common.port` and `common.registry` will contain the port of the most recent
instance of FakeRegistry. Usually these there is only one instance and so
this has the same value as the per-server attributes.

This means that if your fixtures make use of the port or server address they
need to be configured _after_ you construct

### Request Bodies

Request bodies are NOT matched against. Two routes for the same URL but different
request bodies will overwrite one another.

### Call Count Assertions

That is, things like `twice()` that assert that the end point will be hit
two times are not supported. This library does not provide any assertions,
just a barebones http server.

### Default Route Behavior

If no route can be found then a `404` response will be provided.

## Construction

const common = require('../common-tap.js')
const mr = common.mockRegistry.compat

### mr(options[, callback]) → Promise(server)

Construct a new mock server. Hybrid, callback/promise constructor. Options
are `port` and `keepNodeAlive`. `keepNodeAlive` defaults to `true` for
compatibility mode and the default value of port comes from `common.port`.

### done()

Resets all of the configured mocks.

### close()

Calls `this.reset()` and `this.server.close()`. To reopen this instance use
`this.listen()`.

### filteringRequestBody()

Does nothing. Bodies are never matched when routing anyway so this is unnecessary.

### get(url) → MockReply
### delete(url) → MockReply
### post(url, body) → MockReply
### put(url, body) → MockReply

Begins to add a route for an HTTP method and URL combo. Does not actually
add it till `reply()` is called on the returned object.

Note that the body argument for post and put is always ignored.

## MockReply methods

### twice() → this

Does nothing. Call count assertions are not supported.

### reply(status, responseBody)

Actually adds the route, set to reply with the associated status and
responseBody.

Currently no mime-types are set.

If `responseBody` is `typeof === 'object'` then `JSON.stringify()` will be
called on it to serialize it, otherwise `String()` will be used.
2 changes: 1 addition & 1 deletion test/tap/audit-fix.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const BB = require('bluebird')

const common = BB.promisifyAll(require('../common-tap.js'))
const fs = require('fs')
const mr = BB.promisify(require('npm-registry-mock'))
const mr = common.fakeRegistry.compat
const path = require('path')
const rimraf = BB.promisify(require('rimraf'))
const Tacks = require('tacks')
Expand Down

0 comments on commit 2d08866

Please sign in to comment.