-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
test: Add fake-registry, npm-registry-mock replacement
- Loading branch information
Showing
4 changed files
with
353 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters