1'use strict' 2const common = require('./common-tap.js') 3const Bluebird = require('bluebird') 4const silentLogger = { 5 http: () => {}, 6 silly: () => {} 7} 8 9const log = process.env.TAP_CHILD_ID 10 ? silentLogger 11 : require('npmlog') 12 13const http = require('http') 14const EventEmitter = require('events') 15// See fake-registry.md for details 16 17class FakeRegistry extends EventEmitter { 18 constructor (opts) { 19 if (!opts) opts = {} 20 super(opts) 21 this.mocks = {} 22 this.port = opts.port || common.port 23 this.registry = 'http://localhost:' + this.port 24 this.server = http.createServer() 25 if (!opts.keepNodeAlive) this.server.unref() 26 this.server.on('request', (req, res) => { 27 if (this.mocks[req.method] && this.mocks[req.method][req.url]) { 28 this.mocks[req.method][req.url](req, res) 29 } else { 30 res.statusCode = 404 31 res.end(JSON.stringify({error: 'not found'})) 32 } 33 log.http('fake-registry', res.statusCode || 'unknown', '→', req.method, req.url) 34 }) 35 this._error = err => { 36 log.silly('fake-registry', err) 37 this.emit('error', err) 38 } 39 this._addErrorHandler() 40 } 41 reset () { 42 this.mocks = {} 43 return this 44 } 45 close () { 46 this.reset() 47 this._removeErrorHandler() 48 return new Promise((resolve, reject) => { 49 this.server.once('error', reject) 50 this.server.once('close', () => { 51 this.removeListener('error', reject) 52 resolve(this) 53 }) 54 this.server.close() 55 }) 56 } 57 _addErrorHandler () { 58 this.server.on('error', this._error) 59 } 60 _removeErrorHandler () { 61 if (!this._error) return 62 this.server.removeListener('error', this._error) 63 } 64 listen (cb) { 65 this._removeErrorHandler() 66 return this._findPort(this.port).then(port => { 67 this._addErrorHandler() 68 this.port = port 69 this.registry = 'http://localhost:' + port 70 common.port = this.port 71 common.registry = this.registry 72 return this 73 }).asCallback(cb) 74 } 75 _findPort (port) { 76 return new Bluebird((resolve, reject) => { 77 let onListening 78 const onError = err => { 79 this.server.removeListener('listening', onListening) 80 if (err.code === 'EADDRINUSE') { 81 return resolve(this._findPort(++port)) 82 } else { 83 return reject(err) 84 } 85 } 86 onListening = () => { 87 this.server.removeListener('error', onError) 88 resolve(port) 89 } 90 this.server.once('error', onError) 91 this.server.once('listening', onListening) 92 this.server.listen(port) 93 }) 94 } 95 96 mock (method, url, respondWith) { 97 log.http('fake-registry', 'mock', method, url, respondWith) 98 if (!this.mocks[method]) this.mocks[method] = {} 99 if (typeof respondWith === 'function') { 100 this.mocks[method][url] = respondWith 101 } else if (Array.isArray(respondWith)) { 102 const [status, body] = respondWith 103 this.mocks[method][url] = (req, res) => { 104 res.statusCode = status 105 if (typeof body === 'object') { 106 res.end(JSON.stringify(body)) 107 } else { 108 res.end(String(body)) 109 } 110 } 111 } else { 112 throw new Error('Invalid args, expected: mr.mock(method, url, [status, body])') 113 } 114 return this 115 } 116 117 // compat 118 done () { 119 this.reset() 120 } 121 filteringRequestBody () { 122 return this 123 } 124 post (url, matchBody) { 125 return this._createReply('POST', url) 126 } 127 get (url) { 128 return this._createReply('GET', url) 129 } 130 put (url, matchBody) { 131 return this._createReply('PUT', url) 132 } 133 delete (url) { 134 return this._createReply('DELETE', url) 135 } 136 _createReply (method, url) { 137 const mr = this 138 return { 139 twice: function () { return this }, 140 reply: function (status, responseBody) { 141 mr.mock(method, url, [status, responseBody]) 142 return mr 143 } 144 } 145 } 146} 147 148module.exports = new FakeRegistry() 149module.exports.FakeRegistry = FakeRegistry 150module.exports.compat = function (opts, cb) { 151 if (arguments.length === 1 && typeof opts === 'function') { 152 cb = opts 153 opts = {} 154 } 155 return new FakeRegistry(Object.assign({keepNodeAlive: true}, opts || {})).listen(cb) 156} 157