recoder-code
Version:
Complete AI-powered development platform with ML model training, plugin registry, real-time collaboration, monitoring, infrastructure automation, and enterprise deployment capabilities
773 lines (683 loc) • 24 kB
JavaScript
var Backend = require('../../../lib/backend');
var PresencePauser = require('./presence-pauser');
var expect = require('chai').expect;
var async = require('async');
var errorHandler = require('../../util').errorHandler;
var sinon = require('sinon');
describe('Presence', function() {
var backend;
var connection1;
var connection2;
var presence1;
var presence2;
var presencePauser;
beforeEach(function(done) {
backend = new Backend({presence: true});
var connectedCount = 0;
connection1 = backend.connect();
connection2 = backend.connect();
var checkConnections = function() {
connectedCount++;
if (connectedCount === 2) done();
};
connection1.on('connected', checkConnections);
connection2.on('connected', checkConnections);
presencePauser = new PresencePauser();
backend.use(backend.MIDDLEWARE_ACTIONS.sendPresence, function(request, callback) {
presencePauser.sendPresence(request, callback);
});
presence1 = connection1.getPresence('test-channel');
presence2 = connection2.getPresence('test-channel');
});
afterEach(function(done) {
connection1.close();
connection2.close();
backend.close(done);
});
it('can subscribe to updates from other clients', function(done) {
var localPresence1 = presence1.create('presence-1');
async.series([
presence2.subscribe.bind(presence2),
function(next) {
localPresence1.submit({index: 5}, errorHandler(done));
presence2.once('receive', function(id, presence) {
expect(id).to.equal('presence-1');
expect(presence).to.eql({index: 5});
next();
});
}
], done);
});
it('can unsubscribe from updates to other clients', function(done) {
var localPresence1 = presence1.create('presence-1');
async.series([
presence2.subscribe.bind(presence2),
presence2.unsubscribe.bind(presence2),
function(next) {
localPresence1.submit({index: 5}, errorHandler(done));
presence2.once('receive', function() {
done(new Error('Should not have received presence update'));
});
next();
}
], done);
});
it('resubscribes after a destroy', function(done) {
var localPresence1 = presence1.create('presence-1');
var presence2a;
async.series([
presence2.subscribe.bind(presence2),
presence2.destroy.bind(presence2),
function(next) {
presence2a = connection2.getPresence('test-channel');
presence2a.subscribe(function(error) {
next(error);
});
},
function(next) {
localPresence1.submit({index: 5}, errorHandler(done));
presence2a.once('receive', function() {
next();
});
}
], done);
});
it('gets presence during a destroy', function(done) {
var localPresence1 = presence1.create('presence-1');
var presence2a;
async.series([
presence2.subscribe.bind(presence2),
function(next) {
presence2.destroy(errorHandler(done));
next();
},
function(next) {
presence2a = connection2.getPresence('test-channel');
presence2a.subscribe(function(error) {
next(error);
});
},
function(next) {
localPresence1.submit({index: 5}, errorHandler(done));
presence2a.once('receive', function() {
next();
});
}
], done);
});
it('destroys old local presence but keeps new local presence when getting during destroy', function(done) {
presence2.create('presence-2');
var presence2a;
async.series([
presence2.subscribe.bind(presence2),
function(next) {
presence2.destroy(function() {
expect(presence2).to.equal(presence2a);
expect(Object.keys(presence2.localPresences)).to.eql(['presence-2a']);
done();
});
next();
},
function(next) {
presence2a = connection2.getPresence('test-channel');
presence2a.create('presence-2a');
presence2a.subscribe(function(error) {
next(error);
});
}
], errorHandler(done));
});
it('does not throw if LocalPresence destroys before Presence', function(done) {
var localPresence1 = presence1.create('presence-1');
async.series([
presence1.subscribe.bind(presence1),
function(next) {
// Middleware that ensures the presence update replies before the
// unsubscribe, so the local presence is destroyed first
var replies = [];
backend.use('reply', function(message, cb) {
if (!replies) return cb();
if (message.reply.a !== 'p') return replies.push(cb);
var _replies = replies;
replies = null;
cb();
_replies.forEach(function(reply) {
reply();
});
});
presence1.destroy(next);
localPresence1.destroy(errorHandler(done));
}
], done);
});
it('throws if trying to create local presence when wanting destroy', function(done) {
presence2.destroy(errorHandler(done));
expect(function() {
presence2.create('presence-2');
}).to.throw('Presence is being destroyed');
done();
});
it('gets presence after destroy unsubscribe', function(done) {
var localPresence2 = presence2.create('presence-2');
var presence2a;
var flushLocalPresence2Destroy;
sinon.stub(localPresence2, 'destroy').callsFake(function(callback) {
flushLocalPresence2Destroy = callback;
});
async.series([
presence2.subscribe.bind(presence2),
function(next) {
presence2.destroy(function() {
expect(connection2.getPresence('test-channel')).to.equal(presence2a);
done();
});
next();
},
// Wait for the destroy unsubscribe callback to start, where we check
// _wantsDestroy for the first time
presence2.unsubscribe.bind(presence2),
function(next) {
presence2a = connection2.getPresence('test-channel');
presence2a.subscribe(function(error) {
next(error);
});
flushLocalPresence2Destroy();
}
], errorHandler(done));
});
it('requests existing presence from other subscribed clients when subscribing', function(done) {
var localPresence1 = presence1.create('presence-1');
async.series([
presence1.subscribe.bind(presence1),
localPresence1.submit.bind(localPresence1, {index: 2}),
function(next) {
presence2.subscribe(errorHandler(done));
presence2.once('receive', function(id, presence) {
expect(id).to.equal('presence-1');
expect(presence).to.eql({index: 2});
next();
});
}
], done);
});
it('removes remote presence when it is set to null', function(done) {
var localPresence1 = presence1.create('presence-1');
async.series([
presence2.subscribe.bind(presence2),
function(next) {
localPresence1.submit({index: 3}, errorHandler(done));
presence2.once('receive', function() {
expect(presence2.remotePresences).to.eql({
'presence-1': {index: 3}
});
next();
});
},
function(next) {
localPresence1.submit(null, errorHandler(done));
presence2.once('receive', function(id, presence) {
expect(presence).to.be.null;
expect(presence2.remotePresences).to.eql({});
next();
});
}
], done);
});
it('does not broadcast null local presence when requested', function(done) {
var localPresence1 = presence1.create('presence-1');
async.series([
presence1.subscribe.bind(presence1),
localPresence1.submit.bind(localPresence1, null),
function(next) {
presence2.subscribe(errorHandler(done));
presence2.once('receive', function() {
done(new Error('should not have received presence'));
});
next();
}
], done);
});
it('destroys its connection reference, unsubscribes and nulls its local presences', function(done) {
var localPresence1 = presence1.create('presence-1');
var localPresence2 = presence2.create('presence-2');
async.series([
presence1.subscribe.bind(presence1),
presence2.subscribe.bind(presence2),
function(next) {
localPresence1.submit({index: 1}, errorHandler(done));
presence2.once('receive', function() {
next();
});
},
function(next) {
localPresence2.submit({index: 2}, errorHandler(done));
presence1.once('receive', function() {
next();
});
},
presence1.destroy.bind(presence1),
function(next) {
expect(presence1.localPresences).to.eql({});
expect(presence2.remotePresences).to.eql({});
expect(connection1._presences).to.eql({});
next();
}
], done);
});
it('supports multiple local presences on a single connection', function(done) {
var localPresence1a = presence1.create('presence-1a');
var localPresence1b = presence1.create('presence-1b');
async.series([
presence2.subscribe.bind(presence2),
function(next) {
localPresence1a.submit({index: 1}, errorHandler(done));
presence2.once('receive', function(id, presence) {
expect(id).to.equal('presence-1a');
expect(presence).to.eql({index: 1});
next();
});
},
function(next) {
localPresence1b.submit({index: 2}, errorHandler(done));
presence2.once('receive', function(id, presence) {
expect(id).to.equal('presence-1b');
expect(presence).to.eql({index: 2});
expect(Object.keys(presence1.localPresences)).to.eql(['presence-1a', 'presence-1b']);
expect(presence2.remotePresences).to.eql({
'presence-1a': {index: 1},
'presence-1b': {index: 2}
});
next();
});
}
], done);
});
it('subscribes once the connection can send', function(done) {
var localPresence1 = presence1.create('presence-1');
connection2._setState('disconnected');
expect(connection2.canSend).to.be.false;
async.series([
function(next) {
presence2.subscribe(next);
connection2._setState('connecting');
connection2._setState('connected');
},
function(next) {
localPresence1.submit({index: 1}, errorHandler(done));
presence2.once('receive', function() {
next();
});
}
], done);
});
it('sends local presence once the connection can send', function(done) {
var localPresence1 = presence1.create('presence-1');
connection1._setState('disconnected');
expect(connection1.canSend).to.be.false;
async.series([
presence2.subscribe.bind(presence2),
function(next) {
localPresence1.submit({index: 1}, errorHandler(done));
presence2.once('receive', function() {
next();
});
connection1._setState('connecting');
connection1._setState('connected');
}
], done);
});
it('re-requests remote presence when reconnecting', function(done) {
var localPresence1 = presence1.create('presence-1');
async.series([
presence1.subscribe.bind(presence1),
presence2.subscribe.bind(presence2),
function(next) {
connection2.once('closed', function() {
next();
});
connection2.close();
expect(connection2.canSend).to.be.false;
},
localPresence1.submit.bind(localPresence1, {index: 1}),
function(next) {
presence2.once('receive', function(id, presence) {
expect(id).to.equal('presence-1');
expect(presence).to.eql({index: 1});
next();
});
backend.connect(connection2);
}
], done);
});
it('calls multiple callbacks if subscribing multiple times in series', function(done) {
var callbacksCalled = 0;
var callback = function(error) {
if (error) return done(error);
callbacksCalled++;
if (callbacksCalled === 3) done();
};
presence1.subscribe(callback);
presence1.subscribe(callback);
presence1.subscribe(callback);
});
it('finishes unsubscribed if calling immediately after subscribe', function(done) {
var localPresence1 = presence1.create('presence-1');
async.series([
function(next) {
var callbackCount = 0;
var callback = function(error) {
if (error) return done(error);
callbackCount++;
if (callbackCount === 2) next();
};
presence2.subscribe(callback);
presence2.unsubscribe(callback);
},
function(next) {
expect(presence2.wantSubscribe).to.be.false;
expect(presence2.subscribed).to.be.false;
localPresence1.submit({index: 1}, next);
presence2.on('receive', function() {
done(new Error('Should not have received presence'));
});
}
], done);
});
it('does not send another subscribe request if already subscribed', function(done) {
var sendPresenceAction = sinon.spy(connection1, '_sendPresenceAction');
async.series([
presence1.subscribe.bind(presence1),
presence1.subscribe.bind(presence1),
function(next) {
expect(sendPresenceAction).to.have.been.calledOnce;
next();
}
], done);
});
it('only subscribes once when calling multiple in parallel', function(done) {
var sendPresenceAction = sinon.spy(connection1, '_sendPresenceAction');
async.series([
function(next) {
async.parallel([
presence1.subscribe.bind(presence1),
presence1.subscribe.bind(presence1)
], next);
},
function(next) {
expect(sendPresenceAction).to.have.been.calledOnce;
next();
}
], done);
});
it('subscribes once when calling again after no callback', function(done) {
var sendPresenceAction = sinon.spy(connection1, '_sendPresenceAction');
presence1.subscribe(); // no callback
async.series([
presence1.subscribe.bind(presence1),
function(next) {
expect(sendPresenceAction).to.have.been.calledOnce;
next();
}
], done);
});
it('does not leak Streams when subscribing the same presence multiple times', function(done) {
var streamsCount;
async.series([
presence1.subscribe.bind(presence1),
function(next) {
streamsCount = backend.pubsub.streamsCount;
// Trick it into sending a duplicate request
presence1.wantSubscribe = false;
next();
},
presence1.subscribe.bind(presence1),
function(next) {
expect(backend.pubsub.streamsCount).to.equal(streamsCount);
next();
}
], done);
});
it('throws an error when trying to create a presence with a non-string ID', function() {
expect(function() {
presence1.create(123);
}).to.throw();
});
['__proto__', 'constructor'].forEach(function(badProp) {
it('Rejects presence with channel ' + badProp, function(done) {
var presence = connection1.getPresence(badProp);
presence.subscribe(function(err) {
expect(err).to.be.an('error').to.haveOwnProperty('message', 'Invalid presence channel');
done();
});
});
});
it('assigns an ID if one is not provided', function() {
var localPresence = presence1.create();
expect(localPresence.presenceId).to.be.ok;
});
it('returns the error if a local presence cannot be destroyed because of a bad submit', function(done) {
var localPresence1 = presence1.create('presence-1');
sinon.stub(localPresence1, 'submit').callsFake(function(value, callback) {
callback(new Error('bad'));
});
localPresence1.destroy(function(error) {
expect(error).to.be.ok;
done();
});
});
it('throws an error if a presence is created with a non-string channel', function() {
expect(function() {
connection1.getPresence(123);
}).to.throw();
});
it('throws an error if a presence is created with an empty string channel', function() {
expect(function() {
connection1.getPresence('');
}).to.throw();
});
it('returns unsubscribe errors when trying to destroy presence', function(done) {
sinon.stub(presence1, 'unsubscribe').callsFake(function(callback) {
callback(new Error('bad'));
});
presence1.destroy(function(error) {
expect(error).to.be.ok;
done();
});
});
it('emits unsubscribe errors when trying to destroy presence', function(done) {
sinon.stub(presence1, 'unsubscribe').callsFake(function(callback) {
callback(new Error('bad'));
});
presence1.once('error', function() {
done();
});
presence1.destroy();
});
it('emits an error when trying to broadcast all presence with an error', function(done) {
presence1.once('error', function() {
done();
});
presence1._broadcastAllLocalPresence(new Error('bad'));
});
it('emits a subscribe error on reconnection', function(done) {
async.series([
presence1.subscribe.bind(presence1),
function(next) {
var handleSubscribe = presence1._handleSubscribe;
sinon.stub(presence1, '_handleSubscribe').callsFake(function(error, seq) {
error = new Error('bad');
handleSubscribe.apply(presence1, [error, seq]);
});
presence1.once('error', function() {
next();
});
connection1._setState('disconnected');
connection1._setState('connecting');
connection1._setState('connected');
}
], done);
});
it('emits a subscribe error on reconnection when there are subscribe requests without callbacks', function(done) {
async.series([
presence1.subscribe.bind(presence1),
function(next) {
var handleSubscribe = presence1._handleSubscribe;
sinon.stub(presence1, '_handleSubscribe').callsFake(function(error, seq) {
error = new Error('bad');
handleSubscribe.apply(presence1, [error, seq]);
});
presence1.once('error', function() {
next();
});
connection1._setState('disconnected');
presence1.subscribe();
connection1._setState('connecting');
connection1._setState('connected');
}
], done);
});
it('ignores presence that arrives out of order', function(done) {
var localPresence1 = presence1.create('presence-1');
async.series([
presence2.subscribe.bind(presence2),
function(next) {
var hasPaused = false;
// Catch the first presence update, but then allow later ones
presencePauser.pause(function() {
if (hasPaused) return false;
hasPaused = true;
return true;
});
localPresence1.submit({index: 2}, next);
},
function(next) {
presence2.once('receive', function(id, presence) {
expect(presence).to.eql({index: 3});
presence2.once('receive', function() {
done(new Error('should not get another presence event'));
});
presencePauser.resume();
next();
});
localPresence1.submit({index: 3}, errorHandler(done));
}
], done);
});
it('adds itself back onto the connection after a destroy and a resubscribe', function(done) {
async.series([
presence1.destroy.bind(presence1),
presence1.subscribe.bind(presence1),
function(next) {
expect(connection1._presences[presence1.channel]).to.equal(presence1);
next();
}
], done);
});
it('broadcasts a null presence when the connection is disconnected', function(done) {
var localPresence1 = presence1.create('presence-1');
async.series([
presence1.subscribe.bind(presence1),
presence2.subscribe.bind(presence2),
function(next) {
localPresence1.submit({index: 3}, errorHandler(done));
presence2.once('receive', function() {
next();
});
},
function(next) {
presence2.once('receive', function(id, presence) {
expect(id).to.equal('presence-1');
expect(presence).to.be.null;
next();
});
connection1.close();
}
], done);
});
describe('middleware', function() {
describe('receivePresence', function() {
it('provides the presence in the middleware', function(done) {
backend.use(backend.MIDDLEWARE_ACTIONS.receivePresence, function(context) {
expect(context.presence.p).to.eql({index: 5});
done();
});
var localPresence1 = presence1.create('presence-1');
localPresence1.submit({index: 5}, errorHandler(done));
});
it('can mutate the presence in the middleware', function(done) {
backend.use(backend.MIDDLEWARE_ACTIONS.receivePresence, function(context, next) {
context.presence.p.index++;
next();
});
var localPresence1 = presence1.create('presence-1');
async.series([
presence2.subscribe.bind(presence2),
function(next) {
presence2.on('receive', function(id, value) {
expect(value).to.eql({index: 6});
expect(connection1.agent.presenceRequests['test-channel']['presence-1'].p).to.eql({index: 6});
next();
});
localPresence1.submit({index: 5}, errorHandler(done));
}
], done);
});
it('can cancel a presence broadcast by erroring', function(done) {
backend.use(backend.MIDDLEWARE_ACTIONS.receivePresence, function(context, next) {
next(new Error('bad!'));
});
var localPresence1 = presence1.create('presence-1');
async.series([
presence2.subscribe.bind(presence2),
function(next) {
presence2.on('receive', function() {
done(new Error('should not have received presence'));
});
localPresence1.submit({index: 5}, function(error) {
expect(error.message).to.contain('bad!');
expect(connection1.agent.presenceRequests['test-channel']).not.to.be.ok;
next();
});
}
], done);
});
});
describe('sendPresence', function() {
// TODO: This functionality is deprecated
it('sends an error to a subscribed client', function(done) {
var localPresence1 = presence1.create('presence-1');
backend.use(backend.MIDDLEWARE_ACTIONS.sendPresence, function(context, next) {
next(new Error('uh-oh!'));
});
async.series([
presence2.subscribe.bind(presence2),
function(next) {
localPresence1.submit({index: 3}, errorHandler(done));
presence2.once('error', function(error) {
expect(error.message).to.equal('uh-oh!');
next();
});
}
], done);
});
it('emits errors on the server instead of sending to the client', function(done) {
var localPresence1 = presence1.create('presence-1');
backend.doNotForwardSendPresenceErrorsToClient = true;
backend.use(backend.MIDDLEWARE_ACTIONS.sendPresence, function(context, next) {
next(new Error('uh-oh!'));
});
async.series([
presence2.subscribe.bind(presence2),
function(next) {
localPresence1.submit({index: 3}, errorHandler(done));
presence2.on('error', errorHandler(done));
backend.errorHandler = function(error) {
expect(error.message).to.equal('uh-oh!');
next();
};
}
], done);
});
});
});
});