added socket session support

This commit is contained in:
Ritchie Martori
2012-06-03 22:14:01 -07:00
parent 066ac6e5e8
commit 9bfcaa9645
18 changed files with 874 additions and 93 deletions

17
TODO.md
View File

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

View File

@@ -105,7 +105,7 @@ ClientStore.prototype.first = function (query, fn) {
* db
* .connect({host: 'localhost', port: 27015, name: 'test'})
* .createStore('testing-store')
* .update({_id: '<an object id>'}, fn)
* .update({id: '<an object 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: '<an object id>'}, fn)
* .remove({id: '<an object id>'}, fn)
*
* @param {Object} query
* @param {Function} callback(err, obj)

104
lib/db.js
View File

@@ -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: '<an object id>'}, fn)
* .update({id: '<an object 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: '<an object id>'}, fn)
* .remove({id: '<an object 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);
});
};

View File

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

View File

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

View File

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

165
lib/server.js Normal file
View File

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

View File

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

9
lib/util/http.js Normal file
View File

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

111
lib/util/uuid.js Normal file
View File

@@ -0,0 +1,111 @@
// Copyright (C) 2010 by Johannes Baagøe <baagoe@baagoe.org>
//
// 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('');
}

View File

@@ -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": "*",

View File

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

View File

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

56
test/server.unit.js Normal file
View File

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

View File

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

View File

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

14
test/util.unit.js Normal file
View File

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