dl
Version:
DreamLab Libs
544 lines (402 loc) • 21 kB
JavaScript
var core = require('core');
var events = require('events');
var rewire = require('rewire');
var WebSocket = require('ws');
var Event = core.event.Event;
var EventEmitter = events.EventEmitter;
var MockDataProvider = require('../../lib/dataprovider/MockDataProvider.js').MockDataProvider;
var WebSocketMockInstance = function () {
EventEmitter.call(this);
this.close = jasmine.createSpy('ws.close');
this.send = jasmine.createSpy('ws.send');
this.ping = jasmine.createSpy('ws.ping');
};
WebSocketMockInstance.prototype = Object.create(EventEmitter.prototype);
WebSocketMockInstance.prototype.onMessage = function (id, method, params) {
this.emit('message', JSON.stringify({ id: id, method: method, params: params }));
};
describe('ForwardingConfigurationManager', function () {
var ForwardingConfigurationManager;
var WebSocketMock;
var wsInstances;
var instance;
var driver;
var wsParams = { compress: false, binary: false };
var sendCb;
var ws1;
var ws2;
beforeEach(function () {
expect(function () {
ForwardingConfigurationManager = require('../../lib/configuration/ForwardingConfigurationManager.js').ForwardingConfigurationManager; // jshint ignore: line
}).not.toThrow();
jasmine.Clock.useMock();
var rewiredForwardingConfigurationManager = rewire('../../lib/configuration/ForwardingConfigurationManager.js');
sendCb = jasmine.createSpy('send callback');
wsInstances = {};
WebSocketMock = jasmine.createSpy('WebSocket');
WebSocketMock.andCallFake(function (endpoint) {
wsInstances[endpoint] = new WebSocketMockInstance(endpoint);
return wsInstances[endpoint];
});
rewiredForwardingConfigurationManager.__set__('WebSocket', WebSocketMock);
ForwardingConfigurationManager = rewiredForwardingConfigurationManager.ForwardingConfigurationManager;
driver = new MockDataProvider('data');
spyOn(driver, 'init').andCallThrough();
spyOn(driver, 'exists').andCallThrough();
spyOn(driver, 'get').andCallThrough();
spyOn(driver, 'remove').andCallThrough();
spyOn(driver, 'set').andCallThrough();
spyOn(driver, 'sync').andCallThrough();
spyOn(driver, 'watch').andCallThrough();
spyOn(driver, 'isConnected').andCallThrough();
instance = new ForwardingConfigurationManager(driver);
});
describe('initialize', function () {
it('should connect to added clients on initialize', function () {
instance.addClient('endpoint');
instance.initialize();
expect(WebSocketMock).toHaveBeenCalledWith('endpoint');
});
it('should not initialize when ready', function () {
instance.initialize();
instance.initialize();
expect(driver.init.callCount).toBe(1);
});
it('should reload all open endpoints when reloaded', function () {
instance.initialize();
instance.setClients(['endpoint1', 'endpoint2', 'endpoint3']);
wsInstances['endpoint3'].readyState = WebSocket.CLOSED;
instance.dispatchEvent(new Event(ForwardingConfigurationManager.Event.RELOADED));
expect(wsInstances['endpoint1'].send).toHaveBeenCalledWith('{"method":"reloaded"}', wsParams, jasmine.any(Function)); // jshint ignore: line
expect(wsInstances['endpoint2'].send).toHaveBeenCalledWith('{"method":"reloaded"}', wsParams, jasmine.any(Function)); // jshint ignore: line
expect(wsInstances['endpoint3'].send).not.toHaveBeenCalled();
});
});
describe('addClient', function () {
beforeEach(function () {
instance.initialize();
});
it('should connect to new client', function () {
var res = instance.addClient('endpoint');
expect(WebSocketMock).toHaveBeenCalledWith('endpoint');
expect(res).toBe(true);
});
it('should not connect to duplicated client', function () {
var res1 = instance.addClient('endpoint');
var res2 = instance.addClient('endpoint');
expect(WebSocketMock.callCount).toBe(1);
expect(res1).toBe(true);
expect(res2).toBe(false);
});
it('should not connect until ready', function () {
var instance2 = new ForwardingConfigurationManager(driver);
instance2.addClient('endpoint');
expect(WebSocketMock).not.toHaveBeenCalled();
});
});
describe('removeClient', function () {
beforeEach(function () {
instance.initialize();
});
it('should disconnect existing client', function () {
instance.addClient('endpoint');
var res = instance.removeClient('endpoint');
expect(wsInstances['endpoint'].close).toHaveBeenCalled();
expect(res).toBe(true);
});
it('should not disconnect not existing client', function () {
var res;
instance.addClient('endpoint');
expect(function () {
res = instance.removeClient('endpoint2');
}).not.toThrow();
expect(res).toBe(false);
});
it('should not reconnect to removed client', function () {
instance.addClient('endpoint');
wsInstances['endpoint'].close.andCallFake(function () {
wsInstances['endpoint'].emit('close');
});
instance.removeClient('endpoint');
jasmine.Clock.tick(ForwardingConfigurationManager.RECONNECT_INTERVAL + 1);
expect(WebSocketMock.callCount).toBe(1);
});
});
describe('setClients', function () {
beforeEach(function () {
instance.initialize();
});
it('should connect to new clients and disconnect missing clients', function () {
instance.addClient('endpoint1');
instance.addClient('endpoint2');
instance.setClients(['endpoint3', 'endpoint2']);
expect(wsInstances['endpoint1'].close).toHaveBeenCalled();
expect(wsInstances['endpoint2'].close).not.toHaveBeenCalled();
expect(WebSocketMock).toHaveBeenCalledWith('endpoint3');
});
it('should not connect until ready', function () {
var instance2 = new ForwardingConfigurationManager(driver);
instance2.setClients(['endpoint1', 'endpoint2']);
expect(WebSocketMock).not.toHaveBeenCalled();
});
});
describe('closing connection', function () {
beforeEach(function () {
instance.initialize();
instance.addClient('endpoint');
ws1 = wsInstances['endpoint'];
});
it('should remove ephemeral nodes on close', function () {
ws1.onMessage(13, 'set', { key: '/key', value: 'value', options: { flags: 1 } });
ws1.emit('close');
expect(driver.remove).toHaveBeenCalledWith('/key', {}, jasmine.any(Function));
});
it('should not try to remove ephemeral nodes removed before when closing connection', function () {
ws1.onMessage(13, 'set', { key: '/key', value: 'value', options: { flags: 1 } });
ws1.onMessage(14, 'remove', { key: '/key' });
ws1.emit('close');
expect(driver.remove.callCount).toBe(1);
});
it('should try to reconnect', function () {
ws1.emit('close');
jasmine.Clock.tick(ForwardingConfigurationManager.RECONNECT_INTERVAL + 1);
expect(WebSocketMock.callCount).toBe(2);
});
it('should increate reconnect interval up to RECONNECT_MAX_INTERVAL', function () {
for (var i = 0; i < 20; i++) {
wsInstances['endpoint'].emit('close');
jasmine.Clock.tick(ForwardingConfigurationManager.RECONNECT_MAX_INTERVAL + 1);
}
wsInstances['endpoint'].emit('close');
jasmine.Clock.tick(ForwardingConfigurationManager.RECONNECT_MAX_INTERVAL - 1);
expect(WebSocketMock.callCount).toBe(21);
jasmine.Clock.tick(2);
expect(WebSocketMock.callCount).toBe(22);
});
});
describe('connection error', function () {
beforeEach(function () {
instance.initialize();
instance.addClient('endpoint');
ws1 = wsInstances['endpoint'];
});
it('should reconnect on error', function () {
ws1.emit('error');
jasmine.Clock.tick(ForwardingConfigurationManager.RECONNECT_INTERVAL + 1);
expect(WebSocketMock.callCount).toBe(2);
});
it('should reconnect only once on multiple errors', function () {
ws1.emit('error');
ws1.emit('error');
jasmine.Clock.tick(ForwardingConfigurationManager.RECONNECT_INTERVAL + 1);
expect(WebSocketMock.callCount).toBe(2);
});
});
describe('ping clients', function () {
beforeEach(function () {
instance.initialize();
instance.setClients(['endpoint1', 'endpoint2']);
ws1 = wsInstances['endpoint1'];
ws2 = wsInstances['endpoint2'];
});
it('should ping clients on each interval', function () {
instance.setPingInterval(13);
jasmine.Clock.tick(28);
expect(ws1.ping).toHaveBeenCalledWith(jasmine.any(Number), {}, true);
expect(ws2.ping).toHaveBeenCalledWith(jasmine.any(Number), {}, true);
expect(ws1.ping.callCount).toBe(2);
expect(ws2.ping.callCount).toBe(2);
});
it('should not ping through closed socket', function () {
instance.setPingInterval(13);
ws2.readyState = WebSocket.CLOSED;
jasmine.Clock.tick(15);
expect(ws1.ping).toHaveBeenCalled();
expect(ws2.ping).not.toHaveBeenCalled();
});
it('should not disconnect after first ping timeout', function () {
instance.setPingInterval(13);
instance.setPingTimeout(10);
jasmine.Clock.tick(13 + 10 + 1);
expect(ws1.close).not.toHaveBeenCalled();
expect(ws2.close).not.toHaveBeenCalled();
});
it('should reconnect after MAX_PING_TIMEOUTS', function () {
instance.setPingInterval(13);
instance.setPingTimeout(10);
jasmine.Clock.tick((ForwardingConfigurationManager.MAX_PING_TIMEOUTS * (10 + 13)) + 1);
expect(ws1.close).toHaveBeenCalled();
expect(ws2.close).toHaveBeenCalled();
});
it('should not disconnect when client replies with pong', function () {
instance.setPingInterval(13);
instance.setPingTimeout(10);
for (var i = 0; i < ForwardingConfigurationManager.MAX_PING_TIMEOUTS + 1; i++) {
jasmine.Clock.tick(13 + 1);
ws1.emit('pong');
ws2.emit('pong');
}
expect(ws1.close).not.toHaveBeenCalled();
expect(ws2.close).not.toHaveBeenCalled();
});
});
describe('client methods', function () {
beforeEach(function () {
instance.initialize();
instance.setClients(['endpoint1', 'endpoint2']);
ws1 = wsInstances['endpoint1'];
ws2 = wsInstances['endpoint2'];
});
describe('exists', function () {
it('should check in dataprovider and return result', function () {
driver.exists.andCallFake(function (key, cb) {
return cb('rc', 'data', 'stat');
});
ws1.onMessage(1, 'exists', { key: '/key1' });
expect(driver.exists).toHaveBeenCalledWith('/key1', jasmine.any(Function));
expect(ws1.send).toHaveBeenCalledWith('{"id":1,"method":"exists","rc":"rc","data":"data","stat":"stat","error":null}', wsParams, jasmine.any(Function)); // jshint ignore: line
expect(ws2.send).not.toHaveBeenCalled();
});
});
describe('get', function () {
it('should get from dataprovider and return result', function () {
driver.get.andCallFake(function (key, cb) {
return cb('rc', 'data', 'stat');
});
ws1.onMessage(2, 'get', { key: '/key2' });
expect(driver.get).toHaveBeenCalledWith('/key2', jasmine.any(Function));
expect(ws1.send).toHaveBeenCalledWith('{"id":2,"method":"get","rc":"rc","data":"data","stat":"stat","error":null}', wsParams, jasmine.any(Function)); // jshint ignore: line
expect(ws2.send).not.toHaveBeenCalled();
});
});
describe('remove', function () {
it('should remove from dataprovider and return result', function () {
driver.remove.andCallFake(function (key, options, cb) {
return cb('rc', 'data', 'stat');
});
ws1.onMessage(3, 'remove', { key: '/key3', options: 'options' });
expect(driver.remove).toHaveBeenCalledWith('/key3', 'options', jasmine.any(Function));
expect(ws1.send).toHaveBeenCalledWith('{"id":3,"method":"remove","rc":"rc","data":"data","stat":"stat","error":null}', wsParams, jasmine.any(Function)); // jshint ignore: line
expect(ws2.send).not.toHaveBeenCalled();
});
});
describe('set', function () {
it('should set to dataprovider and return result', function () {
driver.set.andCallFake(function (key, value, options, cb) {
return cb('rc', 'data', 'stat');
});
ws1.onMessage(4, 'set', { key: '/key4', value: 'value', options: 'options' });
expect(driver.set).toHaveBeenCalledWith('/key4', 'value', 'options', jasmine.any(Function));
expect(ws1.send).toHaveBeenCalledWith('{"id":4,"method":"set","rc":"rc","data":"data","stat":"stat","error":null}', wsParams, jasmine.any(Function)); // jshint ignore: line
expect(ws2.send).not.toHaveBeenCalled();
});
});
describe('sync', function () {
it('should sync via dataprovider and return result', function () {
driver.sync.andCallFake(function (key, cb) {
return cb('rc', 'data', 'stat');
});
ws1.onMessage(5, 'sync', { key: '/key5' });
expect(driver.sync).toHaveBeenCalledWith('/key5', jasmine.any(Function));
expect(ws1.send).toHaveBeenCalledWith('{"id":5,"method":"sync","rc":"rc","data":"data","stat":"stat","error":null}', wsParams, jasmine.any(Function)); // jshint ignore: line
expect(ws2.send).not.toHaveBeenCalled();
});
});
describe('watch', function () {
it('should watch on dataprovider and return result for the first watch', function () {
driver.watch.andCallFake(function (key, cb) {
return cb('rc', 'data', 'stat');
});
ws1.onMessage(6, 'watch', { key: '/key6' });
expect(driver.watch).toHaveBeenCalledWith('/key6', jasmine.any(Function));
expect(ws1.send).toHaveBeenCalledWith('{"id":6,"method":"client-watch-init","rc":"rc","data":"data","stat":"stat","error":null}', wsParams, jasmine.any(Function)); // jshint ignore: line
expect(ws2.send).not.toHaveBeenCalled();
});
it('should get from dataprovider and return result for second watch on the same endpoint', function () {
driver.get.andCallFake(function (key, cb) {
return cb('rc', 'data', 'stat');
});
ws1.onMessage(7, 'watch', { key: '/key7' });
ws1.onMessage(8, 'watch', { key: '/key7' });
expect(driver.watch).toHaveBeenCalledWith('/key7', jasmine.any(Function));
expect(driver.watch.callCount).toBe(1);
expect(ws1.send).toHaveBeenCalledWith('{"id":8,"method":"client-watch-init","rc":"rc","data":"data","stat":"stat","error":null}', wsParams, jasmine.any(Function)); // jshint ignore: line
expect(ws2.send).not.toHaveBeenCalled();
});
it('should get from dataprovider and return result for second watch on new endpoint', function () {
driver.get.andCallFake(function (key, cb) {
return cb('rc', 'data', 'stat');
});
ws1.onMessage(9, 'watch', { key: '/key8' });
ws2.onMessage(10, 'watch', { key: '/key8' });
expect(driver.watch).toHaveBeenCalledWith('/key8', jasmine.any(Function));
expect(driver.watch.callCount).toBe(1);
expect(ws2.send).toHaveBeenCalledWith('{"id":10,"method":"client-watch-init","rc":"rc","data":"data","stat":"stat","error":null}', wsParams, jasmine.any(Function)); // jshint ignore: line
});
it('should send notification on change to all endpoints', function () {
var watchCb;
driver.get.andCallFake(function (key, cb) {
return cb('rc1', 'data1', 'stat1');
});
driver.watch.andCallFake(function (key, cb) {
watchCb = cb;
return cb('rc2', 'data2', 'stat2');
});
ws1.onMessage(11, 'watch', { key: '/key9' });
ws2.onMessage(12, 'watch', { key: '/key9' });
watchCb('rc3', 'data3', 'stat3');
expect(ws1.send).toHaveBeenCalledWith('{"method":"notify","params":{"key":"/key9","rc":"rc3","data":"data3","stat":"stat3","error":null}}', wsParams, jasmine.any(Function)); // jshint ignore: line
expect(ws2.send).toHaveBeenCalledWith('{"method":"notify","params":{"key":"/key9","rc":"rc3","data":"data3","stat":"stat3","error":null}}', wsParams, jasmine.any(Function)); // jshint ignore: line
});
it('should not throw on change when endpoints disconnected', function () {
var watchCb;
driver.watch.andCallFake(function (key, cb) {
watchCb = cb;
return cb('rc', 'data', 'stat');
});
ws1.onMessage(11, 'watch', { key: '/key9' });
instance.removeClient('endpoint1');
expect(function () {
watchCb('rc1', 'data1', 'stat1');
}).not.toThrow();
});
});
it('should not throw on method with wrong params', function () {
expect(function () {
ws1.onMessage(7, 'sync', null);
expect(ws1.send).toHaveBeenCalledWith('{"id":7,"method":"sync","rc":-1002,"data":"Wrong params","error":null}', wsParams, jasmine.any(Function)); // jshint ignore: line
}).not.toThrow();
});
it('should not throw on unknown method', function () {
expect(function () {
ws1.onMessage(7, 'unknown', { key: '/key10' });
expect(ws1.send).toHaveBeenCalledWith('{"id":7,"method":"unknown","rc":-1001,"data":"Unknown method","error":null}', wsParams, jasmine.any(Function)); // jshint ignore: line
}).not.toThrow();
});
it('should not throw on malformed message', function () {
expect(function () {
ws1.emit('message', '@$%@#@$@#');
}).not.toThrow();
});
it('should return with ZCONNECTIONLOSS when driver not connected', function () {
driver.isConnected.andReturn(false);
ws1.onMessage(8, 'get', { key: '/key11' });
expect(ws1.send).toHaveBeenCalledWith('{"id":8,"method":"get","rc":-4,"error":null}', wsParams, jasmine.any(Function));
});
it('should queue client calls', function () {
var callbacks = {};
driver.get.andCallFake(function (key, _callback) {
callbacks[key] = _callback;
});
instance.setMaxConcurrentQueries(2);
ws1.onMessage(101, 'get', { 'key': '/key101' });
ws1.onMessage(102, 'get', { 'key': '/key102' });
ws1.onMessage(103, 'get', { 'key': '/key103' });
expect(driver.get.callCount).toBe(2);
callbacks['/key101']();
callbacks['/key102']();
callbacks['/key103']();
expect(driver.get.callCount).toBe(3);
});
});
});