Merge pull request #255 from hybridgroup/feature/extract-out-api

Extract API from core
This commit is contained in:
Ron Evans 2015-01-08 11:22:59 -08:00
commit 25f12345b9
14 changed files with 61 additions and 1021 deletions

View File

@ -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));
};

View File

@ -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);
};
};

View File

@ -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 });
}
};

View File

@ -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-----

View File

@ -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-----

View File

@ -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-----

View File

@ -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

View File

@ -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"
}
}

View File

@ -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);
});
});
});
});

View File

@ -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();
});
});

View File

@ -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);
});
});

View File

@ -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;
beforeEach(function() {
instance = { listen: spy() };
opts = { https: false };
API = stub().returns(instance);
Cylon.api(API, opts);
});
it("passes configuration to the API constructor", function() {
Cylon.config({ api: { port: "1234" }});
Cylon.api();
expect(Cylon.api_instance.port).to.be.eql("1234");
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;
});
});
});

View File

@ -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];
}
};

View File

@ -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;
};