dl
Version:
DreamLab Libs
801 lines (603 loc) • 26.4 kB
JavaScript
var events = require('events');
var rewire = require('rewire');
var WebSocket = require('ws');
var EventEmitter = events.EventEmitter;
var WebSocketConnectionMock = function () {
EventEmitter.call(this);
this.readyState = WebSocket.OPEN;
};
WebSocketConnectionMock.prototype = Object.create(EventEmitter.prototype);
WebSocketConnectionMock.prototype.send = function (data, opts, cb) {
return cb();
};
WebSocketConnectionMock.prototype.onMessage = function (data, cb) {
this.emit('message', JSON.stringify(data));
if (cb) {
process.nextTick(cb);
}
};
WebSocketConnectionMock.prototype.onClose = function () {
this.readyState = WebSocket.CLOSED;
this.emit('close');
};
var WebSocketServerMock = function () {
EventEmitter.call(this);
this.connections = [];
};
WebSocketServerMock.prototype = Object.create(EventEmitter.prototype);
WebSocketServerMock.prototype.onConnect = function () {
var ws = new WebSocketConnectionMock();
spyOn(ws, 'send').andCallThrough();
this.connections.push(ws);
this.emit('connection', ws);
return ws;
};
describe('WebSocketDataProvider', function () {
var WebSocketDataProvider;
var instance;
var params = { compress: false, binary: false, server: 'server' };
var wssMock;
var cb1;
var cb2;
var cb3;
var conn;
beforeEach(function () {
expect(function () {
WebSocketDataProvider = require('../../lib/dataprovider/WebSocketDataProvider.js').WebSocketDataProvider;
}).not.toThrow();
var rewiredWebSocketDataProvider = rewire('../../lib/dataprovider/WebSocketDataProvider.js');
var WebSocketServerSpy = jasmine.createSpy('wss');
WebSocketServerSpy.andCallFake(function (p) {
expect(p).toEqual(params);
wssMock = new WebSocketServerMock(p);
return wssMock;
});
WebSocketServerSpy.prototype = Object.create(WebSocketServerMock.prototype);
rewiredWebSocketDataProvider.__set__('WebSocketServer', WebSocketServerSpy);
cb1 = jasmine.createSpy('cb1');
cb2 = jasmine.createSpy('cb2');
cb3 = jasmine.createSpy('cb3');
instance = new rewiredWebSocketDataProvider.WebSocketDataProvider(params);
jasmine.Clock.useMock();
});
describe('new connection', function () {
beforeEach(function () {
instance.init(cb1, cb2);
});
it('should call ready callback', function () {
wssMock.onConnect();
expect(cb1).toHaveBeenCalledWith(null, true);
});
it('should call reloaded callback', function () {
wssMock.onConnect();
expect(cb2).toHaveBeenCalledWith(null, true);
});
it('should call reloaded but no ready on reconnect', function () {
wssMock.onConnect();
wssMock.onConnect();
expect(cb1.callCount).toBe(1);
expect(cb2.callCount).toBe(2);
});
});
describe('reloaded', function () {
beforeEach(function () {
instance.init(function () {}, cb2);
conn = wssMock.onConnect();
});
it('should call reloaded calback on reloaded', function (done) {
conn.onMessage({ method: 'reloaded' }, function () {
expect(cb2.callCount).toBe(2);
done();
});
});
});
describe('dropping connection', function () {
beforeEach(function () {
instance.init(cb1, cb2);
conn = wssMock.onConnect();
});
it('should call reloaded callback when there is another open connection', function () {
wssMock.onConnect();
conn.onClose();
expect(cb1.callCount).toBe(1);
expect(cb2.callCount).toBe(3);
});
it('should not call reloaded callback when there is no another open connection', function () {
conn.onClose();
expect(cb1.callCount).toBe(1);
expect(cb2.callCount).toBe(1);
});
it('should reassign watchers when there is another open connection', function (done) {
var newConn1 = wssMock.onConnect();
var newConn2 = wssMock.onConnect();
instance.watch('/key', function () {});
instance.watch('/key2', function () {});
conn.onMessage({ id: 1, rc: 0, error: null }, function () {
newConn1.onMessage({ id: 2, rc: 0, error: null }, function () {
expect(conn.send).toHaveBeenCalledWith('{"id":1,"method":"watch","params":{"key":"/key"}}',
jasmine.any(Object), jasmine.any(Function));
conn.onClose();
expect(newConn1.send).toHaveBeenCalledWith('{"id":3,"method":"watch","params":{"key":"/key"}}',
jasmine.any(Object), jasmine.any(Function));
expect(newConn1.send).not.toHaveBeenCalledWith('{"id":4,"method":"watch","params":{"key":"/key2"}}',
jasmine.any(Object), jasmine.any(Function));
expect(newConn2.send).not.toHaveBeenCalledWith('{"id":4,"method":"watch","params":{"key":"/key2"}}',
jasmine.any(Object), jasmine.any(Function));
done();
});
});
});
});
describe('handling incorrect message', function () {
beforeEach(function () {
instance.init(cb1, cb2);
conn = wssMock.onConnect();
});
it('should not throw when failed to parse message', function () {
expect(function () {
conn.emit('message', 'xxxxx{xx"xx');
}).not.toThrow();
});
it('should not throw when message is unknown', function () {
expect(function () {
conn.onMessage({});
}).not.toThrow();
});
it('should not throw when there is no callback for message', function () {
expect(function () {
conn.onMessage({ id: 123 });
}).not.toThrow();
});
});
describe('exists', function () {
beforeEach(function () {
instance.init(function () {}, function () {});
conn = wssMock.onConnect();
});
it('should return callback without error when checked', function (done) {
instance.exists('/key', function (rc, stat) {
expect(rc).toBe(null);
expect(stat).toBe('stat');
done();
});
expect(conn.send).toHaveBeenCalledWith('{"id":1,"method":"exists","params":{"key":"/key"}}',
jasmine.any(Object), jasmine.any(Function));
conn.onMessage({ id: 1, rc: 0, error: null, data: 'stat' });
});
it('should retry on ZCONNECTIONLOSS', function (done) {
cb1.andCallFake(function () {
expect(cb1).toHaveBeenCalledWith(null, 'stat');
expect(cb1.callCount).toBe(1);
done();
});
instance.exists('/key2', cb1);
conn.onClose();
var newConn = wssMock.onConnect();
newConn.onMessage({ id: 2, rc: 0, error: null, data: 'stat' });
});
it('should retry when not connected', function (done) {
conn.readyState = WebSocket.CLOSED;
cb1.andCallFake(function () {
expect(cb1.callCount).toBe(1);
done();
});
instance.exists('/key3', cb1);
var newConn = wssMock.onConnect();
newConn.onMessage({ id: 1, rc: 0, error: null });
});
it('should return unhandled error', function (done) {
instance.exists('/key4.1', function (rc, data) {
expect(rc).toBe(123);
expect(data).toBe('some error');
done();
});
conn.onMessage({ id: 1, rc: 123, error: 'some error' });
});
it('should return connection error', function (done) {
conn.send.andCallFake(function (_data, _opts, cb) {
return cb('error');
});
instance.exists('/key4.2', cb1);
conn.onMessage({ id: 1, rc: 0 }, function () {
expect(cb1).toHaveBeenCalledWith('error', undefined);
expect(cb1.callCount).toBe(1);
done();
});
});
it('should use all connections', function () {
var conn2 = wssMock.onConnect();
var conn3 = wssMock.onConnect();
instance.exists('/key5');
instance.exists('/key5');
instance.exists('/key5');
expect(conn.send.callCount).toBe(1);
expect(conn2.send.callCount).toBe(1);
expect(conn3.send.callCount).toBe(1);
});
it('should use sticky connection when using ephemeral key', function (done) {
var conn2 = wssMock.onConnect();
var conn3 = wssMock.onConnect();
instance.set('/key6', 'value', { flags: 1 }, function () {
instance.exists('/key6');
expect(conn.send.callCount).toBe(2);
expect(conn2.send.callCount).toBe(0);
expect(conn3.send.callCount).toBe(0);
done();
});
conn.onMessage({ id: 1, rc: 0, data: { createdInThisSession: true } });
});
});
describe('get', function () {
beforeEach(function () {
instance.init(function () {}, function () {});
conn = wssMock.onConnect();
});
it('should return callback without error when get', function (done) {
instance.get('/key', function (rc, data, stat) {
expect(rc).toBe(null);
expect(data).toBe('data');
expect(stat).toBe('stat');
done();
});
expect(conn.send).toHaveBeenCalledWith('{"id":1,"method":"get","params":{"key":"/key"}}',
jasmine.any(Object), jasmine.any(Function));
conn.onMessage({ id: 1, rc: 0, error: null, stat: 'stat', data: 'data' });
});
it('should retry on ZCONNECTIONLOSS', function (done) {
cb1.andCallFake(function () {
expect(cb1).toHaveBeenCalledWith(null, 'data', 'stat');
expect(cb1.callCount).toBe(1);
done();
});
instance.get('/key2', cb1);
conn.onClose();
var newConn = wssMock.onConnect();
newConn.onMessage({ id: 2, rc: 0, error: null, stat: 'stat', data: 'data' });
});
it('should retry when not connected', function (done) {
conn.readyState = WebSocket.CLOSED;
cb1.andCallFake(function () {
expect(cb1.callCount).toBe(1);
done();
});
instance.get('/key3', cb1);
var newConn = wssMock.onConnect();
newConn.onMessage({ id: 1, rc: 0, error: null });
});
it('should return unhandled error', function (done) {
instance.get('/key4', function (rc, data) {
expect(rc).toBe(123);
expect(data).toBe('some error');
done();
});
conn.onMessage({ id: 1, rc: 123, error: 'some error' });
});
it('should use all connections', function () {
var conn2 = wssMock.onConnect();
var conn3 = wssMock.onConnect();
instance.get('/key5');
instance.get('/key5');
instance.get('/key5');
expect(conn.send.callCount).toBe(1);
expect(conn2.send.callCount).toBe(1);
expect(conn3.send.callCount).toBe(1);
});
it('should use sticky connection when using ephemeral key', function (done) {
var conn2 = wssMock.onConnect();
var conn3 = wssMock.onConnect();
instance.set('/key6', 'value', { flags: 1 }, function () {
instance.get('/key6');
expect(conn.send.callCount).toBe(2);
expect(conn2.send.callCount).toBe(0);
expect(conn3.send.callCount).toBe(0);
done();
});
conn.onMessage({ id: 1, rc: 0, data: { createdInThisSession: true } });
});
});
describe('remove', function () {
beforeEach(function () {
instance.init(function () {}, function () {});
conn = wssMock.onConnect();
});
it('should return callback without error when removed', function (done) {
instance.remove('/key', { 'a': 'b' }, function (rc, data) {
expect(rc).toBe(null);
expect(data).toBe(null);
done();
});
expect(conn.send).toHaveBeenCalledWith('{"id":1,"method":"remove","params":{"key":"/key","options":{"a":"b"}}}', // jshint ignore: line
jasmine.any(Object), jasmine.any(Function));
conn.onMessage({ id: 1, rc: 0, error: null });
});
it('should retry on ZCONNECTIONLOSS', function (done) {
cb1.andCallFake(function () {
expect(cb1).toHaveBeenCalledWith(null, null);
expect(cb1.callCount).toBe(1);
done();
});
instance.remove('/key2', {}, cb1);
conn.onClose();
var newConn = wssMock.onConnect();
newConn.onMessage({ id: 2, rc: 0, error: null });
});
it('should retry when not connected', function (done) {
conn.readyState = WebSocket.CLOSED;
cb1.andCallFake(function () {
expect(cb1.callCount).toBe(1);
done();
});
instance.remove('/key3', {}, cb1);
var newConn = wssMock.onConnect();
newConn.onMessage({ id: 1, rc: 0, error: null });
});
it('should return unhandled error', function (done) {
instance.remove('/key4', {}, function (rc, data) {
expect(rc).toBe(123);
expect(data).toBe('some error');
done();
});
conn.onMessage({ id: 1, rc: 123, error: 'some error' });
});
it('should use all connections', function () {
var conn2 = wssMock.onConnect();
var conn3 = wssMock.onConnect();
instance.remove('/key5');
instance.remove('/key5');
instance.remove('/key5');
expect(conn.send.callCount).toBe(1);
expect(conn2.send.callCount).toBe(1);
expect(conn3.send.callCount).toBe(1);
});
it('should use sticky connection when using ephemeral key', function (done) {
var conn2 = wssMock.onConnect();
var conn3 = wssMock.onConnect();
instance.set('/key6', 'value', { flags: 1 }, function () {
instance.remove('/key6');
expect(conn.send.callCount).toBe(2);
expect(conn2.send.callCount).toBe(0);
expect(conn3.send.callCount).toBe(0);
done();
});
conn.onMessage({ id: 1, rc: 0, data: { createdInThisSession: true } });
});
});
describe('set', function () {
beforeEach(function () {
instance.init(function () {}, function () {});
conn = wssMock.onConnect();
});
it('should return callback without error when set', function (done) {
instance.set('/key', 'value', {'a': 'b'}, function (rc, stat) {
expect(rc).toBe(null);
expect(stat).toBe('stat');
done();
});
expect(conn.send).toHaveBeenCalledWith('{"id":1,"method":"set","params":{"key":"/key","value":"value","options":{"a":"b"}}}', // jshint ignore: line
jasmine.any(Object), jasmine.any(Function));
conn.onMessage({ id: 1, rc: 0, error: null, data: 'stat' });
});
it('should retry on ZCONNECTIONLOSS', function (done) {
cb1.andCallFake(function () {
expect(cb1).toHaveBeenCalledWith(null, 'stat');
expect(cb1.callCount).toBe(1);
done();
});
instance.set('/key2', 'value', {}, cb1);
conn.onClose();
var newConn = wssMock.onConnect();
newConn.onMessage({ id: 2, rc: 0, error: null, data: 'stat' });
});
it('should retry when not connected', function (done) {
conn.readyState = WebSocket.CLOSED;
cb1.andCallFake(function () {
expect(cb1.callCount).toBe(1);
done();
});
instance.set('/key3', 'value', {}, cb1);
var newConn = wssMock.onConnect();
newConn.onMessage({ id: 1, rc: 0, error: null });
});
it('should timeout when not connected longer than for DEFER_TIMEOUT', function (done) {
conn.readyState = WebSocket.CLOSED;
cb1.andCallFake(function (err) {
expect(err).toBeTruthy();
expect(cb1.callCount).toBe(1);
done();
});
instance.set('/whatever', 'value', {}, cb1);
jasmine.Clock.tick(WebSocketDataProvider.DEFER_TIMEOUT + 1);
});
it('should return unhandled error', function (done) {
instance.set('/key4', 'value', {}, function (rc, data) {
expect(rc).toBe(123);
expect(data).toBe('some error');
done();
});
conn.onMessage({ id: 1, rc: 123, error: 'some error' });
});
it('should use all connections', function () {
var conn2 = wssMock.onConnect();
var conn3 = wssMock.onConnect();
instance.set('/key5', 'value');
instance.set('/key5', 'value');
instance.set('/key5', 'value');
expect(conn.send.callCount).toBe(1);
expect(conn2.send.callCount).toBe(1);
expect(conn3.send.callCount).toBe(1);
});
it('should use sticky connection when using ephemeral key', function (done) {
var conn2 = wssMock.onConnect();
var conn3 = wssMock.onConnect();
instance.set('/key6', 'value', { flags: 1 }, function () {
instance.set('/key6', 'value');
expect(conn.send.callCount).toBe(2);
expect(conn2.send.callCount).toBe(0);
expect(conn3.send.callCount).toBe(0);
done();
});
conn.onMessage({ id: 1, rc: 0, data: { createdInThisSession: true } });
});
});
describe('sync', function () {
beforeEach(function () {
instance.init(function () {}, function () {});
conn = wssMock.onConnect();
});
it('should return callback without error when synced', function (done) {
instance.sync('/key', function (rc, data) {
expect(rc).toBe(null);
expect(data).toBe(null);
done();
});
expect(conn.send).toHaveBeenCalledWith('{"id":1,"method":"sync","params":{"key":"/key"}}',
jasmine.any(Object), jasmine.any(Function));
conn.onMessage({ id: 1, rc: 0, error: null });
});
it('should retry on ZCONNECTIONLOSS', function (done) {
cb1.andCallFake(function () {
expect(cb1).toHaveBeenCalledWith(null, null);
expect(cb1.callCount).toBe(1);
done();
});
instance.sync('/key2', cb1);
conn.onClose();
var newConn = wssMock.onConnect();
newConn.onMessage({ id: 2, rc: 0, error: null });
});
it('should retry when not connected', function (done) {
conn.readyState = WebSocket.CLOSED;
cb1.andCallFake(function () {
expect(cb1.callCount).toBe(1);
done();
});
instance.sync('/key3', cb1);
var newConn = wssMock.onConnect();
newConn.onMessage({ id: 1, rc: 0, error: null });
});
it('should return unhandled error', function (done) {
instance.sync('/key4', function (rc, data) {
expect(rc).toBe(123);
expect(data).toBe('some error');
done();
});
conn.onMessage({ id: 1, rc: 123, error: 'some error' });
});
it('should use all connections', function () {
var conn2 = wssMock.onConnect();
var conn3 = wssMock.onConnect();
instance.sync('/key5');
instance.sync('/key5');
instance.sync('/key5');
expect(conn.send.callCount).toBe(1);
expect(conn2.send.callCount).toBe(1);
expect(conn3.send.callCount).toBe(1);
});
it('should use sticky connection when using ephemeral key', function (done) {
var conn2 = wssMock.onConnect();
var conn3 = wssMock.onConnect();
instance.set('/key6', 'value', { flags: 1 }, function () {
instance.sync('/key6');
expect(conn.send.callCount).toBe(2);
expect(conn2.send.callCount).toBe(0);
expect(conn3.send.callCount).toBe(0);
done();
});
conn.onMessage({ id: 1, rc: 0, data: { createdInThisSession: true } });
});
});
describe('watch', function () {
beforeEach(function () {
instance.init(function () {}, function () {});
conn = wssMock.onConnect();
});
it('should return callback without error and with data on success', function (done) {
instance.watch('/key', function (rc, data, stat) {
expect(rc).toBe(null);
expect(data).toBe('data');
expect(stat).toBe('stat');
done();
});
expect(conn.send).toHaveBeenCalledWith('{"id":1,"method":"watch","params":{"key":"/key"}}',
jasmine.any(Object), jasmine.any(Function));
conn.onMessage({ id: 1, rc: 0, error: null, stat: 'stat', data: 'data' });
});
it('should retry on ZCONNECTIONLOSS', function (done) {
cb1.andCallFake(function () {
expect(cb1).toHaveBeenCalledWith(null, 'data', 'stat');
expect(cb1.callCount).toBe(1);
done();
});
instance.watch('/key2', cb1);
conn.onClose();
var newConn = wssMock.onConnect();
newConn.onMessage({ id: 2, rc: 0, error: null, stat: 'stat', data: 'data' });
});
it('should retry when not connected', function (done) {
conn.readyState = WebSocket.CLOSED;
cb1.andCallFake(function () {
expect(cb1.callCount).toBe(1);
done();
});
instance.watch('/key3', cb1);
var newConn = wssMock.onConnect();
newConn.onMessage({ id: 1, rc: 0, error: null });
});
it('should return unhandled error', function (done) {
instance.watch('/key4', function (rc, data) {
expect(rc).toBe(123);
expect(data).toBe('some error');
done();
});
conn.onMessage({ id: 1, rc: 123, error: 'some error' });
});
it('should stick to one connection', function () {
var conn2 = wssMock.onConnect();
var conn3 = wssMock.onConnect();
instance.watch('/key5');
instance.watch('/key5');
expect(conn.send.callCount).toBe(2);
expect(conn2.send.callCount).toBe(0);
expect(conn3.send.callCount).toBe(0);
});
it('should use sticky connection when using ephemeral key', function (done) {
var conn2 = wssMock.onConnect();
var conn3 = wssMock.onConnect();
instance.set('/key6', 'value', { flags: 1 }, function () {
instance.watch('/key6');
expect(conn.send.callCount).toBe(2);
expect(conn2.send.callCount).toBe(0);
expect(conn3.send.callCount).toBe(0);
done();
});
conn.onMessage({ id: 1, rc: 0, data: { createdInThisSession: true } });
});
it('should call callback with data on watch notify', function (done) {
instance.watch('/key7', cb1);
instance.watch('/key7', cb2);
instance.watch('/key8', cb3);
conn.onMessage({ id: 1, rc: 0, error: null, stat: 'stat', data: 'data' }, function () {
conn.onMessage({ id: 2, rc: 0, error: null, stat: 'stat', data: 'data' }, function () {
conn.onMessage({ method: 'notify', params: { key: '/key7', rc: 0, stat: 'stat1', data: 'data1' }}, function () { // jshint ignore: line
expect(cb1).toHaveBeenCalledWith(null, 'data1', 'stat1');
expect(cb2).toHaveBeenCalledWith(null, 'data1', 'stat1');
expect(cb1.callCount).toBe(2);
expect(cb2.callCount).toBe(2);
expect(cb3.callCount).toBe(0);
done();
});
});
});
});
it('should call callback with error on watch notify', function (done) {
instance.watch('/key9', cb1);
conn.onMessage({ id: 1, rc: 0, error: null, stat: 'stat', data: 'data' }, function () {
conn.onMessage({ method: 'notify', params: { key: '/key9', rc: 1234, error: 'error' } }, function () {
expect(cb1).toHaveBeenCalledWith(1234, 'error');
expect(cb1.callCount).toBe(2);
done();
});
});
});
});
});