sharedb
Version:
JSON OT database backend
346 lines (307 loc) • 11.3 kB
JavaScript
var sinon = require('sinon');
var expect = require('chai').expect;
var Backend = require('../../lib/backend');
var Connection = require('../../lib/client/connection');
var LegacyConnection = require('sharedb-legacy/lib/client').Connection;
var StreamSocket = require('../../lib/stream-socket');
describe('client connection', function() {
beforeEach(function() {
this.backend = new Backend();
});
it('ends the agent stream when a connection is closed after connect', function(done) {
var connection = this.backend.connect();
connection.agent.stream.on('end', function() {
done();
});
connection.on('connected', function() {
connection.close();
});
});
it('ends the agent stream when a connection is immediately closed', function(done) {
var connection = this.backend.connect();
connection.agent.stream.on('end', function() {
done();
});
connection.close();
});
it('emits closed event on call to connection.close()', function(done) {
var connection = this.backend.connect();
connection.on('closed', function() {
done();
});
connection.close();
});
it('ends the agent stream on call to agent.close()', function(done) {
var isDone = false;
var finish = function() {
if (!isDone) done();
};
this.backend.use('connect', function(request, next) {
request.agent.stream.on('close', finish);
request.agent.stream.on('end', finish);
request.agent.close();
next();
});
this.backend.connect();
});
it('emits stopped event on call to agent.close()', function(done) {
this.backend.use('connect', function(request, next) {
request.agent.close();
next();
});
var connection = this.backend.connect();
connection.on('stopped', function() {
done();
});
});
it('subscribing to same doc closes old stream and adds new stream to agent', function(done) {
var connection = this.backend.connect();
var agent = connection.agent;
var collection = 'test';
var docId = 'abcd-1234';
var doc = connection.get(collection, docId);
doc.subscribe(function(err) {
if (err) return done(err);
var originalStream = agent.subscribedDocs[collection][docId];
doc.subscribe(function() {
if (err) return done(err);
expect(originalStream).to.have.property('open', false);
var newStream = agent.subscribedDocs[collection][docId];
expect(newStream).to.have.property('open', true);
expect(newStream).to.not.equal(originalStream);
connection.close();
done();
});
});
});
it('emits socket errors as "connection error" events', function(done) {
var connection = this.backend.connect();
connection.on('connection error', function(err) {
expect(err.message).equal('Test');
done();
});
connection.socket.onerror({message: 'Test'});
});
it('connects to a Backend that binds its socket late', function(done) {
var backend = this.backend;
var socket = new StreamSocket();
var connection = new Connection(socket);
socket._open();
var doc = connection.get('test', '123');
doc.fetch(done);
socket.stream.on('data', function() {
// Registering a stream triggers data to get flushed in our tests.
// In production, aw web socket might lose messages any time between
// connection and calling backend.listen()
});
process.nextTick(function() {
backend.listen(socket.stream);
});
});
it('connects when binding the Connection late', function(done) {
var backend = this.backend;
var socket = new StreamSocket();
socket._open();
backend.listen(socket.stream);
process.nextTick(function() {
var connection = new Connection(socket);
var doc = connection.get('test', '123');
doc.fetch(done);
});
});
describe('ping/pong', function() {
it('pings the backend', function(done) {
var connection = this.backend.connect();
connection.on('pong', function() {
done();
});
connection.on('connected', function() {
connection.ping();
});
});
it('handles errors', function(done) {
this.backend.use('receive', function(request, next) {
var error = request.data.a === 'pp' && new Error('bad');
next(error);
});
var connection = this.backend.connect();
connection.on('error', function(error) {
expect(error.message).to.equal('bad');
done();
});
connection.on('connected', function() {
connection.ping();
});
});
it('throws if pinging offline', function() {
var connection = this.backend.connect();
expect(function() {
connection.ping();
}).to.throw('Socket must be CONNECTED to ping');
});
});
describe('backend.agentsCount', function() {
it('updates after connect and connection.close()', function(done) {
var backend = this.backend;
expect(backend.agentsCount).equal(0);
var connection = backend.connect();
expect(backend.agentsCount).equal(1);
connection.on('connected', function() {
connection.close();
setTimeout(function() {
expect(backend.agentsCount).equal(0);
done();
}, 10);
});
});
it('updates after connection socket stream emits "close"', function(done) {
var backend = this.backend;
var connection = backend.connect();
connection.on('connected', function() {
connection.socket.stream.emit('close');
expect(backend.agentsCount).equal(0);
done();
});
});
it('updates correctly after stream emits both "end" and "close"', function(done) {
var backend = this.backend;
var connection = backend.connect();
connection.on('connected', function() {
connection.socket.stream.emit('end');
connection.socket.stream.emit('close');
expect(backend.agentsCount).equal(0);
done();
});
});
it('does not increment when agent connect is rejected', function() {
var backend = this.backend;
backend.use('connect', function(request, next) {
next({message: 'Error'});
});
expect(backend.agentsCount).equal(0);
backend.connect();
expect(backend.agentsCount).equal(0);
});
});
describe('state management using setSocket', function() {
it('initial connection.state is connecting, if socket.readyState is CONNECTING', function() {
// https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket-connecting
var socket = {readyState: 0};
var connection = new Connection(socket);
expect(connection.state).equal('connecting');
});
it('initial connection.state is connecting and init handshake, if socket.readyState is OPEN', function() {
var socket = {
readyState: 1,
send: sinon.spy()
};
var connection = new Connection(socket);
expect(connection.state).equal('connecting');
expect(socket.send.calledOnce).to.be.true;
var message = JSON.parse(socket.send.getCall(0).args[0]);
expect(message.a).to.equal('hs');
});
it('initial connection.state is disconnected, if socket.readyState is CLOSING', function() {
// https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket-closing
var socket = {readyState: 2};
var connection = new Connection(socket);
expect(connection.state).equal('disconnected');
});
it('initial connection.state is disconnected, if socket.readyState is CLOSED', function() {
// https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket-closed
var socket = {readyState: 3};
var connection = new Connection(socket);
expect(connection.state).equal('disconnected');
});
it('initial state is connecting', function() {
var connection = this.backend.connect();
expect(connection.state).equal('connecting');
});
it('after connected event is emitted, state is connected', function(done) {
var connection = this.backend.connect();
connection.on('connected', function() {
expect(connection.state).equal('connected');
done();
});
});
it('when connection is manually closed, state is closed', function(done) {
var connection = this.backend.connect();
connection.on('connected', function() {
connection.close();
});
connection.on('closed', function() {
expect(connection.state).equal('closed');
done();
});
});
it('when connection is disconnected, state is disconnected', function(done) {
var connection = this.backend.connect();
connection.on('connected', function() {
// Mock a disconnection by providing a reason
connection.socket.close('foo');
});
connection.on('disconnected', function() {
expect(connection.state).equal('disconnected');
done();
});
});
});
it('persists its id and seq when reconnecting', function(done) {
var backend = this.backend;
backend.connect(null, null, function(connection) {
var id = connection.id;
expect(id).to.be.ok;
var doc = connection.get('test', '123');
doc.create({foo: 'bar'}, function(error) {
if (error) return done(error);
expect(connection.seq).to.equal(2);
connection.close();
backend.connect(connection, null, function() {
expect(connection.id).to.equal(id);
expect(connection.seq).to.equal(2);
done();
});
});
});
});
it('still connects to legacy clients, whose ID changes on reconnection', function(done) {
var currentBackend = this.backend;
var socket = new StreamSocket();
var legacyClient = new LegacyConnection(socket);
currentBackend.connect(legacyClient);
var doc = legacyClient.get('test', '123');
doc.create({foo: 'bar'}, function(error) {
if (error) return done(error);
var initialId = legacyClient.id;
expect(initialId).to.equal(legacyClient.agent.clientId);
expect(legacyClient.agent.src).to.be.null;
legacyClient.close();
currentBackend.connect(legacyClient);
doc.submitOp({p: ['baz'], oi: 'qux'}, function(error) {
if (error) return done(error);
var newId = legacyClient.id;
expect(newId).not.to.equal(initialId);
expect(newId).to.equal(legacyClient.agent.clientId);
expect(legacyClient.agent.src).to.be.null;
done();
});
});
});
it('errors when submitting an op with a very large seq', function(done) {
this.backend.connect(null, null, function(connection) {
var doc = connection.get('test', '123');
doc.create({foo: 'bar'}, function(error) {
if (error) return done(error);
connection.sendOp(doc, {
op: {p: ['foo'], od: 'bar'},
src: connection.id,
seq: Number.MAX_SAFE_INTEGER
});
doc.once('error', function(error) {
expect(error.code).to.equal('ERR_CONNECTION_SEQ_INTEGER_OVERFLOW');
done();
});
});
});
});
});