connect-waterline
Version:
Waterline session store for Express and Connect
772 lines (614 loc) • 22.3 kB
JavaScript
/**
* Module dependencies.
*/
it.optional = require('it-optional');
var session = require('express-session');
var ConnectWaterline = require('../../connect-waterline');
var WaterlineStore = ConnectWaterline(session);
var assert = require('assert');
var _ = require('lodash');
var Waterline = require('waterline');
var testAdapter = 'sails-memory';
var settings = { config: {} };
if (process.env.ADAPTER_NAME){
testAdapter = process.env.ADAPTER_NAME;
settings = require('../integration/config/' + testAdapter + '.json');
}
settings.config.adapter = 'default';
var Adapter = require(testAdapter);
var options = {
adapters: {
'default': Adapter
},
collection: 'sessionTable',
connections: {
'connect-waterline': settings.config
}
};
var lazyOptions = _.defaults({ touchAfter: 2 /*seconds*/ }, options)
// Create a connect cookie instance
var make_cookie = function () {
var cookie = new session.Cookie();
cookie.maxAge = 10000; // This sets cookie.expire through a setter
cookie.secure = true;
cookie.domain = 'cow.com';
return cookie;
};
function getWaterlineModel(stringify, cb) { // getMongooseConnection()
if (!cb && stringify) {
cb = stringify;
stringify = undefined;
}
var waterline = new Waterline();
// Apply options to collection definition
var collection = _.cloneDeep(ConnectWaterline.defaultModelDefinition);
collection.tableName = 'custom_sessions';
collection.attributes.session = stringify === false ? 'json' : 'string';
waterline.loadCollection(Waterline.Collection.extend(collection));
waterline.initialize({
adapters: options.adapters,
connections: options.connections
}, function (err, ontology) {
if (err) { return cb(err); }
cb(null, ontology.collections.sessions);
});
}
// Create session data
var make_data = function () {
return {
foo: 'bar',
baz: {
cow: 'moo',
chicken: 'cluck'
},
num: 1,
cookie: make_cookie()
};
};
var make_data_no_cookie = function () {
return {
foo: 'bar',
baz: {
cow: 'moo',
fish: 'blub',
fox: 'nobody knows!'
},
num: 2
};
};
// Given a session id, input data, and session, make sure the stored data matches in the input data
var assert_session_equals = function (sid, data, session) {
if (typeof session.session === 'string') {
// Compare stringified JSON
assert.strictEqual(session.session, JSON.stringify(data));
}
else {
// Can't do a deepEqual for the whole session as we need the toJSON() version of the cookie
// Make sure the session data in intact
for (var prop in data) {
if (prop === 'cookie') {
// Make sure the cookie is intact
// deepEqual is choking with expires date comparison, let's circumvent that for now
assert.equal(new Date(session.session.cookie.expires).getTime(), new Date(data.cookie.expires).getTime());
var cookieJSON = data.cookie.toJSON();
delete session.session.cookie.expires;
delete cookieJSON.expires;
assert.deepEqual(session.session.cookie, cookieJSON);
}
else {
assert.deepEqual(session.session[prop], data[prop]);
}
}
}
// Make sure the ID matches
assert.strictEqual(session.sid, sid);
};
var open_db = function (options, testVars, callback) {
if(!callback){
callback = testVars;
testVars = undefined;
}
var store = new WaterlineStore(options);
store.once('connected', function () {
if(!testVars){
return callback(this, this.waterline, this.collection);
}
testVars.store = this;
testVars.waterline = this.waterline;
testVars.collection = this.collection;
callback();
});
};
var cleanup_store = function (store, cb) {
store.waterline.teardown(cb);
};
var cleanup = function (store, waterline, collection, callback) {
if(arguments.length === 2){
var testVars = store;
callback = waterline;
store = testVars.store;
waterline = testVars.waterline;
collection = testVars.collection;
}
collection.drop(function () {
//db.close();
cleanup_store(store, callback);
});
};
describe('connect-waterline', function(){
after(function(){
if(it.optional.count){
console.log("\n \033[0;36mPending optional tests: " + it.optional.count + "\033[0m");
}
});
it('should throw error with empty options', function (done) {
assert.throws(
function () {
new MongoStore({});
},
Error);
done();
});
describe('creating new connection', function(done){
describe('stringify', function(done){
runTests('new_connection');
});
describe('no stringify', function(done){
runNoStringifyTests('new_connection');
});
describe('default expiration', function(done){
var testVars = {};
var defaultExpirationTime = 10101; // defaultExpirationTime is deprecated, so we use ttl
before(function(done){
var ttl = defaultExpirationTime / 1000;
var optionsWithExpirationTime = _.defaults({ ttl: ttl }, options);
open_db(optionsWithExpirationTime, testVars, done);
});
after(function(done){
cleanup(testVars, done);
});
it('should set session with default expiration', function (done) {
var sid = 'test_set_expires-sid';
var data = make_data_no_cookie();
var timeBeforeSet = new Date().valueOf();
testVars.store.set(sid, data, function (err) {
assert.equal(err, null);
// Verify it was saved
testVars.collection.findOne({ sid: sid }, function (err, session) {
assert.deepEqual(session.session, JSON.stringify(data));
assert.strictEqual(session.sid, sid);
assert.notEqual(session.expires, null);
var timeAfterSet = new Date().valueOf();
// +1000 because sails-postgresql only has 1s granularity
assert.ok(timeBeforeSet + defaultExpirationTime <= (session.expires.valueOf() + 1000),
(timeBeforeSet + defaultExpirationTime) + ' <= ' + session.expires.valueOf() + ', diff: ' +
(session.expires.valueOf() - (timeBeforeSet + defaultExpirationTime)) + ' ms');
assert.ok(session.expires.valueOf() <= timeAfterSet + defaultExpirationTime,
session.expires.valueOf() + ' <= ' + (timeAfterSet + defaultExpirationTime) + ', diff: ' +
(timeAfterSet + defaultExpirationTime - session.expires.valueOf()) + ' ms');
done();
});
});
});
});
describe('without default expiration', function(done){
var testVars = {};
before(function(done){
open_db(options, testVars, done);
});
after(function(done){
cleanup(testVars, done);
});
it('should set session without default expiration', function (done) {
var defaultExpirationTime = 1000 * 60 * 60 * 24 * 14;
var sid = 'test_set_expires-sid';
var data = make_data_no_cookie();
var timeBeforeSet = new Date().valueOf();
testVars.store.set(sid, data, function (err) {
assert.equal(err, null);
// Verify it was saved
testVars.collection.findOne({ sid: sid }, function (err, session) {
assert.deepEqual(session.session, JSON.stringify(data));
assert.strictEqual(session.sid, sid);
assert.notEqual(session.expires, null);
var timeAfterSet = new Date().valueOf();
assert.ok(timeBeforeSet + defaultExpirationTime <= session.expires.valueOf() + 1000);
assert.ok(session.expires.valueOf() <= timeAfterSet + defaultExpirationTime);
done();
});
});
});
});
describe('custom serializer', function(){
var testVars = {};
before(function(done){
var serializerOptions = _.defaults({
serialize: function (obj) {
obj.ice = 'test-1';
return JSON.stringify(obj);
},
sessionType: 'string'
}, options);
open_db(serializerOptions, testVars, done);
});
after(function(done){
cleanup(testVars, done);
});
it.optional('should set session with custom serializer', function (done) {
var sid = 'test_set_custom_serializer-sid';
var data = make_data(),
dataWithIce = JSON.parse(JSON.stringify(data));
dataWithIce.ice = 'test-1';
testVars.store.set(sid, data, function (err) {
assert.equal(err, null);
testVars.collection.findOne({ sid: sid }, function (err, session) {
assert.deepEqual(session.session, JSON.stringify(dataWithIce));
assert.strictEqual(session.sid, sid);
done();
});
});
});
});
describe('custom unserializer', function(){
var store, waterline, collection;
before(function(done){
var unserializerOptions = _.defaults({
unserialize: function (obj) {
obj.ice = 'test-2';
return obj;
},
}, options);
function open_db_done (_store, _waterline, _collection) {
store = _store;
waterline = _waterline;
collection = _collection;
done();
}
open_db(unserializerOptions, open_db_done);
});
after(function(done){
cleanup(store, waterline, collection, done);
});
it.optional('should get session with custom unserializer', function (done) {
var sid = 'test_get_custom_unserializer-sid';
var data = make_data();
store.set(sid, data, function (err) {
assert.equal(err, null);
store.get(sid, function (err, session) {
data.ice = 'test-2';
data.cookie = data.cookie.toJSON();
assert.equal(err, null);
assert.deepEqual(session, data);
done();
});
});
});
});
describe('touch', function(){
var testVars = {};
before(function(done){
open_db(options, testVars, done);
});
after(function(done){
cleanup(testVars, done);
});
it('should touch session', function (done) {
var sid = 'test_touch-sid',
data = make_data();
testVars.store.set(sid, data, function (err) {
assert.equal(err, null);
// Verify it was saved
testVars.collection.findOne({ sid: sid }, function (err, session) {
assert.equal(err, null);
assert_session_equals(sid, data, session);
// touch the session
testVars.store.touch(sid, session.session, function (err) {
assert.equal(err, null);
// find the touched session
testVars.collection.findOne({ sid: sid }, function (err, session2) {
assert.equal(err, null);
// check if both expiry date are different
assert.ok(session2.expires.getTime() > session.expires.getTime());
done();
});
});
});
});
});
});
describe('lazy touch', function(){
var testVars;
beforeEach(function(done){
testVars = {};
open_db(lazyOptions, testVars, done);
});
afterEach(function(done){
cleanup(testVars, done);
});
it('should lazy touch session sync', function (done) {
var sid = 'test_lazy_touch-sid-sync',
data = make_data(),
lastModifiedBeforeTouch,
lastModifiedAfterTouch;
testVars.store.set(sid, data, function (err) {
assert.equal(err, null);
// Verify it was saved
testVars.collection.findOne({ sid: sid }, function (err, session) {
assert.equal(err, null);
lastModifiedBeforeTouch = session.lastModified.getTime();
// touch the session
testVars.store.touch(sid, session, function (err) {
assert.equal(err, null);
testVars.collection.findOne({ sid: sid }, function (err, session2) {
assert.equal(err, null);
lastModifiedAfterTouch = session2.lastModified.getTime();
assert.strictEqual(lastModifiedBeforeTouch, lastModifiedAfterTouch);
done();
});
});
});
});
});
it('should lazy touch session async', function (done) {
this.timeout(4000);
var sid = 'test_lazy_touch-sid',
data = make_data(),
lastModifiedBeforeTouch,
lastModifiedAfterTouch;
testVars.store.set(sid, data, function (err) {
assert.equal(err, null);
// Verify it was saved
testVars.collection.findOne({ sid: sid }, function (err, session) {
assert.equal(err, null);
lastModifiedBeforeTouch = session.lastModified.getTime();
setTimeout(function () {
// touch the session
testVars.store.touch(sid, session, function (err) {
assert.equal(err, null);
testVars.collection.findOne({ sid: sid }, function (err, session2) {
assert.equal(err, null);
lastModifiedAfterTouch = session2.lastModified.getTime();
assert.ok(lastModifiedAfterTouch > lastModifiedBeforeTouch);
done();
});
});
}, 3000);
});
});
});
});
});
describe('previously instantiated model', function(){
describe('auto remove interval', function(){
var store, waterline, collection;
before(function(done){
function open_db_done (_store, _waterline, _collection) {
store = _store;
waterline = _waterline;
collection = _collection;
done();
}
getWaterlineModel(function (err, model) {
assert.equal(err, null);
assert(model, 'model must exist');
var autoRemoveOptions = _.defaults({ autoRemoveInterval: 1/60 /*min*/ }, { model: model });
var sessions = [
{ sid: 'test_remove-interval-1', session: JSON.stringify({ key1: 1, key2: 'two' }) },
{ sid: 'test_remove-interval-2', session: JSON.stringify({ key1: 1, key2: 'two' }), expires: new Date() },
{ sid: 'test_remove-interval-3', session: JSON.stringify({ key1: 1, key2: 'two' }), expires: new Date(new Date().valueOf() + 60*60*1000) }
];
// save a few sessions
model.create(sessions, function (err, ses) {
assert(!err, err);
assert.equal(ses.length, 3);
open_db(autoRemoveOptions, open_db_done);
});
});
});
after(function(done){
cleanup(store, waterline, collection, done);
});
it('should remove expired sessions and keep the others', function(done){
this.timeout(10000);
// let's wait a bit and then check if sessions were correctly cleaned
setTimeout(function(){
collection.find({ sort: 'sid ASC' }).then(function(sessions){
assert.equal(sessions.length, 2);
assert.equal(sessions[0].sid, 'test_remove-interval-1');
assert.equal(sessions[1].sid, 'test_remove-interval-3');
done();
}).catch(done);
}, 2 * 1000);
});
});
describe('stringify', function(){
runTests('instantiated_model');
});
describe('no stringify', function(){
runNoStringifyTests('instantiated_model');
});
});
});
function runTests(testType){
var store, waterline, collection;
// tests setup
beforeEach(function(done){
function open_db_done (_store, _waterline, _collection) {
store = _store;
waterline = _waterline;
collection = _collection;
done();
}
if(testType === 'instantiated_model'){
getWaterlineModel(function (err, model) {
assert.equal(err, null);
open_db({ model: model }, open_db_done);
});
}
else {
open_db(options, open_db_done);
}
});
afterEach(function(done){
cleanup(store, waterline, collection, function(){
store = undefined;
waterline = undefined;
collection = undefined;
done();
});
});
it('should set session', function (done) {
var sid = 'test_set-sid';
var data = make_data();
store.set(sid, data, function (err) {
assert.equal(err, null);
// Verify it was saved
collection.findOne({ sid: sid }, function (err, session) {
assert_session_equals(sid, data, session);
done();
});
});
});
it('should set expires', function (done) {
var sid = 'test_set_expires-sid';
var data = make_data();
store.set(sid, data, function (err) {
assert.equal(err, null);
// Verify it was saved
collection.findOne({ sid: sid }, function (err, session) {
assert_session_equals(sid, data, session);
done();
});
});
});
it('should get session', function (done) {
var sid = 'test_get-sid';
collection.create({ sid: sid, session: JSON.stringify({ key1: 1, key2: 'two' }) }, function (err, ses) {
assert.equal(err, null);
store.get(sid, function (err, session) {
assert.equal(err, null);
assert.deepEqual(session, { key1: 1, key2: 'two' });
done();
});
});
});
it('should set and get session', function (done) {
var sid = 'test_set_get-sid';
var data = make_data();
data.expires = new Date(new Date().getTime() + 1*60*60*1000);
store.set(sid, data, function (err) {
assert.equal(err, null);
// Verify it was saved
store.get(sid, function (err, session) {
assert.equal(err, null);
// assert_session_equals fails when comparing expires dates, lets circumvent that for now
assert.equal(new Date(data.expires).getTime(), new Date(session.expires).getTime());
delete data.expires;
delete session.expires;
assert_session_equals(sid, data, { sid: sid, session: session });
done();
});
});
});
it('should store length', function (done) {
var sid = 'test_length-sid';
collection.create({ sid: sid, session: JSON.stringify({ key1: 1, key2: 'two' }) }, function () {
store.length(function (err, length) {
assert.equal(err, null);
assert.strictEqual(length, 1);
done();
});
});
});
it('should destroy session', function (done) {
var sid = 'test_destroy_ok-sid';
collection.create({ sid: sid, session: JSON.stringify({ key1: 1, key2: 'two' }) }, function () {
store.destroy(sid, function (err) {
assert.equal(err, null);
done();
});
});
});
it('should clear store', function (done) {
var sid = 'test_length-sid';
collection.create({ sid: sid, key1: 1, key2: 'two' }, function () {
store.clear(function () {
collection.count(function (err, count) {
assert.strictEqual(count, 0);
done();
});
});
});
});
}
function runNoStringifyTests(testType){
var store, waterline, collection;
// tests setup
beforeEach(function(done){
function open_db_done (_store, _waterline, _collection) {
store = _store;
waterline = _waterline;
collection = _collection;
done();
}
if(testType === 'instantiated_model'){
getWaterlineModel(false, function (err, model) {
assert.equal(err, null);
assert(model, 'model must exist')
var noStringifyOptions = _.defaults({ stringify: false }, { model: model });
open_db(noStringifyOptions, open_db_done);
});
}
else {
open_db(_.defaults({ stringify: false }, options), open_db_done);
}
});
afterEach(function(done){
cleanup(store, waterline, collection, function(){
store = undefined;
waterline = undefined;
collection = undefined;
done();
});
});
it.optional('should set session without stringify', function (done) {
var sid = 'test_set-sid';
var data = make_data();
store.set(sid, data, function (err) {
assert.equal(err, null);
// Verify it was saved
collection.findOne({ sid: sid }, function (err, session) {
assert_session_equals(sid, data, session);
done();
});
});
});
it.optional('should set session cookie without stringify', function (done) {
var origSession = make_data();
var cookie = origSession.cookie;
var sid = 'test_set-sid';
store.set(sid, origSession, function (err) {
assert.equal(err, null);
collection.findOne({ sid: sid }, function (err, session) {
// Make sure cookie came out intact
assert.strictEqual(origSession.cookie, cookie);
// Make sure the fields made it back intact
assert.equal(new Date(cookie.expires).getTime(), new Date(session.session.cookie.expires).getTime());
assert.equal(cookie.secure, session.session.cookie.secure);
done();
});
});
});
it.optional('should set expires without stringify', function (done) {
var sid = 'test_set_expires-sid';
var data = make_data();
store.set(sid, data, function (err) {
assert.equal(err, null);
// Verify it was saved
collection.findOne({ sid: sid }, function (err, session) {
assert_session_equals(sid, data, session);
done();
});
});
});
}