diff --git a/lib/interfaces.js b/lib/interfaces.js new file mode 100644 index 0000000..28e807b --- /dev/null +++ b/lib/interfaces.js @@ -0,0 +1,154 @@ +"use strict"; + +var fs = require("fs"); +var mqtt = require("mqtt"); +var mows = require("mows"); +var http = require("http"); +var https = require("https"); +var st = require("st"); + +var Client = require("./client"); + +module.exports = { + serverFactory: serverFactory, + + mqttFactory: mqttFactory, + mqttsFactory: mqttsFactory, + httpFactory: httpFactory, + httpsFactory: httpsFactory, + + buildWrap: buildWrap, + buildServe: buildServe, +}; + +/** + * Build internal server for the interface. + * + * @api private + * @param {Object} iface Interface description + * @param {Object} fallback Fallback values + * @param {mosca.Server} mosca Target mosca server + * @return {any.Server} Built server + */ +function serverFactory(iface, fallback, mosca) { + var factories = { + "mqtt": mqttFactory, + "mqtts": mqttsFactory, + "http": httpFactory, + "https": httpsFactory, + }; + + var type = iface.type; // no fallback + var factory = factories[type] || type; + return factory(iface, fallback, mosca); +} + +function mqttFactory(iface, fallback, mosca) { + var wrap = buildWrap(mosca); + var server = mqtt.createServer(wrap); + return server; +} + +function mqttsFactory(iface, fallback, mosca) { + var credentials = iface.credentials || fallback.credentials; + if (credentials === undefined) { + throw new Error("missing credentials for mqtts server"); + } + + var wrap = buildWrap(mosca); + var server = mqtt.createSecureServer(credentials.keyPath, credentials.certPath, wrap); + return server; +} + +function httpFactory(iface, fallback, mosca) { + var serve = buildServe(iface, mosca); + var server = http.createServer(serve); + + mosca.attachHttpServer(server); // REFACTOR? + return server; +} + +function httpsFactory(iface, fallback, mosca) { + var credentials = iface.credentials || fallback.credentials; + if (credentials === undefined) { + throw new Error("missing credentials for https server"); + } + + var serve = buildServe(iface, mosca); + var server = https.createServer({ + key: fs.readFileSync(credentials.keyPath), + cert: fs.readFileSync(credentials.certPath), + }, serve); + + mosca.attachHttpServer(server); // REFACTOR? + return server; +} + + +/** + * Create the wrapper for mqtt server to disable Nagle algorithm. + * + * @param {Object} iface Inrerface from `interfaces` + * @return {Function} Wrap function + */ +function buildWrap(mosca) { + return function wrap(connection) { + connection.stream.setNoDelay(true); + new Client(connection, mosca); // REFACTOR? + }; +} + +/** + * Create the serve logic for http server. + * + * @param {Object} iface Inrerface from `interfaces` + * @return {Function} Serve function + */ +function buildServe(iface, mosca) { + var mounts = []; + var logger = mosca.logger.child({ service: 'http bundle' }); + + if (iface.bundle) { + mounts.push(st({ + path: __dirname + "/../public", + url: "/", + dot: true, + index: false, + passthrough: true + })); + } + + if (iface.static) { + mounts.push(st({ + path: iface.static, + dot: true, + url: "/", + index: "index.html", + passthrough: true + })); + } + + return function serve(req, res) { + + logger.info({ req: req }); + + var cmounts = [].concat(mounts); + + res.on('finish', function() { + logger.info({ res: res }); + }); + + function handle() { + var mount = cmounts.shift(); + + if (mount) { + mount(req, res, handle); + } else { + res.statusCode = 404; + res.end("Not Found\n"); + } + } + + handle(); + }; +} diff --git a/lib/options.js b/lib/options.js new file mode 100644 index 0000000..198ae06 --- /dev/null +++ b/lib/options.js @@ -0,0 +1,344 @@ +"use strict"; + +var bunyan = require("bunyan"); +var extend = require("extend"); +var deepcopy = require("deepcopy"); +var jsonschema = require("jsonschema"); +var serializers = require("./serializers"); + +module.exports = { + modernize: modernize, + validate: validate, + populate: populate, + + defaultsModern: defaultsModern, + defaultsLegacy: defaultsLegacy, +}; + + +/** + * Modernize options. + * This function does not populate missing fields with defaults. + * + * @api private + * @param {Object} options Legacy options + * @return {Object} Modernized options + */ +function modernize(legacy) { + var modernized = {}; + + // "plain copyable" conserved options + var conserved = [ + "id", + "host", + "maxInflightMessages", + "stats", + "publishNewClient", + ]; + + // copy all conserved options + conserved.forEach(function (name) { + if (legacy.hasOwnProperty(name)) { + modernized[name] = legacy[name]; + } + }); + + // TODO: copy `backend` carefully + if (legacy.hasOwnProperty('backend')) { + modernized.backend = legacy.backend; + } + + // TODO: copy `ascoltatore` carefully + if (legacy.hasOwnProperty('ascoltatore')) { + modernized.ascoltatore = legacy.ascoltatore; + } + + // TODO: copy `persistence` carefully + if (legacy.hasOwnProperty('persistence')) { + modernized.persistence = legacy.persistence; + } + + // TODO: copy `logger` carefully + if (legacy.hasOwnProperty('logger')) { + modernized.logger = legacy.logger; + } + + // construct `credentials` + if (legacy.hasOwnProperty('credentials')) { + // copy as is + modernized.credentials = deepcopy(legacy.credentials); + } else if (legacy.hasOwnProperty('secure')) { + // construct from `secure` + modernized.credentials = {}; + if (legacy.secure.hasOwnProperty('keyPath')) { + modernized.credentials.keyPath = legacy.secure.keyPath; + } + if (legacy.secure.hasOwnProperty('certPath')) { + modernized.credentials.certPath = legacy.secure.certPath; + } + } // else no credentials were provided + + // construct `interfaces` + if (legacy.hasOwnProperty('interfaces')) { + // copy as is + modernized.interfaces = deepcopy(legacy.interfaces); + } else { + // construct from legacy keys + modernized.interfaces = []; + + // translate mqtt options + var mqtt_enabled = !legacy.onlyHttp && (typeof legacy.secure === 'undefined' || legacy.allowNonSecure); + if (mqtt_enabled) { + var mqtt_interface = { type: 'mqtt' }; + + if (legacy.hasOwnProperty('port')) { + mqtt_interface.port = legacy.port; + } + + modernized.interfaces.push(mqtt_interface); + } + + // translate mqtts options + var mqtts_enabled = !legacy.onlyHttp && legacy.secure; + if (mqtts_enabled) { + var mqtts_interface = { type: 'mqtts' }; + + if (legacy.secure.hasOwnProperty('port')) { + mqtts_interface.port = legacy.secure.port; + } + + modernized.interfaces.push(mqtts_interface); + } + + // translate http options + var http_enabled = !!(legacy.http); + if (http_enabled) { + var http_interface = { type: 'http' }; + + if (legacy.http.hasOwnProperty('port')) { + http_interface.port = legacy.http.port; + } + + if (legacy.http.hasOwnProperty('bundle')) { + http_interface.bundle = legacy.http.bundle; + } + + if (legacy.http.hasOwnProperty('static')) { + http_interface.static = legacy.http.static; + } + + modernized.interfaces.push(http_interface); + } + + // translate https options + var https_enabled = !!(legacy.https); + if (https_enabled) { + var https_interface = { type: 'https' }; + + if (legacy.https.hasOwnProperty('port')) { + https_interface.port = legacy.https.port; + } + + if (legacy.https.hasOwnProperty('bundle')) { + https_interface.bundle = legacy.https.bundle; + } + + if (legacy.https.hasOwnProperty('static')) { + https_interface.static = legacy.https.static; + } + + modernized.interfaces.push(https_interface); + } + + // NOTE: there are ways end up with no interfaces at all, for example + // `httpOnly: true` with undefined http and https + } + + return modernized; +} + + +/** + * Validate modern options. + * + * @api private + * @param {Object} options Modern options + * @return {jsonschema.ValidatorResult} Result of validation + */ +function validate(opts, validationOptions) { + var validator = new jsonschema.Validator(); + + // custom function type + validator.types.function = function testFunction(instance) { + return instance instanceof Function; + }; + + validator.addSchema({ + id: '/Credentials', + type: 'object', + additionalProperties: false, + properties: { + 'keyPath': { type: 'string', required: true }, + 'certPath': { type: 'string', required: true }, + } + }); + + validator.addSchema({ + id: '/Interface', + type: 'object', + properties: { + 'type': { type: ['string', 'function'], required: true }, + 'host': { type: ['string', 'null'] }, + 'port': { type: ['integer'] }, + 'credentials': { $ref: '/Credentials' }, + } + }); + + validator.addSchema({ + id: '/Options', + type: 'object', + additionalProperties: false, + properties: { + 'id': { type: 'string' }, + 'host': { type: ['string', 'null'] }, + 'interfaces': { + type: 'array', + items: { $ref: '/Interface' } + }, + 'credentials': { $ref: '/Credentials' }, + + 'backend': { type: 'object' }, // TODO + 'ascoltatore': { type: 'object' }, // TODO + 'persistence': { type: 'object' }, // TODO + 'logger': { type: 'object' }, // TODO + + 'maxInflightMessages': { type: 'integer' }, + 'stats': { type: 'boolean' }, + 'publishNewClient': { type: 'boolean' }, + } + }); + + var result = validator.validate(opts, '/Options', validationOptions); + + // check empty interfaces + if (opts.hasOwnProperty('interfaces')) { + if (opts.interfaces.length === 0) { + result.addError('no interfaces were defined'); + } + } + + // check required credentials + if (opts.hasOwnProperty('interfaces')) { + var hasCredentials = opts.hasOwnProperty('credentials'); + var reqCredentials = opts.interfaces.some(function (iface) { + var req = (iface.type === 'mqtts' || iface.type === 'https'); + var has = iface.hasOwnProperty('credentials'); + return req && !has; + }); + + if (reqCredentials && !hasCredentials) { + result.addError('one of the defiend interfaces requires credentials'); + } + } + + // TODO: check conflicting backend and ascoltatore + + return result; +} + + +/** + * Populate missing fields in modern options. + * + * @api private + * @param {Object} options Modern options + * @return {Object} Populated options + */ +function populate(opts) { + var defaults = defaultsModern(); + + // do not extend `interfaces` + if (opts.hasOwnProperty('interfaces')) { + delete defaults.interfaces; + } + var populated = extend(true, defaults, opts); + + populated.interfaces.forEach(function (iface) { + if (typeof iface.port === "undefined") { + switch (iface.type) { + case "mqtt": iface.port = 1883; break; + case "mqtts": iface.port = 8883; break; + case "http": iface.port = 3000; break; + case "https": iface.port = 3001; break; + } + } + }); + + return populated; +} + + +/** + * Construct legacy default options. + * + * @api private + * @return {Object} Legacy options + */ +function defaultsLegacy() { + return { + port: 1883, + host: null, + backend: { + json: false, + wildcardOne: '+', + wildcardSome: '#' + }, + stats: true, + publishNewClient: true, + maxInflightMessages: 1024, + logger: { + name: "mosca", + level: 40, + serializers: { + client: serializers.clientSerializer, + packet: serializers.packetSerializer, + req: bunyan.stdSerializers.req, + res: bunyan.stdSerializers.res + } + } + }; +} + + +/** + * Construct modern default options. + * + * @api private + * @return {Object} Modern options + */ +function defaultsModern() { + return { + host: null, + interfaces: [ + { type: "mqtt", port: 1883 } + ], + backend: { + json: false, + wildcardOne: '+', + wildcardSome: '#' + }, + stats: true, + publishNewClient: true, + maxInflightMessages: 1024, + logger: { + name: "mosca", + level: 40, + serializers: { + client: serializers.clientSerializer, + packet: serializers.packetSerializer, + req: bunyan.stdSerializers.req, + res: bunyan.stdSerializers.res + } + } + }; +} diff --git a/lib/serializers.js b/lib/serializers.js new file mode 100644 index 0000000..88fd4e4 --- /dev/null +++ b/lib/serializers.js @@ -0,0 +1,46 @@ +"use strict"; + +module.exports = { + clientSerializer: clientSerializer, + packetSerializer: packetSerializer +}; + +/** + * Serializises a client for Bunyan. + * + * @api private + */ +function clientSerializer(client) { + return client.id; +} + +/** + * Serializises a packet for Bunyan. + * + * @api private + */ +function packetSerializer(packet) { + var result = {}; + + if (packet.messageId) { + result.messageId = packet.messageId; + } + + if (packet.topic) { + result.topic = packet.topic; + } + + if (packet.qos) { + result.qos = packet.qos; + } + + if (packet.unsubscriptions) { + result.unsubscriptions = packet.unsubscriptions; + } + + if (packet.subscriptions) { + result.subscriptions = packet.subscriptions; + } + + return result; +} diff --git a/lib/server.js b/lib/server.js index 0018f30..2ef3e82 100644 --- a/lib/server.js +++ b/lib/server.js @@ -37,31 +37,11 @@ var extend = require("extend"); var Client = require("./client"); var Stats = require("./stats"); var shortid = require("shortid"); -var st = require("st"); -var url = require("url"); var persistence = require('./persistence'); -var defaults = { - port: 1883, - host: null, - backend: { - json: false, - wildcardOne: '+', - wildcardSome: '#' - }, - stats: true, - publishNewClient: true, - maxInflightMessages: 1024, - logger: { - name: "mosca", - level: 40, - serializers: { - client: clientSerializer, - packet: packetSerializer, - req: bunyan.stdSerializers.req, - res: bunyan.stdSerializers.res - } - } -}; +var options = require('./options'); +var interfaces = require('./interfaces'); + +var defaults = options.defaultsLegacy(); var nop = function() {}; /** @@ -72,8 +52,8 @@ var nop = function() {}; * EventEmitter. * * Options: - * - `port`, the port where to create the server. * - `host`, the IP address of the server (see http://nodejs.org/api/net.html#net_server_listen_port_host_backlog_callback). + * - `interfaces`, list of network interfaces with necessary options. * - `backend`, all the options for creating the Ascoltatore * that will power this server. * - `ascoltatore`, the ascoltatore to use (instead of `backend`). @@ -83,18 +63,23 @@ var nop = function() {}; * - `persistence`, the options for the persistence. * A sub-key `factory` is used to specify what persistence * to use. - * - `secure`, an object that includes three properties: - * - `port`, the port that will be used to open the secure server + * - `credentials`, credentials for secure connection, inclides two properties: * - `keyPath`, the path to the key * - `certPath`, the path to the certificate - * - `allowNonSecure`, starts both the secure and the unsecure sevrver. - * - `http`, an object that includes the properties: - * - `port`, the port that will be used to open the http server - * - `bundle`, serve the bundled mqtt client - * - `static`, serve a directory * - `stats`, publish the stats every 10s (default false). * - `publishNewClient`, publish message to topic "$SYS/{broker-id}/new/clients" when new client connects. * + * Interface may contain following properties: + * - `type`, name of a build-in type or a custom type factory + * - `port`, target port, overrides default port infered from `type` + * - `host`, target host, overrides + * + * Built-in interface types: + * - `mqtt`, normal mqtt, port: 1883 + * - `mqtts`, mqtt over ssl, port: 8883, requires `credentials` + * - `http`, mqtt over websocket, port: 3000 + * - `https`, mqtt over secure websocket, port: 3001, requires `credentials` + * * Events: * - `clientConnected`, when a client is connected; * the client is passed as a parameter. @@ -114,6 +99,14 @@ var nop = function() {}; * @api public */ function Server(opts, callback) { + var modernOpts = options.modernize(opts); + var validationResult = options.validate(modernOpts); + if (validationResult.errors.length > 0) { + var errMessage = validationResult.errors[0].message; + callback(new Error(errMessage)); + } + + var modernOpts = options.populate(modernOpts); if (!(this instanceof Server)) { return new Server(opts, callback); @@ -121,11 +114,27 @@ function Server(opts, callback) { EventEmitter.call(this); - this.opts = extend(true, {}, defaults, opts); + if (true) { // REFACTOR: kludge for tests that rely on options structure + this.opts = extend(true, {}, defaults, opts); + this.modernOpts = modernOpts; + + if (this.opts.secure) { + this.opts.secure.port = this.opts.secure.port || 8883; + } + if (this.opts.http) { + this.opts.http.port = this.opts.http.port || 3000; + } + if (this.opts.https) { + this.opts.https.port = this.opts.https.port || 3001; + } + } else { // REFACTOR: enable this once test are updated + this.opts = modernOpts; + } callback = callback || function() {}; - var persistenceFactory = this.opts.persistence && this.opts.persistence.factory; + // REFACTOR: partially move to options.validate and options.populate? + var persistenceFactory = this.modernOpts.persistence && this.modernOpts.persistence.factory; if (persistenceFactory) { if (typeof persistenceFactory === 'string') { var factoryName = persistenceFactory; @@ -135,7 +144,7 @@ function Server(opts, callback) { } } - this.persistence = persistenceFactory(this.opts.persistence); + this.persistence = persistenceFactory(this.modernOpts.persistence); this.persistence.wire(this); } else { this.persistence = null; @@ -145,109 +154,70 @@ function Server(opts, callback) { this.clients = {}; this.closed = false; - if (this.opts.logger.childOf) { - this.logger = this.opts.logger.childOf; - delete this.opts.logger.childOf; - delete this.opts.logger.name; - this.logger = this.logger.child(this.opts.logger); + if (this.modernOpts.logger.childOf) { + this.logger = this.modernOpts.logger.childOf; + delete this.modernOpts.logger.childOf; + delete this.modernOpts.logger.name; + this.logger = this.logger.child(this.modernOpts.logger); } else { - this.logger = bunyan.createLogger(this.opts.logger); + this.logger = bunyan.createLogger(this.modernOpts.logger); } - if(this.opts.stats) { + if(this.modernOpts.stats) { new Stats().wire(this); } var that = this; - var serveWrap = function(connection) { - // disable Nagle algorithm - connection.stream.setNoDelay(true); - new Client(connection, that); - }; - // each Server has a dummy id for logging purposes - this.id = this.opts.id || shortid.generate(); + this.id = this.modernOpts.id || shortid.generate(); - this.ascoltatore = opts.ascoltatore || ascoltatori.build(this.opts.backend); + this.ascoltatore = this.modernOpts.ascoltatore || ascoltatori.build(this.modernOpts.backend); this.ascoltatore.on("error", this.emit.bind(this)); // initialize servers list this.servers = []; - that.once("ready", function() { - callback(null, that); - }); - async.series([ - function(cb) { - that.ascoltatore.on("ready", cb); - }, - function(cb) { - var server = null; - var func = null; - if (that.opts.http) { - server = http.createServer(that.buildServe(that.opts.http)); - that.attachHttpServer(server); - that.servers.push(server); - that.opts.http.port = that.opts.http.port || 3000; - server.listen(that.opts.http.port, that.opts.host, cb); - } else { - cb(); - } + // async.series: wait for ascoltatore + function (done) { + that.ascoltatore.on("ready", done); }, - function(cb) { - var server = null; - if (that.opts.https) { - server = https.createServer({ - key: fs.readFileSync(that.opts.secure.keyPath), - cert: fs.readFileSync(that.opts.secure.certPath) - }, that.buildServe(that.opts.https)); - that.attachHttpServer(server); + + // async.series: iterate over defined interfaces, build servers and listen + function (done) { + async.eachSeries(that.modernOpts.interfaces, function (iface, dn) { + var fallback = that.modernOpts; + var host = iface.host || that.modernOpts.host; + var port = iface.port || that.modernOpts.port; + + var server = interfaces.serverFactory(iface, fallback, that); that.servers.push(server); - that.opts.https.port = that.opts.https.port || 3001; - server.listen(that.opts.https.port, that.opts.host, cb); - } else { - cb(); - } + server.maxConnections = 100000; + server.listen(port, host, dn); + }, done); }, - function(cb) { - var server = null; - if (that.opts.secure && !that.opts.onlyHttp) { - server = mqtt.createSecureServer(that.opts.secure.keyPath, that.opts.secure.certPath, serveWrap); - that.servers.push(server); - that.opts.secure.port = that.opts.secure.port || 8883; - server.listen(that.opts.secure.port, that.opts.host, cb); - } else { - cb(); - } - }, function(cb) { - if ((typeof that.opts.secure === 'undefined' || that.opts.allowNonSecure) && !that.opts.onlyHttp) { - var server = mqtt.createServer(serveWrap); - that.servers.push(server); - server.listen(that.opts.port, that.opts.host, cb); - } else { - cb(); - } - }, function(cb) { - var logInfo = { - port: (that.opts.onlyHttp ? undefined : that.opts.port), - securePort: (that.opts.secure || {}).port, - httpPort: (that.opts.http || {}).port, - httpsPort: (that.opts.https || {}).port - }; - that.logger.info(logInfo, "server started"); + // async.series: log startup information + function (done) { + var logInfo = {}; - that.servers.forEach(function(server) { - server.maxConnections = 100000; + that.modernOpts.interfaces.forEach(function (iface) { + var name = iface.type; + if (typeof name !== "string") { + name = iface.type.name; + } + logInfo[name] = iface.port; }); + + that.logger.info(logInfo, "server started"); that.emit("ready"); + done(null); } ]); that.on("clientConnected", function(client) { - if(that.opts.publishNewClient) { + if(that.modernOpts.publishNewClient) { that.publish({ topic: "$SYS/" + that.id + "/new/clients", payload: client.id @@ -257,6 +227,10 @@ function Server(opts, callback) { this.clients[client.id] = client; }); + that.once("ready", function() { + callback(null, that); + }); + that.on('ready', function() { that.ascoltatore.subscribe( "$SYS/+/new/clients", @@ -326,13 +300,13 @@ Server.prototype.publish = function publish(packet, client, callback) { retain: packet.retain }; - var options = { + var opts = { qos: packet.qos, messageId: newPacket.messageId }; if (client) { - options.clientId = client.id; + opts.clientId = client.id; } that.storePacket(newPacket, function() { @@ -344,7 +318,7 @@ Server.prototype.publish = function publish(packet, client, callback) { that.ascoltatore.publish( packet.topic, packet.payload, - options, + opts, function() { that.published(packet, client, function() { logger.debug({ packet: packet }, "published packet"); @@ -585,62 +559,6 @@ Server.prototype.attachHttpServer = function(server) { }); }; -/** - * Create the serve logic for an http server. - * - * @api public - * @param {Object} opts The same options of the constructor's - * options, http or https. - */ -Server.prototype.buildServe = function(opts) { - var mounts = []; - var logger = this.logger.child({ service: 'http bundle' }); - - if (opts.bundle) { - mounts.push(st({ - path: __dirname + "/../public", - url: "/", - dot: true, - index: false, - passthrough: true - })); - } - - if (opts.static) { - mounts.push(st({ - path: opts.static, - dot: true, - url: "/", - index: "index.html", - passthrough: true - })); - } - - return function serve(req, res) { - - logger.info({ req: req }); - - var cmounts = [].concat(mounts); - - res.on('finish', function() { - logger.info({ res: res }); - }); - - function handle() { - var mount = cmounts.shift(); - - if (mount) { - mount(req, res, handle); - } else { - res.statusCode = 404; - res.end("Not Found\n"); - } - } - - handle(); - }; -}; - Server.prototype.nextDedupId = function() { return this._dedupId++; }; @@ -648,43 +566,3 @@ Server.prototype.nextDedupId = function() { Server.prototype.generateUniqueId = function() { return shortid.generate(); }; - -/** - * Serializises a client for Bunyan. - * - * @api private - */ -function clientSerializer(client) { - return client.id; -} - -/** - * Serializises a packet for Bunyan. - * - * @api private - */ -function packetSerializer(packet) { - var result = {}; - - if (packet.messageId) { - result.messageId = packet.messageId; - } - - if (packet.topic) { - result.topic = packet.topic; - } - - if (packet.qos) { - result.qos = packet.qos; - } - - if (packet.unsubscriptions) { - result.unsubscriptions = packet.unsubscriptions; - } - - if (packet.subscriptions) { - result.subscriptions = packet.subscriptions; - } - - return result; -} diff --git a/package.json b/package.json index 5fecd79..e37a177 100644 --- a/package.json +++ b/package.json @@ -73,8 +73,10 @@ "browserify": "~5.9.1", "bunyan": "~1.0.0", "commander": "~2.3.0", + "deepcopy": "^0.3.3", "extend": "~1.3.0", "json-buffer": "~2.0.7", + "jsonschema": "^1.0.0", "level-sublevel": "~5.2.0", "level-ttl": "~0.6.1", "levelup": "~0.18.6", diff --git a/test/options.js b/test/options.js new file mode 100644 index 0000000..2e7cf5e --- /dev/null +++ b/test/options.js @@ -0,0 +1,595 @@ +var options = require("../lib/options"); + +var legacyKeys = [ + "port", + "secure", + "http", + "https", + "allowNonSecure", + "onlyHttp" +]; + +var deeplegacy = { + port: 1883, + host: null, + secure: { + port: 8883, + keyPath: "path/to/key", + certPath: "path/to/cert" + }, + http: { + port: 3000, + bundle: true, + static: "path/to/static" + }, + https: { + port: 3001, + bundle: true, + static: "path/to/static" + }, + allowNonSecure: false, + onlyHttp: false +}; + +describe("mocha.options", function () { + + describe("modern defaults", function () { + + it("should not contain legacy keys", function () { + var modern = options.defaultsModern(); + + legacyKeys.forEach(function (key) { + expect(modern).to.not.have.property(key); + }); + }); + + it("should contain fallback host", function () { + var modern = options.defaultsModern(); + + expect(modern).to.have.property("host"); + expect(modern.host).to.be.equal(null); + }); + + it("should contain single mqtt interface", function () { + var modern = options.defaultsModern(); + + expect(modern).to.have.property("interfaces"); + expect(modern.interfaces).to.be.deep.equal( + [ + { type: "mqtt", port: 1883 } + ] + ); + }); + + it("should not contain credentials", function () { + var modern = options.defaultsModern(); + expect(modern).to.not.have.property("credentials"); + }); + + }); + + describe("modernize", function () { + + it("should not try to change passed options", function () { + var legacy = options.defaultsLegacy(); // necessary + Object.freeze(legacy); + + var fn = function () { var modern = options.modernize(legacy); }; + expect(fn).to.not.throw(TypeError); + }); + + it("should correctly modernize legacy defaults", function () { + var legacy = options.defaultsLegacy(); + var modern = options.defaultsModern(); + var modernized = options.modernize(legacy); + + expect(modernized).to.be.deep.equal(modern); + }); + + it("should change {} into a signle mqtt interface", function () { + var legacy = {}; + var modernized = options.modernize(legacy); + + expect(modernized).to.be.deep.equal({ + interfaces: [ + { type: 'mqtt' }, + ] + }); + }); + + it("should not change modern defaults", function () { + var modern = options.defaultsModern(); + var modernized = options.modernize(modern); + + expect(modernized).to.be.deep.equal(modern); + }); + + it("should remove legacy parameters", function () { + var modernized = options.modernize(deeplegacy); + + legacyKeys.forEach(function (key) { + expect(modernized).to.not.have.property(key); + }); + }); + + it("should not override or affect defined `interfaces`", function () { + var legacy = { + interfaces: [] + }; + + var modernized = options.modernize(legacy); + + expect(modernized).to.have.property("interfaces"); + expect(modernized.interfaces).to.be.deep.equal([]); + }); + + it("should not override or affect defined `credentials`", function () { + var legacy = { + secure: { + keyPath: "legacy/path", + certPath: "legacy/path", + }, + credentials: { + keyPath: "modern/path", + certPath: "modern/path", + } + }; + + var modernized = options.modernize(legacy); + + expect(modernized).to.not.have.property("secure"); + expect(modernized).to.have.property("credentials"); + expect(modernized.credentials).to.be.deep.equal({ + keyPath: "modern/path", + certPath: "modern/path", + }); + }); + + it("should not break custom interface type", function () { + var factory = function () {}; // mock + + var legacy = { + host: "localhost", + interfaces: [ + { type: factory, port: 1234 }, + ] + }; + + var modernized = options.modernize(legacy); + + expect(modernized).to.have.property("interfaces"); + expect(modernized.interfaces).to.be.deep.equal([ + { type: factory, port: 1234 }, + ]); + }); + + it("should not override custom host, ports and credentials", function () { + var credentials = { + keyPath: "path/to/key", + certPath: "path/to/cert", + }; + + var modern = { + host: "localhost", + interfaces: [ + { type: "mqtt", host: "::", port: 8080, credentials: credentials }, + { type: "mqtts", host: "[::]", port: 8081, credentials: credentials }, + { type: "http", host: "127.0.0.1", port: 8082, credentials: credentials }, + { type: "https", host: "0.0.0.0", port: 8083, credentials: credentials }, + ] + }; + + var populated = options.modernize(modern); + + expect(populated).to.have.property("interfaces"); + expect(populated.interfaces).to.be.deep.equal([ + { type: "mqtt", host: "::", port: 8080, credentials: credentials }, + { type: "mqtts", host: "[::]", port: 8081, credentials: credentials }, + { type: "http", host: "127.0.0.1", port: 8082, credentials: credentials }, + { type: "https", host: "0.0.0.0", port: 8083, credentials: credentials }, + ]); + }); + + describe("sample configurations", function () { + + it("should correctly modernize mqtt configuration", function () { + var legacy = { + port: 1883, + host: "localhost" + }; + var modernized = options.modernize(legacy); + var result = options.validate(modernized); + expect(result.errors).to.be.deep.equal([]); + + expect(modernized).to.have.property("host"); + expect(modernized.host).to.be.equal("localhost"); + + expect(modernized).to.not.have.property("port"); + expect(modernized).to.have.property("interfaces"); + expect(modernized.interfaces).to.be.deep.equal([ + { type: "mqtt", port: 1883 }, // port was specified + ]); + }); + + it("should correctly modernize mqtts configuration", function () { + var credentials = { + keyPath: "path/to/key", + certPath: "path/to/cert", + }; + + var legacy = { + host: "127.0.0.1", + secure: { + port: 8883, + keyPath: "path/to/key", + certPath: "path/to/cert" + } + }; + + var modernized = options.modernize(legacy); + var result = options.validate(modernized); + expect(result.errors).to.be.deep.equal([]); + + expect(modernized).to.have.property("host"); + expect(modernized.host).to.be.equal("127.0.0.1"); + + expect(modernized).to.not.have.property("secure"); + expect(modernized).to.have.property("credentials"); + expect(modernized.credentials).to.be.deep.equal(credentials); + + expect(modernized).to.have.property("interfaces"); + expect(modernized.interfaces).to.be.deep.equal([ + { type: "mqtts", port: 8883 }, // port was specified + ]); + }); + + it("should correctly modernize mqtt+mqtts configuration", function () { + var credentials = { + keyPath: "path/to/key", + certPath: "path/to/cert" + }; + + var legacy = { + host: "localhost", + secure: { + port: 8883, + keyPath: "path/to/key", + certPath: "path/to/cert" + }, + allowNonSecure: true + }; + + var modernized = options.modernize(legacy); + var result = options.validate(modernized); + expect(result.errors).to.be.deep.equal([]); + + expect(modernized).to.have.property("host"); + expect(modernized.host).to.be.equal("localhost"); + + expect(modernized).to.not.have.property("secure"); + expect(modernized).to.have.property("credentials"); + expect(modernized.credentials).to.be.deep.equal(credentials); + + expect(modernized).to.have.property("interfaces"); + expect(modernized.interfaces).to.be.deep.equal([ + { type: "mqtt" }, // port was not specified + { type: "mqtts", port: 8883 }, // port was specified + ]); + }); + + it("should correctly modernize mqtt+http configuration", function () { + var legacy = { + host: "localhost", + http: { + port: 8000, + bundle: true, + static: "path/to/static", + } + }; + + var modernized = options.modernize(legacy); + var result = options.validate(modernized); + expect(result.errors).to.be.deep.equal([]); + + expect(modernized).to.have.property("host"); + expect(modernized.host).to.be.equal("localhost"); + + expect(modernized).to.not.have.property("http"); + + expect(modernized).to.have.property("interfaces"); + expect(modernized.interfaces).to.be.deep.equal([ + { type: "mqtt" }, // port was not specified + { type: "http", // port was specified + port: 8000, + bundle: true, + static: "path/to/static" }, + ]); + }); + + it("should correctly modernize mqtts+https configuration", function () { + var credentials = { + keyPath: "path/to/key", + certPath: "path/to/cert" + }; + + var legacy = { + host: "localhost", + secure: { + port: 9000, + keyPath: "path/to/key", + certPath: "path/to/cert" + }, + https: { + port: 8001, + bundle: true, + static: "path/to/static" + } + }; + + var modernized = options.modernize(legacy); + var result = options.validate(modernized); + expect(result.errors).to.be.deep.equal([]); + + expect(modernized).to.have.property("host"); + expect(modernized.host).to.be.equal("localhost"); + + expect(modernized).to.not.have.property("secure"); + expect(modernized).to.have.property("credentials"); + expect(modernized.credentials).to.be.deep.equal(credentials); + + expect(modernized).to.not.have.property("https"); + + expect(modernized).to.have.property("interfaces"); + expect(modernized.interfaces).to.be.deep.equal([ + { type: "mqtts", port: 9000 }, // port was specified + { type: "https", // port was specified + port: 8001, + bundle: true, + static: "path/to/static" }, + ]); + }); + + it("should correctly modernize http-only configuration", function () { + var legacy = { + host: "localhost", + onlyHttp: true, + http: { + bundle: true, + static: "path/to/static" + } + }; + + var modernized = options.modernize(legacy); + var result = options.validate(modernized); + expect(result.errors).to.be.deep.equal([]); + + expect(modernized).to.have.property("host"); + expect(modernized.host).to.be.equal("localhost"); + + expect(modernized).to.not.have.property("http"); + expect(modernized).to.not.have.property("onlyHttp"); + + expect(modernized).to.have.property("interfaces"); + expect(modernized.interfaces).to.be.deep.equal([ + { type: "http", // port was not specified + bundle: true, + static: "path/to/static" }, + ]); + }); + + it("should correctly modernize https-only configuration", function () { + var credentials = { + keyPath: "path/to/key", + certPath: "path/to/cert" + }; + + var legacy = { + host: "localhost", + onlyHttp: true, // secure is used only for credentials + secure: { + keyPath: "path/to/key", + certPath: "path/to/cert" + }, + https: { + port: 8001, + bundle: true, + static: "path/to/static" + } + }; + + var modernized = options.modernize(legacy); + var result = options.validate(modernized); + expect(result.errors).to.be.deep.equal([]); + + expect(modernized).to.have.property("host"); + expect(modernized.host).to.be.equal("localhost"); + + expect(modernized).to.not.have.property("https"); + expect(modernized).to.not.have.property("onlyHttp"); + + expect(modernized).to.not.have.property("secure"); + expect(modernized).to.have.property("credentials"); + expect(modernized.credentials).to.be.deep.equal(credentials); + + expect(modernized).to.have.property("interfaces"); + expect(modernized.interfaces).to.be.deep.equal([ + { type: "https", // port was specified + port: 8001, + bundle: true, + static: "path/to/static" }, + ]); + }); + + it("should correctly modernize http+https configuration", function () { + var credentials = { + keyPath: "path/to/key", + certPath: "path/to/cert" + }; + + var legacy = { + host: "localhost", + onlyHttp: true, // secure is used only for credentials + secure: { + keyPath: "path/to/key", + certPath: "path/to/cert" + }, + http: { + port: 8000, + bundle: true, + static: "path/to/static" + }, + https: { + port: 8001, + bundle: true, + static: "path/to/static" + } + }; + + var modernized = options.modernize(legacy); + var result = options.validate(modernized); + expect(result.errors).to.be.deep.equal([]); + + expect(modernized).to.have.property("host"); + expect(modernized.host).to.be.equal("localhost"); + + expect(modernized).to.not.have.property("http"); + expect(modernized).to.not.have.property("https"); + expect(modernized).to.not.have.property("onlyHttp"); + + expect(modernized).to.not.have.property("secure"); + expect(modernized).to.have.property("credentials"); + expect(modernized.credentials).to.be.deep.equal(credentials); + + expect(modernized).to.have.property("interfaces"); + expect(modernized.interfaces).to.be.deep.equal([ + { type: "http", // port was specified + port: 8000, + bundle: true, + static: "path/to/static" }, + { type: "https", // port was specified + port: 8001, + bundle: true, + static: "path/to/static" } + ]); + }); + + it("should correctly modernize complex configuration", function () { + var credentials = { + keyPath: "path/to/key", + certPath: "path/to/cert" + }; + + var legacy = { + port: 1883, + host: "127.0.0.1", + secure: { + port: 8883, + keyPath: "path/to/key", + certPath: "path/to/cert" + }, + http: { + port: 3000, + bundle: true, + static: "path/to/static" + }, + https: { + port: 3001, + bundle: true, + static: "path/to/static" + }, + onlyHttp: false, + allowNonSecure: true + }; + + var modernized = options.modernize(legacy); + var result = options.validate(modernized); + expect(result.errors).to.be.deep.equal([]); + + expect(modernized).to.not.have.property("port"); + + expect(modernized).to.have.property("host"); + expect(modernized.host).to.be.equal("127.0.0.1"); + + expect(modernized).to.not.have.property("http"); + expect(modernized).to.not.have.property("https"); + expect(modernized).to.not.have.property("onlyHttp"); + expect(modernized).to.not.have.property("allowNonSecure"); + + expect(modernized).to.not.have.property("secure"); + expect(modernized).to.have.property("credentials"); + expect(modernized.credentials).to.be.deep.equal(credentials); + + expect(modernized).to.have.property("interfaces"); + expect(modernized.interfaces).to.be.deep.equal([ + { type: "mqtt", port: 1883 }, // port was specified + { type: "mqtts", port: 8883 }, // port was specified + { type: "http", // port was specified + port: 3000, + bundle: true, + static: "path/to/static" }, + { type: "https", // port was specified + port: 3001, + bundle: true, + static: "path/to/static" } + ]); + }); + + }); + + }); + + describe("populate", function () { + + it("should turn {} into modern defaults", function () { + var modern = {}; + var populated = options.populate(modern); + var defmodern = options.defaultsModern(); + expect(populated).to.be.deep.equal(defmodern); + }); + + it("should not change modern defaults", function () { + var defmodern = options.defaultsModern(); + var populated = options.populate(defmodern); + expect(populated).to.be.deep.equal(defmodern); + }); + + it("should populate default ports", function () { + var modern = { + interfaces: [ + { type: "mqtt" }, + { type: "mqtts" }, + { type: "http" }, + { type: "https" } + ] + }; + + var populated = options.populate(modern); + expect(populated.interfaces).to.be.deep.equal([ + { type: "mqtt", port: 1883 }, + { type: "mqtts", port: 8883 }, + { type: "http", port: 3000 }, + { type: "https", port: 3001 } + ]); + }); + + }); + + describe("validate", function () { + + it("should not complain to modern defaults", function () { + var modern = options.defaultsModern(); + var result = options.validate(modern); + + expect(result.errors).to.be.deep.equal([]); + }); + + it("should not complain to modernized options", function () { + var modernized = options.modernize(deeplegacy); + var result = options.validate(modernized); + + expect(result.errors).to.be.deep.equal([]); + }); + + }); + +});