From 9bfcaa9645e99861cf28b898e50cd71cbdbdd783 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Sun, 3 Jun 2012 22:14:01 -0700 Subject: [PATCH] added socket session support --- TODO.md | 17 +- lib/client/index.js | 4 +- lib/db.js | 104 +++++++++- lib/http.js | 31 --- test/http-server.unit.js => lib/package.js | 0 lib/resources/collection.js | 12 +- lib/router.js | 11 +- lib/server.js | 165 ++++++++++++++++ lib/session.js | 212 +++++++++++++++++++-- lib/util/http.js | 9 + lib/util/uuid.js | 111 +++++++++++ package.json | 3 +- test/collection.unit.js | 10 +- test/db.unit.js | 52 ++++- test/server.unit.js | 56 ++++++ test/sessions.unit.js | 142 ++++++++++++++ test/support.js | 14 +- test/util.unit.js | 14 ++ 18 files changed, 874 insertions(+), 93 deletions(-) delete mode 100644 lib/http.js rename test/http-server.unit.js => lib/package.js (100%) create mode 100644 lib/server.js create mode 100644 lib/util/http.js create mode 100644 lib/util/uuid.js create mode 100644 test/server.unit.js create mode 100644 test/util.unit.js diff --git a/TODO.md b/TODO.md index 6a764a4..127da0a 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,10 @@ # high - - repl - - [x] events should support io - - event io should be wrappable in `set(key, fn)` so it only runs when `key` is selected + + - server init + - [x] var server = new Server(config); + - [x] server.defineResource({path: '/todos', ...}) + - config + - $ dpd create hello-world - session w/ socket.io connection - user collection - file system / file store @@ -9,8 +12,7 @@ - json body parsing - resource crud / resource http api - dashboard / dev integration - - client lib / client project - - integration test port + - http client - cli - docs - separate modules / projects @@ -18,13 +20,16 @@ # medium - travis ci + - repl + - socket client - dates should be sortable - dates should be JS date objects in events - dates should be transfered as JSON dates over http + - event io should be wrappable in `set(key, fn)` so it only runs when `key` is selected # low - - _id should be id + - [x] _id should be id - /__dashboard should be /dashboard - /resources should be /__resources diff --git a/lib/client/index.js b/lib/client/index.js index a75806c..23fe531 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -105,7 +105,7 @@ ClientStore.prototype.first = function (query, fn) { * db * .connect({host: 'localhost', port: 27015, name: 'test'}) * .createStore('testing-store') - * .update({_id: ''}, fn) + * .update({id: ''}, fn) * * @param {Object} query * @param {Object} object @@ -124,7 +124,7 @@ ClientStore.prototype.update = function (query, object, fn) { * db * .connect({host: 'localhost', port: 27015, name: 'test'}) * .createStore('testing-store') - * .remove({_id: ''}, fn) + * .remove({id: ''}, fn) * * @param {Object} query * @param {Function} callback(err, obj) diff --git a/lib/db.js b/lib/db.js index 63fe709..abf3d11 100644 --- a/lib/db.js +++ b/lib/db.js @@ -1,7 +1,8 @@ var db = module.exports = {} , util = require('util') , EventEmitter = require('events').EventEmitter - , mongodb = require('mongodb'); + , mongodb = require('mongodb') + , uuid = require('./util/uuid'); /** * Create a new database connection with the given options. You can start making @@ -71,7 +72,7 @@ Db.prototype.open = function (fn) { var self = this , mdb = new mongodb.Db(this.options.name, new mongodb.Server(this.options.host, this.options.port)); - self.connecting = true; + self.connecting = true; self._mdb = mdb; mdb.open(function (err) { self.connecting = false; @@ -80,6 +81,17 @@ Db.prototype.open = function (fn) { }) } +/** + * Drop the underlying database. + * + * @param {Function} callback + * @api private + */ + +Db.prototype.drop = function (fn) { + this._mdb.dropDatabase(fn); +} + /** * Create a new database store (eg. a collection). * @@ -110,6 +122,7 @@ function Store(namespace, db) { this.namespace = namespace; this._db = db; } +module.exports.Store = Store; function collection(store, fn) { var db = store._db @@ -121,7 +134,7 @@ function collection(store, fn) { mdb.collection(store.namespace, function (err, collection) { if(err || !collection) return fn(err || Error('collection was undefined or an error occured')); - fn(null, collection) + fn(null, collection); }); } @@ -132,6 +145,49 @@ function collection(store, fn) { } }; +/** + * Change public IDs to private IDs. + * + * IDs are generated with a psuedo random number generator. + * 24 hexidecimal chars, ~2 trillion combinations. + * + * @param {Object} object + * @return {Object} + */ + +Store.prototype.identify = function (object) { + if(!object) return; + if(typeof object != 'object') throw new Error('identify requires an object'); + var store = this; + function set(object) { + if(object._id) { + object.id = object._id; + delete object._id; + } else { + var u = object.id || store.createUniqueIdentifier(); + object._id = u; + delete object.id; + } + } + if(Array.isArray(object)) { + object.forEach(set); + } else { + set(object); + } + return object; +}; + +/** + * Create a unique identifier. Override this is derrived stores + * to change the way IDs are generated. + * + * @return {String} + */ + +Store.prototype.createUniqueIdentifier = function() { + return uuid.create(); +}; + /** * Insert an object into the store. * @@ -147,10 +203,14 @@ function collection(store, fn) { */ Store.prototype.insert = function (object, fn) { + var store = this; + this.identify(object); collection(this, function (err, col) { col.insert(object, function (err, result) { - if(Array.isArray(result) && !Array.isArray(object)) result = result[0]; - fn(err, result); + if(Array.isArray(result) && !Array.isArray(object)) { + result = result[0]; + } + fn(err, store.identify(result)); }); }); }; @@ -170,6 +230,7 @@ Store.prototype.insert = function (object, fn) { */ Store.prototype.find = function (query, fn) { + var store = this; if(typeof query == 'function') { fn = query; query = {}; @@ -177,7 +238,7 @@ Store.prototype.find = function (query, fn) { collection(this, function (err, col) { col.find(query).toArray(function (err, arr) { if(arr.length === 0) arr = undefined; - fn(err, arr); + fn(err, store.identify(arr)); }); }); }; @@ -197,8 +258,12 @@ Store.prototype.find = function (query, fn) { */ Store.prototype.first = function (query, fn) { + this.identify(query); + var store = this; collection(this, function (err, col) { - col.findOne(query, fn); + col.findOne(query, function (err, result) { + fn(err, store.identify(result)); + }); }); }; @@ -210,7 +275,7 @@ Store.prototype.first = function (query, fn) { * db * .connect({host: 'localhost', port: 27015, name: 'test'}) * .createStore('testing-store') - * .update({_id: ''}, fn) + * .update({id: ''}, fn) * * @param {Object} query * @param {Object} object @@ -218,8 +283,16 @@ Store.prototype.first = function (query, fn) { */ Store.prototype.update = function (query, object, fn) { + var store = this; + if(typeof query == 'string') query = {id: query}; + if(typeof query != 'object') throw new Error('update requires a query object or string id'); + if(query.id) store.identify(query); + collection(this, function (err, col) { - col.update(query, object, fn); + col.update(query, object, function(err) { + store.identify(query); + fn(err); + }); }); }; @@ -231,13 +304,22 @@ Store.prototype.update = function (query, object, fn) { * db * .connect({host: 'localhost', port: 27015, name: 'test'}) * .createStore('testing-store') - * .remove({_id: ''}, fn) + * .remove({id: ''}, fn) * * @param {Object} query * @param {Function} callback(err, obj) */ Store.prototype.remove = function (query, fn) { + var store = this; + if(typeof query === 'string') query = {id: query}; + if(typeof query == 'function') { + fn = query; + query = {}; + } + if(query.id) { + store.identify(query); + } collection(this, function (err, col) { col.remove(query, fn); }); @@ -258,7 +340,9 @@ Store.prototype.remove = function (query, fn) { */ Store.prototype.rename = function (namespace, fn) { + var store = this; collection(this, function (err, col) { + store.namespace = namespace; col.rename(namespace, fn); }); }; diff --git a/lib/http.js b/lib/http.js deleted file mode 100644 index 69397ba..0000000 --- a/lib/http.js +++ /dev/null @@ -1,31 +0,0 @@ -var http = require('http') - , Router = require('./router') - , db = require('./db') - , resources = require('./resources'); - -/** - * Create an http server with the given options and create a `Router` to handle its requests. - * - * Options: - * - * - `db` the database connection info - * - `host` the server's hostname - * - `port` the server's port - * - * Example: - * - * var server = new Server({port: 3000, db: {host: 'localhost', port: 27015, name: 'my-db'}}); - * - * server.listen(); - * - * @param {Object} options - * @return {HttpServer} - */ - -function Server(options) { - var server = http.createServer(options.host, options.port) - , db = db.connect(options.db) - , router = new Router(resources.build(db.createStore('resources'))); - - server.on('request', router.route); -} \ No newline at end of file diff --git a/test/http-server.unit.js b/lib/package.js similarity index 100% rename from test/http-server.unit.js rename to lib/package.js diff --git a/lib/resources/collection.js b/lib/resources/collection.js index a630580..43e838a 100644 --- a/lib/resources/collection.js +++ b/lib/resources/collection.js @@ -279,7 +279,7 @@ Collection.prototype.remove = function (session, query, fn) { var collection = this , store = this.store; - if(!(query && query._id)) return fn('You must include a query with an _id when deleting an object from a collection.'); + if(!(query && query.id)) return fn('You must include a query with an id when deleting an object from a collection.'); store.find(query, function (err) { if(err) return fn(err); collection.execListener('Delete', session, query, null, function (err) { @@ -313,14 +313,14 @@ Collection.prototype.save = function (session, item, query, fn) { if(!item) return fn('You must include an object when saving or updating.'); - // handle _id on either body or query - if(item._id) { - query._id = item._id; - delete item._id; + // handle id on either body or query + if(item.id) { + query.id = item.id; + delete item.id; } // handle upsert - store[query._id ? 'update' : 'insert'].apply(store, query._id ? [query, item, fn] : [item, fn]); + store[query.id ? 'update' : 'insert'].apply(store, query.id ? [query, item, fn] : [item, fn]); } module.exports = Collection; \ No newline at end of file diff --git a/lib/router.js b/lib/router.js index 96b6425..db2f3f0 100644 --- a/lib/router.js +++ b/lib/router.js @@ -16,7 +16,7 @@ function Router(resources) { /** * Route requests to resources with matching root paths. - * Generate a `ctx` object andhand it to the resource, along with the `res` by calling its `resource.handle(ctx, res, next)` method. + * Generate a `ctx` object and hand it to the resource, along with the `res` by calling its `resource.handle(ctx, next)` method. * If a resource calls `next()`, move on to the next resource. * * If all matching resources call next(), or if the router does not find a resource, respond with `404`. @@ -29,16 +29,19 @@ function Router(resources) { Router.prototype.route = function (req, res) { var router = this , url = req.url - , resources = this.matchResources(url); + , resources = this.matchResources(url) + , i = 0; //TODO: Handle edge case where next() is called more than once function nextResource() { - var resource = resources.shift() + var resource = resources[i++] , ctx; if (resource) { ctx = new Context(resource, req, res); - resource.handle(ctx, nextResource); + process.nextTick(function () { + resource.handle(ctx, nextResource); + }); } else { res.statusCode = 404; res.end("Not Found"); diff --git a/lib/server.js b/lib/server.js new file mode 100644 index 0000000..293defe --- /dev/null +++ b/lib/server.js @@ -0,0 +1,165 @@ +var http = require('http') + , Router = require('./router') + , db = require('./db') + , util = require('util') + , resources = require('./resources') + , Resource = require('./resource') + , SessionStore = require('./session').SessionStore + , fs = require('fs') + , io = require('socket.io') + , setupReqRes = require('./util/http').setup; + +function extend(origin, add) { + // don't do anything if add isn't an object + if (!add || typeof add !== 'object') return origin; + + var keys = Object.keys(add); + var i = keys.length; + while (i--) { + origin[keys[i]] = add[keys[i]]; + } + return origin; +}; + +/** + * Create an http server with the given options and create a `Router` to handle its requests. + * + * Options: + * + * - `db` the database connection info + * - `host` the server's hostname + * - `port` the server's port + * + * Example: + * + * var server = new Server({port: 3000, db: {host: 'localhost', port: 27015, name: 'my-db'}}); + * + * server.listen(); + * + * @param {Object} options + * @return {HttpServer} + */ + +function Server(options) { + http.Server.call(this); + + // defaults + this.options = options = extend({ + port: 2403, + host: 'localhost', + db: {port: 27017, host: '127.0.0.1', name: 'deployd'} + }, options); + + // an object to map a server to its stores + this.stores = {}; + + // back all memory stores with a db + this.db = db.connect(options.db); + + // persist resources in a store + var resourceStore = this.resources = resources.build(this.createStore('resources')); + + // use socket io for a session based realtime channel + this.sockets = io.listen(this).sockets; + + // persist sessions in a store + var sessionStore = this.sessions = new SessionStore('sessions', this.db, this.sockets); + + this.on('request', function (req, res) { + // add utilites to req and res + setupReqRes(req, res); + + sessionStore.createSession(function(err, session) { + if(err) { + res.statusCode = 500; + res.end('error when creating session ' + err); + } else { + // (re)set the session id + req.cookies.set('sid', session.sid); + + req.session = session; + resourceStore.find(function (err, resources) { + if(err) { + res.statusCode = 500; + res.end('error when finding resources ' + err); + } else { + var router = new Router(resources); + router.route(req, res); + } + }); + } + }); + }); +} +util.inherits(Server, http.Server); + + +/** + * Start listening for incoming connections. + * + * @return {Server} for chaining + */ + +Server.prototype.listen = function(port, host) { + return http.Server.prototype.listen.call(this, port || this.options.port, host || this.options.host); +}; + +/** + * Create a new `Store` for persisting data using the database info that was passed to the server when it was created. + * + * Example: + * + * // Create a new server + * var server = new Server({port: 3000, db: {host: 'localhost', port: 27015, name: 'my-db'}}); + * + * // Attach a store to the server + * var todos = server.createStore('todos'); + * + * // Use the store to CRUD data + * todos.insert({name: 'go to the store', done: true}, ...); // see `Store` for more info + * + * @param {String} namespace + * @return {Store} + */ + +Server.prototype.createStore = function(namespace) { + return (this.stores[namespace] = this.db.createStore(namespace)); +}; + +/** + * Define or update a resource based on the given description. + * + * + * @param {Object} description + * @param {Function} callback(err) + * @return {Resource} + */ + +Server.prototype.defineResource = function(description, fn) { + var resources = this.resources; + resources.first({path: description.path}, function (err, res) { + if(err) return fn(err); + if(res && res.id) { + resources.update({id: description.id}, description, fn); + } else { + resources.insert(description, fn); + } + }) +}; + + +/** + * Get a single resource. + * + * + * @param {String} path + * @return {Resource} + */ + +Server.prototype.getResource = function(path, fn) { + var resource; + + this.resources.first({path: path}, fn); +}; + +module.exports = Server; \ No newline at end of file diff --git a/lib/session.js b/lib/session.js index b3afdab..79c3ffe 100644 --- a/lib/session.js +++ b/lib/session.js @@ -1,27 +1,199 @@ +var Store = require('./db').Store + , util = require('util') + , uuid = require('./util/uuid') + , Cookies = require('cookies') + , EventEmitter = require('events').EventEmitter; + /** - * Initialize a connection with a client (or user) that can be established - * and torn down at a later time. An established communication session may - * involve more than one message in each direction. This is implemented in - * both HTTP and WebSockets. Each WebSocket connection is bound to a session. - * - * Example: - * - * var session = new Session(connection); - * - * session.on('connected', function () { - * session.emit('hello world'); - * }); - * + * A store for persisting sessions inbetween connection / disconnection. + * Automatically creates session IDs on inserted objects. */ -function Session(connection, sid, store) { +function SessionStore(namespace, db, sockets) { + this.sockets = sockets; + // socket queue + var socketQueue = this.socketQueue = new EventEmitter() + , socketIndex = this.socketIndex = {}; + + // TODO - sockets.on('connection', ...) - map to a session id based on socket.handshake.headers + sockets && sockets.on('connection', function (socket) { + // NOTE: do not use set here ever, the `Cookies` api is meant to get a req, res + // but we are just using it for a cookie parser + var cookies = new Cookies(socket.handshake) + , sid = cookies.get('sid'); + + if(sid) { + // index sockets against their session id + socketIndex[sid] = socket; + socketQueue.emit(sid, socket); + } + }) + + Store.apply(this, arguments); +} +util.inherits(SessionStore, Store); +exports.SessionStore = SessionStore; + +SessionStore.prototype.createUniqueIdentifier = function() { + return uuid.create(128); +}; + +/** + * Create a new `Session` based on an optional `sid` (session id). + * + * @param {String} sid + * @param {Function} callback(err, session) + */ + +SessionStore.prototype.createSession = function(sid, fn) { + var socketIndex = this.socketIndex; + + if(typeof sid == 'function') { + fn = sid; + sid = undefined; + } + if(sid) { + this.find({id: sid}, function(err, s) { + if(err || !s) return fn(err); + fn(err, new Session(s, this, socketIndex[sid])); + }); + } else { + sid = this.createUniqueIdentifier(); + fn(null, new Session({id: sid}, this, socketIndex[sid])); + this.insert({id: sid}, function(err, s) { + if(err) console.error(err); + }); + } +}; + +/** + * An in memory representation of a client or user connection that can be saved to disk. + * Data will be passed around via a `Context` to resources. + * + * Example: + * + * var session = new Session({id: 'my-sid', new SessionStore('sessions', db)}); + * + * session.set({uid: 'my-uid'}).save(); + * + * @param {Object} data + * @param {Store} store + * @param {Socket} socket + */ + +function Session(data, store, socket) { + this.data = data; + if(data && data.id) this.sid = data.id; + this.store = store; + + // create faux socket, to queue any events until + // a real socket is available + var socketWrapper = this.socket = { + on: function () { + // if we have a real socket, use it + if(this._socket) { + this._socket.apply(this._socket, arguments); + } else { + // otherwise add to bind queue + var queue = this._bindQueue = this._bindQueue || []; + queue.push(arguments); + } + }, + emit: function () { + // if we have a real socket, use it + if(this._socket) { + this._socket.apply(this._socket, arguments); + } else { + // otherwise add to bind queue + var queue = this._emitQueue = this._bindQueue || []; + queue.push(arguments); + } + }, + _socket: socket + } + + // resolve queue once a socket is ready + store.socketQueue.once(this.sid, function (socket) { + socketWrapper._socket = socket; + // drain bind queue + if(socketWrapper._bindQueue && socketWrapper._bindQueue.length) { + socketWrapper._bindQueue.forEach(function (args) { + socket.on.apply(socket, args); + }) + } + // drain emit queue + if(socketWrapper._emitQueue && socketWrapper._emitQueue.length) { + socketWrapper._emitQueue.forEach(function (args) { + socket.emit.apply(socket, args); + }) + } + }) } -Session.prototype.remove = function (fn) { - -} +/** + * Set properties on the in memory representation of a session. + * + * @param {Object} changes + * @return {Session} this for chaining + */ -Session.prototype.save = function (fn) { - -} \ No newline at end of file +Session.prototype.set = function(object) { + var session = this + , data = session.data || (session.data = {}); + + + Object.keys(object).forEach(function(key) { + data[key] = object[key]; + }); + return this; +}; + +/** + * Save the in memory representation of a session to its store. + * + * @param {Function} callback(err, data) + * @return {Session} this for chaining + */ + +Session.prototype.save = function(fn) { + var session = this + , data = this.data + , query = {id: data.id}; + + session.remove(function (err) { + if(err) return fn(err); + session.store.insert(data, function (err, res) { + fn(err, res); + }); + }); + return this; +}; + +/** + * Reset the session using the data in its store. + * + * @param {Function} callback(err, data) + * @return {Session} this for chaining + */ + +Session.prototype.fetch = function(fn) { + var session = this; + this.store.first({id: this.data.id}, function (err, data) { + session.set(data); + fn(err, data); + }); + return this; +}; + +/** + * Remove the session. + * + * @param {Function} callback(err, data) + * @return {Session} this for chaining + */ + +Session.prototype.remove = function(fn) { + this.store.remove({id: this.data.id}, fn); + return this; +}; \ No newline at end of file diff --git a/lib/util/http.js b/lib/util/http.js new file mode 100644 index 0000000..f191bc6 --- /dev/null +++ b/lib/util/http.js @@ -0,0 +1,9 @@ +var Cookies = require('cookies'); + +/** + * A utility for setting up a request and response. + */ + +exports.setup = function(req, res) { + req.cookies = res.cookies = new Cookies(req, res); +} \ No newline at end of file diff --git a/lib/util/uuid.js b/lib/util/uuid.js new file mode 100644 index 0000000..9490bce --- /dev/null +++ b/lib/util/uuid.js @@ -0,0 +1,111 @@ +// Copyright (C) 2010 by Johannes Baagøe +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, copy, +// modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +// BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +function Alea() { + function Mash() { + var n = 0xefc8249d; + + var mash = function(data) { + data = data.toString(); + for (var i = 0; i < data.length; i++) { + n += data.charCodeAt(i); + var h = 0.02519603282416938 * n; + n = h >>> 0; + h -= n; + h *= n; + n = h >>> 0; + h -= n; + n += h * 0x100000000; // 2^32 + } + return (n >>> 0) * 2.3283064365386963e-10; // 2^-32 + }; + + mash.version = 'Mash 0.9'; + return mash; + } + + return (function (args) { + var s0 = 0; + var s1 = 0; + var s2 = 0; + var c = 1; + + if (args.length == 0) { + args = [+new Date]; + } + var mash = Mash(); + s0 = mash(' '); + s1 = mash(' '); + s2 = mash(' '); + + for (var i = 0; i < args.length; i++) { + s0 -= mash(args[i]); + if (s0 < 0) { + s0 += 1; + } + s1 -= mash(args[i]); + if (s1 < 0) { + s1 += 1; + } + s2 -= mash(args[i]); + if (s2 < 0) { + s2 += 1; + } + } + mash = null; + + var random = function() { + var t = 2091639 * s0 + c * 2.3283064365386963e-10; // 2^-32 + s0 = s1; + s1 = s2; + return s2 = t - (c = t | 0); + }; + random.uint32 = function() { + return random() * 0x100000000; // 2^32 + }; + random.fract53 = function() { + return random() + + (random() * 0x200000 | 0) * 1.1102230246251565e-16; // 2^-53 + }; + random.version = 'Alea 0.9'; + random.args = args; + return random; + + } (Array.prototype.slice.call(arguments))); +} + +// instantiate RNG. use the default seed, which is current time. +var random = new Alea(); + +// Modified RFC 4122 v4 UUID +exports.create = function (length) { + length = length || 16; + var s = []; + var hexDigits = "0123456789abcdef"; + for (var i = 0; i < length; i++) { + s[i] = hexDigits.substr(Math.floor(random() * 0x10), 1); + } + s[length - 3] = hexDigits.substr((s[length - 3] & 0x3) | 0x8, 1); + + // return the uuid + return s.join(''); +} \ No newline at end of file diff --git a/package.json b/package.json index fe69242..fd4b743 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "async-eval": "0.1.1", "mongodb": "1.0.2", "request": "2.x.x", - "commander": "0.6.0" + "commander": "0.6.0", + "cookies": ">= 0.3.0" }, "devDependencies": { "mocha": "*", diff --git a/test/collection.unit.js b/test/collection.unit.js index 14846f9..cd627a0 100644 --- a/test/collection.unit.js +++ b/test/collection.unit.js @@ -139,7 +139,7 @@ describe('collection', function(){ it('should handle PUT', function(done) { var testData = [{test: true}, {test: false}]; - example('PUT', '/foo', {test: {type: 'boolean'}}, {test: false, _id: 7}, null, + example('PUT', '/foo', {test: {type: 'boolean'}}, {test: false, id: 7}, null, function (req, res, method, path, properties, body) { expect(res.statusCode).to.equal(200); }, @@ -149,7 +149,7 @@ describe('collection', function(){ }) it('should handle DELETE', function(done) { - example('DELETE', '/foo', {test: {type: 'boolean'}}, null, {_id: 7}, + example('DELETE', '/foo', {test: {type: 'boolean'}}, null, {id: 7}, function (req, res, method, path, properties, body) { expect(res.statusCode).to.equal(200); }, @@ -177,16 +177,16 @@ describe('collection', function(){ var widgets = db.connect(TEST_DB).createStore('widgets'); var c = new Collection({ - onGet: 'var item = this; widgets.insert({foo:"bar"}, function(err, widget) { item._id = widget._id })', + onGet: 'var item = this; widgets.insert({foo:"bar"}, function(err, widget) { item.id = widget.id })', resources: { widgets: widgets } }); - var items = [{_id: 1}, {_id: 1}, {_id: 1}]; + var items = [{id: 1}, {id: 1}, {id: 1}]; c.execListener('Get', {}, {}, items, function (err, result) { for(var i = 0; i < items.length; i++) { - expect(result[i]._id).to.not.equal(1); + expect(result[i].id).to.not.equal(1); } done(err); }) diff --git a/test/db.unit.js b/test/db.unit.js index 286c530..39c3833 100644 --- a/test/db.unit.js +++ b/test/db.unit.js @@ -1,7 +1,8 @@ var db = require('../lib/db') , TEST_DB = {name: 'test-db', host: 'localhost', port: 27017} , tester = db.connect(TEST_DB) - , store = tester.createStore('test-store'); + , store = tester.createStore('test-store') + , Store = require('../lib/db').Store; beforeEach(function(done){ store.remove(function () { @@ -25,8 +26,28 @@ describe('db', function(){ }) }) +describe('db', function(){ + // TODO: this takes forever, move to integration? + // describe('.drop(fn)', function(){ + // it('should drop the database', function(done) { + // var tester = db.connect(TEST_DB); + // tester.on('connected', function () { + // store.insert({foo: 'bar'}, function () { + // tester.drop(function (err) { + // expect(err).to.not.exist; + // store.find(function (err, res) { + // expect(res).to.not.exist; + // done(err); + // }) + // }) + // }) + // }) + // }) + // }) +}) + describe('store', function(){ - + describe('.find(query, fn)', function(){ it('should not find anything when the store is empty', function(done) { store.find(function (err, empty) { @@ -35,16 +56,33 @@ describe('store', function(){ }) }) - it('should pass the query to the underline database', function(done) { + it('should pass the query to the underlying database', function(done) { store.insert([{i:1},{i:2},{i:3}], function () { store.find({i: {$lt: 3}}, function (err, result) { expect(result).to.exist; + result.forEach(function (obj) { + expect(obj.id).to.be.a('string'); + }); expect(result).to.have.length(2); done(err); }) }) }) }) + + describe('.identify(object)', function() { + it('should add an _id to the object', function() { + var object = {}; + store.identify(object); + expect(object._id.length).to.equal(16); + }) + + it('should change _id to id', function() { + var object = {_id: 'aaaaaaaabbbbbbbb'}; + store.identify(object); + expect(object.id.length).to.equal(16); + }) + }) describe('.remove(query, fn)', function(){ it('should remove all the objects that match the query', function(done) { @@ -75,7 +113,7 @@ describe('store', function(){ describe('.insert(namespace, object, fn)', function(){ it('should insert the given object into the namespace', function(done) { store.insert({testing: 123}, function (err, result) { - expect(result._id).to.exist; + expect(result.id).to.exist; expect(result.testing).to.equal(123); done(); }) @@ -84,9 +122,9 @@ describe('store', function(){ it('should insert the given array into the namespace', function(done) { store.insert([{a:1}, {b:2}], function (err, result) { expect(Array.isArray(result)).to.equal(true); - expect(result[0]._id).to.exist; + expect(result[0].id).to.exist; expect(result[0].a).to.equal(1); - expect(result[1]._id).to.exist; + expect(result[1].id).to.exist; expect(result[1].b).to.equal(2); expect(result).to.have.length(2); done(err); @@ -98,7 +136,7 @@ describe('store', function(){ it('should update only the properties provided', function(done) { store.insert({foo: 'bar'}, function (err, result) { expect(err).to.not.exist; - var query = {_id: result._id}; + var query = {id: result.id}; store.update(query, {foo: 'baz'}, function (err) { expect(err).to.not.exist; store.first(query, function (err, result) { diff --git a/test/server.unit.js b/test/server.unit.js new file mode 100644 index 0000000..f646fb4 --- /dev/null +++ b/test/server.unit.js @@ -0,0 +1,56 @@ +var Server = require('../lib/server') + , Db = require('../lib/db').Db + , Store = require('../lib/db').Store + , Router = require('../lib/router'); + +describe('Server', function() { + describe('.listen()', function() { + it('should start a new deployd server', function(done) { + var server = new Server() + , defaultOptions = { + port: 2403, + host: 'localhost', + db: { + name: 'deployd', + port: 27017, + host: '127.0.0.1' + } + }; + + server.listen(); + expect(server.db instanceof Db).to.equal(true); + expect(server.options).to.eql(defaultOptions); + server.on('listening', function () { + done(); + }); + }); + }); + + describe('.createStore(namespace)', function() { + it('should create a store with the given name', function() { + var server = new Server() + , store = server.createStore('foo'); + + expect(store instanceof Store).to.equal(true); + expect(server.stores.foo).to.equal(store); + }); + }); + + describe('.defineResource(resource)', function() { + it('should create a resource based on its path', function(done) { + var server = new Server(); + + server.defineResource({ + path: '/todos', + type: 'Collection', + properties: { + title: {type: 'string'}, + order: {type: 'number'}, + done: {type: 'boolean'} + } + }, function (err) { + done(err); + }); + }); + }); +}); diff --git a/test/sessions.unit.js b/test/sessions.unit.js index e69de29..7051fea 100644 --- a/test/sessions.unit.js +++ b/test/sessions.unit.js @@ -0,0 +1,142 @@ +var SessionStore = require('../lib/session').SessionStore + , db = require('../lib/db') + , EventEmitter = require('events').EventEmitter; + +describe('SessionStore', function() { + it('should bind sockets to sessions', function() { + var sockets = new EventEmitter() + , store = new SessionStore('sessions', db.connect(TEST_DB), sockets) + , fauxSocket = { + handshake: {headers: {cookie: 'name=value; name2=value2; sid=123'}} + }; + + sockets.emit('connection', fauxSocket); + + expect(store.socketIndex['123']).to.equal(fauxSocket); + }); + + describe('.createUniqueIdentifier()', function() { + it('should create a session id', function() { + var store = new SessionStore() + , sid = store.createUniqueIdentifier(); + + expect(sid.length).to.equal(128); + }) + }) + + describe('.createSession(fn)', function() { + it('should create a session', function(done) { + var store = new SessionStore('sessions', db.connect(TEST_DB)) + , sid = store.createUniqueIdentifier(); + + store.createSession(function (err, session) { + expect(session.sid).to.have.length(128); + done(err); + }) + }) + }) +}) + +describe('Session', function() { + function createSession(fn) { + var store = new SessionStore('sessions', db.connect(TEST_DB)); + + store.createSession(function (err, session) { + expect(session.sid).to.have.length(128); + fn(err, session); + }) + } + + + it('should make the socket available from the session', function(done) { + var sockets = new EventEmitter() + , store = new SessionStore('sessions', db.connect(TEST_DB), sockets); + + store.createSession(function (err, session) { + var fauxSocket = {handshake: { headers: {cookie: 'name=value; name2=value2; sid=' + session.sid} } }; + sockets.emit('connection', fauxSocket); + expect(session.socket._socket).to.equal(fauxSocket); + done(err); + }); + }); + + it('should make sockets available even before they exist', function(done) { + this.timeout(100); + + var sockets = new EventEmitter() + , fauxSocket = new EventEmitter() + , store = new SessionStore('sessions', db.connect(TEST_DB), sockets); + + store.createSession(function (err, session) { + // generate faux headers + fauxSocket.handshake = { headers: {cookie: 'name=value; name2=value2; sid=' + session.sid} }; + + // bind to an event even before a connection has been made + session.socket.on('test', function (data) { + expect(data).to.equal(123); + done(); + }); + + sockets.emit('connection', fauxSocket); + + fauxSocket.emit('test', 123); + }); + }); + + describe('.set(changes)', function() { + it('should set the changes to a sessions data', function(done) { + createSession(function (err, session) { + session.set({foo: 'bar'}); + expect(session.data).to.eql({id: session.sid, foo: 'bar'}); + done(err); + }) + }) + }) + + describe('.save(fn)', function() { + it('should persist the session data in the store', function(done) { + createSession(function (err, session) { + session.set({foo: 'bar'}).save(function (err, data) { + session.store.first({id: session.sid}, function (err, sdata) { + expect(sdata.foo).to.equal('bar'); + done(err); + }) + }) + }) + }) + }) + + describe('.remove(fn)', function() { + it('should remove the session data from the store', function(done) { + createSession(function (err, session) { + session.set({foo: 'bar'}).save(function (err, data) { + session.store.first({id: session.sid}, function (err, sdata) { + expect(sdata.foo).to.equal('bar'); + session.remove(function () { + session.store.first({id: session.sid}, function (err, sdata) { + expect(sdata).to.not.exist; + done(err); + }) + }) + }) + }) + }) + }) + }) + + describe('.fetch(fn)', function() { + it('should fetch the session data from the store', function(done) { + createSession(function (err, session) { + session.set({foo: 'bar'}).save(function (err, data) { + session.store.first({id: session.sid}, function (err, sdata) { + session.data = {id: session.sid, foo: 'not-bar'}; + session.fetch(function (err) { + expect(session.data).to.eql({id: session.sid, foo: 'bar'}); + done(err); + }) + }) + }) + }) + }) + }) +}) \ No newline at end of file diff --git a/test/support.js b/test/support.js index a5b8dea..bebbd26 100644 --- a/test/support.js +++ b/test/support.js @@ -6,6 +6,7 @@ expect = require('chai').expect; request = require('request'); http = require('http'); TEST_DB = {name: 'test-db', host: 'localhost', port: 27017}; +mongodb = require('mongodb'); // request mock var port = 7000; @@ -30,4 +31,15 @@ freq = function(url, options, fn, callback) { .on('listening', function () { request(options); }) -} \ No newline at end of file +} + +// before(function (done) { +// var mdb = new mongodb.Db(TEST_DB.name, new mongodb.Server(TEST_DB.host, TEST_DB.port)); + +// mdb.open(function (err) { +// mdb.dropDatabase(function (err) { +// done(err); +// mdb.close(); +// }); +// }) +// }) \ No newline at end of file diff --git a/test/util.unit.js b/test/util.unit.js new file mode 100644 index 0000000..a0bcffe --- /dev/null +++ b/test/util.unit.js @@ -0,0 +1,14 @@ +describe('uuid', function() { + describe('.create()', function() { + var uuid = require('../lib/util/uuid'); + var used = {}; + // max number of objects that must not conflict + // total of about 2 trillion possible combinations + var i = 1000; // replace this with a larger number to really test + while(i--) { + var next = uuid.create(); + if(used[next]) throw 'already used' + used[next] = 1; + } + }); +}); \ No newline at end of file