diff --git a/services/real-time/test/unit/js/AuthorizationManagerTests.js b/services/real-time/test/unit/js/AuthorizationManagerTests.js index d3aa6be9fa..3093017a39 100644 --- a/services/real-time/test/unit/js/AuthorizationManagerTests.js +++ b/services/real-time/test/unit/js/AuthorizationManagerTests.js @@ -9,214 +9,312 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const chai = require("chai"); -chai.should(); -const { - expect -} = chai; -const sinon = require("sinon"); -const SandboxedModule = require('sandboxed-module'); -const path = require("path"); -const modulePath = '../../../app/js/AuthorizationManager'; +const chai = require('chai') +chai.should() +const { expect } = chai +const sinon = require('sinon') +const SandboxedModule = require('sandboxed-module') +const path = require('path') +const modulePath = '../../../app/js/AuthorizationManager' -describe('AuthorizationManager', function() { - beforeEach(function() { - this.client = - {ol_context: {}}; +describe('AuthorizationManager', function () { + beforeEach(function () { + this.client = { ol_context: {} } - return this.AuthorizationManager = SandboxedModule.require(modulePath, {requires: {}});}); + return (this.AuthorizationManager = SandboxedModule.require(modulePath, { + requires: {} + })) + }) - describe("assertClientCanViewProject", function() { - it("should allow the readOnly privilegeLevel", function(done) { - this.client.ol_context.privilege_level = "readOnly"; - return this.AuthorizationManager.assertClientCanViewProject(this.client, (error) => { - expect(error).to.be.null; - return done(); - }); - }); + describe('assertClientCanViewProject', function () { + it('should allow the readOnly privilegeLevel', function (done) { + this.client.ol_context.privilege_level = 'readOnly' + return this.AuthorizationManager.assertClientCanViewProject( + this.client, + (error) => { + expect(error).to.be.null + return done() + } + ) + }) - it("should allow the readAndWrite privilegeLevel", function(done) { - this.client.ol_context.privilege_level = "readAndWrite"; - return this.AuthorizationManager.assertClientCanViewProject(this.client, (error) => { - expect(error).to.be.null; - return done(); - }); - }); + it('should allow the readAndWrite privilegeLevel', function (done) { + this.client.ol_context.privilege_level = 'readAndWrite' + return this.AuthorizationManager.assertClientCanViewProject( + this.client, + (error) => { + expect(error).to.be.null + return done() + } + ) + }) - it("should allow the owner privilegeLevel", function(done) { - this.client.ol_context.privilege_level = "owner"; - return this.AuthorizationManager.assertClientCanViewProject(this.client, (error) => { - expect(error).to.be.null; - return done(); - }); - }); + it('should allow the owner privilegeLevel', function (done) { + this.client.ol_context.privilege_level = 'owner' + return this.AuthorizationManager.assertClientCanViewProject( + this.client, + (error) => { + expect(error).to.be.null + return done() + } + ) + }) - return it("should return an error with any other privilegeLevel", function(done) { - this.client.ol_context.privilege_level = "unknown"; - return this.AuthorizationManager.assertClientCanViewProject(this.client, (error) => { - error.message.should.equal("not authorized"); - return done(); - }); - }); - }); + return it('should return an error with any other privilegeLevel', function (done) { + this.client.ol_context.privilege_level = 'unknown' + return this.AuthorizationManager.assertClientCanViewProject( + this.client, + (error) => { + error.message.should.equal('not authorized') + return done() + } + ) + }) + }) - describe("assertClientCanEditProject", function() { - it("should not allow the readOnly privilegeLevel", function(done) { - this.client.ol_context.privilege_level = "readOnly"; - return this.AuthorizationManager.assertClientCanEditProject(this.client, (error) => { - error.message.should.equal("not authorized"); - return done(); - }); - }); + describe('assertClientCanEditProject', function () { + it('should not allow the readOnly privilegeLevel', function (done) { + this.client.ol_context.privilege_level = 'readOnly' + return this.AuthorizationManager.assertClientCanEditProject( + this.client, + (error) => { + error.message.should.equal('not authorized') + return done() + } + ) + }) - it("should allow the readAndWrite privilegeLevel", function(done) { - this.client.ol_context.privilege_level = "readAndWrite"; - return this.AuthorizationManager.assertClientCanEditProject(this.client, (error) => { - expect(error).to.be.null; - return done(); - }); - }); + it('should allow the readAndWrite privilegeLevel', function (done) { + this.client.ol_context.privilege_level = 'readAndWrite' + return this.AuthorizationManager.assertClientCanEditProject( + this.client, + (error) => { + expect(error).to.be.null + return done() + } + ) + }) - it("should allow the owner privilegeLevel", function(done) { - this.client.ol_context.privilege_level = "owner"; - return this.AuthorizationManager.assertClientCanEditProject(this.client, (error) => { - expect(error).to.be.null; - return done(); - }); - }); + it('should allow the owner privilegeLevel', function (done) { + this.client.ol_context.privilege_level = 'owner' + return this.AuthorizationManager.assertClientCanEditProject( + this.client, + (error) => { + expect(error).to.be.null + return done() + } + ) + }) - return it("should return an error with any other privilegeLevel", function(done) { - this.client.ol_context.privilege_level = "unknown"; - return this.AuthorizationManager.assertClientCanEditProject(this.client, (error) => { - error.message.should.equal("not authorized"); - return done(); - }); - }); - }); + return it('should return an error with any other privilegeLevel', function (done) { + this.client.ol_context.privilege_level = 'unknown' + return this.AuthorizationManager.assertClientCanEditProject( + this.client, + (error) => { + error.message.should.equal('not authorized') + return done() + } + ) + }) + }) - // check doc access for project + // check doc access for project - describe("assertClientCanViewProjectAndDoc", function() { - beforeEach(function() { - this.doc_id = "12345"; - this.callback = sinon.stub(); - return this.client.ol_context = {};}); + describe('assertClientCanViewProjectAndDoc', function () { + beforeEach(function () { + this.doc_id = '12345' + this.callback = sinon.stub() + return (this.client.ol_context = {}) + }) - describe("when not authorised at the project level", function() { - beforeEach(function() { - return this.client.ol_context.privilege_level = "unknown"; - }); + describe('when not authorised at the project level', function () { + beforeEach(function () { + return (this.client.ol_context.privilege_level = 'unknown') + }) - it("should not allow access", function() { - return this.AuthorizationManager.assertClientCanViewProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized")); - }); + it('should not allow access', function () { + return this.AuthorizationManager.assertClientCanViewProjectAndDoc( + this.client, + this.doc_id, + (err) => err.message.should.equal('not authorized') + ) + }) - return describe("even when authorised at the doc level", function() { - beforeEach(function(done) { - return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, done); - }); + return describe('even when authorised at the doc level', function () { + beforeEach(function (done) { + return this.AuthorizationManager.addAccessToDoc( + this.client, + this.doc_id, + done + ) + }) - return it("should not allow access", function() { - return this.AuthorizationManager.assertClientCanViewProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized")); - }); - }); - }); + return it('should not allow access', function () { + return this.AuthorizationManager.assertClientCanViewProjectAndDoc( + this.client, + this.doc_id, + (err) => err.message.should.equal('not authorized') + ) + }) + }) + }) - return describe("when authorised at the project level", function() { - beforeEach(function() { - return this.client.ol_context.privilege_level = "readOnly"; - }); + return describe('when authorised at the project level', function () { + beforeEach(function () { + return (this.client.ol_context.privilege_level = 'readOnly') + }) - describe("and not authorised at the document level", function() { return it("should not allow access", function() { - return this.AuthorizationManager.assertClientCanViewProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized")); - }); }); + describe('and not authorised at the document level', function () { + return it('should not allow access', function () { + return this.AuthorizationManager.assertClientCanViewProjectAndDoc( + this.client, + this.doc_id, + (err) => err.message.should.equal('not authorized') + ) + }) + }) - describe("and authorised at the document level", function() { - beforeEach(function(done) { - return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, done); - }); + describe('and authorised at the document level', function () { + beforeEach(function (done) { + return this.AuthorizationManager.addAccessToDoc( + this.client, + this.doc_id, + done + ) + }) - return it("should allow access", function() { - this.AuthorizationManager.assertClientCanViewProjectAndDoc(this.client, this.doc_id, this.callback); - return this.callback - .calledWith(null) - .should.equal(true); - }); - }); + return it('should allow access', function () { + this.AuthorizationManager.assertClientCanViewProjectAndDoc( + this.client, + this.doc_id, + this.callback + ) + return this.callback.calledWith(null).should.equal(true) + }) + }) - return describe("when document authorisation is added and then removed", function() { - beforeEach(function(done) { - return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, () => { - return this.AuthorizationManager.removeAccessToDoc(this.client, this.doc_id, done); - }); - }); + return describe('when document authorisation is added and then removed', function () { + beforeEach(function (done) { + return this.AuthorizationManager.addAccessToDoc( + this.client, + this.doc_id, + () => { + return this.AuthorizationManager.removeAccessToDoc( + this.client, + this.doc_id, + done + ) + } + ) + }) - return it("should deny access", function() { - return this.AuthorizationManager.assertClientCanViewProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized")); - }); - }); - }); - }); + return it('should deny access', function () { + return this.AuthorizationManager.assertClientCanViewProjectAndDoc( + this.client, + this.doc_id, + (err) => err.message.should.equal('not authorized') + ) + }) + }) + }) + }) - return describe("assertClientCanEditProjectAndDoc", function() { - beforeEach(function() { - this.doc_id = "12345"; - this.callback = sinon.stub(); - return this.client.ol_context = {};}); + return describe('assertClientCanEditProjectAndDoc', function () { + beforeEach(function () { + this.doc_id = '12345' + this.callback = sinon.stub() + return (this.client.ol_context = {}) + }) - describe("when not authorised at the project level", function() { - beforeEach(function() { - return this.client.ol_context.privilege_level = "readOnly"; - }); + describe('when not authorised at the project level', function () { + beforeEach(function () { + return (this.client.ol_context.privilege_level = 'readOnly') + }) - it("should not allow access", function() { - return this.AuthorizationManager.assertClientCanEditProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized")); - }); + it('should not allow access', function () { + return this.AuthorizationManager.assertClientCanEditProjectAndDoc( + this.client, + this.doc_id, + (err) => err.message.should.equal('not authorized') + ) + }) - return describe("even when authorised at the doc level", function() { - beforeEach(function(done) { - return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, done); - }); + return describe('even when authorised at the doc level', function () { + beforeEach(function (done) { + return this.AuthorizationManager.addAccessToDoc( + this.client, + this.doc_id, + done + ) + }) - return it("should not allow access", function() { - return this.AuthorizationManager.assertClientCanEditProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized")); - }); - }); - }); + return it('should not allow access', function () { + return this.AuthorizationManager.assertClientCanEditProjectAndDoc( + this.client, + this.doc_id, + (err) => err.message.should.equal('not authorized') + ) + }) + }) + }) - return describe("when authorised at the project level", function() { - beforeEach(function() { - return this.client.ol_context.privilege_level = "readAndWrite"; - }); + return describe('when authorised at the project level', function () { + beforeEach(function () { + return (this.client.ol_context.privilege_level = 'readAndWrite') + }) - describe("and not authorised at the document level", function() { return it("should not allow access", function() { - return this.AuthorizationManager.assertClientCanEditProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized")); - }); }); + describe('and not authorised at the document level', function () { + return it('should not allow access', function () { + return this.AuthorizationManager.assertClientCanEditProjectAndDoc( + this.client, + this.doc_id, + (err) => err.message.should.equal('not authorized') + ) + }) + }) - describe("and authorised at the document level", function() { - beforeEach(function(done) { - return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, done); - }); + describe('and authorised at the document level', function () { + beforeEach(function (done) { + return this.AuthorizationManager.addAccessToDoc( + this.client, + this.doc_id, + done + ) + }) - return it("should allow access", function() { - this.AuthorizationManager.assertClientCanEditProjectAndDoc(this.client, this.doc_id, this.callback); - return this.callback - .calledWith(null) - .should.equal(true); - }); - }); + return it('should allow access', function () { + this.AuthorizationManager.assertClientCanEditProjectAndDoc( + this.client, + this.doc_id, + this.callback + ) + return this.callback.calledWith(null).should.equal(true) + }) + }) - return describe("when document authorisation is added and then removed", function() { - beforeEach(function(done) { - return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, () => { - return this.AuthorizationManager.removeAccessToDoc(this.client, this.doc_id, done); - }); - }); + return describe('when document authorisation is added and then removed', function () { + beforeEach(function (done) { + return this.AuthorizationManager.addAccessToDoc( + this.client, + this.doc_id, + () => { + return this.AuthorizationManager.removeAccessToDoc( + this.client, + this.doc_id, + done + ) + } + ) + }) - return it("should deny access", function() { - return this.AuthorizationManager.assertClientCanEditProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized")); - }); - }); - }); - }); -}); + return it('should deny access', function () { + return this.AuthorizationManager.assertClientCanEditProjectAndDoc( + this.client, + this.doc_id, + (err) => err.message.should.equal('not authorized') + ) + }) + }) + }) + }) +}) diff --git a/services/real-time/test/unit/js/ChannelManagerTests.js b/services/real-time/test/unit/js/ChannelManagerTests.js index 1b71565975..6026f6ab5c 100644 --- a/services/real-time/test/unit/js/ChannelManagerTests.js +++ b/services/real-time/test/unit/js/ChannelManagerTests.js @@ -9,272 +9,430 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const chai = require('chai'); -const should = chai.should(); -const { - expect -} = chai; -const sinon = require("sinon"); -const modulePath = "../../../app/js/ChannelManager.js"; -const SandboxedModule = require('sandboxed-module'); +const chai = require('chai') +const should = chai.should() +const { expect } = chai +const sinon = require('sinon') +const modulePath = '../../../app/js/ChannelManager.js' +const SandboxedModule = require('sandboxed-module') -describe('ChannelManager', function() { - beforeEach(function() { - this.rclient = {}; - this.other_rclient = {}; - return this.ChannelManager = SandboxedModule.require(modulePath, { requires: { - "settings-sharelatex": (this.settings = {}), - "metrics-sharelatex": (this.metrics = {inc: sinon.stub(), summary: sinon.stub()}), - "logger-sharelatex": (this.logger = { log: sinon.stub(), warn: sinon.stub(), error: sinon.stub() }) - } - });}); +describe('ChannelManager', function () { + beforeEach(function () { + this.rclient = {} + this.other_rclient = {} + return (this.ChannelManager = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': (this.settings = {}), + 'metrics-sharelatex': (this.metrics = { + inc: sinon.stub(), + summary: sinon.stub() + }), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + warn: sinon.stub(), + error: sinon.stub() + }) + } + })) + }) - describe("subscribe", function() { + describe('subscribe', function () { + describe('when there is no existing subscription for this redis client', function () { + beforeEach(function (done) { + this.rclient.subscribe = sinon.stub().resolves() + this.ChannelManager.subscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + return setTimeout(done) + }) - describe("when there is no existing subscription for this redis client", function() { - beforeEach(function(done) { - this.rclient.subscribe = sinon.stub().resolves(); - this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); - return setTimeout(done); - }); + return it('should subscribe to the redis channel', function () { + return this.rclient.subscribe + .calledWithExactly('applied-ops:1234567890abcdef') + .should.equal(true) + }) + }) - return it("should subscribe to the redis channel", function() { - return this.rclient.subscribe.calledWithExactly("applied-ops:1234567890abcdef").should.equal(true); - }); - }); + describe('when there is an existing subscription for this redis client', function () { + beforeEach(function (done) { + this.rclient.subscribe = sinon.stub().resolves() + this.ChannelManager.subscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + this.ChannelManager.subscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + return setTimeout(done) + }) - describe("when there is an existing subscription for this redis client", function() { - beforeEach(function(done) { - this.rclient.subscribe = sinon.stub().resolves(); - this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); - this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); - return setTimeout(done); - }); + return it('should subscribe to the redis channel again', function () { + return this.rclient.subscribe.callCount.should.equal(2) + }) + }) - return it("should subscribe to the redis channel again", function() { - return this.rclient.subscribe.callCount.should.equal(2); - }); - }); + describe('when subscribe errors', function () { + beforeEach(function (done) { + this.rclient.subscribe = sinon + .stub() + .onFirstCall() + .rejects(new Error('some redis error')) + .onSecondCall() + .resolves() + const p = this.ChannelManager.subscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + p.then(() => done(new Error('should not subscribe but fail'))).catch( + (err) => { + err.message.should.equal('some redis error') + this.ChannelManager.getClientMapEntry(this.rclient) + .has('applied-ops:1234567890abcdef') + .should.equal(false) + this.ChannelManager.subscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + // subscribe is wrapped in Promise, delay other assertions + return setTimeout(done) + } + ) + return null + }) - describe("when subscribe errors", function() { - beforeEach(function(done) { - this.rclient.subscribe = sinon.stub() - .onFirstCall().rejects(new Error("some redis error")) - .onSecondCall().resolves(); - const p = this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); - p.then(() => done(new Error('should not subscribe but fail'))).catch(err => { - err.message.should.equal("some redis error"); - this.ChannelManager.getClientMapEntry(this.rclient).has("applied-ops:1234567890abcdef").should.equal(false); - this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); - // subscribe is wrapped in Promise, delay other assertions - return setTimeout(done); - }); - return null; - }); + it('should have recorded the error', function () { + return expect( + this.metrics.inc.calledWithExactly('subscribe.failed.applied-ops') + ).to.equal(true) + }) - it("should have recorded the error", function() { - return expect(this.metrics.inc.calledWithExactly("subscribe.failed.applied-ops")).to.equal(true); - }); + it('should subscribe again', function () { + return this.rclient.subscribe.callCount.should.equal(2) + }) - it("should subscribe again", function() { - return this.rclient.subscribe.callCount.should.equal(2); - }); + return it('should cleanup', function () { + return this.ChannelManager.getClientMapEntry(this.rclient) + .has('applied-ops:1234567890abcdef') + .should.equal(false) + }) + }) - return it("should cleanup", function() { - return this.ChannelManager.getClientMapEntry(this.rclient).has("applied-ops:1234567890abcdef").should.equal(false); - }); - }); + describe('when subscribe errors and the clientChannelMap entry was replaced', function () { + beforeEach(function (done) { + this.rclient.subscribe = sinon + .stub() + .onFirstCall() + .rejects(new Error('some redis error')) + .onSecondCall() + .resolves() + this.first = this.ChannelManager.subscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + // ignore error + this.first.catch(() => {}) + expect( + this.ChannelManager.getClientMapEntry(this.rclient).get( + 'applied-ops:1234567890abcdef' + ) + ).to.equal(this.first) - describe("when subscribe errors and the clientChannelMap entry was replaced", function() { - beforeEach(function(done) { - this.rclient.subscribe = sinon.stub() - .onFirstCall().rejects(new Error("some redis error")) - .onSecondCall().resolves(); - this.first = this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); - // ignore error - this.first.catch((() => {})); - expect(this.ChannelManager.getClientMapEntry(this.rclient).get("applied-ops:1234567890abcdef")).to.equal(this.first); + this.rclient.unsubscribe = sinon.stub().resolves() + this.ChannelManager.unsubscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + this.second = this.ChannelManager.subscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + // should get replaced immediately + expect( + this.ChannelManager.getClientMapEntry(this.rclient).get( + 'applied-ops:1234567890abcdef' + ) + ).to.equal(this.second) - this.rclient.unsubscribe = sinon.stub().resolves(); - this.ChannelManager.unsubscribe(this.rclient, "applied-ops", "1234567890abcdef"); - this.second = this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); - // should get replaced immediately - expect(this.ChannelManager.getClientMapEntry(this.rclient).get("applied-ops:1234567890abcdef")).to.equal(this.second); + // let the first subscribe error -> unsubscribe -> subscribe + return setTimeout(done) + }) - // let the first subscribe error -> unsubscribe -> subscribe - return setTimeout(done); - }); + return it('should cleanup the second subscribePromise', function () { + return expect( + this.ChannelManager.getClientMapEntry(this.rclient).has( + 'applied-ops:1234567890abcdef' + ) + ).to.equal(false) + }) + }) - return it("should cleanup the second subscribePromise", function() { - return expect(this.ChannelManager.getClientMapEntry(this.rclient).has("applied-ops:1234567890abcdef")).to.equal(false); - }); - }); + return describe('when there is an existing subscription for another redis client but not this one', function () { + beforeEach(function (done) { + this.other_rclient.subscribe = sinon.stub().resolves() + this.ChannelManager.subscribe( + this.other_rclient, + 'applied-ops', + '1234567890abcdef' + ) + this.rclient.subscribe = sinon.stub().resolves() // discard the original stub + this.ChannelManager.subscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + return setTimeout(done) + }) - return describe("when there is an existing subscription for another redis client but not this one", function() { - beforeEach(function(done) { - this.other_rclient.subscribe = sinon.stub().resolves(); - this.ChannelManager.subscribe(this.other_rclient, "applied-ops", "1234567890abcdef"); - this.rclient.subscribe = sinon.stub().resolves(); // discard the original stub - this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); - return setTimeout(done); - }); + return it('should subscribe to the redis channel on this redis client', function () { + return this.rclient.subscribe + .calledWithExactly('applied-ops:1234567890abcdef') + .should.equal(true) + }) + }) + }) - return it("should subscribe to the redis channel on this redis client", function() { - return this.rclient.subscribe.calledWithExactly("applied-ops:1234567890abcdef").should.equal(true); - }); - }); - }); + describe('unsubscribe', function () { + describe('when there is no existing subscription for this redis client', function () { + beforeEach(function (done) { + this.rclient.unsubscribe = sinon.stub().resolves() + this.ChannelManager.unsubscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + return setTimeout(done) + }) - describe("unsubscribe", function() { + return it('should unsubscribe from the redis channel', function () { + return this.rclient.unsubscribe.called.should.equal(true) + }) + }) - describe("when there is no existing subscription for this redis client", function() { - beforeEach(function(done) { - this.rclient.unsubscribe = sinon.stub().resolves(); - this.ChannelManager.unsubscribe(this.rclient, "applied-ops", "1234567890abcdef"); - return setTimeout(done); - }); + describe('when there is an existing subscription for this another redis client but not this one', function () { + beforeEach(function (done) { + this.other_rclient.subscribe = sinon.stub().resolves() + this.rclient.unsubscribe = sinon.stub().resolves() + this.ChannelManager.subscribe( + this.other_rclient, + 'applied-ops', + '1234567890abcdef' + ) + this.ChannelManager.unsubscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + return setTimeout(done) + }) - return it("should unsubscribe from the redis channel", function() { - return this.rclient.unsubscribe.called.should.equal(true); - }); - }); + return it('should still unsubscribe from the redis channel on this client', function () { + return this.rclient.unsubscribe.called.should.equal(true) + }) + }) + describe('when unsubscribe errors and completes', function () { + beforeEach(function (done) { + this.rclient.subscribe = sinon.stub().resolves() + this.ChannelManager.subscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + this.rclient.unsubscribe = sinon + .stub() + .rejects(new Error('some redis error')) + this.ChannelManager.unsubscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + setTimeout(done) + return null + }) - describe("when there is an existing subscription for this another redis client but not this one", function() { - beforeEach(function(done) { - this.other_rclient.subscribe = sinon.stub().resolves(); - this.rclient.unsubscribe = sinon.stub().resolves(); - this.ChannelManager.subscribe(this.other_rclient, "applied-ops", "1234567890abcdef"); - this.ChannelManager.unsubscribe(this.rclient, "applied-ops", "1234567890abcdef"); - return setTimeout(done); - }); + it('should have cleaned up', function () { + return this.ChannelManager.getClientMapEntry(this.rclient) + .has('applied-ops:1234567890abcdef') + .should.equal(false) + }) - return it("should still unsubscribe from the redis channel on this client", function() { - return this.rclient.unsubscribe.called.should.equal(true); - }); - }); + return it('should not error out when subscribing again', function (done) { + const p = this.ChannelManager.subscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + p.then(() => done()).catch(done) + return null + }) + }) - describe("when unsubscribe errors and completes", function() { - beforeEach(function(done) { - this.rclient.subscribe = sinon.stub().resolves(); - this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); - this.rclient.unsubscribe = sinon.stub().rejects(new Error("some redis error")); - this.ChannelManager.unsubscribe(this.rclient, "applied-ops", "1234567890abcdef"); - setTimeout(done); - return null; - }); + describe('when unsubscribe errors and another client subscribes at the same time', function () { + beforeEach(function (done) { + this.rclient.subscribe = sinon.stub().resolves() + this.ChannelManager.subscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + let rejectSubscribe + this.rclient.unsubscribe = () => + new Promise((resolve, reject) => (rejectSubscribe = reject)) + this.ChannelManager.unsubscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) - it("should have cleaned up", function() { - return this.ChannelManager.getClientMapEntry(this.rclient).has("applied-ops:1234567890abcdef").should.equal(false); - }); + setTimeout(() => { + // delay, actualUnsubscribe should not see the new subscribe request + this.ChannelManager.subscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + .then(() => setTimeout(done)) + .catch(done) + return setTimeout(() => + // delay, rejectSubscribe is not defined immediately + rejectSubscribe(new Error('redis error')) + ) + }) + return null + }) - return it("should not error out when subscribing again", function(done) { - const p = this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); - p.then(() => done()).catch(done); - return null; - }); - }); + it('should have recorded the error', function () { + return expect( + this.metrics.inc.calledWithExactly('unsubscribe.failed.applied-ops') + ).to.equal(true) + }) - describe("when unsubscribe errors and another client subscribes at the same time", function() { - beforeEach(function(done) { - this.rclient.subscribe = sinon.stub().resolves(); - this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); - let rejectSubscribe; - this.rclient.unsubscribe = () => new Promise((resolve, reject) => rejectSubscribe = reject); - this.ChannelManager.unsubscribe(this.rclient, "applied-ops", "1234567890abcdef"); + it('should have subscribed', function () { + return this.rclient.subscribe.called.should.equal(true) + }) - setTimeout(() => { - // delay, actualUnsubscribe should not see the new subscribe request - this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef") - .then(() => setTimeout(done)).catch(done); - return setTimeout(() => // delay, rejectSubscribe is not defined immediately - rejectSubscribe(new Error("redis error"))); - }); - return null; - }); + return it('should have discarded the finished Promise', function () { + return this.ChannelManager.getClientMapEntry(this.rclient) + .has('applied-ops:1234567890abcdef') + .should.equal(false) + }) + }) - it("should have recorded the error", function() { - return expect(this.metrics.inc.calledWithExactly("unsubscribe.failed.applied-ops")).to.equal(true); - }); + return describe('when there is an existing subscription for this redis client', function () { + beforeEach(function (done) { + this.rclient.subscribe = sinon.stub().resolves() + this.rclient.unsubscribe = sinon.stub().resolves() + this.ChannelManager.subscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + this.ChannelManager.unsubscribe( + this.rclient, + 'applied-ops', + '1234567890abcdef' + ) + return setTimeout(done) + }) - it("should have subscribed", function() { - return this.rclient.subscribe.called.should.equal(true); - }); + return it('should unsubscribe from the redis channel', function () { + return this.rclient.unsubscribe + .calledWithExactly('applied-ops:1234567890abcdef') + .should.equal(true) + }) + }) + }) - return it("should have discarded the finished Promise", function() { - return this.ChannelManager.getClientMapEntry(this.rclient).has("applied-ops:1234567890abcdef").should.equal(false); - }); - }); + return describe('publish', function () { + describe("when the channel is 'all'", function () { + beforeEach(function () { + this.rclient.publish = sinon.stub() + return this.ChannelManager.publish( + this.rclient, + 'applied-ops', + 'all', + 'random-message' + ) + }) - return describe("when there is an existing subscription for this redis client", function() { - beforeEach(function(done) { - this.rclient.subscribe = sinon.stub().resolves(); - this.rclient.unsubscribe = sinon.stub().resolves(); - this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); - this.ChannelManager.unsubscribe(this.rclient, "applied-ops", "1234567890abcdef"); - return setTimeout(done); - }); + return it('should publish on the base channel', function () { + return this.rclient.publish + .calledWithExactly('applied-ops', 'random-message') + .should.equal(true) + }) + }) - return it("should unsubscribe from the redis channel", function() { - return this.rclient.unsubscribe.calledWithExactly("applied-ops:1234567890abcdef").should.equal(true); - }); - }); - }); + describe('when the channel has an specific id', function () { + describe('when the individual channel setting is false', function () { + beforeEach(function () { + this.rclient.publish = sinon.stub() + this.settings.publishOnIndividualChannels = false + return this.ChannelManager.publish( + this.rclient, + 'applied-ops', + '1234567890abcdef', + 'random-message' + ) + }) - return describe("publish", function() { + return it('should publish on the per-id channel', function () { + this.rclient.publish + .calledWithExactly('applied-ops', 'random-message') + .should.equal(true) + return this.rclient.publish.calledOnce.should.equal(true) + }) + }) - describe("when the channel is 'all'", function() { - beforeEach(function() { - this.rclient.publish = sinon.stub(); - return this.ChannelManager.publish(this.rclient, "applied-ops", "all", "random-message"); - }); + return describe('when the individual channel setting is true', function () { + beforeEach(function () { + this.rclient.publish = sinon.stub() + this.settings.publishOnIndividualChannels = true + return this.ChannelManager.publish( + this.rclient, + 'applied-ops', + '1234567890abcdef', + 'random-message' + ) + }) - return it("should publish on the base channel", function() { - return this.rclient.publish.calledWithExactly("applied-ops", "random-message").should.equal(true); - }); - }); + return it('should publish on the per-id channel', function () { + this.rclient.publish + .calledWithExactly('applied-ops:1234567890abcdef', 'random-message') + .should.equal(true) + return this.rclient.publish.calledOnce.should.equal(true) + }) + }) + }) - describe("when the channel has an specific id", function() { + return describe('metrics', function () { + beforeEach(function () { + this.rclient.publish = sinon.stub() + return this.ChannelManager.publish( + this.rclient, + 'applied-ops', + 'all', + 'random-message' + ) + }) - describe("when the individual channel setting is false", function() { - beforeEach(function() { - this.rclient.publish = sinon.stub(); - this.settings.publishOnIndividualChannels = false; - return this.ChannelManager.publish(this.rclient, "applied-ops", "1234567890abcdef", "random-message"); - }); - - return it("should publish on the per-id channel", function() { - this.rclient.publish.calledWithExactly("applied-ops", "random-message").should.equal(true); - return this.rclient.publish.calledOnce.should.equal(true); - }); - }); - - return describe("when the individual channel setting is true", function() { - beforeEach(function() { - this.rclient.publish = sinon.stub(); - this.settings.publishOnIndividualChannels = true; - return this.ChannelManager.publish(this.rclient, "applied-ops", "1234567890abcdef", "random-message"); - }); - - return it("should publish on the per-id channel", function() { - this.rclient.publish.calledWithExactly("applied-ops:1234567890abcdef", "random-message").should.equal(true); - return this.rclient.publish.calledOnce.should.equal(true); - }); - }); - }); - - return describe("metrics", function() { - beforeEach(function() { - this.rclient.publish = sinon.stub(); - return this.ChannelManager.publish(this.rclient, "applied-ops", "all", "random-message"); - }); - - return it("should track the payload size", function() { - return this.metrics.summary.calledWithExactly( - "redis.publish.applied-ops", - "random-message".length - ).should.equal(true); - }); - }); - }); -}); + return it('should track the payload size', function () { + return this.metrics.summary + .calledWithExactly( + 'redis.publish.applied-ops', + 'random-message'.length + ) + .should.equal(true) + }) + }) + }) +}) diff --git a/services/real-time/test/unit/js/ConnectedUsersManagerTests.js b/services/real-time/test/unit/js/ConnectedUsersManagerTests.js index f6b026fd1a..8e84c41130 100644 --- a/services/real-time/test/unit/js/ConnectedUsersManagerTests.js +++ b/services/real-time/test/unit/js/ConnectedUsersManagerTests.js @@ -12,218 +12,398 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const should = require('chai').should(); -const SandboxedModule = require('sandboxed-module'); -const assert = require('assert'); -const path = require('path'); -const sinon = require('sinon'); -const modulePath = path.join(__dirname, "../../../app/js/ConnectedUsersManager"); -const { - expect -} = require("chai"); -const tk = require("timekeeper"); +const should = require('chai').should() +const SandboxedModule = require('sandboxed-module') +const assert = require('assert') +const path = require('path') +const sinon = require('sinon') +const modulePath = path.join(__dirname, '../../../app/js/ConnectedUsersManager') +const { expect } = require('chai') +const tk = require('timekeeper') +describe('ConnectedUsersManager', function () { + beforeEach(function () { + this.settings = { + redis: { + realtime: { + key_schema: { + clientsInProject({ project_id }) { + return `clients_in_project:${project_id}` + }, + connectedUser({ project_id, client_id }) { + return `connected_user:${project_id}:${client_id}` + } + } + } + } + } + this.rClient = { + auth() {}, + setex: sinon.stub(), + sadd: sinon.stub(), + get: sinon.stub(), + srem: sinon.stub(), + del: sinon.stub(), + smembers: sinon.stub(), + expire: sinon.stub(), + hset: sinon.stub(), + hgetall: sinon.stub(), + exec: sinon.stub(), + multi: () => { + return this.rClient + } + } + tk.freeze(new Date()) -describe("ConnectedUsersManager", function() { + this.ConnectedUsersManager = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': this.settings, + 'logger-sharelatex': { log() {} }, + 'redis-sharelatex': { + createClient: () => { + return this.rClient + } + } + } + }) + this.client_id = '32132132' + this.project_id = 'dskjh2u21321' + this.user = { + _id: 'user-id-123', + first_name: 'Joe', + last_name: 'Bloggs', + email: 'joe@example.com' + } + return (this.cursorData = { + row: 12, + column: 9, + doc_id: '53c3b8c85fee64000023dc6e' + }) + }) - beforeEach(function() { + afterEach(function () { + return tk.reset() + }) - this.settings = { - redis: { - realtime: { - key_schema: { - clientsInProject({project_id}) { return `clients_in_project:${project_id}`; }, - connectedUser({project_id, client_id}){ return `connected_user:${project_id}:${client_id}`; } - } - } - } - }; - this.rClient = { - auth() {}, - setex:sinon.stub(), - sadd:sinon.stub(), - get: sinon.stub(), - srem:sinon.stub(), - del:sinon.stub(), - smembers:sinon.stub(), - expire:sinon.stub(), - hset:sinon.stub(), - hgetall:sinon.stub(), - exec:sinon.stub(), - multi: () => { return this.rClient; } - }; - tk.freeze(new Date()); + describe('updateUserPosition', function () { + beforeEach(function () { + return this.rClient.exec.callsArgWith(0) + }) - this.ConnectedUsersManager = SandboxedModule.require(modulePath, { requires: { - "settings-sharelatex":this.settings, - "logger-sharelatex": { log() {} - }, - "redis-sharelatex": { createClient:() => { - return this.rClient; - } - } - } - } - ); - this.client_id = "32132132"; - this.project_id = "dskjh2u21321"; - this.user = { - _id: "user-id-123", - first_name: "Joe", - last_name: "Bloggs", - email: "joe@example.com" - }; - return this.cursorData = { row: 12, column: 9, doc_id: '53c3b8c85fee64000023dc6e' };}); + it('should set a key with the date and give it a ttl', function (done) { + return this.ConnectedUsersManager.updateUserPosition( + this.project_id, + this.client_id, + this.user, + null, + (err) => { + this.rClient.hset + .calledWith( + `connected_user:${this.project_id}:${this.client_id}`, + 'last_updated_at', + Date.now() + ) + .should.equal(true) + return done() + } + ) + }) - afterEach(function() { return tk.reset(); }); + it('should set a key with the user_id', function (done) { + return this.ConnectedUsersManager.updateUserPosition( + this.project_id, + this.client_id, + this.user, + null, + (err) => { + this.rClient.hset + .calledWith( + `connected_user:${this.project_id}:${this.client_id}`, + 'user_id', + this.user._id + ) + .should.equal(true) + return done() + } + ) + }) - describe("updateUserPosition", function() { - beforeEach(function() { - return this.rClient.exec.callsArgWith(0); - }); + it('should set a key with the first_name', function (done) { + return this.ConnectedUsersManager.updateUserPosition( + this.project_id, + this.client_id, + this.user, + null, + (err) => { + this.rClient.hset + .calledWith( + `connected_user:${this.project_id}:${this.client_id}`, + 'first_name', + this.user.first_name + ) + .should.equal(true) + return done() + } + ) + }) - it("should set a key with the date and give it a ttl", function(done){ - return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> { - this.rClient.hset.calledWith(`connected_user:${this.project_id}:${this.client_id}`, "last_updated_at", Date.now()).should.equal(true); - return done(); - }); - }); + it('should set a key with the last_name', function (done) { + return this.ConnectedUsersManager.updateUserPosition( + this.project_id, + this.client_id, + this.user, + null, + (err) => { + this.rClient.hset + .calledWith( + `connected_user:${this.project_id}:${this.client_id}`, + 'last_name', + this.user.last_name + ) + .should.equal(true) + return done() + } + ) + }) - it("should set a key with the user_id", function(done){ - return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> { - this.rClient.hset.calledWith(`connected_user:${this.project_id}:${this.client_id}`, "user_id", this.user._id).should.equal(true); - return done(); - }); - }); + it('should set a key with the email', function (done) { + return this.ConnectedUsersManager.updateUserPosition( + this.project_id, + this.client_id, + this.user, + null, + (err) => { + this.rClient.hset + .calledWith( + `connected_user:${this.project_id}:${this.client_id}`, + 'email', + this.user.email + ) + .should.equal(true) + return done() + } + ) + }) - it("should set a key with the first_name", function(done){ - return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> { - this.rClient.hset.calledWith(`connected_user:${this.project_id}:${this.client_id}`, "first_name", this.user.first_name).should.equal(true); - return done(); - }); - }); + it('should push the client_id on to the project list', function (done) { + return this.ConnectedUsersManager.updateUserPosition( + this.project_id, + this.client_id, + this.user, + null, + (err) => { + this.rClient.sadd + .calledWith(`clients_in_project:${this.project_id}`, this.client_id) + .should.equal(true) + return done() + } + ) + }) - it("should set a key with the last_name", function(done){ - return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> { - this.rClient.hset.calledWith(`connected_user:${this.project_id}:${this.client_id}`, "last_name", this.user.last_name).should.equal(true); - return done(); - }); - }); + it('should add a ttl to the project set so it stays clean', function (done) { + return this.ConnectedUsersManager.updateUserPosition( + this.project_id, + this.client_id, + this.user, + null, + (err) => { + this.rClient.expire + .calledWith( + `clients_in_project:${this.project_id}`, + 24 * 4 * 60 * 60 + ) + .should.equal(true) + return done() + } + ) + }) - it("should set a key with the email", function(done){ - return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> { - this.rClient.hset.calledWith(`connected_user:${this.project_id}:${this.client_id}`, "email", this.user.email).should.equal(true); - return done(); - }); - }); + it('should add a ttl to the connected user so it stays clean', function (done) { + return this.ConnectedUsersManager.updateUserPosition( + this.project_id, + this.client_id, + this.user, + null, + (err) => { + this.rClient.expire + .calledWith( + `connected_user:${this.project_id}:${this.client_id}`, + 60 * 15 + ) + .should.equal(true) + return done() + } + ) + }) - it("should push the client_id on to the project list", function(done){ - return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> { - this.rClient.sadd.calledWith(`clients_in_project:${this.project_id}`, this.client_id).should.equal(true); - return done(); - }); - }); + return it('should set the cursor position when provided', function (done) { + return this.ConnectedUsersManager.updateUserPosition( + this.project_id, + this.client_id, + this.user, + this.cursorData, + (err) => { + this.rClient.hset + .calledWith( + `connected_user:${this.project_id}:${this.client_id}`, + 'cursorData', + JSON.stringify(this.cursorData) + ) + .should.equal(true) + return done() + } + ) + }) + }) - it("should add a ttl to the project set so it stays clean", function(done){ - return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> { - this.rClient.expire.calledWith(`clients_in_project:${this.project_id}`, 24 * 4 * 60 * 60).should.equal(true); - return done(); - }); - }); + describe('markUserAsDisconnected', function () { + beforeEach(function () { + return this.rClient.exec.callsArgWith(0) + }) - it("should add a ttl to the connected user so it stays clean", function(done) { - return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, null, err=> { - this.rClient.expire.calledWith(`connected_user:${this.project_id}:${this.client_id}`, 60 * 15).should.equal(true); - return done(); - }); - }); + it('should remove the user from the set', function (done) { + return this.ConnectedUsersManager.markUserAsDisconnected( + this.project_id, + this.client_id, + (err) => { + this.rClient.srem + .calledWith(`clients_in_project:${this.project_id}`, this.client_id) + .should.equal(true) + return done() + } + ) + }) - return it("should set the cursor position when provided", function(done){ - return this.ConnectedUsersManager.updateUserPosition(this.project_id, this.client_id, this.user, this.cursorData, err=> { - this.rClient.hset.calledWith(`connected_user:${this.project_id}:${this.client_id}`, "cursorData", JSON.stringify(this.cursorData)).should.equal(true); - return done(); - }); - }); - }); + it('should delete the connected_user string', function (done) { + return this.ConnectedUsersManager.markUserAsDisconnected( + this.project_id, + this.client_id, + (err) => { + this.rClient.del + .calledWith(`connected_user:${this.project_id}:${this.client_id}`) + .should.equal(true) + return done() + } + ) + }) - describe("markUserAsDisconnected", function() { - beforeEach(function() { - return this.rClient.exec.callsArgWith(0); - }); + return it('should add a ttl to the connected user set so it stays clean', function (done) { + return this.ConnectedUsersManager.markUserAsDisconnected( + this.project_id, + this.client_id, + (err) => { + this.rClient.expire + .calledWith( + `clients_in_project:${this.project_id}`, + 24 * 4 * 60 * 60 + ) + .should.equal(true) + return done() + } + ) + }) + }) - it("should remove the user from the set", function(done){ - return this.ConnectedUsersManager.markUserAsDisconnected(this.project_id, this.client_id, err=> { - this.rClient.srem.calledWith(`clients_in_project:${this.project_id}`, this.client_id).should.equal(true); - return done(); - }); - }); + describe('_getConnectedUser', function () { + it('should return a connected user if there is a user object', function (done) { + const cursorData = JSON.stringify({ cursorData: { row: 1 } }) + this.rClient.hgetall.callsArgWith(1, null, { + connected_at: new Date(), + user_id: this.user._id, + last_updated_at: `${Date.now()}`, + cursorData + }) + return this.ConnectedUsersManager._getConnectedUser( + this.project_id, + this.client_id, + (err, result) => { + result.connected.should.equal(true) + result.client_id.should.equal(this.client_id) + return done() + } + ) + }) - it("should delete the connected_user string", function(done){ - return this.ConnectedUsersManager.markUserAsDisconnected(this.project_id, this.client_id, err=> { - this.rClient.del.calledWith(`connected_user:${this.project_id}:${this.client_id}`).should.equal(true); - return done(); - }); - }); + it('should return a not connected user if there is no object', function (done) { + this.rClient.hgetall.callsArgWith(1, null, null) + return this.ConnectedUsersManager._getConnectedUser( + this.project_id, + this.client_id, + (err, result) => { + result.connected.should.equal(false) + result.client_id.should.equal(this.client_id) + return done() + } + ) + }) - return it("should add a ttl to the connected user set so it stays clean", function(done){ - return this.ConnectedUsersManager.markUserAsDisconnected(this.project_id, this.client_id, err=> { - this.rClient.expire.calledWith(`clients_in_project:${this.project_id}`, 24 * 4 * 60 * 60).should.equal(true); - return done(); - }); - }); - }); + return it('should return a not connected user if there is an empty object', function (done) { + this.rClient.hgetall.callsArgWith(1, null, {}) + return this.ConnectedUsersManager._getConnectedUser( + this.project_id, + this.client_id, + (err, result) => { + result.connected.should.equal(false) + result.client_id.should.equal(this.client_id) + return done() + } + ) + }) + }) - describe("_getConnectedUser", function() { - - it("should return a connected user if there is a user object", function(done){ - const cursorData = JSON.stringify({cursorData:{row:1}}); - this.rClient.hgetall.callsArgWith(1, null, {connected_at:new Date(), user_id: this.user._id, last_updated_at: `${Date.now()}`, cursorData}); - return this.ConnectedUsersManager._getConnectedUser(this.project_id, this.client_id, (err, result)=> { - result.connected.should.equal(true); - result.client_id.should.equal(this.client_id); - return done(); - }); - }); - - it("should return a not connected user if there is no object", function(done){ - this.rClient.hgetall.callsArgWith(1, null, null); - return this.ConnectedUsersManager._getConnectedUser(this.project_id, this.client_id, (err, result)=> { - result.connected.should.equal(false); - result.client_id.should.equal(this.client_id); - return done(); - }); - }); - - return it("should return a not connected user if there is an empty object", function(done){ - this.rClient.hgetall.callsArgWith(1, null, {}); - return this.ConnectedUsersManager._getConnectedUser(this.project_id, this.client_id, (err, result)=> { - result.connected.should.equal(false); - result.client_id.should.equal(this.client_id); - return done(); - }); - }); - }); - - return describe("getConnectedUsers", function() { - - beforeEach(function() { - this.users = ["1234", "5678", "9123", "8234"]; - this.rClient.smembers.callsArgWith(1, null, this.users); - this.ConnectedUsersManager._getConnectedUser = sinon.stub(); - this.ConnectedUsersManager._getConnectedUser.withArgs(this.project_id, this.users[0]).callsArgWith(2, null, {connected:true, client_age: 2, client_id:this.users[0]}); - this.ConnectedUsersManager._getConnectedUser.withArgs(this.project_id, this.users[1]).callsArgWith(2, null, {connected:false, client_age: 1, client_id:this.users[1]}); - this.ConnectedUsersManager._getConnectedUser.withArgs(this.project_id, this.users[2]).callsArgWith(2, null, {connected:true, client_age: 3, client_id:this.users[2]}); - return this.ConnectedUsersManager._getConnectedUser.withArgs(this.project_id, this.users[3]).callsArgWith(2, null, {connected:true, client_age: 11, client_id:this.users[3]}); - }); // connected but old - - return it("should only return the users in the list which are still in redis and recently updated", function(done){ - return this.ConnectedUsersManager.getConnectedUsers(this.project_id, (err, users)=> { - users.length.should.equal(2); - users[0].should.deep.equal({client_id:this.users[0], client_age: 2, connected:true}); - users[1].should.deep.equal({client_id:this.users[2], client_age: 3, connected:true}); - return done(); - }); - }); - }); -}); + return describe('getConnectedUsers', function () { + beforeEach(function () { + this.users = ['1234', '5678', '9123', '8234'] + this.rClient.smembers.callsArgWith(1, null, this.users) + this.ConnectedUsersManager._getConnectedUser = sinon.stub() + this.ConnectedUsersManager._getConnectedUser + .withArgs(this.project_id, this.users[0]) + .callsArgWith(2, null, { + connected: true, + client_age: 2, + client_id: this.users[0] + }) + this.ConnectedUsersManager._getConnectedUser + .withArgs(this.project_id, this.users[1]) + .callsArgWith(2, null, { + connected: false, + client_age: 1, + client_id: this.users[1] + }) + this.ConnectedUsersManager._getConnectedUser + .withArgs(this.project_id, this.users[2]) + .callsArgWith(2, null, { + connected: true, + client_age: 3, + client_id: this.users[2] + }) + return this.ConnectedUsersManager._getConnectedUser + .withArgs(this.project_id, this.users[3]) + .callsArgWith(2, null, { + connected: true, + client_age: 11, + client_id: this.users[3] + }) + }) // connected but old + return it('should only return the users in the list which are still in redis and recently updated', function (done) { + return this.ConnectedUsersManager.getConnectedUsers( + this.project_id, + (err, users) => { + users.length.should.equal(2) + users[0].should.deep.equal({ + client_id: this.users[0], + client_age: 2, + connected: true + }) + users[1].should.deep.equal({ + client_id: this.users[2], + client_age: 3, + connected: true + }) + return done() + } + ) + }) + }) +}) diff --git a/services/real-time/test/unit/js/DocumentUpdaterControllerTests.js b/services/real-time/test/unit/js/DocumentUpdaterControllerTests.js index 8b62b381a0..532346f359 100644 --- a/services/real-time/test/unit/js/DocumentUpdaterControllerTests.js +++ b/services/real-time/test/unit/js/DocumentUpdaterControllerTests.js @@ -10,200 +10,250 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const SandboxedModule = require('sandboxed-module'); -const sinon = require('sinon'); -require('chai').should(); -const modulePath = require('path').join(__dirname, '../../../app/js/DocumentUpdaterController'); -const MockClient = require("./helpers/MockClient"); +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const modulePath = require('path').join( + __dirname, + '../../../app/js/DocumentUpdaterController' +) +const MockClient = require('./helpers/MockClient') -describe("DocumentUpdaterController", function() { - beforeEach(function() { - this.project_id = "project-id-123"; - this.doc_id = "doc-id-123"; - this.callback = sinon.stub(); - this.io = { "mock": "socket.io" }; - this.rclient = []; - this.RoomEvents = { on: sinon.stub() }; - return this.EditorUpdatesController = SandboxedModule.require(modulePath, { requires: { - "logger-sharelatex": (this.logger = { error: sinon.stub(), log: sinon.stub(), warn: sinon.stub() }), - "settings-sharelatex": (this.settings = { - redis: { - documentupdater: { - key_schema: { - pendingUpdates({doc_id}) { return `PendingUpdates:${doc_id}`; } - } - }, - pubsub: null - } - }), - "redis-sharelatex" : (this.redis = { - createClient: name => { - let rclientStub; - this.rclient.push(rclientStub = {name}); - return rclientStub; - } - }), - "./SafeJsonParse": (this.SafeJsonParse = - {parse: (data, cb) => cb(null, JSON.parse(data))}), - "./EventLogger": (this.EventLogger = {checkEventOrder: sinon.stub()}), - "./HealthCheckManager": {check: sinon.stub()}, - "metrics-sharelatex": (this.metrics = {inc: sinon.stub()}), - "./RoomManager" : (this.RoomManager = { eventSource: sinon.stub().returns(this.RoomEvents)}), - "./ChannelManager": (this.ChannelManager = {}) - } - });}); +describe('DocumentUpdaterController', function () { + beforeEach(function () { + this.project_id = 'project-id-123' + this.doc_id = 'doc-id-123' + this.callback = sinon.stub() + this.io = { mock: 'socket.io' } + this.rclient = [] + this.RoomEvents = { on: sinon.stub() } + return (this.EditorUpdatesController = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': (this.logger = { + error: sinon.stub(), + log: sinon.stub(), + warn: sinon.stub() + }), + 'settings-sharelatex': (this.settings = { + redis: { + documentupdater: { + key_schema: { + pendingUpdates({ doc_id }) { + return `PendingUpdates:${doc_id}` + } + } + }, + pubsub: null + } + }), + 'redis-sharelatex': (this.redis = { + createClient: (name) => { + let rclientStub + this.rclient.push((rclientStub = { name })) + return rclientStub + } + }), + './SafeJsonParse': (this.SafeJsonParse = { + parse: (data, cb) => cb(null, JSON.parse(data)) + }), + './EventLogger': (this.EventLogger = { checkEventOrder: sinon.stub() }), + './HealthCheckManager': { check: sinon.stub() }, + 'metrics-sharelatex': (this.metrics = { inc: sinon.stub() }), + './RoomManager': (this.RoomManager = { + eventSource: sinon.stub().returns(this.RoomEvents) + }), + './ChannelManager': (this.ChannelManager = {}) + } + })) + }) - describe("listenForUpdatesFromDocumentUpdater", function() { - beforeEach(function() { - this.rclient.length = 0; // clear any existing clients - this.EditorUpdatesController.rclientList = [this.redis.createClient("first"), this.redis.createClient("second")]; - this.rclient[0].subscribe = sinon.stub(); - this.rclient[0].on = sinon.stub(); - this.rclient[1].subscribe = sinon.stub(); - this.rclient[1].on = sinon.stub(); - return this.EditorUpdatesController.listenForUpdatesFromDocumentUpdater(); - }); - - it("should subscribe to the doc-updater stream", function() { - return this.rclient[0].subscribe.calledWith("applied-ops").should.equal(true); - }); + describe('listenForUpdatesFromDocumentUpdater', function () { + beforeEach(function () { + this.rclient.length = 0 // clear any existing clients + this.EditorUpdatesController.rclientList = [ + this.redis.createClient('first'), + this.redis.createClient('second') + ] + this.rclient[0].subscribe = sinon.stub() + this.rclient[0].on = sinon.stub() + this.rclient[1].subscribe = sinon.stub() + this.rclient[1].on = sinon.stub() + return this.EditorUpdatesController.listenForUpdatesFromDocumentUpdater() + }) - it("should register a callback to handle updates", function() { - return this.rclient[0].on.calledWith("message").should.equal(true); - }); + it('should subscribe to the doc-updater stream', function () { + return this.rclient[0].subscribe + .calledWith('applied-ops') + .should.equal(true) + }) - return it("should subscribe to any additional doc-updater stream", function() { - this.rclient[1].subscribe.calledWith("applied-ops").should.equal(true); - return this.rclient[1].on.calledWith("message").should.equal(true); - }); - }); + it('should register a callback to handle updates', function () { + return this.rclient[0].on.calledWith('message').should.equal(true) + }) - describe("_processMessageFromDocumentUpdater", function() { - describe("with bad JSON", function() { - beforeEach(function() { - this.SafeJsonParse.parse = sinon.stub().callsArgWith(1, new Error("oops")); - return this.EditorUpdatesController._processMessageFromDocumentUpdater(this.io, "applied-ops", "blah"); - }); - - return it("should log an error", function() { - return this.logger.error.called.should.equal(true); - }); - }); + return it('should subscribe to any additional doc-updater stream', function () { + this.rclient[1].subscribe.calledWith('applied-ops').should.equal(true) + return this.rclient[1].on.calledWith('message').should.equal(true) + }) + }) - describe("with update", function() { - beforeEach(function() { - this.message = { - doc_id: this.doc_id, - op: {t: "foo", p: 12} - }; - this.EditorUpdatesController._applyUpdateFromDocumentUpdater = sinon.stub(); - return this.EditorUpdatesController._processMessageFromDocumentUpdater(this.io, "applied-ops", JSON.stringify(this.message)); - }); + describe('_processMessageFromDocumentUpdater', function () { + describe('with bad JSON', function () { + beforeEach(function () { + this.SafeJsonParse.parse = sinon + .stub() + .callsArgWith(1, new Error('oops')) + return this.EditorUpdatesController._processMessageFromDocumentUpdater( + this.io, + 'applied-ops', + 'blah' + ) + }) - return it("should apply the update", function() { - return this.EditorUpdatesController._applyUpdateFromDocumentUpdater - .calledWith(this.io, this.doc_id, this.message.op) - .should.equal(true); - }); - }); + return it('should log an error', function () { + return this.logger.error.called.should.equal(true) + }) + }) - return describe("with error", function() { - beforeEach(function() { - this.message = { - doc_id: this.doc_id, - error: "Something went wrong" - }; - this.EditorUpdatesController._processErrorFromDocumentUpdater = sinon.stub(); - return this.EditorUpdatesController._processMessageFromDocumentUpdater(this.io, "applied-ops", JSON.stringify(this.message)); - }); + describe('with update', function () { + beforeEach(function () { + this.message = { + doc_id: this.doc_id, + op: { t: 'foo', p: 12 } + } + this.EditorUpdatesController._applyUpdateFromDocumentUpdater = sinon.stub() + return this.EditorUpdatesController._processMessageFromDocumentUpdater( + this.io, + 'applied-ops', + JSON.stringify(this.message) + ) + }) - return it("should process the error", function() { - return this.EditorUpdatesController._processErrorFromDocumentUpdater - .calledWith(this.io, this.doc_id, this.message.error) - .should.equal(true); - }); - }); - }); + return it('should apply the update', function () { + return this.EditorUpdatesController._applyUpdateFromDocumentUpdater + .calledWith(this.io, this.doc_id, this.message.op) + .should.equal(true) + }) + }) - describe("_applyUpdateFromDocumentUpdater", function() { - beforeEach(function() { - this.sourceClient = new MockClient(); - this.otherClients = [new MockClient(), new MockClient()]; - this.update = { - op: [ {t: "foo", p: 12} ], - meta: { source: this.sourceClient.publicId - }, - v: (this.version = 42), - doc: this.doc_id - }; - return this.io.sockets = - {clients: sinon.stub().returns([this.sourceClient, ...Array.from(this.otherClients), this.sourceClient])}; - }); // include a duplicate client - - describe("normally", function() { - beforeEach(function() { - return this.EditorUpdatesController._applyUpdateFromDocumentUpdater(this.io, this.doc_id, this.update); - }); + return describe('with error', function () { + beforeEach(function () { + this.message = { + doc_id: this.doc_id, + error: 'Something went wrong' + } + this.EditorUpdatesController._processErrorFromDocumentUpdater = sinon.stub() + return this.EditorUpdatesController._processMessageFromDocumentUpdater( + this.io, + 'applied-ops', + JSON.stringify(this.message) + ) + }) - it("should send a version bump to the source client", function() { - this.sourceClient.emit - .calledWith("otUpdateApplied", {v: this.version, doc: this.doc_id}) - .should.equal(true); - return this.sourceClient.emit.calledOnce.should.equal(true); - }); + return it('should process the error', function () { + return this.EditorUpdatesController._processErrorFromDocumentUpdater + .calledWith(this.io, this.doc_id, this.message.error) + .should.equal(true) + }) + }) + }) - it("should get the clients connected to the document", function() { - return this.io.sockets.clients - .calledWith(this.doc_id) - .should.equal(true); - }); + describe('_applyUpdateFromDocumentUpdater', function () { + beforeEach(function () { + this.sourceClient = new MockClient() + this.otherClients = [new MockClient(), new MockClient()] + this.update = { + op: [{ t: 'foo', p: 12 }], + meta: { source: this.sourceClient.publicId }, + v: (this.version = 42), + doc: this.doc_id + } + return (this.io.sockets = { + clients: sinon + .stub() + .returns([ + this.sourceClient, + ...Array.from(this.otherClients), + this.sourceClient + ]) + }) + }) // include a duplicate client - return it("should send the full update to the other clients", function() { - return Array.from(this.otherClients).map((client) => - client.emit - .calledWith("otUpdateApplied", this.update) - .should.equal(true)); - }); - }); - - return describe("with a duplicate op", function() { - beforeEach(function() { - this.update.dup = true; - return this.EditorUpdatesController._applyUpdateFromDocumentUpdater(this.io, this.doc_id, this.update); - }); - - it("should send a version bump to the source client as usual", function() { - return this.sourceClient.emit - .calledWith("otUpdateApplied", {v: this.version, doc: this.doc_id}) - .should.equal(true); - }); + describe('normally', function () { + beforeEach(function () { + return this.EditorUpdatesController._applyUpdateFromDocumentUpdater( + this.io, + this.doc_id, + this.update + ) + }) - return it("should not send anything to the other clients (they've already had the op)", function() { - return Array.from(this.otherClients).map((client) => - client.emit - .calledWith("otUpdateApplied") - .should.equal(false)); - }); - }); - }); + it('should send a version bump to the source client', function () { + this.sourceClient.emit + .calledWith('otUpdateApplied', { v: this.version, doc: this.doc_id }) + .should.equal(true) + return this.sourceClient.emit.calledOnce.should.equal(true) + }) - return describe("_processErrorFromDocumentUpdater", function() { - beforeEach(function() { - this.clients = [new MockClient(), new MockClient()]; - this.io.sockets = - {clients: sinon.stub().returns(this.clients)}; - return this.EditorUpdatesController._processErrorFromDocumentUpdater(this.io, this.doc_id, "Something went wrong"); - }); + it('should get the clients connected to the document', function () { + return this.io.sockets.clients + .calledWith(this.doc_id) + .should.equal(true) + }) - it("should log a warning", function() { - return this.logger.warn.called.should.equal(true); - }); + return it('should send the full update to the other clients', function () { + return Array.from(this.otherClients).map((client) => + client.emit + .calledWith('otUpdateApplied', this.update) + .should.equal(true) + ) + }) + }) - return it("should disconnect all clients in that document", function() { - this.io.sockets.clients.calledWith(this.doc_id).should.equal(true); - return Array.from(this.clients).map((client) => - client.disconnect.called.should.equal(true)); - }); - }); -}); + return describe('with a duplicate op', function () { + beforeEach(function () { + this.update.dup = true + return this.EditorUpdatesController._applyUpdateFromDocumentUpdater( + this.io, + this.doc_id, + this.update + ) + }) + it('should send a version bump to the source client as usual', function () { + return this.sourceClient.emit + .calledWith('otUpdateApplied', { v: this.version, doc: this.doc_id }) + .should.equal(true) + }) + + return it("should not send anything to the other clients (they've already had the op)", function () { + return Array.from(this.otherClients).map((client) => + client.emit.calledWith('otUpdateApplied').should.equal(false) + ) + }) + }) + }) + + return describe('_processErrorFromDocumentUpdater', function () { + beforeEach(function () { + this.clients = [new MockClient(), new MockClient()] + this.io.sockets = { clients: sinon.stub().returns(this.clients) } + return this.EditorUpdatesController._processErrorFromDocumentUpdater( + this.io, + this.doc_id, + 'Something went wrong' + ) + }) + + it('should log a warning', function () { + return this.logger.warn.called.should.equal(true) + }) + + return it('should disconnect all clients in that document', function () { + this.io.sockets.clients.calledWith(this.doc_id).should.equal(true) + return Array.from(this.clients).map((client) => + client.disconnect.called.should.equal(true) + ) + }) + }) +}) diff --git a/services/real-time/test/unit/js/DocumentUpdaterManagerTests.js b/services/real-time/test/unit/js/DocumentUpdaterManagerTests.js index 49b08fa2b2..dc42b52140 100644 --- a/services/real-time/test/unit/js/DocumentUpdaterManagerTests.js +++ b/services/real-time/test/unit/js/DocumentUpdaterManagerTests.js @@ -10,258 +10,372 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -require('chai').should(); -const sinon = require("sinon"); -const SandboxedModule = require('sandboxed-module'); -const path = require("path"); -const modulePath = '../../../app/js/DocumentUpdaterManager'; +require('chai').should() +const sinon = require('sinon') +const SandboxedModule = require('sandboxed-module') +const path = require('path') +const modulePath = '../../../app/js/DocumentUpdaterManager' -describe('DocumentUpdaterManager', function() { - beforeEach(function() { - let Timer; - this.project_id = "project-id-923"; - this.doc_id = "doc-id-394"; - this.lines = ["one", "two", "three"]; - this.version = 42; - this.settings = { - apis: { documentupdater: {url: "http://doc-updater.example.com"} - }, - redis: { documentupdater: { - key_schema: { - pendingUpdates({doc_id}) { return `PendingUpdates:${doc_id}`; } - } - } - }, - maxUpdateSize: 7 * 1024 * 1024 - }; - this.rclient = {auth() {}}; +describe('DocumentUpdaterManager', function () { + beforeEach(function () { + let Timer + this.project_id = 'project-id-923' + this.doc_id = 'doc-id-394' + this.lines = ['one', 'two', 'three'] + this.version = 42 + this.settings = { + apis: { documentupdater: { url: 'http://doc-updater.example.com' } }, + redis: { + documentupdater: { + key_schema: { + pendingUpdates({ doc_id }) { + return `PendingUpdates:${doc_id}` + } + } + } + }, + maxUpdateSize: 7 * 1024 * 1024 + } + this.rclient = { auth() {} } - return this.DocumentUpdaterManager = SandboxedModule.require(modulePath, { - requires: { - 'settings-sharelatex':this.settings, - 'logger-sharelatex': (this.logger = {log: sinon.stub(), error: sinon.stub(), warn: sinon.stub()}), - 'request': (this.request = {}), - 'redis-sharelatex' : { createClient: () => this.rclient - }, - 'metrics-sharelatex': (this.Metrics = { - summary: sinon.stub(), - Timer: (Timer = class Timer { - done() {} - }) - }) - }, - globals: { - JSON: (this.JSON = Object.create(JSON)) - } - } - ); - }); // avoid modifying JSON object directly + return (this.DocumentUpdaterManager = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': this.settings, + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub(), + warn: sinon.stub() + }), + request: (this.request = {}), + 'redis-sharelatex': { createClient: () => this.rclient }, + 'metrics-sharelatex': (this.Metrics = { + summary: sinon.stub(), + Timer: (Timer = class Timer { + done() {} + }) + }) + }, + globals: { + JSON: (this.JSON = Object.create(JSON)) + } + })) + }) // avoid modifying JSON object directly - describe("getDocument", function() { - beforeEach(function() { - return this.callback = sinon.stub(); - }); + describe('getDocument', function () { + beforeEach(function () { + return (this.callback = sinon.stub()) + }) - describe("successfully", function() { - beforeEach(function() { - this.body = JSON.stringify({ - lines: this.lines, - version: this.version, - ops: (this.ops = ["mock-op-1", "mock-op-2"]), - ranges: (this.ranges = {"mock": "ranges"})}); - this.fromVersion = 2; - this.request.get = sinon.stub().callsArgWith(1, null, {statusCode: 200}, this.body); - return this.DocumentUpdaterManager.getDocument(this.project_id, this.doc_id, this.fromVersion, this.callback); - }); + describe('successfully', function () { + beforeEach(function () { + this.body = JSON.stringify({ + lines: this.lines, + version: this.version, + ops: (this.ops = ['mock-op-1', 'mock-op-2']), + ranges: (this.ranges = { mock: 'ranges' }) + }) + this.fromVersion = 2 + this.request.get = sinon + .stub() + .callsArgWith(1, null, { statusCode: 200 }, this.body) + return this.DocumentUpdaterManager.getDocument( + this.project_id, + this.doc_id, + this.fromVersion, + this.callback + ) + }) - it('should get the document from the document updater', function() { - const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}?fromVersion=${this.fromVersion}`; - return this.request.get.calledWith(url).should.equal(true); - }); + it('should get the document from the document updater', function () { + const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}?fromVersion=${this.fromVersion}` + return this.request.get.calledWith(url).should.equal(true) + }) - return it("should call the callback with the lines, version, ranges and ops", function() { - return this.callback.calledWith(null, this.lines, this.version, this.ranges, this.ops).should.equal(true); - }); - }); + return it('should call the callback with the lines, version, ranges and ops', function () { + return this.callback + .calledWith(null, this.lines, this.version, this.ranges, this.ops) + .should.equal(true) + }) + }) - describe("when the document updater API returns an error", function() { - beforeEach(function() { - this.request.get = sinon.stub().callsArgWith(1, (this.error = new Error("something went wrong")), null, null); - return this.DocumentUpdaterManager.getDocument(this.project_id, this.doc_id, this.fromVersion, this.callback); - }); + describe('when the document updater API returns an error', function () { + beforeEach(function () { + this.request.get = sinon + .stub() + .callsArgWith( + 1, + (this.error = new Error('something went wrong')), + null, + null + ) + return this.DocumentUpdaterManager.getDocument( + this.project_id, + this.doc_id, + this.fromVersion, + this.callback + ) + }) - return it("should return an error to the callback", function() { - return this.callback.calledWith(this.error).should.equal(true); - }); - }); + return it('should return an error to the callback', function () { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) + ;[404, 422].forEach((statusCode) => + describe(`when the document updater returns a ${statusCode} status code`, function () { + beforeEach(function () { + this.request.get = sinon + .stub() + .callsArgWith(1, null, { statusCode }, '') + return this.DocumentUpdaterManager.getDocument( + this.project_id, + this.doc_id, + this.fromVersion, + this.callback + ) + }) - [404, 422].forEach(statusCode => describe(`when the document updater returns a ${statusCode} status code`, function() { - beforeEach(function() { - this.request.get = sinon.stub().callsArgWith(1, null, { statusCode }, ""); - return this.DocumentUpdaterManager.getDocument(this.project_id, this.doc_id, this.fromVersion, this.callback); - }); + return it('should return the callback with an error', function () { + this.callback.called.should.equal(true) + const err = this.callback.getCall(0).args[0] + err.should.have.property('statusCode', statusCode) + err.should.have.property( + 'message', + 'doc updater could not load requested ops' + ) + this.logger.error.called.should.equal(false) + return this.logger.warn.called.should.equal(true) + }) + }) + ) - return it("should return the callback with an error", function() { - this.callback.called.should.equal(true); - const err = this.callback.getCall(0).args[0]; - err.should.have.property('statusCode', statusCode); - err.should.have.property('message', "doc updater could not load requested ops"); - this.logger.error.called.should.equal(false); - return this.logger.warn.called.should.equal(true); - }); - })); + return describe('when the document updater returns a failure error code', function () { + beforeEach(function () { + this.request.get = sinon + .stub() + .callsArgWith(1, null, { statusCode: 500 }, '') + return this.DocumentUpdaterManager.getDocument( + this.project_id, + this.doc_id, + this.fromVersion, + this.callback + ) + }) - return describe("when the document updater returns a failure error code", function() { - beforeEach(function() { - this.request.get = sinon.stub().callsArgWith(1, null, { statusCode: 500 }, ""); - return this.DocumentUpdaterManager.getDocument(this.project_id, this.doc_id, this.fromVersion, this.callback); - }); + return it('should return the callback with an error', function () { + this.callback.called.should.equal(true) + const err = this.callback.getCall(0).args[0] + err.should.have.property('statusCode', 500) + err.should.have.property( + 'message', + 'doc updater returned a non-success status code: 500' + ) + return this.logger.error.called.should.equal(true) + }) + }) + }) - return it("should return the callback with an error", function() { - this.callback.called.should.equal(true); - const err = this.callback.getCall(0).args[0]; - err.should.have.property('statusCode', 500); - err.should.have.property('message', "doc updater returned a non-success status code: 500"); - return this.logger.error.called.should.equal(true); - }); - }); - }); + describe('flushProjectToMongoAndDelete', function () { + beforeEach(function () { + return (this.callback = sinon.stub()) + }) - describe('flushProjectToMongoAndDelete', function() { - beforeEach(function() { - return this.callback = sinon.stub(); - }); + describe('successfully', function () { + beforeEach(function () { + this.request.del = sinon + .stub() + .callsArgWith(1, null, { statusCode: 204 }, '') + return this.DocumentUpdaterManager.flushProjectToMongoAndDelete( + this.project_id, + this.callback + ) + }) - describe("successfully", function() { - beforeEach(function() { - this.request.del = sinon.stub().callsArgWith(1, null, {statusCode: 204}, ""); - return this.DocumentUpdaterManager.flushProjectToMongoAndDelete(this.project_id, this.callback); - }); + it('should delete the project from the document updater', function () { + const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}?background=true` + return this.request.del.calledWith(url).should.equal(true) + }) - it('should delete the project from the document updater', function() { - const url = `${this.settings.apis.documentupdater.url}/project/${this.project_id}?background=true`; - return this.request.del.calledWith(url).should.equal(true); - }); + return it('should call the callback with no error', function () { + return this.callback.calledWith(null).should.equal(true) + }) + }) - return it("should call the callback with no error", function() { - return this.callback.calledWith(null).should.equal(true); - }); - }); + describe('when the document updater API returns an error', function () { + beforeEach(function () { + this.request.del = sinon + .stub() + .callsArgWith( + 1, + (this.error = new Error('something went wrong')), + null, + null + ) + return this.DocumentUpdaterManager.flushProjectToMongoAndDelete( + this.project_id, + this.callback + ) + }) - describe("when the document updater API returns an error", function() { - beforeEach(function() { - this.request.del = sinon.stub().callsArgWith(1, (this.error = new Error("something went wrong")), null, null); - return this.DocumentUpdaterManager.flushProjectToMongoAndDelete(this.project_id, this.callback); - }); + return it('should return an error to the callback', function () { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) - return it("should return an error to the callback", function() { - return this.callback.calledWith(this.error).should.equal(true); - }); - }); + return describe('when the document updater returns a failure error code', function () { + beforeEach(function () { + this.request.del = sinon + .stub() + .callsArgWith(1, null, { statusCode: 500 }, '') + return this.DocumentUpdaterManager.flushProjectToMongoAndDelete( + this.project_id, + this.callback + ) + }) - return describe("when the document updater returns a failure error code", function() { - beforeEach(function() { - this.request.del = sinon.stub().callsArgWith(1, null, { statusCode: 500 }, ""); - return this.DocumentUpdaterManager.flushProjectToMongoAndDelete(this.project_id, this.callback); - }); + return it('should return the callback with an error', function () { + this.callback.called.should.equal(true) + const err = this.callback.getCall(0).args[0] + err.should.have.property('statusCode', 500) + return err.should.have.property( + 'message', + 'document updater returned a failure status code: 500' + ) + }) + }) + }) - return it("should return the callback with an error", function() { - this.callback.called.should.equal(true); - const err = this.callback.getCall(0).args[0]; - err.should.have.property('statusCode', 500); - return err.should.have.property('message', "document updater returned a failure status code: 500"); - }); - }); - }); + return describe('queueChange', function () { + beforeEach(function () { + this.change = { + doc: '1234567890', + op: [{ d: 'test', p: 345 }], + v: 789 + } + this.rclient.rpush = sinon.stub().yields() + return (this.callback = sinon.stub()) + }) - return describe('queueChange', function() { - beforeEach(function() { - this.change = { - "doc":"1234567890", - "op":[{"d":"test", "p":345}], - "v": 789 - }; - this.rclient.rpush = sinon.stub().yields(); - return this.callback = sinon.stub(); - }); + describe('successfully', function () { + beforeEach(function () { + return this.DocumentUpdaterManager.queueChange( + this.project_id, + this.doc_id, + this.change, + this.callback + ) + }) - describe("successfully", function() { - beforeEach(function() { - return this.DocumentUpdaterManager.queueChange(this.project_id, this.doc_id, this.change, this.callback); - }); + it('should push the change', function () { + return this.rclient.rpush + .calledWith( + `PendingUpdates:${this.doc_id}`, + JSON.stringify(this.change) + ) + .should.equal(true) + }) - it("should push the change", function() { - return this.rclient.rpush - .calledWith(`PendingUpdates:${this.doc_id}`, JSON.stringify(this.change)) - .should.equal(true); - }); + return it('should notify the doc updater of the change via the pending-updates-list queue', function () { + return this.rclient.rpush + .calledWith( + 'pending-updates-list', + `${this.project_id}:${this.doc_id}` + ) + .should.equal(true) + }) + }) - return it("should notify the doc updater of the change via the pending-updates-list queue", function() { - return this.rclient.rpush - .calledWith("pending-updates-list", `${this.project_id}:${this.doc_id}`) - .should.equal(true); - }); - }); + describe('with error talking to redis during rpush', function () { + beforeEach(function () { + this.rclient.rpush = sinon + .stub() + .yields(new Error('something went wrong')) + return this.DocumentUpdaterManager.queueChange( + this.project_id, + this.doc_id, + this.change, + this.callback + ) + }) - describe("with error talking to redis during rpush", function() { - beforeEach(function() { - this.rclient.rpush = sinon.stub().yields(new Error("something went wrong")); - return this.DocumentUpdaterManager.queueChange(this.project_id, this.doc_id, this.change, this.callback); - }); + return it('should return an error', function () { + return this.callback + .calledWithExactly(sinon.match(Error)) + .should.equal(true) + }) + }) - return it("should return an error", function() { - return this.callback.calledWithExactly(sinon.match(Error)).should.equal(true); - }); - }); + describe('with null byte corruption', function () { + beforeEach(function () { + this.JSON.stringify = () => '["bad bytes! \u0000 <- here"]' + return this.DocumentUpdaterManager.queueChange( + this.project_id, + this.doc_id, + this.change, + this.callback + ) + }) - describe("with null byte corruption", function() { - beforeEach(function() { - this.JSON.stringify = () => '["bad bytes! \u0000 <- here"]'; - return this.DocumentUpdaterManager.queueChange(this.project_id, this.doc_id, this.change, this.callback); - }); + it('should return an error', function () { + return this.callback + .calledWithExactly(sinon.match(Error)) + .should.equal(true) + }) - it("should return an error", function() { - return this.callback.calledWithExactly(sinon.match(Error)).should.equal(true); - }); + return it('should not push the change onto the pending-updates-list queue', function () { + return this.rclient.rpush.called.should.equal(false) + }) + }) - return it("should not push the change onto the pending-updates-list queue", function() { - return this.rclient.rpush.called.should.equal(false); - }); - }); + describe('when the update is too large', function () { + beforeEach(function () { + this.change = { + op: { p: 12, t: 'update is too large'.repeat(1024 * 400) } + } + return this.DocumentUpdaterManager.queueChange( + this.project_id, + this.doc_id, + this.change, + this.callback + ) + }) - describe("when the update is too large", function() { - beforeEach(function() { - this.change = {op: {p: 12,t: "update is too large".repeat(1024 * 400)}}; - return this.DocumentUpdaterManager.queueChange(this.project_id, this.doc_id, this.change, this.callback); - }); + it('should return an error', function () { + return this.callback + .calledWithExactly(sinon.match(Error)) + .should.equal(true) + }) - it("should return an error", function() { - return this.callback.calledWithExactly(sinon.match(Error)).should.equal(true); - }); + it('should add the size to the error', function () { + return this.callback.args[0][0].updateSize.should.equal(7782422) + }) - it("should add the size to the error", function() { - return this.callback.args[0][0].updateSize.should.equal(7782422); - }); + return it('should not push the change onto the pending-updates-list queue', function () { + return this.rclient.rpush.called.should.equal(false) + }) + }) - return it("should not push the change onto the pending-updates-list queue", function() { - return this.rclient.rpush.called.should.equal(false); - }); - }); + return describe('with invalid keys', function () { + beforeEach(function () { + this.change = { + op: [{ d: 'test', p: 345 }], + version: 789 // not a valid key + } + return this.DocumentUpdaterManager.queueChange( + this.project_id, + this.doc_id, + this.change, + this.callback + ) + }) - return describe("with invalid keys", function() { - beforeEach(function() { - this.change = { - "op":[{"d":"test", "p":345}], - "version": 789 // not a valid key - }; - return this.DocumentUpdaterManager.queueChange(this.project_id, this.doc_id, this.change, this.callback); - }); - - return it("should remove the invalid keys from the change", function() { - return this.rclient.rpush - .calledWith(`PendingUpdates:${this.doc_id}`, JSON.stringify({op:this.change.op})) - .should.equal(true); - }); - }); - }); -}); + return it('should remove the invalid keys from the change', function () { + return this.rclient.rpush + .calledWith( + `PendingUpdates:${this.doc_id}`, + JSON.stringify({ op: this.change.op }) + ) + .should.equal(true) + }) + }) + }) +}) diff --git a/services/real-time/test/unit/js/DrainManagerTests.js b/services/real-time/test/unit/js/DrainManagerTests.js index 6d6c8b826e..7ed7f1e06e 100644 --- a/services/real-time/test/unit/js/DrainManagerTests.js +++ b/services/real-time/test/unit/js/DrainManagerTests.js @@ -9,111 +9,124 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const should = require('chai').should(); -const sinon = require("sinon"); -const SandboxedModule = require('sandboxed-module'); -const path = require("path"); -const modulePath = path.join(__dirname, "../../../app/js/DrainManager"); +const should = require('chai').should() +const sinon = require('sinon') +const SandboxedModule = require('sandboxed-module') +const path = require('path') +const modulePath = path.join(__dirname, '../../../app/js/DrainManager') -describe("DrainManager", function() { - beforeEach(function() { - this.DrainManager = SandboxedModule.require(modulePath, { requires: { - "logger-sharelatex": (this.logger = {log: sinon.stub()}) - } - } - ); - return this.io = { - sockets: { - clients: sinon.stub() - } - }; - }); +describe('DrainManager', function () { + beforeEach(function () { + this.DrainManager = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': (this.logger = { log: sinon.stub() }) + } + }) + return (this.io = { + sockets: { + clients: sinon.stub() + } + }) + }) - describe("startDrainTimeWindow", function() { - beforeEach(function() { - this.clients = []; - for (let i = 0; i <= 5399; i++) { - this.clients[i] = { - id: i, - emit: sinon.stub() - }; - } - this.io.sockets.clients.returns(this.clients); - return this.DrainManager.startDrain = sinon.stub(); - }); + describe('startDrainTimeWindow', function () { + beforeEach(function () { + this.clients = [] + for (let i = 0; i <= 5399; i++) { + this.clients[i] = { + id: i, + emit: sinon.stub() + } + } + this.io.sockets.clients.returns(this.clients) + return (this.DrainManager.startDrain = sinon.stub()) + }) - return it("should set a drain rate fast enough", function(done){ - this.DrainManager.startDrainTimeWindow(this.io, 9); - this.DrainManager.startDrain.calledWith(this.io, 10).should.equal(true); - return done(); - }); - }); + return it('should set a drain rate fast enough', function (done) { + this.DrainManager.startDrainTimeWindow(this.io, 9) + this.DrainManager.startDrain.calledWith(this.io, 10).should.equal(true) + return done() + }) + }) + return describe('reconnectNClients', function () { + beforeEach(function () { + this.clients = [] + for (let i = 0; i <= 9; i++) { + this.clients[i] = { + id: i, + emit: sinon.stub() + } + } + return this.io.sockets.clients.returns(this.clients) + }) - return describe("reconnectNClients", function() { - beforeEach(function() { - this.clients = []; - for (let i = 0; i <= 9; i++) { - this.clients[i] = { - id: i, - emit: sinon.stub() - }; - } - return this.io.sockets.clients.returns(this.clients); - }); + return describe('after first pass', function () { + beforeEach(function () { + return this.DrainManager.reconnectNClients(this.io, 3) + }) - return describe("after first pass", function() { - beforeEach(function() { - return this.DrainManager.reconnectNClients(this.io, 3); - }); - - it("should reconnect the first 3 clients", function() { - return [0, 1, 2].map((i) => - this.clients[i].emit.calledWith("reconnectGracefully").should.equal(true)); - }); - - it("should not reconnect any more clients", function() { - return [3, 4, 5, 6, 7, 8, 9].map((i) => - this.clients[i].emit.calledWith("reconnectGracefully").should.equal(false)); - }); - - return describe("after second pass", function() { - beforeEach(function() { - return this.DrainManager.reconnectNClients(this.io, 3); - }); - - it("should reconnect the next 3 clients", function() { - return [3, 4, 5].map((i) => - this.clients[i].emit.calledWith("reconnectGracefully").should.equal(true)); - }); - - it("should not reconnect any more clients", function() { - return [6, 7, 8, 9].map((i) => - this.clients[i].emit.calledWith("reconnectGracefully").should.equal(false)); - }); - - it("should not reconnect the first 3 clients again", function() { - return [0, 1, 2].map((i) => - this.clients[i].emit.calledOnce.should.equal(true)); - }); - - return describe("after final pass", function() { - beforeEach(function() { - return this.DrainManager.reconnectNClients(this.io, 100); - }); - - it("should not reconnect the first 6 clients again", function() { - return [0, 1, 2, 3, 4, 5].map((i) => - this.clients[i].emit.calledOnce.should.equal(true)); - }); - - return it("should log out that it reached the end", function() { - return this.logger.log - .calledWith("All clients have been told to reconnectGracefully") - .should.equal(true); - }); - }); - }); - }); - }); -}); + it('should reconnect the first 3 clients', function () { + return [0, 1, 2].map((i) => + this.clients[i].emit + .calledWith('reconnectGracefully') + .should.equal(true) + ) + }) + + it('should not reconnect any more clients', function () { + return [3, 4, 5, 6, 7, 8, 9].map((i) => + this.clients[i].emit + .calledWith('reconnectGracefully') + .should.equal(false) + ) + }) + + return describe('after second pass', function () { + beforeEach(function () { + return this.DrainManager.reconnectNClients(this.io, 3) + }) + + it('should reconnect the next 3 clients', function () { + return [3, 4, 5].map((i) => + this.clients[i].emit + .calledWith('reconnectGracefully') + .should.equal(true) + ) + }) + + it('should not reconnect any more clients', function () { + return [6, 7, 8, 9].map((i) => + this.clients[i].emit + .calledWith('reconnectGracefully') + .should.equal(false) + ) + }) + + it('should not reconnect the first 3 clients again', function () { + return [0, 1, 2].map((i) => + this.clients[i].emit.calledOnce.should.equal(true) + ) + }) + + return describe('after final pass', function () { + beforeEach(function () { + return this.DrainManager.reconnectNClients(this.io, 100) + }) + + it('should not reconnect the first 6 clients again', function () { + return [0, 1, 2, 3, 4, 5].map((i) => + this.clients[i].emit.calledOnce.should.equal(true) + ) + }) + + return it('should log out that it reached the end', function () { + return this.logger.log + .calledWith('All clients have been told to reconnectGracefully') + .should.equal(true) + }) + }) + }) + }) + }) +}) diff --git a/services/real-time/test/unit/js/EventLoggerTests.js b/services/real-time/test/unit/js/EventLoggerTests.js index 2d3b298e20..7152f92ce7 100644 --- a/services/real-time/test/unit/js/EventLoggerTests.js +++ b/services/real-time/test/unit/js/EventLoggerTests.js @@ -8,99 +8,150 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -require('chai').should(); -const { - expect -} = require("chai"); -const SandboxedModule = require('sandboxed-module'); -const modulePath = '../../../app/js/EventLogger'; -const sinon = require("sinon"); -const tk = require("timekeeper"); +require('chai').should() +const { expect } = require('chai') +const SandboxedModule = require('sandboxed-module') +const modulePath = '../../../app/js/EventLogger' +const sinon = require('sinon') +const tk = require('timekeeper') -describe('EventLogger', function() { - beforeEach(function() { - this.start = Date.now(); - tk.freeze(new Date(this.start)); - this.EventLogger = SandboxedModule.require(modulePath, { requires: { - "logger-sharelatex": (this.logger = {error: sinon.stub(), warn: sinon.stub()}), - "metrics-sharelatex": (this.metrics = {inc: sinon.stub()}) - } - }); - this.channel = "applied-ops"; - this.id_1 = "random-hostname:abc-1"; - this.message_1 = "message-1"; - this.id_2 = "random-hostname:abc-2"; - return this.message_2 = "message-2"; - }); +describe('EventLogger', function () { + beforeEach(function () { + this.start = Date.now() + tk.freeze(new Date(this.start)) + this.EventLogger = SandboxedModule.require(modulePath, { + requires: { + 'logger-sharelatex': (this.logger = { + error: sinon.stub(), + warn: sinon.stub() + }), + 'metrics-sharelatex': (this.metrics = { inc: sinon.stub() }) + } + }) + this.channel = 'applied-ops' + this.id_1 = 'random-hostname:abc-1' + this.message_1 = 'message-1' + this.id_2 = 'random-hostname:abc-2' + return (this.message_2 = 'message-2') + }) - afterEach(function() { return tk.reset(); }); + afterEach(function () { + return tk.reset() + }) - return describe('checkEventOrder', function() { + return describe('checkEventOrder', function () { + describe('when the events are in order', function () { + beforeEach(function () { + this.EventLogger.checkEventOrder( + this.channel, + this.id_1, + this.message_1 + ) + return (this.status = this.EventLogger.checkEventOrder( + this.channel, + this.id_2, + this.message_2 + )) + }) - describe('when the events are in order', function() { - beforeEach(function() { - this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1); - return this.status = this.EventLogger.checkEventOrder(this.channel, this.id_2, this.message_2); - }); + it('should accept events in order', function () { + return expect(this.status).to.be.undefined + }) - it('should accept events in order', function() { - return expect(this.status).to.be.undefined; - }); + return it('should increment the valid event metric', function () { + return this.metrics.inc.calledWith(`event.${this.channel}.valid`, 1) + .should.equal.true + }) + }) - return it('should increment the valid event metric', function() { - return this.metrics.inc.calledWith(`event.${this.channel}.valid`, 1) - .should.equal.true; - }); - }); + describe('when there is a duplicate events', function () { + beforeEach(function () { + this.EventLogger.checkEventOrder( + this.channel, + this.id_1, + this.message_1 + ) + return (this.status = this.EventLogger.checkEventOrder( + this.channel, + this.id_1, + this.message_1 + )) + }) - describe('when there is a duplicate events', function() { - beforeEach(function() { - this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1); - return this.status = this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1); - }); + it('should return "duplicate" for the same event', function () { + return expect(this.status).to.equal('duplicate') + }) - it('should return "duplicate" for the same event', function() { - return expect(this.status).to.equal("duplicate"); - }); + return it('should increment the duplicate event metric', function () { + return this.metrics.inc.calledWith(`event.${this.channel}.duplicate`, 1) + .should.equal.true + }) + }) - return it('should increment the duplicate event metric', function() { - return this.metrics.inc.calledWith(`event.${this.channel}.duplicate`, 1) - .should.equal.true; - }); - }); + describe('when there are out of order events', function () { + beforeEach(function () { + this.EventLogger.checkEventOrder( + this.channel, + this.id_1, + this.message_1 + ) + this.EventLogger.checkEventOrder( + this.channel, + this.id_2, + this.message_2 + ) + return (this.status = this.EventLogger.checkEventOrder( + this.channel, + this.id_1, + this.message_1 + )) + }) - describe('when there are out of order events', function() { - beforeEach(function() { - this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1); - this.EventLogger.checkEventOrder(this.channel, this.id_2, this.message_2); - return this.status = this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1); - }); + it('should return "out-of-order" for the event', function () { + return expect(this.status).to.equal('out-of-order') + }) - it('should return "out-of-order" for the event', function() { - return expect(this.status).to.equal("out-of-order"); - }); + return it('should increment the out-of-order event metric', function () { + return this.metrics.inc.calledWith( + `event.${this.channel}.out-of-order`, + 1 + ).should.equal.true + }) + }) - return it('should increment the out-of-order event metric', function() { - return this.metrics.inc.calledWith(`event.${this.channel}.out-of-order`, 1) - .should.equal.true; - }); - }); - - return describe('after MAX_STALE_TIME_IN_MS', function() { return it('should flush old entries', function() { - let status; - this.EventLogger.MAX_EVENTS_BEFORE_CLEAN = 10; - this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1); - for (let i = 1; i <= 8; i++) { - status = this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1); - expect(status).to.equal("duplicate"); - } - // the next event should flush the old entries aboce - this.EventLogger.MAX_STALE_TIME_IN_MS=1000; - tk.freeze(new Date(this.start + (5 * 1000))); - // because we flushed the entries this should not be a duplicate - this.EventLogger.checkEventOrder(this.channel, 'other-1', this.message_2); - status = this.EventLogger.checkEventOrder(this.channel, this.id_1, this.message_1); - return expect(status).to.be.undefined; - }); }); - }); -}); \ No newline at end of file + return describe('after MAX_STALE_TIME_IN_MS', function () { + return it('should flush old entries', function () { + let status + this.EventLogger.MAX_EVENTS_BEFORE_CLEAN = 10 + this.EventLogger.checkEventOrder( + this.channel, + this.id_1, + this.message_1 + ) + for (let i = 1; i <= 8; i++) { + status = this.EventLogger.checkEventOrder( + this.channel, + this.id_1, + this.message_1 + ) + expect(status).to.equal('duplicate') + } + // the next event should flush the old entries aboce + this.EventLogger.MAX_STALE_TIME_IN_MS = 1000 + tk.freeze(new Date(this.start + 5 * 1000)) + // because we flushed the entries this should not be a duplicate + this.EventLogger.checkEventOrder( + this.channel, + 'other-1', + this.message_2 + ) + status = this.EventLogger.checkEventOrder( + this.channel, + this.id_1, + this.message_1 + ) + return expect(status).to.be.undefined + }) + }) + }) +}) diff --git a/services/real-time/test/unit/js/RoomManagerTests.js b/services/real-time/test/unit/js/RoomManagerTests.js index b356d0ee5e..3aee509af5 100644 --- a/services/real-time/test/unit/js/RoomManagerTests.js +++ b/services/real-time/test/unit/js/RoomManagerTests.js @@ -10,357 +10,410 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const chai = require('chai'); -const { - expect -} = chai; -const should = chai.should(); -const sinon = require("sinon"); -const modulePath = "../../../app/js/RoomManager.js"; -const SandboxedModule = require('sandboxed-module'); +const chai = require('chai') +const { expect } = chai +const should = chai.should() +const sinon = require('sinon') +const modulePath = '../../../app/js/RoomManager.js' +const SandboxedModule = require('sandboxed-module') -describe('RoomManager', function() { - beforeEach(function() { - this.project_id = "project-id-123"; - this.doc_id = "doc-id-456"; - this.other_doc_id = "doc-id-789"; - this.client = {namespace: {name: ''}, id: "first-client"}; - this.RoomManager = SandboxedModule.require(modulePath, { requires: { - "settings-sharelatex": (this.settings = {}), - "logger-sharelatex": (this.logger = { log: sinon.stub(), warn: sinon.stub(), error: sinon.stub() }), - "metrics-sharelatex": (this.metrics = { gauge: sinon.stub() }) - } - }); - this.RoomManager._clientsInRoom = sinon.stub(); - this.RoomManager._clientAlreadyInRoom = sinon.stub(); - this.RoomEvents = this.RoomManager.eventSource(); - sinon.spy(this.RoomEvents, 'emit'); - return sinon.spy(this.RoomEvents, 'once'); - }); - - describe("emitOnCompletion", function() { return describe("when a subscribe errors", function() { - afterEach(function() { - return process.removeListener("unhandledRejection", this.onUnhandled); - }); +describe('RoomManager', function () { + beforeEach(function () { + this.project_id = 'project-id-123' + this.doc_id = 'doc-id-456' + this.other_doc_id = 'doc-id-789' + this.client = { namespace: { name: '' }, id: 'first-client' } + this.RoomManager = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': (this.settings = {}), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + warn: sinon.stub(), + error: sinon.stub() + }), + 'metrics-sharelatex': (this.metrics = { gauge: sinon.stub() }) + } + }) + this.RoomManager._clientsInRoom = sinon.stub() + this.RoomManager._clientAlreadyInRoom = sinon.stub() + this.RoomEvents = this.RoomManager.eventSource() + sinon.spy(this.RoomEvents, 'emit') + return sinon.spy(this.RoomEvents, 'once') + }) - beforeEach(function(done) { - this.onUnhandled = error => { - this.unhandledError = error; - return done(new Error(`unhandledRejection: ${error.message}`)); - }; - process.on("unhandledRejection", this.onUnhandled); + describe('emitOnCompletion', function () { + return describe('when a subscribe errors', function () { + afterEach(function () { + return process.removeListener('unhandledRejection', this.onUnhandled) + }) - let reject; - const subscribePromise = new Promise((_, r) => reject = r); - const promises = [subscribePromise]; - const eventName = "project-subscribed-123"; - this.RoomEvents.once(eventName, () => setTimeout(done, 100)); - this.RoomManager.emitOnCompletion(promises, eventName); - return setTimeout(() => reject(new Error("subscribe failed"))); - }); + beforeEach(function (done) { + this.onUnhandled = (error) => { + this.unhandledError = error + return done(new Error(`unhandledRejection: ${error.message}`)) + } + process.on('unhandledRejection', this.onUnhandled) - return it("should keep going", function() { - return expect(this.unhandledError).to.not.exist; - }); - }); }); + let reject + const subscribePromise = new Promise((_, r) => (reject = r)) + const promises = [subscribePromise] + const eventName = 'project-subscribed-123' + this.RoomEvents.once(eventName, () => setTimeout(done, 100)) + this.RoomManager.emitOnCompletion(promises, eventName) + return setTimeout(() => reject(new Error('subscribe failed'))) + }) - describe("joinProject", function() { - - describe("when the project room is empty", function() { + return it('should keep going', function () { + return expect(this.unhandledError).to.not.exist + }) + }) + }) - beforeEach(function(done) { - this.RoomManager._clientsInRoom - .withArgs(this.client, this.project_id) - .onFirstCall().returns(0); - this.client.join = sinon.stub(); - this.callback = sinon.stub(); - this.RoomEvents.on('project-active', id => { - return setTimeout(() => { - return this.RoomEvents.emit(`project-subscribed-${id}`); - } - , 100); - }); - return this.RoomManager.joinProject(this.client, this.project_id, err => { - this.callback(err); - return done(); - }); - }); + describe('joinProject', function () { + describe('when the project room is empty', function () { + beforeEach(function (done) { + this.RoomManager._clientsInRoom + .withArgs(this.client, this.project_id) + .onFirstCall() + .returns(0) + this.client.join = sinon.stub() + this.callback = sinon.stub() + this.RoomEvents.on('project-active', (id) => { + return setTimeout(() => { + return this.RoomEvents.emit(`project-subscribed-${id}`) + }, 100) + }) + return this.RoomManager.joinProject( + this.client, + this.project_id, + (err) => { + this.callback(err) + return done() + } + ) + }) - it("should emit a 'project-active' event with the id", function() { - return this.RoomEvents.emit.calledWithExactly('project-active', this.project_id).should.equal(true); - }); + it("should emit a 'project-active' event with the id", function () { + return this.RoomEvents.emit + .calledWithExactly('project-active', this.project_id) + .should.equal(true) + }) - it("should listen for the 'project-subscribed-id' event", function() { - return this.RoomEvents.once.calledWith(`project-subscribed-${this.project_id}`).should.equal(true); - }); + it("should listen for the 'project-subscribed-id' event", function () { + return this.RoomEvents.once + .calledWith(`project-subscribed-${this.project_id}`) + .should.equal(true) + }) - return it("should join the room using the id", function() { - return this.client.join.calledWithExactly(this.project_id).should.equal(true); - }); - }); + return it('should join the room using the id', function () { + return this.client.join + .calledWithExactly(this.project_id) + .should.equal(true) + }) + }) - return describe("when there are other clients in the project room", function() { + return describe('when there are other clients in the project room', function () { + beforeEach(function () { + this.RoomManager._clientsInRoom + .withArgs(this.client, this.project_id) + .onFirstCall() + .returns(123) + .onSecondCall() + .returns(124) + this.client.join = sinon.stub() + return this.RoomManager.joinProject(this.client, this.project_id) + }) - beforeEach(function() { - this.RoomManager._clientsInRoom - .withArgs(this.client, this.project_id) - .onFirstCall().returns(123) - .onSecondCall().returns(124); - this.client.join = sinon.stub(); - return this.RoomManager.joinProject(this.client, this.project_id); - }); + it('should join the room using the id', function () { + return this.client.join.called.should.equal(true) + }) - it("should join the room using the id", function() { - return this.client.join.called.should.equal(true); - }); + return it('should not emit any events', function () { + return this.RoomEvents.emit.called.should.equal(false) + }) + }) + }) - return it("should not emit any events", function() { - return this.RoomEvents.emit.called.should.equal(false); - }); - }); - }); + describe('joinDoc', function () { + describe('when the doc room is empty', function () { + beforeEach(function (done) { + this.RoomManager._clientsInRoom + .withArgs(this.client, this.doc_id) + .onFirstCall() + .returns(0) + this.client.join = sinon.stub() + this.callback = sinon.stub() + this.RoomEvents.on('doc-active', (id) => { + return setTimeout(() => { + return this.RoomEvents.emit(`doc-subscribed-${id}`) + }, 100) + }) + return this.RoomManager.joinDoc(this.client, this.doc_id, (err) => { + this.callback(err) + return done() + }) + }) + it("should emit a 'doc-active' event with the id", function () { + return this.RoomEvents.emit + .calledWithExactly('doc-active', this.doc_id) + .should.equal(true) + }) - describe("joinDoc", function() { + it("should listen for the 'doc-subscribed-id' event", function () { + return this.RoomEvents.once + .calledWith(`doc-subscribed-${this.doc_id}`) + .should.equal(true) + }) - describe("when the doc room is empty", function() { + return it('should join the room using the id', function () { + return this.client.join + .calledWithExactly(this.doc_id) + .should.equal(true) + }) + }) - beforeEach(function(done) { - this.RoomManager._clientsInRoom - .withArgs(this.client, this.doc_id) - .onFirstCall().returns(0); - this.client.join = sinon.stub(); - this.callback = sinon.stub(); - this.RoomEvents.on('doc-active', id => { - return setTimeout(() => { - return this.RoomEvents.emit(`doc-subscribed-${id}`); - } - , 100); - }); - return this.RoomManager.joinDoc(this.client, this.doc_id, err => { - this.callback(err); - return done(); - }); - }); + return describe('when there are other clients in the doc room', function () { + beforeEach(function () { + this.RoomManager._clientsInRoom + .withArgs(this.client, this.doc_id) + .onFirstCall() + .returns(123) + .onSecondCall() + .returns(124) + this.client.join = sinon.stub() + return this.RoomManager.joinDoc(this.client, this.doc_id) + }) - it("should emit a 'doc-active' event with the id", function() { - return this.RoomEvents.emit.calledWithExactly('doc-active', this.doc_id).should.equal(true); - }); + it('should join the room using the id', function () { + return this.client.join.called.should.equal(true) + }) - it("should listen for the 'doc-subscribed-id' event", function() { - return this.RoomEvents.once.calledWith(`doc-subscribed-${this.doc_id}`).should.equal(true); - }); + return it('should not emit any events', function () { + return this.RoomEvents.emit.called.should.equal(false) + }) + }) + }) - return it("should join the room using the id", function() { - return this.client.join.calledWithExactly(this.doc_id).should.equal(true); - }); - }); + describe('leaveDoc', function () { + describe('when doc room will be empty after this client has left', function () { + beforeEach(function () { + this.RoomManager._clientAlreadyInRoom + .withArgs(this.client, this.doc_id) + .returns(true) + this.RoomManager._clientsInRoom + .withArgs(this.client, this.doc_id) + .onCall(0) + .returns(0) + this.client.leave = sinon.stub() + return this.RoomManager.leaveDoc(this.client, this.doc_id) + }) - return describe("when there are other clients in the doc room", function() { + it('should leave the room using the id', function () { + return this.client.leave + .calledWithExactly(this.doc_id) + .should.equal(true) + }) - beforeEach(function() { - this.RoomManager._clientsInRoom - .withArgs(this.client, this.doc_id) - .onFirstCall().returns(123) - .onSecondCall().returns(124); - this.client.join = sinon.stub(); - return this.RoomManager.joinDoc(this.client, this.doc_id); - }); + return it("should emit a 'doc-empty' event with the id", function () { + return this.RoomEvents.emit + .calledWithExactly('doc-empty', this.doc_id) + .should.equal(true) + }) + }) - it("should join the room using the id", function() { - return this.client.join.called.should.equal(true); - }); + describe('when there are other clients in the doc room', function () { + beforeEach(function () { + this.RoomManager._clientAlreadyInRoom + .withArgs(this.client, this.doc_id) + .returns(true) + this.RoomManager._clientsInRoom + .withArgs(this.client, this.doc_id) + .onCall(0) + .returns(123) + this.client.leave = sinon.stub() + return this.RoomManager.leaveDoc(this.client, this.doc_id) + }) - return it("should not emit any events", function() { - return this.RoomEvents.emit.called.should.equal(false); - }); - }); - }); + it('should leave the room using the id', function () { + return this.client.leave + .calledWithExactly(this.doc_id) + .should.equal(true) + }) + return it('should not emit any events', function () { + return this.RoomEvents.emit.called.should.equal(false) + }) + }) - describe("leaveDoc", function() { + return describe('when the client is not in the doc room', function () { + beforeEach(function () { + this.RoomManager._clientAlreadyInRoom + .withArgs(this.client, this.doc_id) + .returns(false) + this.RoomManager._clientsInRoom + .withArgs(this.client, this.doc_id) + .onCall(0) + .returns(0) + this.client.leave = sinon.stub() + return this.RoomManager.leaveDoc(this.client, this.doc_id) + }) - describe("when doc room will be empty after this client has left", function() { + it('should not leave the room', function () { + return this.client.leave.called.should.equal(false) + }) - beforeEach(function() { - this.RoomManager._clientAlreadyInRoom - .withArgs(this.client, this.doc_id) - .returns(true); - this.RoomManager._clientsInRoom - .withArgs(this.client, this.doc_id) - .onCall(0).returns(0); - this.client.leave = sinon.stub(); - return this.RoomManager.leaveDoc(this.client, this.doc_id); - }); + return it('should not emit any events', function () { + return this.RoomEvents.emit.called.should.equal(false) + }) + }) + }) - it("should leave the room using the id", function() { - return this.client.leave.calledWithExactly(this.doc_id).should.equal(true); - }); + return describe('leaveProjectAndDocs', function () { + return describe('when the client is connected to the project and multiple docs', function () { + beforeEach(function () { + this.RoomManager._roomsClientIsIn = sinon + .stub() + .returns([this.project_id, this.doc_id, this.other_doc_id]) + this.client.join = sinon.stub() + return (this.client.leave = sinon.stub()) + }) - return it("should emit a 'doc-empty' event with the id", function() { - return this.RoomEvents.emit.calledWithExactly('doc-empty', this.doc_id).should.equal(true); - }); - }); + describe('when this is the only client connected', function () { + beforeEach(function (done) { + // first call is for the join, + // second for the leave + this.RoomManager._clientsInRoom + .withArgs(this.client, this.doc_id) + .onCall(0) + .returns(0) + .onCall(1) + .returns(0) + this.RoomManager._clientsInRoom + .withArgs(this.client, this.other_doc_id) + .onCall(0) + .returns(0) + .onCall(1) + .returns(0) + this.RoomManager._clientsInRoom + .withArgs(this.client, this.project_id) + .onCall(0) + .returns(0) + .onCall(1) + .returns(0) + this.RoomManager._clientAlreadyInRoom + .withArgs(this.client, this.doc_id) + .returns(true) + .withArgs(this.client, this.other_doc_id) + .returns(true) + .withArgs(this.client, this.project_id) + .returns(true) + this.RoomEvents.on('project-active', (id) => { + return setTimeout(() => { + return this.RoomEvents.emit(`project-subscribed-${id}`) + }, 100) + }) + this.RoomEvents.on('doc-active', (id) => { + return setTimeout(() => { + return this.RoomEvents.emit(`doc-subscribed-${id}`) + }, 100) + }) + // put the client in the rooms + return this.RoomManager.joinProject( + this.client, + this.project_id, + () => { + return this.RoomManager.joinDoc(this.client, this.doc_id, () => { + return this.RoomManager.joinDoc( + this.client, + this.other_doc_id, + () => { + // now leave the project + this.RoomManager.leaveProjectAndDocs(this.client) + return done() + } + ) + }) + } + ) + }) + it('should leave all the docs', function () { + this.client.leave.calledWithExactly(this.doc_id).should.equal(true) + return this.client.leave + .calledWithExactly(this.other_doc_id) + .should.equal(true) + }) - describe("when there are other clients in the doc room", function() { + it('should leave the project', function () { + return this.client.leave + .calledWithExactly(this.project_id) + .should.equal(true) + }) - beforeEach(function() { - this.RoomManager._clientAlreadyInRoom - .withArgs(this.client, this.doc_id) - .returns(true); - this.RoomManager._clientsInRoom - .withArgs(this.client, this.doc_id) - .onCall(0).returns(123); - this.client.leave = sinon.stub(); - return this.RoomManager.leaveDoc(this.client, this.doc_id); - }); + it("should emit a 'doc-empty' event with the id for each doc", function () { + this.RoomEvents.emit + .calledWithExactly('doc-empty', this.doc_id) + .should.equal(true) + return this.RoomEvents.emit + .calledWithExactly('doc-empty', this.other_doc_id) + .should.equal(true) + }) - it("should leave the room using the id", function() { - return this.client.leave.calledWithExactly(this.doc_id).should.equal(true); - }); + return it("should emit a 'project-empty' event with the id for the project", function () { + return this.RoomEvents.emit + .calledWithExactly('project-empty', this.project_id) + .should.equal(true) + }) + }) - return it("should not emit any events", function() { - return this.RoomEvents.emit.called.should.equal(false); - }); - }); + return describe('when other clients are still connected', function () { + beforeEach(function () { + this.RoomManager._clientsInRoom + .withArgs(this.client, this.doc_id) + .onFirstCall() + .returns(123) + .onSecondCall() + .returns(122) + this.RoomManager._clientsInRoom + .withArgs(this.client, this.other_doc_id) + .onFirstCall() + .returns(123) + .onSecondCall() + .returns(122) + this.RoomManager._clientsInRoom + .withArgs(this.client, this.project_id) + .onFirstCall() + .returns(123) + .onSecondCall() + .returns(122) + this.RoomManager._clientAlreadyInRoom + .withArgs(this.client, this.doc_id) + .returns(true) + .withArgs(this.client, this.other_doc_id) + .returns(true) + .withArgs(this.client, this.project_id) + .returns(true) + return this.RoomManager.leaveProjectAndDocs(this.client) + }) - return describe("when the client is not in the doc room", function() { + it('should leave all the docs', function () { + this.client.leave.calledWithExactly(this.doc_id).should.equal(true) + return this.client.leave + .calledWithExactly(this.other_doc_id) + .should.equal(true) + }) - beforeEach(function() { - this.RoomManager._clientAlreadyInRoom - .withArgs(this.client, this.doc_id) - .returns(false); - this.RoomManager._clientsInRoom - .withArgs(this.client, this.doc_id) - .onCall(0).returns(0); - this.client.leave = sinon.stub(); - return this.RoomManager.leaveDoc(this.client, this.doc_id); - }); + it('should leave the project', function () { + return this.client.leave + .calledWithExactly(this.project_id) + .should.equal(true) + }) - it("should not leave the room", function() { - return this.client.leave.called.should.equal(false); - }); - - return it("should not emit any events", function() { - return this.RoomEvents.emit.called.should.equal(false); - }); - }); - }); - - - return describe("leaveProjectAndDocs", function() { return describe("when the client is connected to the project and multiple docs", function() { - - beforeEach(function() { - this.RoomManager._roomsClientIsIn = sinon.stub().returns([this.project_id, this.doc_id, this.other_doc_id]); - this.client.join = sinon.stub(); - return this.client.leave = sinon.stub(); - }); - - describe("when this is the only client connected", function() { - - beforeEach(function(done) { - // first call is for the join, - // second for the leave - this.RoomManager._clientsInRoom - .withArgs(this.client, this.doc_id) - .onCall(0).returns(0) - .onCall(1).returns(0); - this.RoomManager._clientsInRoom - .withArgs(this.client, this.other_doc_id) - .onCall(0).returns(0) - .onCall(1).returns(0); - this.RoomManager._clientsInRoom - .withArgs(this.client, this.project_id) - .onCall(0).returns(0) - .onCall(1).returns(0); - this.RoomManager._clientAlreadyInRoom - .withArgs(this.client, this.doc_id) - .returns(true) - .withArgs(this.client, this.other_doc_id) - .returns(true) - .withArgs(this.client, this.project_id) - .returns(true); - this.RoomEvents.on('project-active', id => { - return setTimeout(() => { - return this.RoomEvents.emit(`project-subscribed-${id}`); - } - , 100); - }); - this.RoomEvents.on('doc-active', id => { - return setTimeout(() => { - return this.RoomEvents.emit(`doc-subscribed-${id}`); - } - , 100); - }); - // put the client in the rooms - return this.RoomManager.joinProject(this.client, this.project_id, () => { - return this.RoomManager.joinDoc(this.client, this.doc_id, () => { - return this.RoomManager.joinDoc(this.client, this.other_doc_id, () => { - // now leave the project - this.RoomManager.leaveProjectAndDocs(this.client); - return done(); - }); - }); - }); - }); - - it("should leave all the docs", function() { - this.client.leave.calledWithExactly(this.doc_id).should.equal(true); - return this.client.leave.calledWithExactly(this.other_doc_id).should.equal(true); - }); - - it("should leave the project", function() { - return this.client.leave.calledWithExactly(this.project_id).should.equal(true); - }); - - it("should emit a 'doc-empty' event with the id for each doc", function() { - this.RoomEvents.emit.calledWithExactly('doc-empty', this.doc_id).should.equal(true); - return this.RoomEvents.emit.calledWithExactly('doc-empty', this.other_doc_id).should.equal(true); - }); - - return it("should emit a 'project-empty' event with the id for the project", function() { - return this.RoomEvents.emit.calledWithExactly('project-empty', this.project_id).should.equal(true); - }); - }); - - return describe("when other clients are still connected", function() { - - beforeEach(function() { - this.RoomManager._clientsInRoom - .withArgs(this.client, this.doc_id) - .onFirstCall().returns(123) - .onSecondCall().returns(122); - this.RoomManager._clientsInRoom - .withArgs(this.client, this.other_doc_id) - .onFirstCall().returns(123) - .onSecondCall().returns(122); - this.RoomManager._clientsInRoom - .withArgs(this.client, this.project_id) - .onFirstCall().returns(123) - .onSecondCall().returns(122); - this.RoomManager._clientAlreadyInRoom - .withArgs(this.client, this.doc_id) - .returns(true) - .withArgs(this.client, this.other_doc_id) - .returns(true) - .withArgs(this.client, this.project_id) - .returns(true); - return this.RoomManager.leaveProjectAndDocs(this.client); - }); - - it("should leave all the docs", function() { - this.client.leave.calledWithExactly(this.doc_id).should.equal(true); - return this.client.leave.calledWithExactly(this.other_doc_id).should.equal(true); - }); - - it("should leave the project", function() { - return this.client.leave.calledWithExactly(this.project_id).should.equal(true); - }); - - return it("should not emit any events", function() { - return this.RoomEvents.emit.called.should.equal(false); - }); - }); - }); }); -}); \ No newline at end of file + return it('should not emit any events', function () { + return this.RoomEvents.emit.called.should.equal(false) + }) + }) + }) + }) +}) diff --git a/services/real-time/test/unit/js/SafeJsonParseTest.js b/services/real-time/test/unit/js/SafeJsonParseTest.js index 58fed31397..4fb558a6b0 100644 --- a/services/real-time/test/unit/js/SafeJsonParseTest.js +++ b/services/real-time/test/unit/js/SafeJsonParseTest.js @@ -11,49 +11,49 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -require('chai').should(); -const { - expect -} = require("chai"); -const SandboxedModule = require('sandboxed-module'); -const modulePath = '../../../app/js/SafeJsonParse'; -const sinon = require("sinon"); +require('chai').should() +const { expect } = require('chai') +const SandboxedModule = require('sandboxed-module') +const modulePath = '../../../app/js/SafeJsonParse' +const sinon = require('sinon') -describe('SafeJsonParse', function() { - beforeEach(function() { - return this.SafeJsonParse = SandboxedModule.require(modulePath, { requires: { - "settings-sharelatex": (this.Settings = { - maxUpdateSize: 16 * 1024 - }), - "logger-sharelatex": (this.logger = {error: sinon.stub()}) - } - });}); +describe('SafeJsonParse', function () { + beforeEach(function () { + return (this.SafeJsonParse = SandboxedModule.require(modulePath, { + requires: { + 'settings-sharelatex': (this.Settings = { + maxUpdateSize: 16 * 1024 + }), + 'logger-sharelatex': (this.logger = { error: sinon.stub() }) + } + })) + }) - return describe("parse", function() { - it("should parse documents correctly", function(done) { - return this.SafeJsonParse.parse('{"foo": "bar"}', (error, parsed) => { - expect(parsed).to.deep.equal({foo: "bar"}); - return done(); - }); - }); - - it("should return an error on bad data", function(done) { - return this.SafeJsonParse.parse('blah', (error, parsed) => { - expect(error).to.exist; - return done(); - }); - }); - - return it("should return an error on oversized data", function(done) { - // we have a 2k overhead on top of max size - const big_blob = Array(16*1024).join("A"); - const data = `{\"foo\": \"${big_blob}\"}`; - this.Settings.maxUpdateSize = 2 * 1024; - return this.SafeJsonParse.parse(data, (error, parsed) => { - this.logger.error.called.should.equal(true); - expect(error).to.exist; - return done(); - }); - }); - }); -}); \ No newline at end of file + return describe('parse', function () { + it('should parse documents correctly', function (done) { + return this.SafeJsonParse.parse('{"foo": "bar"}', (error, parsed) => { + expect(parsed).to.deep.equal({ foo: 'bar' }) + return done() + }) + }) + + it('should return an error on bad data', function (done) { + return this.SafeJsonParse.parse('blah', (error, parsed) => { + expect(error).to.exist + return done() + }) + }) + + return it('should return an error on oversized data', function (done) { + // we have a 2k overhead on top of max size + const big_blob = Array(16 * 1024).join('A') + const data = `{\"foo\": \"${big_blob}\"}` + this.Settings.maxUpdateSize = 2 * 1024 + return this.SafeJsonParse.parse(data, (error, parsed) => { + this.logger.error.called.should.equal(true) + expect(error).to.exist + return done() + }) + }) + }) +}) diff --git a/services/real-time/test/unit/js/SessionSocketsTests.js b/services/real-time/test/unit/js/SessionSocketsTests.js index a57a58bfac..f4ae34bf78 100644 --- a/services/real-time/test/unit/js/SessionSocketsTests.js +++ b/services/real-time/test/unit/js/SessionSocketsTests.js @@ -9,168 +9,189 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const {EventEmitter} = require('events'); -const {expect} = require('chai'); -const SandboxedModule = require('sandboxed-module'); -const modulePath = '../../../app/js/SessionSockets'; -const sinon = require('sinon'); +const { EventEmitter } = require('events') +const { expect } = require('chai') +const SandboxedModule = require('sandboxed-module') +const modulePath = '../../../app/js/SessionSockets' +const sinon = require('sinon') -describe('SessionSockets', function() { - before(function() { - this.SessionSocketsModule = SandboxedModule.require(modulePath); - this.io = new EventEmitter(); - this.id1 = Math.random().toString(); - this.id2 = Math.random().toString(); - const redisResponses = { - error: [new Error('Redis: something went wrong'), null], - unknownId: [null, null] - }; - redisResponses[this.id1] = [null, {user: {_id: '123'}}]; - redisResponses[this.id2] = [null, {user: {_id: 'abc'}}]; +describe('SessionSockets', function () { + before(function () { + this.SessionSocketsModule = SandboxedModule.require(modulePath) + this.io = new EventEmitter() + this.id1 = Math.random().toString() + this.id2 = Math.random().toString() + const redisResponses = { + error: [new Error('Redis: something went wrong'), null], + unknownId: [null, null] + } + redisResponses[this.id1] = [null, { user: { _id: '123' } }] + redisResponses[this.id2] = [null, { user: { _id: 'abc' } }] - this.sessionStore = { - get: sinon.stub().callsFake((id, fn) => fn.apply(null, redisResponses[id])) - }; - this.cookieParser = function(req, res, next) { - req.signedCookies = req._signedCookies; - return next(); - }; - this.SessionSockets = this.SessionSocketsModule(this.io, this.sessionStore, this.cookieParser, 'ol.sid'); - return this.checkSocket = (socket, fn) => { - this.SessionSockets.once('connection', fn); - return this.io.emit('connection', socket); - }; - }); + this.sessionStore = { + get: sinon + .stub() + .callsFake((id, fn) => fn.apply(null, redisResponses[id])) + } + this.cookieParser = function (req, res, next) { + req.signedCookies = req._signedCookies + return next() + } + this.SessionSockets = this.SessionSocketsModule( + this.io, + this.sessionStore, + this.cookieParser, + 'ol.sid' + ) + return (this.checkSocket = (socket, fn) => { + this.SessionSockets.once('connection', fn) + return this.io.emit('connection', socket) + }) + }) - describe('without cookies', function() { - before(function() { - return this.socket = {handshake: {}};}); + describe('without cookies', function () { + before(function () { + return (this.socket = { handshake: {} }) + }) - it('should return a lookup error', function(done) { - return this.checkSocket(this.socket, (error) => { - expect(error).to.exist; - expect(error.message).to.equal('could not look up session by key'); - return done(); - }); - }); + it('should return a lookup error', function (done) { + return this.checkSocket(this.socket, (error) => { + expect(error).to.exist + expect(error.message).to.equal('could not look up session by key') + return done() + }) + }) - return it('should not query redis', function(done) { - return this.checkSocket(this.socket, () => { - expect(this.sessionStore.get.called).to.equal(false); - return done(); - }); - }); - }); + return it('should not query redis', function (done) { + return this.checkSocket(this.socket, () => { + expect(this.sessionStore.get.called).to.equal(false) + return done() + }) + }) + }) - describe('with a different cookie', function() { - before(function() { - return this.socket = {handshake: {_signedCookies: {other: 1}}};}); + describe('with a different cookie', function () { + before(function () { + return (this.socket = { handshake: { _signedCookies: { other: 1 } } }) + }) - it('should return a lookup error', function(done) { - return this.checkSocket(this.socket, (error) => { - expect(error).to.exist; - expect(error.message).to.equal('could not look up session by key'); - return done(); - }); - }); + it('should return a lookup error', function (done) { + return this.checkSocket(this.socket, (error) => { + expect(error).to.exist + expect(error.message).to.equal('could not look up session by key') + return done() + }) + }) - return it('should not query redis', function(done) { - return this.checkSocket(this.socket, () => { - expect(this.sessionStore.get.called).to.equal(false); - return done(); - }); - }); - }); + return it('should not query redis', function (done) { + return this.checkSocket(this.socket, () => { + expect(this.sessionStore.get.called).to.equal(false) + return done() + }) + }) + }) - describe('with a valid cookie and a failing session lookup', function() { - before(function() { - return this.socket = {handshake: {_signedCookies: {'ol.sid': 'error'}}};}); + describe('with a valid cookie and a failing session lookup', function () { + before(function () { + return (this.socket = { + handshake: { _signedCookies: { 'ol.sid': 'error' } } + }) + }) - it('should query redis', function(done) { - return this.checkSocket(this.socket, () => { - expect(this.sessionStore.get.called).to.equal(true); - return done(); - }); - }); + it('should query redis', function (done) { + return this.checkSocket(this.socket, () => { + expect(this.sessionStore.get.called).to.equal(true) + return done() + }) + }) - return it('should return a redis error', function(done) { - return this.checkSocket(this.socket, (error) => { - expect(error).to.exist; - expect(error.message).to.equal('Redis: something went wrong'); - return done(); - }); - }); - }); + return it('should return a redis error', function (done) { + return this.checkSocket(this.socket, (error) => { + expect(error).to.exist + expect(error.message).to.equal('Redis: something went wrong') + return done() + }) + }) + }) - describe('with a valid cookie and no matching session', function() { - before(function() { - return this.socket = {handshake: {_signedCookies: {'ol.sid': 'unknownId'}}};}); + describe('with a valid cookie and no matching session', function () { + before(function () { + return (this.socket = { + handshake: { _signedCookies: { 'ol.sid': 'unknownId' } } + }) + }) - it('should query redis', function(done) { - return this.checkSocket(this.socket, () => { - expect(this.sessionStore.get.called).to.equal(true); - return done(); - }); - }); + it('should query redis', function (done) { + return this.checkSocket(this.socket, () => { + expect(this.sessionStore.get.called).to.equal(true) + return done() + }) + }) - return it('should return a lookup error', function(done) { - return this.checkSocket(this.socket, (error) => { - expect(error).to.exist; - expect(error.message).to.equal('could not look up session by key'); - return done(); - }); - }); - }); + return it('should return a lookup error', function (done) { + return this.checkSocket(this.socket, (error) => { + expect(error).to.exist + expect(error.message).to.equal('could not look up session by key') + return done() + }) + }) + }) - describe('with a valid cookie and a matching session', function() { - before(function() { - return this.socket = {handshake: {_signedCookies: {'ol.sid': this.id1}}};}); + describe('with a valid cookie and a matching session', function () { + before(function () { + return (this.socket = { + handshake: { _signedCookies: { 'ol.sid': this.id1 } } + }) + }) - it('should query redis', function(done) { - return this.checkSocket(this.socket, () => { - expect(this.sessionStore.get.called).to.equal(true); - return done(); - }); - }); + it('should query redis', function (done) { + return this.checkSocket(this.socket, () => { + expect(this.sessionStore.get.called).to.equal(true) + return done() + }) + }) - it('should not return an error', function(done) { - return this.checkSocket(this.socket, (error) => { - expect(error).to.not.exist; - return done(); - }); - }); + it('should not return an error', function (done) { + return this.checkSocket(this.socket, (error) => { + expect(error).to.not.exist + return done() + }) + }) - return it('should return the session', function(done) { - return this.checkSocket(this.socket, (error, s, session) => { - expect(session).to.deep.equal({user: {_id: '123'}}); - return done(); - }); - }); - }); + return it('should return the session', function (done) { + return this.checkSocket(this.socket, (error, s, session) => { + expect(session).to.deep.equal({ user: { _id: '123' } }) + return done() + }) + }) + }) - return describe('with a different valid cookie and matching session', function() { - before(function() { - return this.socket = {handshake: {_signedCookies: {'ol.sid': this.id2}}};}); + return describe('with a different valid cookie and matching session', function () { + before(function () { + return (this.socket = { + handshake: { _signedCookies: { 'ol.sid': this.id2 } } + }) + }) - it('should query redis', function(done) { - return this.checkSocket(this.socket, () => { - expect(this.sessionStore.get.called).to.equal(true); - return done(); - }); - }); + it('should query redis', function (done) { + return this.checkSocket(this.socket, () => { + expect(this.sessionStore.get.called).to.equal(true) + return done() + }) + }) - it('should not return an error', function(done) { - return this.checkSocket(this.socket, (error) => { - expect(error).to.not.exist; - return done(); - }); - }); + it('should not return an error', function (done) { + return this.checkSocket(this.socket, (error) => { + expect(error).to.not.exist + return done() + }) + }) - return it('should return the other session', function(done) { - return this.checkSocket(this.socket, (error, s, session) => { - expect(session).to.deep.equal({user: {_id: 'abc'}}); - return done(); - }); - }); - }); -}); + return it('should return the other session', function (done) { + return this.checkSocket(this.socket, (error, s, session) => { + expect(session).to.deep.equal({ user: { _id: 'abc' } }) + return done() + }) + }) + }) +}) diff --git a/services/real-time/test/unit/js/WebApiManagerTests.js b/services/real-time/test/unit/js/WebApiManagerTests.js index c868cbaf0e..2d792b563b 100644 --- a/services/real-time/test/unit/js/WebApiManagerTests.js +++ b/services/real-time/test/unit/js/WebApiManagerTests.js @@ -9,109 +9,154 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const chai = require('chai'); -const should = chai.should(); -const sinon = require("sinon"); -const modulePath = "../../../app/js/WebApiManager.js"; -const SandboxedModule = require('sandboxed-module'); -const { CodedError } = require('../../../app/js/Errors'); +const chai = require('chai') +const should = chai.should() +const sinon = require('sinon') +const modulePath = '../../../app/js/WebApiManager.js' +const SandboxedModule = require('sandboxed-module') +const { CodedError } = require('../../../app/js/Errors') -describe('WebApiManager', function() { - beforeEach(function() { - this.project_id = "project-id-123"; - this.user_id = "user-id-123"; - this.user = {_id: this.user_id}; - this.callback = sinon.stub(); - return this.WebApiManager = SandboxedModule.require(modulePath, { requires: { - "request": (this.request = {}), - "settings-sharelatex": (this.settings = { - apis: { - web: { - url: "http://web.example.com", - user: "username", - pass: "password" - } - } - }), - "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }) - } - });}); +describe('WebApiManager', function () { + beforeEach(function () { + this.project_id = 'project-id-123' + this.user_id = 'user-id-123' + this.user = { _id: this.user_id } + this.callback = sinon.stub() + return (this.WebApiManager = SandboxedModule.require(modulePath, { + requires: { + request: (this.request = {}), + 'settings-sharelatex': (this.settings = { + apis: { + web: { + url: 'http://web.example.com', + user: 'username', + pass: 'password' + } + } + }), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub() + }) + } + })) + }) - return describe("joinProject", function() { - describe("successfully", function() { - beforeEach(function() { - this.response = { - project: { name: "Test project" }, - privilegeLevel: "owner", - isRestrictedUser: true - }; - this.request.post = sinon.stub().callsArgWith(1, null, {statusCode: 200}, this.response); - return this.WebApiManager.joinProject(this.project_id, this.user, this.callback); - }); + return describe('joinProject', function () { + describe('successfully', function () { + beforeEach(function () { + this.response = { + project: { name: 'Test project' }, + privilegeLevel: 'owner', + isRestrictedUser: true + } + this.request.post = sinon + .stub() + .callsArgWith(1, null, { statusCode: 200 }, this.response) + return this.WebApiManager.joinProject( + this.project_id, + this.user, + this.callback + ) + }) - it("should send a request to web to join the project", function() { - return this.request.post - .calledWith({ - url: `${this.settings.apis.web.url}/project/${this.project_id}/join`, - qs: { - user_id: this.user_id - }, - auth: { - user: this.settings.apis.web.user, - pass: this.settings.apis.web.pass, - sendImmediately: true - }, - json: true, - jar: false, - headers: {} - }) - .should.equal(true); - }); + it('should send a request to web to join the project', function () { + return this.request.post + .calledWith({ + url: `${this.settings.apis.web.url}/project/${this.project_id}/join`, + qs: { + user_id: this.user_id + }, + auth: { + user: this.settings.apis.web.user, + pass: this.settings.apis.web.pass, + sendImmediately: true + }, + json: true, + jar: false, + headers: {} + }) + .should.equal(true) + }) - return it("should return the project, privilegeLevel, and restricted flag", function() { - return this.callback - .calledWith(null, this.response.project, this.response.privilegeLevel, this.response.isRestrictedUser) - .should.equal(true); - }); - }); + return it('should return the project, privilegeLevel, and restricted flag', function () { + return this.callback + .calledWith( + null, + this.response.project, + this.response.privilegeLevel, + this.response.isRestrictedUser + ) + .should.equal(true) + }) + }) - describe("with an error from web", function() { - beforeEach(function() { - this.request.post = sinon.stub().callsArgWith(1, null, {statusCode: 500}, null); - return this.WebApiManager.joinProject(this.project_id, this.user_id, this.callback); - }); + describe('with an error from web', function () { + beforeEach(function () { + this.request.post = sinon + .stub() + .callsArgWith(1, null, { statusCode: 500 }, null) + return this.WebApiManager.joinProject( + this.project_id, + this.user_id, + this.callback + ) + }) - return it("should call the callback with an error", function() { - return this.callback - .calledWith(sinon.match({message: "non-success status code from web: 500"})) - .should.equal(true); - }); - }); + return it('should call the callback with an error', function () { + return this.callback + .calledWith( + sinon.match({ message: 'non-success status code from web: 500' }) + ) + .should.equal(true) + }) + }) - describe("with no data from web", function() { - beforeEach(function() { - this.request.post = sinon.stub().callsArgWith(1, null, {statusCode: 200}, null); - return this.WebApiManager.joinProject(this.project_id, this.user_id, this.callback); - }); + describe('with no data from web', function () { + beforeEach(function () { + this.request.post = sinon + .stub() + .callsArgWith(1, null, { statusCode: 200 }, null) + return this.WebApiManager.joinProject( + this.project_id, + this.user_id, + this.callback + ) + }) - return it("should call the callback with an error", function() { - return this.callback - .calledWith(sinon.match({message: "no data returned from joinProject request"})) - .should.equal(true); - }); - }); + return it('should call the callback with an error', function () { + return this.callback + .calledWith( + sinon.match({ + message: 'no data returned from joinProject request' + }) + ) + .should.equal(true) + }) + }) - return describe("when the project is over its rate limit", function() { - beforeEach(function() { - this.request.post = sinon.stub().callsArgWith(1, null, {statusCode: 429}, null); - return this.WebApiManager.joinProject(this.project_id, this.user_id, this.callback); - }); + return describe('when the project is over its rate limit', function () { + beforeEach(function () { + this.request.post = sinon + .stub() + .callsArgWith(1, null, { statusCode: 429 }, null) + return this.WebApiManager.joinProject( + this.project_id, + this.user_id, + this.callback + ) + }) - return it("should call the callback with a TooManyRequests error code", function() { - return this.callback - .calledWith(sinon.match({message: "rate-limit hit when joining project", code: "TooManyRequests"})) - .should.equal(true); - }); - }); - }); -}); + return it('should call the callback with a TooManyRequests error code', function () { + return this.callback + .calledWith( + sinon.match({ + message: 'rate-limit hit when joining project', + code: 'TooManyRequests' + }) + ) + .should.equal(true) + }) + }) + }) +}) diff --git a/services/real-time/test/unit/js/WebsocketControllerTests.js b/services/real-time/test/unit/js/WebsocketControllerTests.js index 58f417d1ca..8d8a39bb74 100644 --- a/services/real-time/test/unit/js/WebsocketControllerTests.js +++ b/services/real-time/test/unit/js/WebsocketControllerTests.js @@ -12,1085 +12,1483 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const chai = require('chai'); -const should = chai.should(); -const sinon = require("sinon"); -const { - expect -} = chai; -const modulePath = "../../../app/js/WebsocketController.js"; -const SandboxedModule = require('sandboxed-module'); -const tk = require("timekeeper"); - -describe('WebsocketController', function() { - beforeEach(function() { - tk.freeze(new Date()); - this.project_id = "project-id-123"; - this.user = { - _id: (this.user_id = "user-id-123"), - first_name: "James", - last_name: "Allen", - email: "james@example.com", - signUpDate: new Date("2014-01-01"), - loginCount: 42 - }; - this.callback = sinon.stub(); - this.client = { - disconnected: false, - id: (this.client_id = "mock-client-id-123"), - publicId: `other-id-${Math.random()}`, - ol_context: {}, - join: sinon.stub(), - leave: sinon.stub() - }; - return this.WebsocketController = SandboxedModule.require(modulePath, { requires: { - "./WebApiManager": (this.WebApiManager = {}), - "./AuthorizationManager": (this.AuthorizationManager = {}), - "./DocumentUpdaterManager": (this.DocumentUpdaterManager = {}), - "./ConnectedUsersManager": (this.ConnectedUsersManager = {}), - "./WebsocketLoadBalancer": (this.WebsocketLoadBalancer = {}), - "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() }), - "metrics-sharelatex": (this.metrics = { - inc: sinon.stub(), - set: sinon.stub() - }), - "./RoomManager": (this.RoomManager = {}) - } - });}); - - afterEach(function() { return tk.reset(); }); - - describe("joinProject", function() { - describe("when authorised", function() { - beforeEach(function() { - this.client.id = "mock-client-id"; - this.project = { - name: "Test Project", - owner: { - _id: (this.owner_id = "mock-owner-id-123") - } - }; - this.privilegeLevel = "owner"; - this.ConnectedUsersManager.updateUserPosition = sinon.stub().callsArg(4); - this.isRestrictedUser = true; - this.WebApiManager.joinProject = sinon.stub().callsArgWith(2, null, this.project, this.privilegeLevel, this.isRestrictedUser); - this.RoomManager.joinProject = sinon.stub().callsArg(2); - return this.WebsocketController.joinProject(this.client, this.user, this.project_id, this.callback); - }); - - it("should load the project from web", function() { - return this.WebApiManager.joinProject - .calledWith(this.project_id, this.user) - .should.equal(true); - }); - - it("should join the project room", function() { - return this.RoomManager.joinProject.calledWith(this.client, this.project_id).should.equal(true); - }); - - it("should set the privilege level on the client", function() { - return this.client.ol_context.privilege_level.should.equal(this.privilegeLevel); - }); - it("should set the user's id on the client", function() { - return this.client.ol_context.user_id.should.equal(this.user._id); - }); - it("should set the user's email on the client", function() { - return this.client.ol_context.email.should.equal(this.user.email); - }); - it("should set the user's first_name on the client", function() { - return this.client.ol_context.first_name.should.equal(this.user.first_name); - }); - it("should set the user's last_name on the client", function() { - return this.client.ol_context.last_name.should.equal(this.user.last_name); - }); - it("should set the user's sign up date on the client", function() { - return this.client.ol_context.signup_date.should.equal(this.user.signUpDate); - }); - it("should set the user's login_count on the client", function() { - return this.client.ol_context.login_count.should.equal(this.user.loginCount); - }); - it("should set the connected time on the client", function() { - return this.client.ol_context.connected_time.should.equal(new Date()); - }); - it("should set the project_id on the client", function() { - return this.client.ol_context.project_id.should.equal(this.project_id); - }); - it("should set the project owner id on the client", function() { - return this.client.ol_context.owner_id.should.equal(this.owner_id); - }); - it("should set the is_restricted_user flag on the client", function() { - return this.client.ol_context.is_restricted_user.should.equal(this.isRestrictedUser); - }); - it("should call the callback with the project, privilegeLevel and protocolVersion", function() { - return this.callback - .calledWith(null, this.project, this.privilegeLevel, this.WebsocketController.PROTOCOL_VERSION) - .should.equal(true); - }); - - it("should mark the user as connected in ConnectedUsersManager", function() { - return this.ConnectedUsersManager.updateUserPosition - .calledWith(this.project_id, this.client.publicId, this.user, null) - .should.equal(true); - }); - - return it("should increment the join-project metric", function() { - return this.metrics.inc.calledWith("editor.join-project").should.equal(true); - }); - }); - - describe("when not authorized", function() { - beforeEach(function() { - this.WebApiManager.joinProject = sinon.stub().callsArgWith(2, null, null, null); - return this.WebsocketController.joinProject(this.client, this.user, this.project_id, this.callback); - }); - - it("should return an error", function() { - return this.callback - .calledWith(sinon.match({message: "not authorized"})) - .should.equal(true); - }); - - return it("should not log an error", function() { - return this.logger.error.called.should.equal(false); - }); - }); - - describe("when the subscribe failed", function() { - beforeEach(function() { - this.client.id = "mock-client-id"; - this.project = { - name: "Test Project", - owner: { - _id: (this.owner_id = "mock-owner-id-123") - } - }; - this.privilegeLevel = "owner"; - this.ConnectedUsersManager.updateUserPosition = sinon.stub().callsArg(4); - this.isRestrictedUser = true; - this.WebApiManager.joinProject = sinon.stub().callsArgWith(2, null, this.project, this.privilegeLevel, this.isRestrictedUser); - this.RoomManager.joinProject = sinon.stub().callsArgWith(2, new Error("subscribe failed")); - return this.WebsocketController.joinProject(this.client, this.user, this.project_id, this.callback); - }); - - return it("should return an error", function() { - this.callback - .calledWith(sinon.match({message: "subscribe failed"})) - .should.equal(true); - return this.callback.args[0][0].message.should.equal("subscribe failed"); - }); - }); - - describe("when the client has disconnected", function() { - beforeEach(function() { - this.client.disconnected = true; - this.WebApiManager.joinProject = sinon.stub().callsArg(2); - return this.WebsocketController.joinProject(this.client, this.user, this.project_id, this.callback); - }); - - it("should not call WebApiManager.joinProject", function() { - return expect(this.WebApiManager.joinProject.called).to.equal(false); - }); - - it("should call the callback with no details", function() { - return expect(this.callback.args[0]).to.deep.equal([]); - }); - - return it("should increment the editor.join-project.disconnected metric with a status", function() { - return expect(this.metrics.inc.calledWith('editor.join-project.disconnected', 1, {status: 'immediately'})).to.equal(true); - }); - }); - - return describe("when the client disconnects while WebApiManager.joinProject is running", function() { - beforeEach(function() { - this.WebApiManager.joinProject = (project, user, cb) => { - this.client.disconnected = true; - return cb(null, this.project, this.privilegeLevel, this.isRestrictedUser); - }; - - return this.WebsocketController.joinProject(this.client, this.user, this.project_id, this.callback); - }); - - it("should call the callback with no details", function() { - return expect(this.callback.args[0]).to.deep.equal([]); - }); - - return it("should increment the editor.join-project.disconnected metric with a status", function() { - return expect(this.metrics.inc.calledWith('editor.join-project.disconnected', 1, {status: 'after-web-api-call'})).to.equal(true); - }); - }); - }); - - describe("leaveProject", function() { - beforeEach(function() { - this.DocumentUpdaterManager.flushProjectToMongoAndDelete = sinon.stub().callsArg(1); - this.ConnectedUsersManager.markUserAsDisconnected = sinon.stub().callsArg(2); - this.WebsocketLoadBalancer.emitToRoom = sinon.stub(); - this.RoomManager.leaveProjectAndDocs = sinon.stub(); - this.clientsInRoom = []; - this.io = { - sockets: { - clients: room_id => { - if (room_id !== this.project_id) { - throw "expected room_id to be project_id"; - } - return this.clientsInRoom; - } - } - }; - this.client.ol_context.project_id = this.project_id; - this.client.ol_context.user_id = this.user_id; - this.WebsocketController.FLUSH_IF_EMPTY_DELAY = 0; - return tk.reset(); - }); // Allow setTimeout to work. - - describe("when the client did not joined a project yet", function() { - beforeEach(function(done) { - this.client.ol_context = {}; - return this.WebsocketController.leaveProject(this.io, this.client, done); - }); - - it("should bail out when calling leaveProject", function() { - this.WebsocketLoadBalancer.emitToRoom.called.should.equal(false); - this.RoomManager.leaveProjectAndDocs.called.should.equal(false); - return this.ConnectedUsersManager.markUserAsDisconnected.called.should.equal(false); - }); - - return it("should not inc any metric", function() { - return this.metrics.inc.called.should.equal(false); - }); - }); - - describe("when the project is empty", function() { - beforeEach(function(done) { - this.clientsInRoom = []; - return this.WebsocketController.leaveProject(this.io, this.client, done); - }); - - it("should end clientTracking.clientDisconnected to the project room", function() { - return this.WebsocketLoadBalancer.emitToRoom - .calledWith(this.project_id, "clientTracking.clientDisconnected", this.client.publicId) - .should.equal(true); - }); - - it("should mark the user as disconnected", function() { - return this.ConnectedUsersManager.markUserAsDisconnected - .calledWith(this.project_id, this.client.publicId) - .should.equal(true); - }); - - it("should flush the project in the document updater", function() { - return this.DocumentUpdaterManager.flushProjectToMongoAndDelete - .calledWith(this.project_id) - .should.equal(true); - }); - - it("should increment the leave-project metric", function() { - return this.metrics.inc.calledWith("editor.leave-project").should.equal(true); - }); - - return it("should track the disconnection in RoomManager", function() { - return this.RoomManager.leaveProjectAndDocs - .calledWith(this.client) - .should.equal(true); - }); - }); - - describe("when the project is not empty", function() { - beforeEach(function() { - this.clientsInRoom = ["mock-remaining-client"]; - return this.WebsocketController.leaveProject(this.io, this.client); - }); - - return it("should not flush the project in the document updater", function() { - return this.DocumentUpdaterManager.flushProjectToMongoAndDelete - .called.should.equal(false); - }); - }); - - describe("when client has not authenticated", function() { - beforeEach(function(done) { - this.client.ol_context.user_id = null; - this.client.ol_context.project_id = null; - return this.WebsocketController.leaveProject(this.io, this.client, done); - }); - - it("should not end clientTracking.clientDisconnected to the project room", function() { - return this.WebsocketLoadBalancer.emitToRoom - .calledWith(this.project_id, "clientTracking.clientDisconnected", this.client.publicId) - .should.equal(false); - }); - - it("should not mark the user as disconnected", function() { - return this.ConnectedUsersManager.markUserAsDisconnected - .calledWith(this.project_id, this.client.publicId) - .should.equal(false); - }); - - it("should not flush the project in the document updater", function() { - return this.DocumentUpdaterManager.flushProjectToMongoAndDelete - .calledWith(this.project_id) - .should.equal(false); - }); - - return it("should not increment the leave-project metric", function() { - return this.metrics.inc.calledWith("editor.leave-project").should.equal(false); - }); - }); - - return describe("when client has not joined a project", function() { - beforeEach(function(done) { - this.client.ol_context.user_id = this.user_id; - this.client.ol_context.project_id = null; - return this.WebsocketController.leaveProject(this.io, this.client, done); - }); - - it("should not end clientTracking.clientDisconnected to the project room", function() { - return this.WebsocketLoadBalancer.emitToRoom - .calledWith(this.project_id, "clientTracking.clientDisconnected", this.client.publicId) - .should.equal(false); - }); - - it("should not mark the user as disconnected", function() { - return this.ConnectedUsersManager.markUserAsDisconnected - .calledWith(this.project_id, this.client.publicId) - .should.equal(false); - }); - - it("should not flush the project in the document updater", function() { - return this.DocumentUpdaterManager.flushProjectToMongoAndDelete - .calledWith(this.project_id) - .should.equal(false); - }); - - return it("should not increment the leave-project metric", function() { - return this.metrics.inc.calledWith("editor.leave-project").should.equal(false); - }); - }); - }); - - describe("joinDoc", function() { - beforeEach(function() { - this.doc_id = "doc-id-123"; - this.doc_lines = ["doc", "lines"]; - this.version = 42; - this.ops = ["mock", "ops"]; - this.ranges = { "mock": "ranges" }; - this.options = {}; - - this.client.ol_context.project_id = this.project_id; - this.client.ol_context.is_restricted_user = false; - this.AuthorizationManager.addAccessToDoc = sinon.stub(); - this.AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, null); - this.DocumentUpdaterManager.getDocument = sinon.stub().callsArgWith(3, null, this.doc_lines, this.version, this.ranges, this.ops); - return this.RoomManager.joinDoc = sinon.stub().callsArg(2); - }); - - describe("works", function() { - beforeEach(function() { - return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, this.options, this.callback); - }); - - it("should check that the client is authorized to view the project", function() { - return this.AuthorizationManager.assertClientCanViewProject - .calledWith(this.client) - .should.equal(true); - }); - - it("should get the document from the DocumentUpdaterManager with fromVersion", function() { - return this.DocumentUpdaterManager.getDocument - .calledWith(this.project_id, this.doc_id, -1) - .should.equal(true); - }); - - it("should add permissions for the client to access the doc", function() { - return this.AuthorizationManager.addAccessToDoc - .calledWith(this.client, this.doc_id) - .should.equal(true); - }); - - it("should join the client to room for the doc_id", function() { - return this.RoomManager.joinDoc - .calledWith(this.client, this.doc_id) - .should.equal(true); - }); - - it("should call the callback with the lines, version, ranges and ops", function() { - return this.callback - .calledWith(null, this.doc_lines, this.version, this.ops, this.ranges) - .should.equal(true); - }); - - return it("should increment the join-doc metric", function() { - return this.metrics.inc.calledWith("editor.join-doc").should.equal(true); - }); - }); - - describe("with a fromVersion", function() { - beforeEach(function() { - this.fromVersion = 40; - return this.WebsocketController.joinDoc(this.client, this.doc_id, this.fromVersion, this.options, this.callback); - }); - - return it("should get the document from the DocumentUpdaterManager with fromVersion", function() { - return this.DocumentUpdaterManager.getDocument - .calledWith(this.project_id, this.doc_id, this.fromVersion) - .should.equal(true); - }); - }); - - describe("with doclines that need escaping", function() { - beforeEach(function() { - this.doc_lines.push(["räksmörgås"]); - return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, this.options, this.callback); - }); - - return it("should call the callback with the escaped lines", function() { - const escaped_lines = this.callback.args[0][1]; - const escaped_word = escaped_lines.pop(); - escaped_word.should.equal('räksmörgÃ¥s'); - // Check that unescaping works - return decodeURIComponent(escape(escaped_word)).should.equal("räksmörgås"); - }); - }); - - describe("with comments that need encoding", function() { - beforeEach(function() { - this.ranges.comments = [{ op: { c: "räksmörgås" } }]; - return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, { encodeRanges: true }, this.callback); - }); - - return it("should call the callback with the encoded comment", function() { - const encoded_comments = this.callback.args[0][4]; - const encoded_comment = encoded_comments.comments.pop(); - const encoded_comment_text = encoded_comment.op.c; - return encoded_comment_text.should.equal('räksmörgÃ¥s'); - }); - }); - - describe("with changes that need encoding", function() { - it("should call the callback with the encoded insert change", function() { - this.ranges.changes = [{ op: { i: "räksmörgås" } }]; - this.WebsocketController.joinDoc(this.client, this.doc_id, -1, { encodeRanges: true }, this.callback); - - const encoded_changes = this.callback.args[0][4]; - const encoded_change = encoded_changes.changes.pop(); - const encoded_change_text = encoded_change.op.i; - return encoded_change_text.should.equal('räksmörgÃ¥s'); - }); - - return it("should call the callback with the encoded delete change", function() { - this.ranges.changes = [{ op: { d: "räksmörgås" } }]; - this.WebsocketController.joinDoc(this.client, this.doc_id, -1, { encodeRanges: true }, this.callback); - - const encoded_changes = this.callback.args[0][4]; - const encoded_change = encoded_changes.changes.pop(); - const encoded_change_text = encoded_change.op.d; - return encoded_change_text.should.equal('räksmörgÃ¥s'); - }); - }); - - describe("when not authorized", function() { - beforeEach(function() { - this.AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, (this.err = new Error("not authorized"))); - return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, this.options, this.callback); - }); - - it("should call the callback with an error", function() { - return this.callback.calledWith(sinon.match({message: "not authorized"})).should.equal(true); - }); - - return it("should not call the DocumentUpdaterManager", function() { - return this.DocumentUpdaterManager.getDocument.called.should.equal(false); - }); - }); - - describe("with a restricted client", function() { - beforeEach(function() { - this.ranges.comments = [{op: {a: 1}}, {op: {a: 2}}]; - this.client.ol_context.is_restricted_user = true; - return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, this.options, this.callback); - }); - - return it("should overwrite ranges.comments with an empty list", function() { - const ranges = this.callback.args[0][4]; - return expect(ranges.comments).to.deep.equal([]); - }); - }); - - describe("when the client has disconnected", function() { - beforeEach(function() { - this.client.disconnected = true; - return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, this.options, this.callback); - }); - - it("should call the callback with no details", function() { - return expect(this.callback.args[0]).to.deep.equal([]); - }); - - it("should increment the editor.join-doc.disconnected metric with a status", function() { - return expect(this.metrics.inc.calledWith('editor.join-doc.disconnected', 1, {status: 'immediately'})).to.equal(true); - }); - - return it("should not get the document", function() { - return expect(this.DocumentUpdaterManager.getDocument.called).to.equal(false); - }); - }); - - describe("when the client disconnects while RoomManager.joinDoc is running", function() { - beforeEach(function() { - this.RoomManager.joinDoc = (client, doc_id, cb) => { - this.client.disconnected = true; - return cb(); - }; - - return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, this.options, this.callback); - }); - - it("should call the callback with no details", function() { - return expect(this.callback.args[0]).to.deep.equal([]); - }); - - it("should increment the editor.join-doc.disconnected metric with a status", function() { - return expect(this.metrics.inc.calledWith('editor.join-doc.disconnected', 1, {status: 'after-joining-room'})).to.equal(true); - }); - - return it("should not get the document", function() { - return expect(this.DocumentUpdaterManager.getDocument.called).to.equal(false); - }); - }); - - return describe("when the client disconnects while DocumentUpdaterManager.getDocument is running", function() { - beforeEach(function() { - this.DocumentUpdaterManager.getDocument = (project_id, doc_id, fromVersion, callback) => { - this.client.disconnected = true; - return callback(null, this.doc_lines, this.version, this.ranges, this.ops); - }; - - return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, this.options, this.callback); - }); - - it("should call the callback with no details", function() { - return expect(this.callback.args[0]).to.deep.equal([]); - }); - - return it("should increment the editor.join-doc.disconnected metric with a status", function() { - return expect(this.metrics.inc.calledWith('editor.join-doc.disconnected', 1, {status: 'after-doc-updater-call'})).to.equal(true); - }); - }); - }); - - describe("leaveDoc", function() { - beforeEach(function() { - this.doc_id = "doc-id-123"; - this.client.ol_context.project_id = this.project_id; - this.RoomManager.leaveDoc = sinon.stub(); - return this.WebsocketController.leaveDoc(this.client, this.doc_id, this.callback); - }); - - it("should remove the client from the doc_id room", function() { - return this.RoomManager.leaveDoc - .calledWith(this.client, this.doc_id).should.equal(true); - }); - - it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - - return it("should increment the leave-doc metric", function() { - return this.metrics.inc.calledWith("editor.leave-doc").should.equal(true); - }); - }); - - describe("getConnectedUsers", function() { - beforeEach(function() { - this.client.ol_context.project_id = this.project_id; - this.users = ["mock", "users"]; - this.WebsocketLoadBalancer.emitToRoom = sinon.stub(); - return this.ConnectedUsersManager.getConnectedUsers = sinon.stub().callsArgWith(1, null, this.users); - }); - - describe("when authorized", function() { - beforeEach(function(done) { - this.AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, null); - return this.WebsocketController.getConnectedUsers(this.client, (...args) => { - this.callback(...Array.from(args || [])); - return done(); - }); - }); - - it("should check that the client is authorized to view the project", function() { - return this.AuthorizationManager.assertClientCanViewProject - .calledWith(this.client) - .should.equal(true); - }); - - it("should broadcast a request to update the client list", function() { - return this.WebsocketLoadBalancer.emitToRoom - .calledWith(this.project_id, "clientTracking.refresh") - .should.equal(true); - }); - - it("should get the connected users for the project", function() { - return this.ConnectedUsersManager.getConnectedUsers - .calledWith(this.project_id) - .should.equal(true); - }); - - it("should return the users", function() { - return this.callback.calledWith(null, this.users).should.equal(true); - }); - - return it("should increment the get-connected-users metric", function() { - return this.metrics.inc.calledWith("editor.get-connected-users").should.equal(true); - }); - }); - - describe("when not authorized", function() { - beforeEach(function() { - this.AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, (this.err = new Error("not authorized"))); - return this.WebsocketController.getConnectedUsers(this.client, this.callback); - }); - - it("should not get the connected users for the project", function() { - return this.ConnectedUsersManager.getConnectedUsers - .called - .should.equal(false); - }); - - return it("should return an error", function() { - return this.callback.calledWith(this.err).should.equal(true); - }); - }); - - describe("when restricted user", function() { - beforeEach(function() { - this.client.ol_context.is_restricted_user = true; - this.AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, null); - return this.WebsocketController.getConnectedUsers(this.client, this.callback); - }); - - it("should return an empty array of users", function() { - return this.callback.calledWith(null, []).should.equal(true); - }); - - return it("should not get the connected users for the project", function() { - return this.ConnectedUsersManager.getConnectedUsers - .called - .should.equal(false); - }); - }); - - return describe("when the client has disconnected", function() { - beforeEach(function() { - this.client.disconnected = true; - this.AuthorizationManager.assertClientCanViewProject = sinon.stub(); - return this.WebsocketController.getConnectedUsers(this.client, this.callback); - }); - - it("should call the callback with no details", function() { - return expect(this.callback.args[0]).to.deep.equal([]); - }); - - return it("should not check permissions", function() { - return expect(this.AuthorizationManager.assertClientCanViewProject.called).to.equal(false); - }); - }); - }); - - describe("updateClientPosition", function() { - beforeEach(function() { - this.WebsocketLoadBalancer.emitToRoom = sinon.stub(); - this.ConnectedUsersManager.updateUserPosition = sinon.stub().callsArgWith(4); - this.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon.stub().callsArgWith(2, null); - return this.update = { - doc_id: (this.doc_id = "doc-id-123"), - row: (this.row = 42), - column: (this.column = 37) - };}); - - describe("with a logged in user", function() { - beforeEach(function() { - this.client.ol_context = { - project_id: this.project_id, - first_name: (this.first_name = "Douglas"), - last_name: (this.last_name = "Adams"), - email: (this.email = "joe@example.com"), - user_id: (this.user_id = "user-id-123") - }; - this.WebsocketController.updateClientPosition(this.client, this.update); - - return this.populatedCursorData = { - doc_id: this.doc_id, - id: this.client.publicId, - name: `${this.first_name} ${this.last_name}`, - row: this.row, - column: this.column, - email: this.email, - user_id: this.user_id - }; - }); - - it("should send the update to the project room with the user's name", function() { - return this.WebsocketLoadBalancer.emitToRoom.calledWith(this.project_id, "clientTracking.clientUpdated", this.populatedCursorData).should.equal(true); - }); - - it("should send the cursor data to the connected user manager", function(done){ - this.ConnectedUsersManager.updateUserPosition.calledWith(this.project_id, this.client.publicId, { - _id: this.user_id, - email: this.email, - first_name: this.first_name, - last_name: this.last_name - }, { - row: this.row, - column: this.column, - doc_id: this.doc_id - }).should.equal(true); - return done(); - }); - - return it("should increment the update-client-position metric at 0.1 frequency", function() { - return this.metrics.inc.calledWith("editor.update-client-position", 0.1).should.equal(true); - }); - }); - - describe("with a logged in user who has no last_name set", function() { - beforeEach(function() { - this.client.ol_context = { - project_id: this.project_id, - first_name: (this.first_name = "Douglas"), - last_name: undefined, - email: (this.email = "joe@example.com"), - user_id: (this.user_id = "user-id-123") - }; - this.WebsocketController.updateClientPosition(this.client, this.update); - - return this.populatedCursorData = { - doc_id: this.doc_id, - id: this.client.publicId, - name: `${this.first_name}`, - row: this.row, - column: this.column, - email: this.email, - user_id: this.user_id - }; - }); - - it("should send the update to the project room with the user's name", function() { - return this.WebsocketLoadBalancer.emitToRoom.calledWith(this.project_id, "clientTracking.clientUpdated", this.populatedCursorData).should.equal(true); - }); - - it("should send the cursor data to the connected user manager", function(done){ - this.ConnectedUsersManager.updateUserPosition.calledWith(this.project_id, this.client.publicId, { - _id: this.user_id, - email: this.email, - first_name: this.first_name, - last_name: undefined - }, { - row: this.row, - column: this.column, - doc_id: this.doc_id - }).should.equal(true); - return done(); - }); - - return it("should increment the update-client-position metric at 0.1 frequency", function() { - return this.metrics.inc.calledWith("editor.update-client-position", 0.1).should.equal(true); - }); - }); - - describe("with a logged in user who has no first_name set", function() { - beforeEach(function() { - this.client.ol_context = { - project_id: this.project_id, - first_name: undefined, - last_name: (this.last_name = "Adams"), - email: (this.email = "joe@example.com"), - user_id: (this.user_id = "user-id-123") - }; - this.WebsocketController.updateClientPosition(this.client, this.update); - - return this.populatedCursorData = { - doc_id: this.doc_id, - id: this.client.publicId, - name: `${this.last_name}`, - row: this.row, - column: this.column, - email: this.email, - user_id: this.user_id - }; - }); - - it("should send the update to the project room with the user's name", function() { - return this.WebsocketLoadBalancer.emitToRoom.calledWith(this.project_id, "clientTracking.clientUpdated", this.populatedCursorData).should.equal(true); - }); - - it("should send the cursor data to the connected user manager", function(done){ - this.ConnectedUsersManager.updateUserPosition.calledWith(this.project_id, this.client.publicId, { - _id: this.user_id, - email: this.email, - first_name: undefined, - last_name: this.last_name - }, { - row: this.row, - column: this.column, - doc_id: this.doc_id - }).should.equal(true); - return done(); - }); - - return it("should increment the update-client-position metric at 0.1 frequency", function() { - return this.metrics.inc.calledWith("editor.update-client-position", 0.1).should.equal(true); - }); - }); - describe("with a logged in user who has no names set", function() { - beforeEach(function() { - this.client.ol_context = { - project_id: this.project_id, - first_name: undefined, - last_name: undefined, - email: (this.email = "joe@example.com"), - user_id: (this.user_id = "user-id-123") - }; - return this.WebsocketController.updateClientPosition(this.client, this.update); - }); - - return it("should send the update to the project name with no name", function() { - return this.WebsocketLoadBalancer.emitToRoom - .calledWith(this.project_id, "clientTracking.clientUpdated", { - doc_id: this.doc_id, - id: this.client.publicId, - user_id: this.user_id, - name: "", - row: this.row, - column: this.column, - email: this.email - }) - .should.equal(true); - }); - }); - - - describe("with an anonymous user", function() { - beforeEach(function() { - this.client.ol_context = { - project_id: this.project_id - }; - return this.WebsocketController.updateClientPosition(this.client, this.update); - }); - - it("should send the update to the project room with no name", function() { - return this.WebsocketLoadBalancer.emitToRoom - .calledWith(this.project_id, "clientTracking.clientUpdated", { - doc_id: this.doc_id, - id: this.client.publicId, - name: "", - row: this.row, - column: this.column - }) - .should.equal(true); - }); - - return it("should not send cursor data to the connected user manager", function(done){ - this.ConnectedUsersManager.updateUserPosition.called.should.equal(false); - return done(); - }); - }); - - return describe("when the client has disconnected", function() { - beforeEach(function() { - this.client.disconnected = true; - this.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon.stub(); - return this.WebsocketController.updateClientPosition(this.client, this.update, this.callback); - }); - - it("should call the callback with no details", function() { - return expect(this.callback.args[0]).to.deep.equal([]); - }); - - return it("should not check permissions", function() { - return expect(this.AuthorizationManager.assertClientCanViewProjectAndDoc.called).to.equal(false); - }); - }); - }); - - describe("applyOtUpdate", function() { - beforeEach(function() { - this.update = {op: {p: 12, t: "foo"}}; - this.client.ol_context.user_id = this.user_id; - this.client.ol_context.project_id = this.project_id; - this.WebsocketController._assertClientCanApplyUpdate = sinon.stub().yields(); - return this.DocumentUpdaterManager.queueChange = sinon.stub().callsArg(3); - }); - - describe("succesfully", function() { - beforeEach(function() { - return this.WebsocketController.applyOtUpdate(this.client, this.doc_id, this.update, this.callback); - }); - - it("should set the source of the update to the client id", function() { - return this.update.meta.source.should.equal(this.client.publicId); - }); - - it("should set the user_id of the update to the user id", function() { - return this.update.meta.user_id.should.equal(this.user_id); - }); - - it("should queue the update", function() { - return this.DocumentUpdaterManager.queueChange - .calledWith(this.project_id, this.doc_id, this.update) - .should.equal(true); - }); - - it("should call the callback", function() { - return this.callback.called.should.equal(true); - }); - - return it("should increment the doc updates", function() { - return this.metrics.inc.calledWith("editor.doc-update").should.equal(true); - }); - }); - - describe("unsuccessfully", function() { - beforeEach(function() { - this.client.disconnect = sinon.stub(); - this.DocumentUpdaterManager.queueChange = sinon.stub().callsArgWith(3, (this.error = new Error("Something went wrong"))); - return this.WebsocketController.applyOtUpdate(this.client, this.doc_id, this.update, this.callback); - }); - - it("should disconnect the client", function() { - return this.client.disconnect.called.should.equal(true); - }); - - it("should log an error", function() { - return this.logger.error.called.should.equal(true); - }); - - return it("should call the callback with the error", function() { - return this.callback.calledWith(this.error).should.equal(true); - }); - }); - - describe("when not authorized", function() { - beforeEach(function() { - this.client.disconnect = sinon.stub(); - this.WebsocketController._assertClientCanApplyUpdate = sinon.stub().yields(this.error = new Error("not authorized")); - return this.WebsocketController.applyOtUpdate(this.client, this.doc_id, this.update, this.callback); - }); - - // This happens in a setTimeout to allow the client a chance to receive the error first. - // I'm not sure how to unit test, but it is acceptance tested. - // it "should disconnect the client", -> - // @client.disconnect.called.should.equal true - - it("should log a warning", function() { - return this.logger.warn.called.should.equal(true); - }); - - return it("should call the callback with the error", function() { - return this.callback.calledWith(this.error).should.equal(true); - }); - }); - - return describe("update_too_large", function() { - beforeEach(function(done) { - this.client.disconnect = sinon.stub(); - this.client.emit = sinon.stub(); - this.client.ol_context.user_id = this.user_id; - this.client.ol_context.project_id = this.project_id; - const error = new Error("update is too large"); - error.updateSize = 7372835; - this.DocumentUpdaterManager.queueChange = sinon.stub().callsArgWith(3, error); - this.WebsocketController.applyOtUpdate(this.client, this.doc_id, this.update, this.callback); - return setTimeout(() => done() - , 1); - }); - - it("should call the callback with no error", function() { - this.callback.called.should.equal(true); - return this.callback.args[0].should.deep.equal([]); - }); - - it("should log a warning with the size and context", function() { - this.logger.warn.called.should.equal(true); - return this.logger.warn.args[0].should.deep.equal([{ - user_id: this.user_id, project_id: this.project_id, doc_id: this.doc_id, updateSize: 7372835 - }, 'update is too large']); - }); - - describe("after 100ms", function() { - beforeEach(function(done) { return setTimeout(done, 100); }); - - it("should send an otUpdateError the client", function() { - return this.client.emit.calledWith('otUpdateError').should.equal(true); - }); - - return it("should disconnect the client", function() { - return this.client.disconnect.called.should.equal(true); - }); - }); - - return describe("when the client disconnects during the next 100ms", function() { - beforeEach(function(done) { - this.client.disconnected = true; - return setTimeout(done, 100); - }); - - it("should not send an otUpdateError the client", function() { - return this.client.emit.calledWith('otUpdateError').should.equal(false); - }); - - it("should not disconnect the client", function() { - return this.client.disconnect.called.should.equal(false); - }); - - return it("should increment the editor.doc-update.disconnected metric with a status", function() { - return expect(this.metrics.inc.calledWith('editor.doc-update.disconnected', 1, {status:'at-otUpdateError'})).to.equal(true); - }); - }); - }); - }); - - return describe("_assertClientCanApplyUpdate", function() { - beforeEach(function() { - this.edit_update = { op: [{i: "foo", p: 42}, {c: "bar", p: 132}] }; // comments may still be in an edit op - this.comment_update = { op: [{c: "bar", p: 132}] }; - this.AuthorizationManager.assertClientCanEditProjectAndDoc = sinon.stub(); - return this.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon.stub(); - }); - - describe("with a read-write client", function() { return it("should return successfully", function(done) { - this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(null); - return this.WebsocketController._assertClientCanApplyUpdate(this.client, this.doc_id, this.edit_update, (error) => { - expect(error).to.be.null; - return done(); - }); - }); }); - - describe("with a read-only client and an edit op", function() { return it("should return an error", function(done) { - this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(new Error("not authorized")); - this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(null); - return this.WebsocketController._assertClientCanApplyUpdate(this.client, this.doc_id, this.edit_update, (error) => { - expect(error.message).to.equal("not authorized"); - return done(); - }); - }); }); - - describe("with a read-only client and a comment op", function() { return it("should return successfully", function(done) { - this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(new Error("not authorized")); - this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(null); - return this.WebsocketController._assertClientCanApplyUpdate(this.client, this.doc_id, this.comment_update, (error) => { - expect(error).to.be.null; - return done(); - }); - }); }); - - return describe("with a totally unauthorized client", function() { return it("should return an error", function(done) { - this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(new Error("not authorized")); - this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(new Error("not authorized")); - return this.WebsocketController._assertClientCanApplyUpdate(this.client, this.doc_id, this.comment_update, (error) => { - expect(error.message).to.equal("not authorized"); - return done(); - }); - }); }); - }); -}); +const chai = require('chai') +const should = chai.should() +const sinon = require('sinon') +const { expect } = chai +const modulePath = '../../../app/js/WebsocketController.js' +const SandboxedModule = require('sandboxed-module') +const tk = require('timekeeper') + +describe('WebsocketController', function () { + beforeEach(function () { + tk.freeze(new Date()) + this.project_id = 'project-id-123' + this.user = { + _id: (this.user_id = 'user-id-123'), + first_name: 'James', + last_name: 'Allen', + email: 'james@example.com', + signUpDate: new Date('2014-01-01'), + loginCount: 42 + } + this.callback = sinon.stub() + this.client = { + disconnected: false, + id: (this.client_id = 'mock-client-id-123'), + publicId: `other-id-${Math.random()}`, + ol_context: {}, + join: sinon.stub(), + leave: sinon.stub() + } + return (this.WebsocketController = SandboxedModule.require(modulePath, { + requires: { + './WebApiManager': (this.WebApiManager = {}), + './AuthorizationManager': (this.AuthorizationManager = {}), + './DocumentUpdaterManager': (this.DocumentUpdaterManager = {}), + './ConnectedUsersManager': (this.ConnectedUsersManager = {}), + './WebsocketLoadBalancer': (this.WebsocketLoadBalancer = {}), + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub(), + warn: sinon.stub() + }), + 'metrics-sharelatex': (this.metrics = { + inc: sinon.stub(), + set: sinon.stub() + }), + './RoomManager': (this.RoomManager = {}) + } + })) + }) + + afterEach(function () { + return tk.reset() + }) + + describe('joinProject', function () { + describe('when authorised', function () { + beforeEach(function () { + this.client.id = 'mock-client-id' + this.project = { + name: 'Test Project', + owner: { + _id: (this.owner_id = 'mock-owner-id-123') + } + } + this.privilegeLevel = 'owner' + this.ConnectedUsersManager.updateUserPosition = sinon.stub().callsArg(4) + this.isRestrictedUser = true + this.WebApiManager.joinProject = sinon + .stub() + .callsArgWith( + 2, + null, + this.project, + this.privilegeLevel, + this.isRestrictedUser + ) + this.RoomManager.joinProject = sinon.stub().callsArg(2) + return this.WebsocketController.joinProject( + this.client, + this.user, + this.project_id, + this.callback + ) + }) + + it('should load the project from web', function () { + return this.WebApiManager.joinProject + .calledWith(this.project_id, this.user) + .should.equal(true) + }) + + it('should join the project room', function () { + return this.RoomManager.joinProject + .calledWith(this.client, this.project_id) + .should.equal(true) + }) + + it('should set the privilege level on the client', function () { + return this.client.ol_context.privilege_level.should.equal( + this.privilegeLevel + ) + }) + it("should set the user's id on the client", function () { + return this.client.ol_context.user_id.should.equal(this.user._id) + }) + it("should set the user's email on the client", function () { + return this.client.ol_context.email.should.equal(this.user.email) + }) + it("should set the user's first_name on the client", function () { + return this.client.ol_context.first_name.should.equal( + this.user.first_name + ) + }) + it("should set the user's last_name on the client", function () { + return this.client.ol_context.last_name.should.equal( + this.user.last_name + ) + }) + it("should set the user's sign up date on the client", function () { + return this.client.ol_context.signup_date.should.equal( + this.user.signUpDate + ) + }) + it("should set the user's login_count on the client", function () { + return this.client.ol_context.login_count.should.equal( + this.user.loginCount + ) + }) + it('should set the connected time on the client', function () { + return this.client.ol_context.connected_time.should.equal(new Date()) + }) + it('should set the project_id on the client', function () { + return this.client.ol_context.project_id.should.equal(this.project_id) + }) + it('should set the project owner id on the client', function () { + return this.client.ol_context.owner_id.should.equal(this.owner_id) + }) + it('should set the is_restricted_user flag on the client', function () { + return this.client.ol_context.is_restricted_user.should.equal( + this.isRestrictedUser + ) + }) + it('should call the callback with the project, privilegeLevel and protocolVersion', function () { + return this.callback + .calledWith( + null, + this.project, + this.privilegeLevel, + this.WebsocketController.PROTOCOL_VERSION + ) + .should.equal(true) + }) + + it('should mark the user as connected in ConnectedUsersManager', function () { + return this.ConnectedUsersManager.updateUserPosition + .calledWith(this.project_id, this.client.publicId, this.user, null) + .should.equal(true) + }) + + return it('should increment the join-project metric', function () { + return this.metrics.inc + .calledWith('editor.join-project') + .should.equal(true) + }) + }) + + describe('when not authorized', function () { + beforeEach(function () { + this.WebApiManager.joinProject = sinon + .stub() + .callsArgWith(2, null, null, null) + return this.WebsocketController.joinProject( + this.client, + this.user, + this.project_id, + this.callback + ) + }) + + it('should return an error', function () { + return this.callback + .calledWith(sinon.match({ message: 'not authorized' })) + .should.equal(true) + }) + + return it('should not log an error', function () { + return this.logger.error.called.should.equal(false) + }) + }) + + describe('when the subscribe failed', function () { + beforeEach(function () { + this.client.id = 'mock-client-id' + this.project = { + name: 'Test Project', + owner: { + _id: (this.owner_id = 'mock-owner-id-123') + } + } + this.privilegeLevel = 'owner' + this.ConnectedUsersManager.updateUserPosition = sinon.stub().callsArg(4) + this.isRestrictedUser = true + this.WebApiManager.joinProject = sinon + .stub() + .callsArgWith( + 2, + null, + this.project, + this.privilegeLevel, + this.isRestrictedUser + ) + this.RoomManager.joinProject = sinon + .stub() + .callsArgWith(2, new Error('subscribe failed')) + return this.WebsocketController.joinProject( + this.client, + this.user, + this.project_id, + this.callback + ) + }) + + return it('should return an error', function () { + this.callback + .calledWith(sinon.match({ message: 'subscribe failed' })) + .should.equal(true) + return this.callback.args[0][0].message.should.equal('subscribe failed') + }) + }) + + describe('when the client has disconnected', function () { + beforeEach(function () { + this.client.disconnected = true + this.WebApiManager.joinProject = sinon.stub().callsArg(2) + return this.WebsocketController.joinProject( + this.client, + this.user, + this.project_id, + this.callback + ) + }) + + it('should not call WebApiManager.joinProject', function () { + return expect(this.WebApiManager.joinProject.called).to.equal(false) + }) + + it('should call the callback with no details', function () { + return expect(this.callback.args[0]).to.deep.equal([]) + }) + + return it('should increment the editor.join-project.disconnected metric with a status', function () { + return expect( + this.metrics.inc.calledWith('editor.join-project.disconnected', 1, { + status: 'immediately' + }) + ).to.equal(true) + }) + }) + + return describe('when the client disconnects while WebApiManager.joinProject is running', function () { + beforeEach(function () { + this.WebApiManager.joinProject = (project, user, cb) => { + this.client.disconnected = true + return cb( + null, + this.project, + this.privilegeLevel, + this.isRestrictedUser + ) + } + + return this.WebsocketController.joinProject( + this.client, + this.user, + this.project_id, + this.callback + ) + }) + + it('should call the callback with no details', function () { + return expect(this.callback.args[0]).to.deep.equal([]) + }) + + return it('should increment the editor.join-project.disconnected metric with a status', function () { + return expect( + this.metrics.inc.calledWith('editor.join-project.disconnected', 1, { + status: 'after-web-api-call' + }) + ).to.equal(true) + }) + }) + }) + + describe('leaveProject', function () { + beforeEach(function () { + this.DocumentUpdaterManager.flushProjectToMongoAndDelete = sinon + .stub() + .callsArg(1) + this.ConnectedUsersManager.markUserAsDisconnected = sinon + .stub() + .callsArg(2) + this.WebsocketLoadBalancer.emitToRoom = sinon.stub() + this.RoomManager.leaveProjectAndDocs = sinon.stub() + this.clientsInRoom = [] + this.io = { + sockets: { + clients: (room_id) => { + if (room_id !== this.project_id) { + throw 'expected room_id to be project_id' + } + return this.clientsInRoom + } + } + } + this.client.ol_context.project_id = this.project_id + this.client.ol_context.user_id = this.user_id + this.WebsocketController.FLUSH_IF_EMPTY_DELAY = 0 + return tk.reset() + }) // Allow setTimeout to work. + + describe('when the client did not joined a project yet', function () { + beforeEach(function (done) { + this.client.ol_context = {} + return this.WebsocketController.leaveProject(this.io, this.client, done) + }) + + it('should bail out when calling leaveProject', function () { + this.WebsocketLoadBalancer.emitToRoom.called.should.equal(false) + this.RoomManager.leaveProjectAndDocs.called.should.equal(false) + return this.ConnectedUsersManager.markUserAsDisconnected.called.should.equal( + false + ) + }) + + return it('should not inc any metric', function () { + return this.metrics.inc.called.should.equal(false) + }) + }) + + describe('when the project is empty', function () { + beforeEach(function (done) { + this.clientsInRoom = [] + return this.WebsocketController.leaveProject(this.io, this.client, done) + }) + + it('should end clientTracking.clientDisconnected to the project room', function () { + return this.WebsocketLoadBalancer.emitToRoom + .calledWith( + this.project_id, + 'clientTracking.clientDisconnected', + this.client.publicId + ) + .should.equal(true) + }) + + it('should mark the user as disconnected', function () { + return this.ConnectedUsersManager.markUserAsDisconnected + .calledWith(this.project_id, this.client.publicId) + .should.equal(true) + }) + + it('should flush the project in the document updater', function () { + return this.DocumentUpdaterManager.flushProjectToMongoAndDelete + .calledWith(this.project_id) + .should.equal(true) + }) + + it('should increment the leave-project metric', function () { + return this.metrics.inc + .calledWith('editor.leave-project') + .should.equal(true) + }) + + return it('should track the disconnection in RoomManager', function () { + return this.RoomManager.leaveProjectAndDocs + .calledWith(this.client) + .should.equal(true) + }) + }) + + describe('when the project is not empty', function () { + beforeEach(function () { + this.clientsInRoom = ['mock-remaining-client'] + return this.WebsocketController.leaveProject(this.io, this.client) + }) + + return it('should not flush the project in the document updater', function () { + return this.DocumentUpdaterManager.flushProjectToMongoAndDelete.called.should.equal( + false + ) + }) + }) + + describe('when client has not authenticated', function () { + beforeEach(function (done) { + this.client.ol_context.user_id = null + this.client.ol_context.project_id = null + return this.WebsocketController.leaveProject(this.io, this.client, done) + }) + + it('should not end clientTracking.clientDisconnected to the project room', function () { + return this.WebsocketLoadBalancer.emitToRoom + .calledWith( + this.project_id, + 'clientTracking.clientDisconnected', + this.client.publicId + ) + .should.equal(false) + }) + + it('should not mark the user as disconnected', function () { + return this.ConnectedUsersManager.markUserAsDisconnected + .calledWith(this.project_id, this.client.publicId) + .should.equal(false) + }) + + it('should not flush the project in the document updater', function () { + return this.DocumentUpdaterManager.flushProjectToMongoAndDelete + .calledWith(this.project_id) + .should.equal(false) + }) + + return it('should not increment the leave-project metric', function () { + return this.metrics.inc + .calledWith('editor.leave-project') + .should.equal(false) + }) + }) + + return describe('when client has not joined a project', function () { + beforeEach(function (done) { + this.client.ol_context.user_id = this.user_id + this.client.ol_context.project_id = null + return this.WebsocketController.leaveProject(this.io, this.client, done) + }) + + it('should not end clientTracking.clientDisconnected to the project room', function () { + return this.WebsocketLoadBalancer.emitToRoom + .calledWith( + this.project_id, + 'clientTracking.clientDisconnected', + this.client.publicId + ) + .should.equal(false) + }) + + it('should not mark the user as disconnected', function () { + return this.ConnectedUsersManager.markUserAsDisconnected + .calledWith(this.project_id, this.client.publicId) + .should.equal(false) + }) + + it('should not flush the project in the document updater', function () { + return this.DocumentUpdaterManager.flushProjectToMongoAndDelete + .calledWith(this.project_id) + .should.equal(false) + }) + + return it('should not increment the leave-project metric', function () { + return this.metrics.inc + .calledWith('editor.leave-project') + .should.equal(false) + }) + }) + }) + + describe('joinDoc', function () { + beforeEach(function () { + this.doc_id = 'doc-id-123' + this.doc_lines = ['doc', 'lines'] + this.version = 42 + this.ops = ['mock', 'ops'] + this.ranges = { mock: 'ranges' } + this.options = {} + + this.client.ol_context.project_id = this.project_id + this.client.ol_context.is_restricted_user = false + this.AuthorizationManager.addAccessToDoc = sinon.stub() + this.AuthorizationManager.assertClientCanViewProject = sinon + .stub() + .callsArgWith(1, null) + this.DocumentUpdaterManager.getDocument = sinon + .stub() + .callsArgWith( + 3, + null, + this.doc_lines, + this.version, + this.ranges, + this.ops + ) + return (this.RoomManager.joinDoc = sinon.stub().callsArg(2)) + }) + + describe('works', function () { + beforeEach(function () { + return this.WebsocketController.joinDoc( + this.client, + this.doc_id, + -1, + this.options, + this.callback + ) + }) + + it('should check that the client is authorized to view the project', function () { + return this.AuthorizationManager.assertClientCanViewProject + .calledWith(this.client) + .should.equal(true) + }) + + it('should get the document from the DocumentUpdaterManager with fromVersion', function () { + return this.DocumentUpdaterManager.getDocument + .calledWith(this.project_id, this.doc_id, -1) + .should.equal(true) + }) + + it('should add permissions for the client to access the doc', function () { + return this.AuthorizationManager.addAccessToDoc + .calledWith(this.client, this.doc_id) + .should.equal(true) + }) + + it('should join the client to room for the doc_id', function () { + return this.RoomManager.joinDoc + .calledWith(this.client, this.doc_id) + .should.equal(true) + }) + + it('should call the callback with the lines, version, ranges and ops', function () { + return this.callback + .calledWith(null, this.doc_lines, this.version, this.ops, this.ranges) + .should.equal(true) + }) + + return it('should increment the join-doc metric', function () { + return this.metrics.inc.calledWith('editor.join-doc').should.equal(true) + }) + }) + + describe('with a fromVersion', function () { + beforeEach(function () { + this.fromVersion = 40 + return this.WebsocketController.joinDoc( + this.client, + this.doc_id, + this.fromVersion, + this.options, + this.callback + ) + }) + + return it('should get the document from the DocumentUpdaterManager with fromVersion', function () { + return this.DocumentUpdaterManager.getDocument + .calledWith(this.project_id, this.doc_id, this.fromVersion) + .should.equal(true) + }) + }) + + describe('with doclines that need escaping', function () { + beforeEach(function () { + this.doc_lines.push(['räksmörgås']) + return this.WebsocketController.joinDoc( + this.client, + this.doc_id, + -1, + this.options, + this.callback + ) + }) + + return it('should call the callback with the escaped lines', function () { + const escaped_lines = this.callback.args[0][1] + const escaped_word = escaped_lines.pop() + escaped_word.should.equal('räksmörgÃ¥s') + // Check that unescaping works + return decodeURIComponent(escape(escaped_word)).should.equal( + 'räksmörgås' + ) + }) + }) + + describe('with comments that need encoding', function () { + beforeEach(function () { + this.ranges.comments = [{ op: { c: 'räksmörgås' } }] + return this.WebsocketController.joinDoc( + this.client, + this.doc_id, + -1, + { encodeRanges: true }, + this.callback + ) + }) + + return it('should call the callback with the encoded comment', function () { + const encoded_comments = this.callback.args[0][4] + const encoded_comment = encoded_comments.comments.pop() + const encoded_comment_text = encoded_comment.op.c + return encoded_comment_text.should.equal('räksmörgÃ¥s') + }) + }) + + describe('with changes that need encoding', function () { + it('should call the callback with the encoded insert change', function () { + this.ranges.changes = [{ op: { i: 'räksmörgås' } }] + this.WebsocketController.joinDoc( + this.client, + this.doc_id, + -1, + { encodeRanges: true }, + this.callback + ) + + const encoded_changes = this.callback.args[0][4] + const encoded_change = encoded_changes.changes.pop() + const encoded_change_text = encoded_change.op.i + return encoded_change_text.should.equal('räksmörgÃ¥s') + }) + + return it('should call the callback with the encoded delete change', function () { + this.ranges.changes = [{ op: { d: 'räksmörgås' } }] + this.WebsocketController.joinDoc( + this.client, + this.doc_id, + -1, + { encodeRanges: true }, + this.callback + ) + + const encoded_changes = this.callback.args[0][4] + const encoded_change = encoded_changes.changes.pop() + const encoded_change_text = encoded_change.op.d + return encoded_change_text.should.equal('räksmörgÃ¥s') + }) + }) + + describe('when not authorized', function () { + beforeEach(function () { + this.AuthorizationManager.assertClientCanViewProject = sinon + .stub() + .callsArgWith(1, (this.err = new Error('not authorized'))) + return this.WebsocketController.joinDoc( + this.client, + this.doc_id, + -1, + this.options, + this.callback + ) + }) + + it('should call the callback with an error', function () { + return this.callback + .calledWith(sinon.match({ message: 'not authorized' })) + .should.equal(true) + }) + + return it('should not call the DocumentUpdaterManager', function () { + return this.DocumentUpdaterManager.getDocument.called.should.equal( + false + ) + }) + }) + + describe('with a restricted client', function () { + beforeEach(function () { + this.ranges.comments = [{ op: { a: 1 } }, { op: { a: 2 } }] + this.client.ol_context.is_restricted_user = true + return this.WebsocketController.joinDoc( + this.client, + this.doc_id, + -1, + this.options, + this.callback + ) + }) + + return it('should overwrite ranges.comments with an empty list', function () { + const ranges = this.callback.args[0][4] + return expect(ranges.comments).to.deep.equal([]) + }) + }) + + describe('when the client has disconnected', function () { + beforeEach(function () { + this.client.disconnected = true + return this.WebsocketController.joinDoc( + this.client, + this.doc_id, + -1, + this.options, + this.callback + ) + }) + + it('should call the callback with no details', function () { + return expect(this.callback.args[0]).to.deep.equal([]) + }) + + it('should increment the editor.join-doc.disconnected metric with a status', function () { + return expect( + this.metrics.inc.calledWith('editor.join-doc.disconnected', 1, { + status: 'immediately' + }) + ).to.equal(true) + }) + + return it('should not get the document', function () { + return expect(this.DocumentUpdaterManager.getDocument.called).to.equal( + false + ) + }) + }) + + describe('when the client disconnects while RoomManager.joinDoc is running', function () { + beforeEach(function () { + this.RoomManager.joinDoc = (client, doc_id, cb) => { + this.client.disconnected = true + return cb() + } + + return this.WebsocketController.joinDoc( + this.client, + this.doc_id, + -1, + this.options, + this.callback + ) + }) + + it('should call the callback with no details', function () { + return expect(this.callback.args[0]).to.deep.equal([]) + }) + + it('should increment the editor.join-doc.disconnected metric with a status', function () { + return expect( + this.metrics.inc.calledWith('editor.join-doc.disconnected', 1, { + status: 'after-joining-room' + }) + ).to.equal(true) + }) + + return it('should not get the document', function () { + return expect(this.DocumentUpdaterManager.getDocument.called).to.equal( + false + ) + }) + }) + + return describe('when the client disconnects while DocumentUpdaterManager.getDocument is running', function () { + beforeEach(function () { + this.DocumentUpdaterManager.getDocument = ( + project_id, + doc_id, + fromVersion, + callback + ) => { + this.client.disconnected = true + return callback( + null, + this.doc_lines, + this.version, + this.ranges, + this.ops + ) + } + + return this.WebsocketController.joinDoc( + this.client, + this.doc_id, + -1, + this.options, + this.callback + ) + }) + + it('should call the callback with no details', function () { + return expect(this.callback.args[0]).to.deep.equal([]) + }) + + return it('should increment the editor.join-doc.disconnected metric with a status', function () { + return expect( + this.metrics.inc.calledWith('editor.join-doc.disconnected', 1, { + status: 'after-doc-updater-call' + }) + ).to.equal(true) + }) + }) + }) + + describe('leaveDoc', function () { + beforeEach(function () { + this.doc_id = 'doc-id-123' + this.client.ol_context.project_id = this.project_id + this.RoomManager.leaveDoc = sinon.stub() + return this.WebsocketController.leaveDoc( + this.client, + this.doc_id, + this.callback + ) + }) + + it('should remove the client from the doc_id room', function () { + return this.RoomManager.leaveDoc + .calledWith(this.client, this.doc_id) + .should.equal(true) + }) + + it('should call the callback', function () { + return this.callback.called.should.equal(true) + }) + + return it('should increment the leave-doc metric', function () { + return this.metrics.inc.calledWith('editor.leave-doc').should.equal(true) + }) + }) + + describe('getConnectedUsers', function () { + beforeEach(function () { + this.client.ol_context.project_id = this.project_id + this.users = ['mock', 'users'] + this.WebsocketLoadBalancer.emitToRoom = sinon.stub() + return (this.ConnectedUsersManager.getConnectedUsers = sinon + .stub() + .callsArgWith(1, null, this.users)) + }) + + describe('when authorized', function () { + beforeEach(function (done) { + this.AuthorizationManager.assertClientCanViewProject = sinon + .stub() + .callsArgWith(1, null) + return this.WebsocketController.getConnectedUsers( + this.client, + (...args) => { + this.callback(...Array.from(args || [])) + return done() + } + ) + }) + + it('should check that the client is authorized to view the project', function () { + return this.AuthorizationManager.assertClientCanViewProject + .calledWith(this.client) + .should.equal(true) + }) + + it('should broadcast a request to update the client list', function () { + return this.WebsocketLoadBalancer.emitToRoom + .calledWith(this.project_id, 'clientTracking.refresh') + .should.equal(true) + }) + + it('should get the connected users for the project', function () { + return this.ConnectedUsersManager.getConnectedUsers + .calledWith(this.project_id) + .should.equal(true) + }) + + it('should return the users', function () { + return this.callback.calledWith(null, this.users).should.equal(true) + }) + + return it('should increment the get-connected-users metric', function () { + return this.metrics.inc + .calledWith('editor.get-connected-users') + .should.equal(true) + }) + }) + + describe('when not authorized', function () { + beforeEach(function () { + this.AuthorizationManager.assertClientCanViewProject = sinon + .stub() + .callsArgWith(1, (this.err = new Error('not authorized'))) + return this.WebsocketController.getConnectedUsers( + this.client, + this.callback + ) + }) + + it('should not get the connected users for the project', function () { + return this.ConnectedUsersManager.getConnectedUsers.called.should.equal( + false + ) + }) + + return it('should return an error', function () { + return this.callback.calledWith(this.err).should.equal(true) + }) + }) + + describe('when restricted user', function () { + beforeEach(function () { + this.client.ol_context.is_restricted_user = true + this.AuthorizationManager.assertClientCanViewProject = sinon + .stub() + .callsArgWith(1, null) + return this.WebsocketController.getConnectedUsers( + this.client, + this.callback + ) + }) + + it('should return an empty array of users', function () { + return this.callback.calledWith(null, []).should.equal(true) + }) + + return it('should not get the connected users for the project', function () { + return this.ConnectedUsersManager.getConnectedUsers.called.should.equal( + false + ) + }) + }) + + return describe('when the client has disconnected', function () { + beforeEach(function () { + this.client.disconnected = true + this.AuthorizationManager.assertClientCanViewProject = sinon.stub() + return this.WebsocketController.getConnectedUsers( + this.client, + this.callback + ) + }) + + it('should call the callback with no details', function () { + return expect(this.callback.args[0]).to.deep.equal([]) + }) + + return it('should not check permissions', function () { + return expect( + this.AuthorizationManager.assertClientCanViewProject.called + ).to.equal(false) + }) + }) + }) + + describe('updateClientPosition', function () { + beforeEach(function () { + this.WebsocketLoadBalancer.emitToRoom = sinon.stub() + this.ConnectedUsersManager.updateUserPosition = sinon + .stub() + .callsArgWith(4) + this.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon + .stub() + .callsArgWith(2, null) + return (this.update = { + doc_id: (this.doc_id = 'doc-id-123'), + row: (this.row = 42), + column: (this.column = 37) + }) + }) + + describe('with a logged in user', function () { + beforeEach(function () { + this.client.ol_context = { + project_id: this.project_id, + first_name: (this.first_name = 'Douglas'), + last_name: (this.last_name = 'Adams'), + email: (this.email = 'joe@example.com'), + user_id: (this.user_id = 'user-id-123') + } + this.WebsocketController.updateClientPosition(this.client, this.update) + + return (this.populatedCursorData = { + doc_id: this.doc_id, + id: this.client.publicId, + name: `${this.first_name} ${this.last_name}`, + row: this.row, + column: this.column, + email: this.email, + user_id: this.user_id + }) + }) + + it("should send the update to the project room with the user's name", function () { + return this.WebsocketLoadBalancer.emitToRoom + .calledWith( + this.project_id, + 'clientTracking.clientUpdated', + this.populatedCursorData + ) + .should.equal(true) + }) + + it('should send the cursor data to the connected user manager', function (done) { + this.ConnectedUsersManager.updateUserPosition + .calledWith( + this.project_id, + this.client.publicId, + { + _id: this.user_id, + email: this.email, + first_name: this.first_name, + last_name: this.last_name + }, + { + row: this.row, + column: this.column, + doc_id: this.doc_id + } + ) + .should.equal(true) + return done() + }) + + return it('should increment the update-client-position metric at 0.1 frequency', function () { + return this.metrics.inc + .calledWith('editor.update-client-position', 0.1) + .should.equal(true) + }) + }) + + describe('with a logged in user who has no last_name set', function () { + beforeEach(function () { + this.client.ol_context = { + project_id: this.project_id, + first_name: (this.first_name = 'Douglas'), + last_name: undefined, + email: (this.email = 'joe@example.com'), + user_id: (this.user_id = 'user-id-123') + } + this.WebsocketController.updateClientPosition(this.client, this.update) + + return (this.populatedCursorData = { + doc_id: this.doc_id, + id: this.client.publicId, + name: `${this.first_name}`, + row: this.row, + column: this.column, + email: this.email, + user_id: this.user_id + }) + }) + + it("should send the update to the project room with the user's name", function () { + return this.WebsocketLoadBalancer.emitToRoom + .calledWith( + this.project_id, + 'clientTracking.clientUpdated', + this.populatedCursorData + ) + .should.equal(true) + }) + + it('should send the cursor data to the connected user manager', function (done) { + this.ConnectedUsersManager.updateUserPosition + .calledWith( + this.project_id, + this.client.publicId, + { + _id: this.user_id, + email: this.email, + first_name: this.first_name, + last_name: undefined + }, + { + row: this.row, + column: this.column, + doc_id: this.doc_id + } + ) + .should.equal(true) + return done() + }) + + return it('should increment the update-client-position metric at 0.1 frequency', function () { + return this.metrics.inc + .calledWith('editor.update-client-position', 0.1) + .should.equal(true) + }) + }) + + describe('with a logged in user who has no first_name set', function () { + beforeEach(function () { + this.client.ol_context = { + project_id: this.project_id, + first_name: undefined, + last_name: (this.last_name = 'Adams'), + email: (this.email = 'joe@example.com'), + user_id: (this.user_id = 'user-id-123') + } + this.WebsocketController.updateClientPosition(this.client, this.update) + + return (this.populatedCursorData = { + doc_id: this.doc_id, + id: this.client.publicId, + name: `${this.last_name}`, + row: this.row, + column: this.column, + email: this.email, + user_id: this.user_id + }) + }) + + it("should send the update to the project room with the user's name", function () { + return this.WebsocketLoadBalancer.emitToRoom + .calledWith( + this.project_id, + 'clientTracking.clientUpdated', + this.populatedCursorData + ) + .should.equal(true) + }) + + it('should send the cursor data to the connected user manager', function (done) { + this.ConnectedUsersManager.updateUserPosition + .calledWith( + this.project_id, + this.client.publicId, + { + _id: this.user_id, + email: this.email, + first_name: undefined, + last_name: this.last_name + }, + { + row: this.row, + column: this.column, + doc_id: this.doc_id + } + ) + .should.equal(true) + return done() + }) + + return it('should increment the update-client-position metric at 0.1 frequency', function () { + return this.metrics.inc + .calledWith('editor.update-client-position', 0.1) + .should.equal(true) + }) + }) + describe('with a logged in user who has no names set', function () { + beforeEach(function () { + this.client.ol_context = { + project_id: this.project_id, + first_name: undefined, + last_name: undefined, + email: (this.email = 'joe@example.com'), + user_id: (this.user_id = 'user-id-123') + } + return this.WebsocketController.updateClientPosition( + this.client, + this.update + ) + }) + + return it('should send the update to the project name with no name', function () { + return this.WebsocketLoadBalancer.emitToRoom + .calledWith(this.project_id, 'clientTracking.clientUpdated', { + doc_id: this.doc_id, + id: this.client.publicId, + user_id: this.user_id, + name: '', + row: this.row, + column: this.column, + email: this.email + }) + .should.equal(true) + }) + }) + + describe('with an anonymous user', function () { + beforeEach(function () { + this.client.ol_context = { + project_id: this.project_id + } + return this.WebsocketController.updateClientPosition( + this.client, + this.update + ) + }) + + it('should send the update to the project room with no name', function () { + return this.WebsocketLoadBalancer.emitToRoom + .calledWith(this.project_id, 'clientTracking.clientUpdated', { + doc_id: this.doc_id, + id: this.client.publicId, + name: '', + row: this.row, + column: this.column + }) + .should.equal(true) + }) + + return it('should not send cursor data to the connected user manager', function (done) { + this.ConnectedUsersManager.updateUserPosition.called.should.equal(false) + return done() + }) + }) + + return describe('when the client has disconnected', function () { + beforeEach(function () { + this.client.disconnected = true + this.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon.stub() + return this.WebsocketController.updateClientPosition( + this.client, + this.update, + this.callback + ) + }) + + it('should call the callback with no details', function () { + return expect(this.callback.args[0]).to.deep.equal([]) + }) + + return it('should not check permissions', function () { + return expect( + this.AuthorizationManager.assertClientCanViewProjectAndDoc.called + ).to.equal(false) + }) + }) + }) + + describe('applyOtUpdate', function () { + beforeEach(function () { + this.update = { op: { p: 12, t: 'foo' } } + this.client.ol_context.user_id = this.user_id + this.client.ol_context.project_id = this.project_id + this.WebsocketController._assertClientCanApplyUpdate = sinon + .stub() + .yields() + return (this.DocumentUpdaterManager.queueChange = sinon + .stub() + .callsArg(3)) + }) + + describe('succesfully', function () { + beforeEach(function () { + return this.WebsocketController.applyOtUpdate( + this.client, + this.doc_id, + this.update, + this.callback + ) + }) + + it('should set the source of the update to the client id', function () { + return this.update.meta.source.should.equal(this.client.publicId) + }) + + it('should set the user_id of the update to the user id', function () { + return this.update.meta.user_id.should.equal(this.user_id) + }) + + it('should queue the update', function () { + return this.DocumentUpdaterManager.queueChange + .calledWith(this.project_id, this.doc_id, this.update) + .should.equal(true) + }) + + it('should call the callback', function () { + return this.callback.called.should.equal(true) + }) + + return it('should increment the doc updates', function () { + return this.metrics.inc + .calledWith('editor.doc-update') + .should.equal(true) + }) + }) + + describe('unsuccessfully', function () { + beforeEach(function () { + this.client.disconnect = sinon.stub() + this.DocumentUpdaterManager.queueChange = sinon + .stub() + .callsArgWith(3, (this.error = new Error('Something went wrong'))) + return this.WebsocketController.applyOtUpdate( + this.client, + this.doc_id, + this.update, + this.callback + ) + }) + + it('should disconnect the client', function () { + return this.client.disconnect.called.should.equal(true) + }) + + it('should log an error', function () { + return this.logger.error.called.should.equal(true) + }) + + return it('should call the callback with the error', function () { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) + + describe('when not authorized', function () { + beforeEach(function () { + this.client.disconnect = sinon.stub() + this.WebsocketController._assertClientCanApplyUpdate = sinon + .stub() + .yields((this.error = new Error('not authorized'))) + return this.WebsocketController.applyOtUpdate( + this.client, + this.doc_id, + this.update, + this.callback + ) + }) + + // This happens in a setTimeout to allow the client a chance to receive the error first. + // I'm not sure how to unit test, but it is acceptance tested. + // it "should disconnect the client", -> + // @client.disconnect.called.should.equal true + + it('should log a warning', function () { + return this.logger.warn.called.should.equal(true) + }) + + return it('should call the callback with the error', function () { + return this.callback.calledWith(this.error).should.equal(true) + }) + }) + + return describe('update_too_large', function () { + beforeEach(function (done) { + this.client.disconnect = sinon.stub() + this.client.emit = sinon.stub() + this.client.ol_context.user_id = this.user_id + this.client.ol_context.project_id = this.project_id + const error = new Error('update is too large') + error.updateSize = 7372835 + this.DocumentUpdaterManager.queueChange = sinon + .stub() + .callsArgWith(3, error) + this.WebsocketController.applyOtUpdate( + this.client, + this.doc_id, + this.update, + this.callback + ) + return setTimeout(() => done(), 1) + }) + + it('should call the callback with no error', function () { + this.callback.called.should.equal(true) + return this.callback.args[0].should.deep.equal([]) + }) + + it('should log a warning with the size and context', function () { + this.logger.warn.called.should.equal(true) + return this.logger.warn.args[0].should.deep.equal([ + { + user_id: this.user_id, + project_id: this.project_id, + doc_id: this.doc_id, + updateSize: 7372835 + }, + 'update is too large' + ]) + }) + + describe('after 100ms', function () { + beforeEach(function (done) { + return setTimeout(done, 100) + }) + + it('should send an otUpdateError the client', function () { + return this.client.emit.calledWith('otUpdateError').should.equal(true) + }) + + return it('should disconnect the client', function () { + return this.client.disconnect.called.should.equal(true) + }) + }) + + return describe('when the client disconnects during the next 100ms', function () { + beforeEach(function (done) { + this.client.disconnected = true + return setTimeout(done, 100) + }) + + it('should not send an otUpdateError the client', function () { + return this.client.emit + .calledWith('otUpdateError') + .should.equal(false) + }) + + it('should not disconnect the client', function () { + return this.client.disconnect.called.should.equal(false) + }) + + return it('should increment the editor.doc-update.disconnected metric with a status', function () { + return expect( + this.metrics.inc.calledWith('editor.doc-update.disconnected', 1, { + status: 'at-otUpdateError' + }) + ).to.equal(true) + }) + }) + }) + }) + + return describe('_assertClientCanApplyUpdate', function () { + beforeEach(function () { + this.edit_update = { + op: [ + { i: 'foo', p: 42 }, + { c: 'bar', p: 132 } + ] + } // comments may still be in an edit op + this.comment_update = { op: [{ c: 'bar', p: 132 }] } + this.AuthorizationManager.assertClientCanEditProjectAndDoc = sinon.stub() + return (this.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon.stub()) + }) + + describe('with a read-write client', function () { + return it('should return successfully', function (done) { + this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(null) + return this.WebsocketController._assertClientCanApplyUpdate( + this.client, + this.doc_id, + this.edit_update, + (error) => { + expect(error).to.be.null + return done() + } + ) + }) + }) + + describe('with a read-only client and an edit op', function () { + return it('should return an error', function (done) { + this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields( + new Error('not authorized') + ) + this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(null) + return this.WebsocketController._assertClientCanApplyUpdate( + this.client, + this.doc_id, + this.edit_update, + (error) => { + expect(error.message).to.equal('not authorized') + return done() + } + ) + }) + }) + + describe('with a read-only client and a comment op', function () { + return it('should return successfully', function (done) { + this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields( + new Error('not authorized') + ) + this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields(null) + return this.WebsocketController._assertClientCanApplyUpdate( + this.client, + this.doc_id, + this.comment_update, + (error) => { + expect(error).to.be.null + return done() + } + ) + }) + }) + + return describe('with a totally unauthorized client', function () { + return it('should return an error', function (done) { + this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields( + new Error('not authorized') + ) + this.AuthorizationManager.assertClientCanViewProjectAndDoc.yields( + new Error('not authorized') + ) + return this.WebsocketController._assertClientCanApplyUpdate( + this.client, + this.doc_id, + this.comment_update, + (error) => { + expect(error.message).to.equal('not authorized') + return done() + } + ) + }) + }) + }) +}) diff --git a/services/real-time/test/unit/js/WebsocketLoadBalancerTests.js b/services/real-time/test/unit/js/WebsocketLoadBalancerTests.js index 0d0c0f6b9d..355e635353 100644 --- a/services/real-time/test/unit/js/WebsocketLoadBalancerTests.js +++ b/services/real-time/test/unit/js/WebsocketLoadBalancerTests.js @@ -9,201 +9,301 @@ * DS102: Remove unnecessary code created because of implicit returns * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -const SandboxedModule = require('sandboxed-module'); -const sinon = require('sinon'); -require('chai').should(); -const modulePath = require('path').join(__dirname, '../../../app/js/WebsocketLoadBalancer'); +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +require('chai').should() +const modulePath = require('path').join( + __dirname, + '../../../app/js/WebsocketLoadBalancer' +) -describe("WebsocketLoadBalancer", function() { - beforeEach(function() { - this.rclient = {}; - this.RoomEvents = {on: sinon.stub()}; - this.WebsocketLoadBalancer = SandboxedModule.require(modulePath, { requires: { - "./RedisClientManager": { - createClientList: () => [] - }, - "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }), - "./SafeJsonParse": (this.SafeJsonParse = - {parse: (data, cb) => cb(null, JSON.parse(data))}), - "./EventLogger": {checkEventOrder: sinon.stub()}, - "./HealthCheckManager": {check: sinon.stub()}, - "./RoomManager" : (this.RoomManager = {eventSource: sinon.stub().returns(this.RoomEvents)}), - "./ChannelManager": (this.ChannelManager = {publish: sinon.stub()}), - "./ConnectedUsersManager": (this.ConnectedUsersManager = {refreshClient: sinon.stub()}) - } - }); - this.io = {}; - this.WebsocketLoadBalancer.rclientPubList = [{publish: sinon.stub()}]; - this.WebsocketLoadBalancer.rclientSubList = [{ - subscribe: sinon.stub(), - on: sinon.stub() - }]; +describe('WebsocketLoadBalancer', function () { + beforeEach(function () { + this.rclient = {} + this.RoomEvents = { on: sinon.stub() } + this.WebsocketLoadBalancer = SandboxedModule.require(modulePath, { + requires: { + './RedisClientManager': { + createClientList: () => [] + }, + 'logger-sharelatex': (this.logger = { + log: sinon.stub(), + error: sinon.stub() + }), + './SafeJsonParse': (this.SafeJsonParse = { + parse: (data, cb) => cb(null, JSON.parse(data)) + }), + './EventLogger': { checkEventOrder: sinon.stub() }, + './HealthCheckManager': { check: sinon.stub() }, + './RoomManager': (this.RoomManager = { + eventSource: sinon.stub().returns(this.RoomEvents) + }), + './ChannelManager': (this.ChannelManager = { publish: sinon.stub() }), + './ConnectedUsersManager': (this.ConnectedUsersManager = { + refreshClient: sinon.stub() + }) + } + }) + this.io = {} + this.WebsocketLoadBalancer.rclientPubList = [{ publish: sinon.stub() }] + this.WebsocketLoadBalancer.rclientSubList = [ + { + subscribe: sinon.stub(), + on: sinon.stub() + } + ] - this.room_id = "room-id"; - this.message = "otUpdateApplied"; - return this.payload = ["argument one", 42];}); + this.room_id = 'room-id' + this.message = 'otUpdateApplied' + return (this.payload = ['argument one', 42]) + }) - describe("emitToRoom", function() { - beforeEach(function() { - return this.WebsocketLoadBalancer.emitToRoom(this.room_id, this.message, ...Array.from(this.payload)); - }); + describe('emitToRoom', function () { + beforeEach(function () { + return this.WebsocketLoadBalancer.emitToRoom( + this.room_id, + this.message, + ...Array.from(this.payload) + ) + }) - return it("should publish the message to redis", function() { - return this.ChannelManager.publish - .calledWith(this.WebsocketLoadBalancer.rclientPubList[0], "editor-events", this.room_id, JSON.stringify({ - room_id: this.room_id, - message: this.message, - payload: this.payload - })) - .should.equal(true); - }); - }); + return it('should publish the message to redis', function () { + return this.ChannelManager.publish + .calledWith( + this.WebsocketLoadBalancer.rclientPubList[0], + 'editor-events', + this.room_id, + JSON.stringify({ + room_id: this.room_id, + message: this.message, + payload: this.payload + }) + ) + .should.equal(true) + }) + }) - describe("emitToAll", function() { - beforeEach(function() { - this.WebsocketLoadBalancer.emitToRoom = sinon.stub(); - return this.WebsocketLoadBalancer.emitToAll(this.message, ...Array.from(this.payload)); - }); + describe('emitToAll', function () { + beforeEach(function () { + this.WebsocketLoadBalancer.emitToRoom = sinon.stub() + return this.WebsocketLoadBalancer.emitToAll( + this.message, + ...Array.from(this.payload) + ) + }) - return it("should emit to the room 'all'", function() { - return this.WebsocketLoadBalancer.emitToRoom - .calledWith("all", this.message, ...Array.from(this.payload)) - .should.equal(true); - }); - }); + return it("should emit to the room 'all'", function () { + return this.WebsocketLoadBalancer.emitToRoom + .calledWith('all', this.message, ...Array.from(this.payload)) + .should.equal(true) + }) + }) - describe("listenForEditorEvents", function() { - beforeEach(function() { - this.WebsocketLoadBalancer._processEditorEvent = sinon.stub(); - return this.WebsocketLoadBalancer.listenForEditorEvents(); - }); + describe('listenForEditorEvents', function () { + beforeEach(function () { + this.WebsocketLoadBalancer._processEditorEvent = sinon.stub() + return this.WebsocketLoadBalancer.listenForEditorEvents() + }) - it("should subscribe to the editor-events channel", function() { - return this.WebsocketLoadBalancer.rclientSubList[0].subscribe - .calledWith("editor-events") - .should.equal(true); - }); + it('should subscribe to the editor-events channel', function () { + return this.WebsocketLoadBalancer.rclientSubList[0].subscribe + .calledWith('editor-events') + .should.equal(true) + }) - return it("should process the events with _processEditorEvent", function() { - return this.WebsocketLoadBalancer.rclientSubList[0].on - .calledWith("message", sinon.match.func) - .should.equal(true); - }); - }); + return it('should process the events with _processEditorEvent', function () { + return this.WebsocketLoadBalancer.rclientSubList[0].on + .calledWith('message', sinon.match.func) + .should.equal(true) + }) + }) - return describe("_processEditorEvent", function() { - describe("with bad JSON", function() { - beforeEach(function() { - this.isRestrictedUser = false; - this.SafeJsonParse.parse = sinon.stub().callsArgWith(1, new Error("oops")); - return this.WebsocketLoadBalancer._processEditorEvent(this.io, "editor-events", "blah"); - }); + return describe('_processEditorEvent', function () { + describe('with bad JSON', function () { + beforeEach(function () { + this.isRestrictedUser = false + this.SafeJsonParse.parse = sinon + .stub() + .callsArgWith(1, new Error('oops')) + return this.WebsocketLoadBalancer._processEditorEvent( + this.io, + 'editor-events', + 'blah' + ) + }) - return it("should log an error", function() { - return this.logger.error.called.should.equal(true); - }); - }); + return it('should log an error', function () { + return this.logger.error.called.should.equal(true) + }) + }) - describe("with a designated room", function() { - beforeEach(function() { - this.io.sockets = { - clients: sinon.stub().returns([ - {id: 'client-id-1', emit: (this.emit1 = sinon.stub()), ol_context: {}}, - {id: 'client-id-2', emit: (this.emit2 = sinon.stub()), ol_context: {}}, - {id: 'client-id-1', emit: (this.emit3 = sinon.stub()), ol_context: {}} // duplicate client - ]) - }; - const data = JSON.stringify({ - room_id: this.room_id, - message: this.message, - payload: this.payload - }); - return this.WebsocketLoadBalancer._processEditorEvent(this.io, "editor-events", data); - }); + describe('with a designated room', function () { + beforeEach(function () { + this.io.sockets = { + clients: sinon.stub().returns([ + { + id: 'client-id-1', + emit: (this.emit1 = sinon.stub()), + ol_context: {} + }, + { + id: 'client-id-2', + emit: (this.emit2 = sinon.stub()), + ol_context: {} + }, + { + id: 'client-id-1', + emit: (this.emit3 = sinon.stub()), + ol_context: {} + } // duplicate client + ]) + } + const data = JSON.stringify({ + room_id: this.room_id, + message: this.message, + payload: this.payload + }) + return this.WebsocketLoadBalancer._processEditorEvent( + this.io, + 'editor-events', + data + ) + }) - return it("should send the message to all (unique) clients in the room", function() { - this.io.sockets.clients - .calledWith(this.room_id) - .should.equal(true); - this.emit1.calledWith(this.message, ...Array.from(this.payload)).should.equal(true); - this.emit2.calledWith(this.message, ...Array.from(this.payload)).should.equal(true); - return this.emit3.called.should.equal(false); - }); - }); // duplicate client should be ignored + return it('should send the message to all (unique) clients in the room', function () { + this.io.sockets.clients.calledWith(this.room_id).should.equal(true) + this.emit1 + .calledWith(this.message, ...Array.from(this.payload)) + .should.equal(true) + this.emit2 + .calledWith(this.message, ...Array.from(this.payload)) + .should.equal(true) + return this.emit3.called.should.equal(false) + }) + }) // duplicate client should be ignored - describe("with a designated room, and restricted clients, not restricted message", function() { - beforeEach(function() { - this.io.sockets = { - clients: sinon.stub().returns([ - {id: 'client-id-1', emit: (this.emit1 = sinon.stub()), ol_context: {}}, - {id: 'client-id-2', emit: (this.emit2 = sinon.stub()), ol_context: {}}, - {id: 'client-id-1', emit: (this.emit3 = sinon.stub()), ol_context: {}}, // duplicate client - {id: 'client-id-4', emit: (this.emit4 = sinon.stub()), ol_context: {is_restricted_user: true}} - ]) - }; - const data = JSON.stringify({ - room_id: this.room_id, - message: this.message, - payload: this.payload - }); - return this.WebsocketLoadBalancer._processEditorEvent(this.io, "editor-events", data); - }); + describe('with a designated room, and restricted clients, not restricted message', function () { + beforeEach(function () { + this.io.sockets = { + clients: sinon.stub().returns([ + { + id: 'client-id-1', + emit: (this.emit1 = sinon.stub()), + ol_context: {} + }, + { + id: 'client-id-2', + emit: (this.emit2 = sinon.stub()), + ol_context: {} + }, + { + id: 'client-id-1', + emit: (this.emit3 = sinon.stub()), + ol_context: {} + }, // duplicate client + { + id: 'client-id-4', + emit: (this.emit4 = sinon.stub()), + ol_context: { is_restricted_user: true } + } + ]) + } + const data = JSON.stringify({ + room_id: this.room_id, + message: this.message, + payload: this.payload + }) + return this.WebsocketLoadBalancer._processEditorEvent( + this.io, + 'editor-events', + data + ) + }) - return it("should send the message to all (unique) clients in the room", function() { - this.io.sockets.clients - .calledWith(this.room_id) - .should.equal(true); - this.emit1.calledWith(this.message, ...Array.from(this.payload)).should.equal(true); - this.emit2.calledWith(this.message, ...Array.from(this.payload)).should.equal(true); - this.emit3.called.should.equal(false); // duplicate client should be ignored - return this.emit4.called.should.equal(true); - }); - }); // restricted client, but should be called + return it('should send the message to all (unique) clients in the room', function () { + this.io.sockets.clients.calledWith(this.room_id).should.equal(true) + this.emit1 + .calledWith(this.message, ...Array.from(this.payload)) + .should.equal(true) + this.emit2 + .calledWith(this.message, ...Array.from(this.payload)) + .should.equal(true) + this.emit3.called.should.equal(false) // duplicate client should be ignored + return this.emit4.called.should.equal(true) + }) + }) // restricted client, but should be called - describe("with a designated room, and restricted clients, restricted message", function() { - beforeEach(function() { - this.io.sockets = { - clients: sinon.stub().returns([ - {id: 'client-id-1', emit: (this.emit1 = sinon.stub()), ol_context: {}}, - {id: 'client-id-2', emit: (this.emit2 = sinon.stub()), ol_context: {}}, - {id: 'client-id-1', emit: (this.emit3 = sinon.stub()), ol_context: {}}, // duplicate client - {id: 'client-id-4', emit: (this.emit4 = sinon.stub()), ol_context: {is_restricted_user: true}} - ]) - }; - const data = JSON.stringify({ - room_id: this.room_id, - message: (this.restrictedMessage = 'new-comment'), - payload: this.payload - }); - return this.WebsocketLoadBalancer._processEditorEvent(this.io, "editor-events", data); - }); + describe('with a designated room, and restricted clients, restricted message', function () { + beforeEach(function () { + this.io.sockets = { + clients: sinon.stub().returns([ + { + id: 'client-id-1', + emit: (this.emit1 = sinon.stub()), + ol_context: {} + }, + { + id: 'client-id-2', + emit: (this.emit2 = sinon.stub()), + ol_context: {} + }, + { + id: 'client-id-1', + emit: (this.emit3 = sinon.stub()), + ol_context: {} + }, // duplicate client + { + id: 'client-id-4', + emit: (this.emit4 = sinon.stub()), + ol_context: { is_restricted_user: true } + } + ]) + } + const data = JSON.stringify({ + room_id: this.room_id, + message: (this.restrictedMessage = 'new-comment'), + payload: this.payload + }) + return this.WebsocketLoadBalancer._processEditorEvent( + this.io, + 'editor-events', + data + ) + }) - return it("should send the message to all (unique) clients in the room, who are not restricted", function() { - this.io.sockets.clients - .calledWith(this.room_id) - .should.equal(true); - this.emit1.calledWith(this.restrictedMessage, ...Array.from(this.payload)).should.equal(true); - this.emit2.calledWith(this.restrictedMessage, ...Array.from(this.payload)).should.equal(true); - this.emit3.called.should.equal(false); // duplicate client should be ignored - return this.emit4.called.should.equal(false); - }); - }); // restricted client, should not be called + return it('should send the message to all (unique) clients in the room, who are not restricted', function () { + this.io.sockets.clients.calledWith(this.room_id).should.equal(true) + this.emit1 + .calledWith(this.restrictedMessage, ...Array.from(this.payload)) + .should.equal(true) + this.emit2 + .calledWith(this.restrictedMessage, ...Array.from(this.payload)) + .should.equal(true) + this.emit3.called.should.equal(false) // duplicate client should be ignored + return this.emit4.called.should.equal(false) + }) + }) // restricted client, should not be called - return describe("when emitting to all", function() { - beforeEach(function() { - this.io.sockets = - {emit: (this.emit = sinon.stub())}; - const data = JSON.stringify({ - room_id: "all", - message: this.message, - payload: this.payload - }); - return this.WebsocketLoadBalancer._processEditorEvent(this.io, "editor-events", data); - }); + return describe('when emitting to all', function () { + beforeEach(function () { + this.io.sockets = { emit: (this.emit = sinon.stub()) } + const data = JSON.stringify({ + room_id: 'all', + message: this.message, + payload: this.payload + }) + return this.WebsocketLoadBalancer._processEditorEvent( + this.io, + 'editor-events', + data + ) + }) - return it("should send the message to all clients", function() { - return this.emit.calledWith(this.message, ...Array.from(this.payload)).should.equal(true); - }); - }); - }); -}); + return it('should send the message to all clients', function () { + return this.emit + .calledWith(this.message, ...Array.from(this.payload)) + .should.equal(true) + }) + }) + }) +}) diff --git a/services/real-time/test/unit/js/helpers/MockClient.js b/services/real-time/test/unit/js/helpers/MockClient.js index 5f9b019db4..fb90a0f7e9 100644 --- a/services/real-time/test/unit/js/helpers/MockClient.js +++ b/services/real-time/test/unit/js/helpers/MockClient.js @@ -3,20 +3,20 @@ */ // TODO: This file was created by bulk-decaffeinate. // Fix any style issues and re-enable lint. -let MockClient; -const sinon = require('sinon'); +let MockClient +const sinon = require('sinon') -let idCounter = 0; +let idCounter = 0 -module.exports = (MockClient = class MockClient { - constructor() { - this.ol_context = {}; - this.join = sinon.stub(); - this.emit = sinon.stub(); - this.disconnect = sinon.stub(); - this.id = idCounter++; - this.publicId = idCounter++; - } +module.exports = MockClient = class MockClient { + constructor() { + this.ol_context = {} + this.join = sinon.stub() + this.emit = sinon.stub() + this.disconnect = sinon.stub() + this.id = idCounter++ + this.publicId = idCounter++ + } - disconnect() {} -}); + disconnect() {} +}