From 9165ab36b448ee3af481b44eb520b410cef148ac Mon Sep 17 00:00:00 2001 From: Andrew Stewart Date: Wed, 7 Jan 2015 15:59:35 -0800 Subject: [PATCH] Extract API from core --- lib/api.js | 123 ------------------- lib/api/auth/basic.js | 59 --------- lib/api/routes.js | 187 ---------------------------- lib/api/ssl/server.crt | 22 ---- lib/api/ssl/server.csr | 18 --- lib/api/ssl/server.key | 27 ---- lib/cylon.js | 40 ++++-- package.json | 3 - spec/lib/api.spec.js | 179 --------------------------- spec/lib/api/auth/basic.spec.js | 130 -------------------- spec/lib/api/routes.spec.js | 211 -------------------------------- spec/lib/cylon.spec.js | 47 ++++--- spec/support/mock_request.js | 20 --- spec/support/mock_response.js | 16 --- 14 files changed, 61 insertions(+), 1021 deletions(-) delete mode 100644 lib/api.js delete mode 100644 lib/api/auth/basic.js delete mode 100644 lib/api/routes.js delete mode 100644 lib/api/ssl/server.crt delete mode 100644 lib/api/ssl/server.csr delete mode 100644 lib/api/ssl/server.key delete mode 100644 spec/lib/api.spec.js delete mode 100644 spec/lib/api/auth/basic.spec.js delete mode 100644 spec/lib/api/routes.spec.js delete mode 100644 spec/support/mock_request.js delete mode 100644 spec/support/mock_response.js diff --git a/lib/api.js b/lib/api.js deleted file mode 100644 index 8fa58e4..0000000 --- a/lib/api.js +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Cylon API - * cylonjs.com - * - * Copyright (c) 2013-2015 The Hybrid Group - * Licensed under the Apache 2.0 license. -*/ - -"use strict"; - -var fs = require("fs"), - path = require("path"); - -var express = require("express"), - bodyParser = require("body-parser"); - -var Logger = require("./logger"), - _ = require("./lodash"); - -var API = module.exports = function API(opts) { - if (opts == null) { - opts = {}; - } - - _.forEach(this.defaults, function(def, name) { - this[name] = _.has(opts, name) ? opts[name] : def; - }, this); - - this.createServer(); - - this.express.set("title", "Cylon API Server"); - - this.express.use(this.setupAuth()); - - this.express.use(bodyParser.json()); - this.express.use(bodyParser.urlencoded({ extended: true })); - - this.express.use(express["static"](__dirname + "/../node_modules/robeaux/")); - - // set CORS headers for API requests - this.express.use(function(req, res, next) { - res.set("Access-Control-Allow-Origin", this.CORS || "*"); - res.set("Access-Control-Allow-Headers", "Content-Type"); - res.set("Content-Type", "application/json"); - return next(); - }.bind(this)); - - // extracts command params from request - this.express.use(function(req, res, next) { - req.commandParams = _.values(req.body); - return next(); - }); - - // load route definitions - this.express.use("/api", require("./api/routes")); - - // error handling - this.express.use(function(err, req, res, next) { - res.status(500).json({ error: err.message || "An error occured."}); - next(); - }); -}; - -API.prototype.defaults = { - host: "127.0.0.1", - port: "3000", - auth: false, - CORS: "", - ssl: { - key: path.normalize(__dirname + "/api/ssl/server.key"), - cert: path.normalize(__dirname + "/api/ssl/server.crt") - } -}; - -API.prototype.createServer = function createServer() { - this.express = express(); - - //configure ssl if requested - if (this.ssl && typeof(this.ssl) === "object") { - var https = require("https"); - - this.server = https.createServer({ - key: fs.readFileSync(this.ssl.key), - cert: fs.readFileSync(this.ssl.cert) - }, this.express); - } else { - var str = "API using insecure connection. "; - str += "We recommend using an SSL certificate with Cylon."; - - Logger.warn(str); - this.server = this.express; - } -}; - -API.prototype.setupAuth = function setupAuth() { - var authfn = function auth(req, res, next) { next(); }; - - if (!!this.auth && typeof(this.auth) === "object" && this.auth.type) { - var type = this.auth.type, - module = "./api/auth/" + type, - filename = path.normalize(__dirname + "/" + module + ".js"), - exists = fs.existsSync(filename); - - if (exists) { - authfn = require(filename)(this.auth); - } - } - - return authfn; -}; - -API.prototype.listen = function() { - this.server.listen(this.port, this.host, null, function() { - var title = this.express.get("title"); - var protocol = this.ssl ? "https" : "http", - str; - - str = "Listening at " + protocol + "://" + this.host + ":" + this.port; - - Logger.info(title + " is now online."); - Logger.info(str); - }.bind(this)); -}; diff --git a/lib/api/auth/basic.js b/lib/api/auth/basic.js deleted file mode 100644 index 875ef03..0000000 --- a/lib/api/auth/basic.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Cylon API - Basic Auth - * cylonjs.com - * - * Copyright (c) 2013-2015 The Hybrid Group - * Licensed under the Apache 2.0 license. -*/ - -"use strict"; - -var http = require("http"); - -var unauthorized = function unauthorized(res) { - res.statusCode = 401; - res.setHeader("WWW-Authenticate", "Basic realm=\"Authorization Required\""); - res.end("Unauthorized"); -}; - -var error = function error(code, msg){ - var err = new Error(msg || http.STATUS_CODES[code]); - err.status = code; - return err; -}; - -module.exports = function(config) { - var user = config.user, - pass = config.pass; - - return function auth(req, res, next) { - var authorization = req.headers.authorization; - - if (!authorization) { - return unauthorized(res); - } - - // malformed - var parts = authorization.split(" "); - - if ("basic" !== parts[0].toLowerCase() || !parts[1]) { - return next(error(400)); - } - - authorization = parts[1]; - - // credentials - authorization = new Buffer(authorization, "base64").toString(); - authorization = authorization.match(/^([^:]+):(.+)$/); - - if (!authorization) { - return unauthorized(res); - } - - if (authorization[1] === user && authorization[2] === pass) { - return next(); - } - - return unauthorized(res); - }; -}; diff --git a/lib/api/routes.js b/lib/api/routes.js deleted file mode 100644 index be104dc..0000000 --- a/lib/api/routes.js +++ /dev/null @@ -1,187 +0,0 @@ -/* jshint maxlen: false */ - -/* - * Cylon API - Route Definitions - * cylonjs.com - * - * Copyright (c) 2013-2015 The Hybrid Group - * Licensed under the Apache 2.0 license. -*/ - -"use strict"; - -var Cylon = require("../cylon"); - -var router = module.exports = require("express").Router(); - -var eventHeaders = { - "Content-Type": "text/event-stream", - "Connection": "keep-alive", - "Cache-Control": "no-cache" -}; - -// Loads up the appropriate Robot/Device/Connection instances, if they are -// present in the route params. -var load = function load(req, res, next) { - var robot = req.params.robot, - device = req.params.device, - connection = req.params.connection; - - if (robot) { - req.robot = Cylon.robots[robot]; - if (!req.robot) { - return res.status(404).json({ - error: "No Robot found with the name " + robot - }); - } - } - - if (device) { - req.device = req.robot.devices[device]; - if (!req.device) { - return res.status(404).json({ - error: "No device found with the name " + device - }); - } - } - - if (connection) { - req.connection = req.robot.connections[connection]; - if (!req.connection) { - return res.status(404).json({ - error: "No connection found with the name " + connection - }); - } - } - - next(); -}; - -router.get("/", function(req, res) { - res.json({ MCP: Cylon.toJSON() }); -}); - -router.get("/events", function(req, res) { - res.json({ events: Cylon.events }); -}); - -router.get("/events/:event", function(req, res) { - var event = req.params.event; - - res.writeHead(200, eventHeaders); - - var writeData = function(data) { - res.write("data: " + JSON.stringify(data) + "\n\n"); - }; - - Cylon.on(event, writeData); - - res.on("close", function() { - Cylon.removeListener(event, writeData); - }); -}); - -router.get("/commands", function(req, res) { - res.json({ commands: Object.keys(Cylon.commands) }); -}); - -router.post("/commands/:command", function(req, res) { - var command = Cylon.commands[req.params.command]; - router.runCommand(req, res, Cylon, command); -}); - -router.get("/robots", function(req, res) { - res.json({ robots: Cylon.toJSON().robots }); -}); - -router.get("/robots/:robot", load, function(req, res) { - res.json({ robot: req.robot }); -}); - -router.get("/robots/:robot/commands", load, function(req, res) { - res.json({ commands: Object.keys(req.robot.commands) }); -}); - -router.all("/robots/:robot/commands/:command", load, function(req, res) { - var command = req.robot.commands[req.params.command]; - router.runCommand(req, res, req.robot, command); -}); - -router.get("/robots/:robot/events", load, function(req, res) { - res.json({ events: req.robot.events }); -}); - -router.all("/robots/:robot/events/:event", load, function(req, res) { - var event = req.params.event; - - res.writeHead(200, eventHeaders); - - var writeData = function(data) { - res.write("data: " + JSON.stringify(data) + "\n\n"); - }; - - req.robot.on(event, writeData); - - res.on("close", function() { - req.robot.removeListener(event, writeData); - }); -}); - -router.get("/robots/:robot/devices", load, function(req, res) { - res.json({ devices: req.robot.toJSON().devices }); -}); - -router.get("/robots/:robot/devices/:device", load, function(req, res) { - res.json({ device: req.device }); -}); - -router.get("/robots/:robot/devices/:device/events", load, function(req, res) { - res.json({ events: req.device.events }); -}); - -router.get("/robots/:robot/devices/:device/events/:event", load, function(req, res) { - var event = req.params.event; - - res.writeHead(200, eventHeaders); - - var writeData = function(data) { - res.write("data: " + JSON.stringify(data) + "\n\n"); - }; - - req.device.on(event, writeData); - - res.on("close", function() { - req.device.removeListener(event, writeData); - }); -}); - -router.get("/robots/:robot/devices/:device/commands", load, function(req, res) { - res.json({ commands: Object.keys(req.device.commands) }); -}); - -router.all("/robots/:robot/devices/:device/commands/:command", load, function(req, res) { - var command = req.device.commands[req.params.command]; - router.runCommand(req, res, req.device, command); -}); - -router.get("/robots/:robot/connections", load, function(req, res) { - res.json({ connections: req.robot.toJSON().connections }); -}); - -router.get("/robots/:robot/connections/:connection", load, function(req, res) { - res.json({ connection: req.connection }); -}); - -// Run an MCP, Robot, or Device command. Process the results immediately, -// or asynchronously if the result is a Promise. -router.runCommand = function(req, res, my, command) { - var result = command.apply(my, req.commandParams); - if (typeof result.then === "function") { - result - .then(function(result) {return res.json({result: result});}) - .catch(function(err) {return res.status(500).json({error: err});}); - } - else { - res.json({ result: result }); - } -}; diff --git a/lib/api/ssl/server.crt b/lib/api/ssl/server.crt deleted file mode 100644 index 985182e..0000000 --- a/lib/api/ssl/server.crt +++ /dev/null @@ -1,22 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDnDCCAoQCCQDMuSNl5mThYDANBgkqhkiG9w0BAQUFADCBjzELMAkGA1UEBhMC -VVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFDASBgNVBAcTC0xvcyBBbmdlbGVzMRkw -FwYDVQQKExBUaGUgSHlicmlkIEdyb3VwMRIwEAYDVQQDEwlsb2NhbGhvc3QxJjAk -BgkqhkiG9w0BCQEWF2N5bG9uanNAaHlicmlkZ3JvdXAuY29tMB4XDTE0MDQwMzIx -MjczM1oXDTE1MDQwMzIxMjczM1owgY8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpD -YWxpZm9ybmlhMRQwEgYDVQQHEwtMb3MgQW5nZWxlczEZMBcGA1UEChMQVGhlIEh5 -YnJpZCBHcm91cDESMBAGA1UEAxMJbG9jYWxob3N0MSYwJAYJKoZIhvcNAQkBFhdj -eWxvbmpzQGh5YnJpZGdyb3VwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC -AQoCggEBALhobzDaWlLBwPUFBZRxQt5fsvxFzkGJ8TxD8Uhp4r5P4Idqa2DKaz7q -oIob/l96t9Szcxi+nib4Cykxt7rw4mmDJFCS+9aV8+u/aFCXjEicCSPkM95f6NOD -a3JvHWeFfYeQOZq0uDS9PTccXpomtvX7ufdmYmPpDNzUcJD+FA8+5tVdtbvoF5aV -su4Ufb7CcoE9KuyeWm7UQpjaKuoYVAa/9eCHQfptwf0iPPlW4NcS5JG0CJOqyyrF -YC30MIlE6/tol8TCPFrbA4HLdtqBKMkOfhSYor12OBKazVWWqk+AkuDQFfXA/OFk -L3VGVFjnUh/RUjCoZQ+CeKAM7grUyUcCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEA -S4knU+SUXmCw+V8ZggGoLW3OelI6vMq+7ThhGeS+ge4ZkQRuqBdJxK7HytC6bysC -L9qQuJsKoEPSDoi48ml7XqRH+kXD4lTpRtDSF8WetLvIjh6lJM5I9xLmevBZYeJT -9a+gm6eS0oZBm3/cHpUJnQAw8M8wYmHB/d/WdNu7fV9m5+PBzvwhxVlVb3yaOdsH -vRh0BZU58NmFcscfV/pTSqTlp59CQGMynHsaaLiKiLje04v+b+wwob/7kuvZvQ9D -kYQkg1EnCGMFIk1Slj1GIS/ewD90JTATJa38wshg2VAQiX8sxqsUPdye/MjheWhF -hxMSOB6xNhHjBiOQRwtGtw== ------END CERTIFICATE----- diff --git a/lib/api/ssl/server.csr b/lib/api/ssl/server.csr deleted file mode 100644 index 2c78f7a..0000000 --- a/lib/api/ssl/server.csr +++ /dev/null @@ -1,18 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIIC9jCCAd4CAQAwgY8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlh -MRQwEgYDVQQHEwtMb3MgQW5nZWxlczEZMBcGA1UEChMQVGhlIEh5YnJpZCBHcm91 -cDESMBAGA1UEAxMJbG9jYWxob3N0MSYwJAYJKoZIhvcNAQkBFhdjeWxvbmpzQGh5 -YnJpZGdyb3VwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALho -bzDaWlLBwPUFBZRxQt5fsvxFzkGJ8TxD8Uhp4r5P4Idqa2DKaz7qoIob/l96t9Sz -cxi+nib4Cykxt7rw4mmDJFCS+9aV8+u/aFCXjEicCSPkM95f6NODa3JvHWeFfYeQ -OZq0uDS9PTccXpomtvX7ufdmYmPpDNzUcJD+FA8+5tVdtbvoF5aVsu4Ufb7CcoE9 -KuyeWm7UQpjaKuoYVAa/9eCHQfptwf0iPPlW4NcS5JG0CJOqyyrFYC30MIlE6/to -l8TCPFrbA4HLdtqBKMkOfhSYor12OBKazVWWqk+AkuDQFfXA/OFkL3VGVFjnUh/R -UjCoZQ+CeKAM7grUyUcCAwEAAaAhMB8GCSqGSIb3DQEJAjESExBUaGUgSHlicmlk -IEdyb3VwMA0GCSqGSIb3DQEBBQUAA4IBAQC1ap6IIWrAg6ZGmMev4Ef7qGLNSAYV -8jg5pG63AHGkZLCMw1ZcT1iZPjOZC1mBzJBy0z9C8fo0ekGvMmmQbQ40i4JgmYH4 -bmzL4+ulySgiR45DokHkRtyZOa2f2/nOcvVpIDF3EwH7L2yb4AFrCHDwfh8TKl2x -oMpy00F23vn7loDxyMkIcR8It+VL1NiJM+TJN9OzGIh0FkJHs2lbRawS30xyxIYy -z0yImdzHG5rXGlPS1aeeVue4H9DfinCICSfd0Bx9iJCqzUD98cmK8x4hVezD9r32 -/6jm88UzF2WgJvuxcQ8EQRiPGCFcLHGB2AFryTWdt6ibCW6gsoRinnvZ ------END CERTIFICATE REQUEST----- diff --git a/lib/api/ssl/server.key b/lib/api/ssl/server.key deleted file mode 100644 index 3e04ef9..0000000 --- a/lib/api/ssl/server.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAuGhvMNpaUsHA9QUFlHFC3l+y/EXOQYnxPEPxSGnivk/gh2pr -YMprPuqgihv+X3q31LNzGL6eJvgLKTG3uvDiaYMkUJL71pXz679oUJeMSJwJI+Qz -3l/o04Nrcm8dZ4V9h5A5mrS4NL09Nxxemia29fu592ZiY+kM3NRwkP4UDz7m1V21 -u+gXlpWy7hR9vsJygT0q7J5abtRCmNoq6hhUBr/14IdB+m3B/SI8+Vbg1xLkkbQI -k6rLKsVgLfQwiUTr+2iXxMI8WtsDgct22oEoyQ5+FJiivXY4EprNVZaqT4CS4NAV -9cD84WQvdUZUWOdSH9FSMKhlD4J4oAzuCtTJRwIDAQABAoIBAQCwIqgZrGXbZ88q -+On0eB4bkqK9zNsNxHjTTD35IZH+nwLhtObtI0o+ZRKD9+sGPYu6sNA9kUw0AnV+ -mktYVl6b0zPrdgjvVHkP8tnrKGVIsSkVzBEy1L7o0Dzfp3wZdeqJgltTBkxvq1T9 -/63oZRQabZ6ZzIQr09yCTLNb+iMkzxuJSbdwVAws+oKqSTVoUKY/Gee66MsJjKkr -FfqwS/tVfTFWXp9CamWz/zBDyUXnrtUhzNxE+3LPbeZ/sMq/V/f1F5jiQNjenMGl -K/qD3HyNbR0P4xWYCe/4/QLSWjsiZHg51d4Nioegii2775ya9qDvtTfUeNNdvCf6 -HMcvUIPxAoGBAOF5pSl/9VqRc1FXS+Kr8cs7DORZ6EPXKzUZL59V0NHoMgKbk+sM -JZv4TeDx26eBoecT2UYWSYMbKLl7sM9YZDJJhgwMeqa47PHk5Cua/Dpfus/pDl67 -S5FT/gyA2tpSpULml2G7YegdPnTRRNQdppcrY5v9LQ9CSPhbSAPY/FspAoGBANFf -gs6JEq46SOMBw+66svwaT2DQJNchwtCKE+2m7OdNufrtTSAVlKlNpGQQxElwYYg1 -xkEA+l7MycXIkSWOGC4JfLSRYlBgYOFzCQCME9rTkboAXn8Iru9p9vnRnfsEOUk4 -Vr8/ehzv4gcf90ZJcnPkQNu5lTkr+s/bNDjAof7vAoGARN3zvU4w8V21nCWOrwgX -jRxXHrP7RiVFNC2iJwd+BW7nP3anYkZOgmn/13HnxizI95xPY6HRCDNWZ/jIkzwL -NnTQdYOmPqAC9wsTSeJHoci1dWVYl0SbmyLNWKJOthpCEcH+gMJL8CpmdiWo4STB -SjDddrqIdb2oLfsrbslqoqkCgYAUJV6OxP25Kf6NaUQTGn/SZi2xIRYKZUM7ka2t -NlyhPQdiL6c2KR1u1Pu2bS6V6mxYEOSMqK1upcHceBoPRQbqlxsavMp69WsdBlad -aN0YNzdUcGinTIyYmNec3iCXYKaqdvNR36e+VQ6opNjEOJj8sb/T5J2JLMQrb+os -c8yinQKBgQCcww6Mr0Wnbw/E9RrwYJqheFWXjdrvbhs5leudzHknL/WXfroOccFr -cduywWcHkGq9ig0fay/3iVS5WRAlVqDOGmf60It60ZE0CEDVsHd3w9BEAeJxkq+6 -vnMZ73dIUKmOozNg0+xK7XsY2YCNdYUaFtKH/ENfsCWfu7r2Kli46A== ------END RSA PRIVATE KEY----- diff --git a/lib/cylon.js b/lib/cylon.js index 442e2cc..dd352ef 100644 --- a/lib/cylon.js +++ b/lib/cylon.js @@ -30,7 +30,7 @@ Cylon.IO = { Utils: require("./io/utils") }; -Cylon.api_instance = null; +Cylon.apiInstances = []; Cylon.robots = {}; Cylon.commands = {}; @@ -70,20 +70,42 @@ Cylon.robot = function robot(opts) { return bot; }; -// Public: Creates a new API based on passed options +// Public: Initializes an API instance based on provided options. // // Returns nothing -Cylon.api = function api(opts) { - if (typeof opts === "object") { - this.config({ api: opts }); +Cylon.api = function api(Server, opts) { + // if only passed options (or nothing), assume HTTP server + if (Server == null || _.isObject(Server) && !_.isFunction(Server)) { + opts = Server; + Server = "http"; } - var API = require("./api"); + opts = opts || {}; - var config = Utils.fetch(Config, "api", {}); + if (_.isString(Server)) { + var req = "cylon-api-" + Server; - this.api_instance = new API(config); - this.api_instance.listen(); + try { + Server = require(req); + } catch (e) { + if (e.code === "MODULE_NOT_FOUND") { + Logger.error("Cannot find the " + req + " API module."); + Logger.error( + "You may be able to install it: `npm install " + req + "`" + ); + + return; + } else { + throw e; + } + } + } + + opts.mcp = this; + + var instance = new Server(opts); + this.apiInstances.push(instance); + instance.listen(); }; // Public: Starts up the API and the robots diff --git a/package.json b/package.json index 8abfa04..2b3e0c3 100644 --- a/package.json +++ b/package.json @@ -44,9 +44,6 @@ "dependencies": { "async": "0.9.0", - "express": "4.10.5", - "body-parser": "1.10.0", - "robeaux": "0.3.0", "lodash": "2.4.1" } } diff --git a/spec/lib/api.spec.js b/spec/lib/api.spec.js deleted file mode 100644 index 7e06439..0000000 --- a/spec/lib/api.spec.js +++ /dev/null @@ -1,179 +0,0 @@ -/* jshint expr:true */ -"use strict"; - -var https = require("https"), - path = require("path"); - -var API = source("api"), - Logger = source("logger"); - -var MockRequest = require("../support/mock_request"), - MockResponse = require("../support/mock_response"); - -describe("API", function() { - var api; - - beforeEach(function() { - api = new API(); - }); - - describe("constructor", function() { - var mod; - - beforeEach(function() { - mod = new API({ - host: "0.0.0.0", - port: "1234" - }); - }); - - it("sets @express to an Express instance", function() { - expect(api.express.listen).to.be.a("function"); - }); - - it("sets default values", function() { - expect(api.host).to.be.eql("127.0.0.1"); - expect(api.port).to.be.eql("3000"); - }); - - it("overrides default values if passed to constructor", function() { - expect(mod.host).to.be.eql("0.0.0.0"); - expect(mod.port).to.be.eql("1234"); - }); - - it("sets the server title", function() { - var title = api.express.get("title"); - expect(title).to.be.eql("Cylon API Server"); - }); - }); - - describe("default", function() { - var d = API.prototype.defaults; - - it("host", function() { - expect(d.host).to.be.eql("127.0.0.1"); - }); - - it("port", function() { - expect(d.port).to.be.eql("3000"); - }); - - it("auth", function() { - expect(d.auth).to.be.eql(false); - }); - - it("CORS", function() { - expect(d.CORS).to.be.eql(""); - }); - - it("ssl", function() { - var sslDir = path.normalize(__dirname + "/../../lib/api/ssl/"); - expect(d.ssl.key).to.be.eql(sslDir + "server.key"); - expect(d.ssl.cert).to.be.eql(sslDir + "server.crt"); - }); - }); - - describe("#createServer", function() { - it("sets @express to an express server", function() { - api.express = null; - api.createServer(); - expect(api.express).to.be.a("function"); - }); - - context("if SSL is configured", function() { - it("sets @server to a https.Server instance", function() { - api.createServer(); - expect(api.server).to.be.an.instanceOf(https.Server); - }); - }); - - context("if SSL is not configured", function() { - beforeEach(function() { - api.ssl = false; - stub(Logger, "warn"); - api.createServer(); - }); - - afterEach(function() { - Logger.warn.restore(); - }); - - it("logs that the API is insecure", function() { - expect(Logger.warn).to.be.calledWithMatch("insecure connection"); - }); - - it("sets @server to @express", function() { - expect(api.server).to.be.eql(api.express); - }); - }); - }); - - describe("#setupAuth", function() { - context("when auth.type is basic", function() { - beforeEach(function() { - api.auth = { type: "basic", user: "user", pass: "pass" }; - }); - - it("returns a basic auth middleware function", function() { - var fn = api.setupAuth(), - req = new MockRequest(), - res = new MockResponse(), - next = spy(); - - var auth = "Basic " + new Buffer("user:pass").toString("base64"); - - req.headers.authorization = auth; - - fn(req, res, next); - expect(next).to.be.called; - }); - }); - - context("when auth is null", function() { - beforeEach(function() { - api.auth = null; - }); - - it("returns a pass-through middleware function", function() { - var fn = api.setupAuth(), - next = spy(); - - fn(null, null, next); - expect(next).to.be.called; - }); - }); - }); - - describe("#listen", function() { - beforeEach(function() { - // we create a plain HTTP server to avoid a log message from Node - api.ssl = false; - api.createServer(); - api.express.set("title", "Cylon API Server"); - - stub(api.server, "listen").yields(); - stub(Logger, "info"); - - api.listen(); - }); - - afterEach(function() { - api.server.listen.restore(); - Logger.info.restore(); - }); - - it("listens on the configured host and port", function() { - expect(api.server.listen).to.be.calledWith("3000", "127.0.0.1"); - }); - - context("when the server is running", function() { - it("logs that it's online and listening", function() { - var online = "Cylon API Server is now online.", - listening = "Listening at http://127.0.0.1:3000"; - - expect(Logger.info).to.be.calledWith(online); - expect(Logger.info).to.be.calledWith(listening); - }); - }); - }); -}); diff --git a/spec/lib/api/auth/basic.spec.js b/spec/lib/api/auth/basic.spec.js deleted file mode 100644 index 7779d7d..0000000 --- a/spec/lib/api/auth/basic.spec.js +++ /dev/null @@ -1,130 +0,0 @@ -/* jshint expr:true */ -"use strict"; - -var basic = source("api/auth/basic"); - -var MockRequest = require("../../../support/mock_request"), - MockResponse = require("../../../support/mock_response"); - -describe("Basic Auth", function() { - var opts = { user: "user", pass: "pass" }, - req, - res, - next; - - basic = basic(opts); - - beforeEach(function() { - req = new MockRequest(); - res = new MockResponse(); - next = spy(); - - var auth = new Buffer("user:pass", "utf8").toString("base64"); - req.headers = { authorization: "Basic " + auth }; - }); - - var checkUnauthorized = function() { - var result; - - beforeEach(function() { - result = basic(req, res, next); - }); - - it("returns false", function() { - expect(result).to.be.falsy; - }); - - it("sends a 401 error", function() { - expect(res.statusCode).to.be.eql(401); - expect(res.end).to.be.calledWith("Unauthorized"); - }); - }; - - var checkError = function() { - var result; - - beforeEach(function() { - result = basic(req, res, next); - }); - - it("triggers next with an error", function() { - expect(next).to.be.called; - }); - }; - - context("with a valid request", function() { - var result; - - beforeEach(function() { - result = basic(req, res, next); - }); - - it("returns true", function() { - expect(result).to.be.truthy; - }); - - it("doesn't modify the response", function() { - expect(res.end).to.not.be.called; - }); - }); - - context("if the user/pass don't match", function() { - beforeEach(function() { - var auth = new Buffer("bad:wrong", "utf8").toString("base64"); - req.headers = { authorization: "Basic " + auth }; - }); - - checkUnauthorized(); - }); - - context("if there is already an authorized user", function() { - var result; - - beforeEach(function() { - req.user = "user"; - result = basic(req, res, next); - }); - - it("returns true", function() { - expect(result).to.be.truthy; - }); - - it("doesn't modify the response", function() { - expect(res.end).to.not.be.called; - }); - }); - - context("if there are no authorization headers", function() { - beforeEach(function() { - delete req.headers.authorization; - }); - - checkUnauthorized(); - }); - - context("the authorization type isn't Basic", function() { - beforeEach(function() { - var auth = new Buffer("user:pass", "utf8").toString("base64"); - req.headers = { authorization: "Digest " + auth }; - }); - - checkError(); - }); - - context("the authorization header is missing content", function() { - beforeEach(function() { - req.headers = { authorization: "Basic" }; - }); - - checkError(); - }); - - context("if the authorization header isn't formatted correctly", function() { - beforeEach(function() { - var auth = new Buffer("user-pass", "utf8").toString("base64"); - req.headers = { authorization: "Basic " + auth }; - }); - - checkUnauthorized(); - }); -}); diff --git a/spec/lib/api/routes.spec.js b/spec/lib/api/routes.spec.js deleted file mode 100644 index 7d44dc3..0000000 --- a/spec/lib/api/routes.spec.js +++ /dev/null @@ -1,211 +0,0 @@ -/* jshint expr:true */ -"use strict"; - -var Cylon = source("cylon"), - router = source("api/routes"); - -var MockRequest = require("../../support/mock_request"), - MockResponse = require("../../support/mock_response"); - -function findRoute(path) { - var routes = router.stack.filter(function(m) { - return m.regexp.test(path); - }); - return routes[0]; -} - -function findFinalHandler(path) { - var handlers = findRoute(path).route.stack.map(function(m) { - return m.handle; - }); - return handlers[handlers.length - 1]; -} - -function MockPromise() { - var my = this; - my.resolve = function() { - var args = arguments; - process.nextTick(function() { - my.thenCallback.apply(null, args); - }); - return my; - }; - my.reject = function() { - var args = arguments; - process.nextTick(function() { - my.errorCallback.apply(null, args); - }); - return my; - }; - my.then = function(thenCallback) { - my.thenCallback = thenCallback; - return my; - }; - my.catch = function(errorCallback) { - my.errorCallback = errorCallback; - return my; - }; - my.deferred = my; -} - -describe("API routes", function() { - var routes = [ - ["GET", "/"], - ["GET", "/commands"], - ["POST", "/commands/command"], - ["GET", "/robots"], - ["GET", "/robots/TestBot"], - ["GET", "/robots/TestBot/commands"], - ["POST", "/robots/TestBot/commands/cmd"], - ["GET", "/robots/TestBot/devices"], - ["GET", "/robots/TestBot/devices/ping"], - ["GET", "/robots/TestBot/devices/ping/commands"], - ["POST", "/robots/TestBot/devices/ping/commands/ping"], - ["GET", "/robots/TestBot/connections"], - ["GET", "/robots/TestBot/connections/loopback"] - ]; - - routes.forEach(function(route) { - var method = route[0], - path = route[1]; - - it("defines a " + method + " route for " + path, function() { - expect(findRoute(path)).to.exist(); - }); - }); - -}); - - -describe("API commands", function() { - - var req, res; - beforeEach(function() { - Cylon.commands.ping = function() { return "pong"; }; - Cylon.commands.pingAsync = function() { - var promise = new MockPromise(); - return promise.resolve("immediate pong"); - }; - req = new MockRequest(); - res = new MockResponse(); - res.status = function(statusCode) { - res.statusCode = statusCode; - return res; - }; - req.device = { - name: "testDevice", - commands: { - announce: function(){return "im here";}, - announceAsync: function() { - var promise = new MockPromise(); - process.nextTick(function(){ - return promise.reject("sorry, sore throat"); - }); - return promise.deferred; - } - } - }; - req.robot = { - name: "fred", - commands: { - speak: function(){return "ahem";}, - speakAsync: function() { - var promise = new MockPromise(); - process.nextTick(function(){ - return promise.resolve("see ya in another cycle"); - }); - return promise.deferred; - } - }, - devices: { - testDevice: req.device - } - }; - }); - afterEach(function() { - delete Cylon.commands.ping; - delete Cylon.commands.pingAsync; - }); - - it("returns the list of MCP commands", function() { - res.json = function(obj){ - expect(obj.commands).to.exist(); - expect(obj.commands.length).to.equal(2); - expect(obj.commands[0]).to.equal("ping"); - }; - findFinalHandler("/commands")(req, res); - }); - - it("invokes an MCP command", function() { - req.params = {command:"ping"}; - res.json = function(obj){ - expect(obj.result).to.equal("pong"); - }; - findFinalHandler("/commands/ping")(req, res); - }); - - it("returns an immediate MCP async command", function() { - req.params = {command:"pingAsync"}; - res.json = function(obj){ - expect(obj.result).to.equal("immediate pong"); - }; - findFinalHandler("/commands/pingAsync")(req, res); - }); - - it("returns the list of robot commands", function() { - req.params = {robot: "fred"}; - res.json = function(obj){ - expect(obj.commands).to.exist(); - expect(obj.commands.length).to.equal(2); - expect(obj.commands[0]).to.equal("speak"); - }; - findFinalHandler("/robots/fred/commands")(req, res); - }); - - it("invokes a robot command", function() { - req.params = {robot: "fred", command:"speak"}; - res.json = function(obj){ - expect(obj.result).to.equal("ahem"); - }; - findFinalHandler("/robots/fred/commands/speak")(req, res); - }); - - it("invokes an asynchronous robot command", function() { - req.params = {robot: "fred", command:"speakAsync"}; - res.json = function(obj){ - expect(obj.result).to.equal("see ya in another cycle"); - }; - findFinalHandler("/robots/fred/commands/speakAsync")(req, res); - }); - - it("returns the list of device commands", function() { - req.params = {robot: "fred", device: "testDevice" }; - res.json = function(obj){ - expect(obj.commands).to.exist(); - expect(obj.commands.length).to.equal(2); - expect(obj.commands[0]).to.equal("announce"); - }; - var path = "/robots/fred/devices/testDevice/commands"; - findFinalHandler(path)(req, res); - }); - - it("invokes a device command", function() { - req.params = {robot: "fred", device: "testDevice", command:"announce"}; - res.json = function(obj){ - expect(obj.result).to.equal("im here"); - }; - var path = "/robots/fred/devices/testDevice/commands/announce"; - findFinalHandler(path)(req, res); - }); - - it("returns correctly for an async device command that errored", function() { - req.params = {robot: "fred", device: "testDevice", command:"announceAsync"}; - res.json = function(obj){ - expect(obj.error).to.equal("sorry, sore throat"); - expect(res.statusCode).to.equal(500); - }; - var path = "/robots/fred/devices/testDevice/commands/announceAsync"; - findFinalHandler(path)(req, res); - }); - -}); diff --git a/spec/lib/cylon.spec.js b/spec/lib/cylon.spec.js index d88ea58..cff8754 100644 --- a/spec/lib/cylon.spec.js +++ b/spec/lib/cylon.spec.js @@ -4,8 +4,7 @@ var Cylon = source("cylon"), Robot = source("robot"); -var API = source("api"), - Logger = source("logger"), +var Logger = source("logger"), Adaptor = source("adaptor"), Driver = source("driver"), Config = source("config"); @@ -24,8 +23,8 @@ describe("Cylon", function() { expect(Cylon.Driver).to.be.eql(Driver); }); - it("sets @api_instance to null by default", function() { - expect(Cylon.api_instance).to.be.eql(null); + it("sets @apiInstances to an empty array by default", function() { + expect(Cylon.apiInstances).to.be.eql([]); }); it("sets @robots to an empty object by default", function() { @@ -60,23 +59,37 @@ describe("Cylon", function() { }); describe("#api", function() { - beforeEach(function() { - stub(API.prototype, "listen"); - }); - afterEach(function() { - API.prototype.listen.restore(); + Cylon.apiInstances = []; }); - it("creates a new API instance", function() { - Cylon.api(); - expect(Cylon.api_instance).to.be.an.instanceOf(API); - }); + context("with a provided API server and opts", function() { + var API, opts, instance; - it("passes configuration to the API constructor", function() { - Cylon.config({ api: { port: "1234" }}); - Cylon.api(); - expect(Cylon.api_instance.port).to.be.eql("1234"); + beforeEach(function() { + instance = { listen: spy() }; + opts = { https: false }; + API = stub().returns(instance); + + Cylon.api(API, opts); + }); + + it("creates an API instance", function() { + expect(API).to.be.calledWithNew; + expect(API).to.be.calledWith(opts); + }); + + it("passes Cylon through to the instance as opts.mcp", function() { + expect(opts.mcp).to.be.eql(Cylon); + }); + + it("stores the API instance in @apiInstances", function() { + expect(Cylon.apiInstances).to.be.eql([instance]); + }); + + it("tells the API instance to start listening", function() { + expect(instance.listen).to.be.called; + }); }); }); diff --git a/spec/support/mock_request.js b/spec/support/mock_request.js deleted file mode 100644 index 8b760ee..0000000 --- a/spec/support/mock_request.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -var sinon = require('sinon'), - spy = sinon.spy, - stub = sinon.stub; - -// A mock version of the http.ClientRequest class -var MockRequest = module.exports = function MockRequest(opts) { - if (opts == null) { - opts = {}; - } - - this.url = "/"; - - this.headers = {}; - - for (var opt in opts) { - this[opt] = opts[opt]; - } -}; diff --git a/spec/support/mock_response.js b/spec/support/mock_response.js deleted file mode 100644 index a2b12df..0000000 --- a/spec/support/mock_response.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; - -var sinon = require('sinon'), - spy = sinon.spy, - stub = sinon.stub; - -// A mock version of http.ServerResponse to be used in tests -var MockResponse = module.exports = function MockResponse() { - this.end = spy(); - - this.headers = {}; -}; - -MockResponse.prototype.setHeader = function setHeader(name, value) { - this.headers[name] = value; -};