From 2ca620e7a0184c0847c23c311282584781ff4369 Mon Sep 17 00:00:00 2001 From: decaffeinate Date: Tue, 23 Jun 2020 18:29:59 +0100 Subject: [PATCH] decaffeinate: Convert AuthorizationManagerTests.coffee and 13 other files to JS --- .../unit/coffee/AuthorizationManagerTests.js | 312 +-- .../test/unit/coffee/ChannelManagerTests.js | 402 ++-- .../unit/coffee/ConnectedUsersManagerTests.js | 325 +-- .../coffee/DocumentUpdaterControllerTests.js | 298 +-- .../coffee/DocumentUpdaterManagerTests.js | 447 +++-- .../test/unit/coffee/DrainManagerTests.js | 156 +- .../test/unit/coffee/EventLoggerTests.js | 151 +- .../test/unit/coffee/RoomManagerTests.js | 533 ++--- .../test/unit/coffee/SafeJsonParseTest.js | 75 +- .../test/unit/coffee/SessionSocketsTests.js | 250 ++- .../test/unit/coffee/WebApiManagerTests.js | 155 +- .../unit/coffee/WebsocketControllerTests.js | 1782 +++++++++-------- .../unit/coffee/WebsocketLoadBalancerTests.js | 307 +-- .../test/unit/coffee/helpers/MockClient.js | 25 +- 14 files changed, 2987 insertions(+), 2231 deletions(-) diff --git a/services/real-time/test/unit/coffee/AuthorizationManagerTests.js b/services/real-time/test/unit/coffee/AuthorizationManagerTests.js index 143218d8b2..626428ed61 100644 --- a/services/real-time/test/unit/coffee/AuthorizationManagerTests.js +++ b/services/real-time/test/unit/coffee/AuthorizationManagerTests.js @@ -1,166 +1,216 @@ -chai = require "chai" -chai.should() -expect = chai.expect -sinon = require("sinon") -SandboxedModule = require('sandboxed-module') -path = require "path" -modulePath = '../../../app/js/AuthorizationManager' +/* + * decaffeinate suggestions: + * 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'; -describe 'AuthorizationManager', -> - beforeEach -> - @client = - ol_context: {} +describe('AuthorizationManager', function() { + beforeEach(function() { + this.client = + {ol_context: {}}; - @AuthorizationManager = SandboxedModule.require modulePath, requires: {} + return this.AuthorizationManager = SandboxedModule.require(modulePath, {requires: {}});}); - describe "assertClientCanViewProject", -> - it "should allow the readOnly privilegeLevel", (done) -> - @client.ol_context.privilege_level = "readOnly" - @AuthorizationManager.assertClientCanViewProject @client, (error) -> - expect(error).to.be.null - 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, function(error) { + expect(error).to.be.null; + return done(); + }); + }); - it "should allow the readAndWrite privilegeLevel", (done) -> - @client.ol_context.privilege_level = "readAndWrite" - @AuthorizationManager.assertClientCanViewProject @client, (error) -> - expect(error).to.be.null - done() + it("should allow the readAndWrite privilegeLevel", function(done) { + this.client.ol_context.privilege_level = "readAndWrite"; + return this.AuthorizationManager.assertClientCanViewProject(this.client, function(error) { + expect(error).to.be.null; + return done(); + }); + }); - it "should allow the owner privilegeLevel", (done) -> - @client.ol_context.privilege_level = "owner" - @AuthorizationManager.assertClientCanViewProject @client, (error) -> - expect(error).to.be.null - done() + it("should allow the owner privilegeLevel", function(done) { + this.client.ol_context.privilege_level = "owner"; + return this.AuthorizationManager.assertClientCanViewProject(this.client, function(error) { + expect(error).to.be.null; + return done(); + }); + }); - it "should return an error with any other privilegeLevel", (done) -> - @client.ol_context.privilege_level = "unknown" - @AuthorizationManager.assertClientCanViewProject @client, (error) -> - error.message.should.equal "not authorized" - 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, function(error) { + error.message.should.equal("not authorized"); + return done(); + }); + }); + }); - describe "assertClientCanEditProject", -> - it "should not allow the readOnly privilegeLevel", (done) -> - @client.ol_context.privilege_level = "readOnly" - @AuthorizationManager.assertClientCanEditProject @client, (error) -> - error.message.should.equal "not authorized" - 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, function(error) { + error.message.should.equal("not authorized"); + return done(); + }); + }); - it "should allow the readAndWrite privilegeLevel", (done) -> - @client.ol_context.privilege_level = "readAndWrite" - @AuthorizationManager.assertClientCanEditProject @client, (error) -> - expect(error).to.be.null - done() + it("should allow the readAndWrite privilegeLevel", function(done) { + this.client.ol_context.privilege_level = "readAndWrite"; + return this.AuthorizationManager.assertClientCanEditProject(this.client, function(error) { + expect(error).to.be.null; + return done(); + }); + }); - it "should allow the owner privilegeLevel", (done) -> - @client.ol_context.privilege_level = "owner" - @AuthorizationManager.assertClientCanEditProject @client, (error) -> - expect(error).to.be.null - done() + it("should allow the owner privilegeLevel", function(done) { + this.client.ol_context.privilege_level = "owner"; + return this.AuthorizationManager.assertClientCanEditProject(this.client, function(error) { + expect(error).to.be.null; + return done(); + }); + }); - it "should return an error with any other privilegeLevel", (done) -> - @client.ol_context.privilege_level = "unknown" - @AuthorizationManager.assertClientCanEditProject @client, (error) -> - error.message.should.equal "not authorized" - 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, function(error) { + error.message.should.equal("not authorized"); + return done(); + }); + }); + }); - # check doc access for project + // check doc access for project - describe "assertClientCanViewProjectAndDoc", -> - beforeEach () -> - @doc_id = "12345" - @callback = sinon.stub() - @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", -> - beforeEach () -> - @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", () -> - @AuthorizationManager.assertClientCanViewProjectAndDoc @client, @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")); + }); - describe "even when authorised at the doc level", -> - beforeEach (done) -> - @AuthorizationManager.addAccessToDoc @client, @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); + }); - it "should not allow access", () -> - @AuthorizationManager.assertClientCanViewProjectAndDoc @client, @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")); + }); + }); + }); - describe "when authorised at the project level", -> - beforeEach () -> - @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", -> - it "should not allow access", () -> - @AuthorizationManager.assertClientCanViewProjectAndDoc @client, @doc_id, (err) -> - err.message.should.equal "not authorized" + describe("and not authorised at the document level", () => 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", -> - beforeEach (done) -> - @AuthorizationManager.addAccessToDoc @client, @doc_id, done + describe("and authorised at the document level", function() { + beforeEach(function(done) { + return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, done); + }); - it "should allow access", () -> - @AuthorizationManager.assertClientCanViewProjectAndDoc @client, @doc_id, @callback - @callback + return it("should allow access", function() { + this.AuthorizationManager.assertClientCanViewProjectAndDoc(this.client, this.doc_id, this.callback); + return this.callback .calledWith(null) - .should.equal true + .should.equal(true); + }); + }); - describe "when document authorisation is added and then removed", -> - beforeEach (done) -> - @AuthorizationManager.addAccessToDoc @client, @doc_id, () => - @AuthorizationManager.removeAccessToDoc @client, @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); + }); + }); - it "should deny access", () -> - @AuthorizationManager.assertClientCanViewProjectAndDoc @client, @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")); + }); + }); + }); + }); - describe "assertClientCanEditProjectAndDoc", -> - beforeEach () -> - @doc_id = "12345" - @callback = sinon.stub() - @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", -> - beforeEach () -> - @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", () -> - @AuthorizationManager.assertClientCanEditProjectAndDoc @client, @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")); + }); - describe "even when authorised at the doc level", -> - beforeEach (done) -> - @AuthorizationManager.addAccessToDoc @client, @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); + }); - it "should not allow access", () -> - @AuthorizationManager.assertClientCanEditProjectAndDoc @client, @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")); + }); + }); + }); - describe "when authorised at the project level", -> - beforeEach () -> - @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", -> - it "should not allow access", () -> - @AuthorizationManager.assertClientCanEditProjectAndDoc @client, @doc_id, (err) -> - err.message.should.equal "not authorized" + describe("and not authorised at the document level", () => 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", -> - beforeEach (done) -> - @AuthorizationManager.addAccessToDoc @client, @doc_id, done + describe("and authorised at the document level", function() { + beforeEach(function(done) { + return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, done); + }); - it "should allow access", () -> - @AuthorizationManager.assertClientCanEditProjectAndDoc @client, @doc_id, @callback - @callback + return it("should allow access", function() { + this.AuthorizationManager.assertClientCanEditProjectAndDoc(this.client, this.doc_id, this.callback); + return this.callback .calledWith(null) - .should.equal true + .should.equal(true); + }); + }); - describe "when document authorisation is added and then removed", -> - beforeEach (done) -> - @AuthorizationManager.addAccessToDoc @client, @doc_id, () => - @AuthorizationManager.removeAccessToDoc @client, @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); + }); + }); - it "should deny access", () -> - @AuthorizationManager.assertClientCanEditProjectAndDoc @client, @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/coffee/ChannelManagerTests.js b/services/real-time/test/unit/coffee/ChannelManagerTests.js index 354e956283..5c148451d0 100644 --- a/services/real-time/test/unit/coffee/ChannelManagerTests.js +++ b/services/real-time/test/unit/coffee/ChannelManagerTests.js @@ -1,220 +1,274 @@ -chai = require('chai') -should = chai.should() -expect = chai.expect -sinon = require("sinon") -modulePath = "../../../app/js/ChannelManager.js" -SandboxedModule = require('sandboxed-module') +/* + * decaffeinate suggestions: + * 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'); -describe 'ChannelManager', -> - beforeEach -> - @rclient = {} - @other_rclient = {} - @ChannelManager = SandboxedModule.require modulePath, requires: - "settings-sharelatex": @settings = {} - "metrics-sharelatex": @metrics = {inc: sinon.stub(), summary: sinon.stub()} - "logger-sharelatex": @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", -> + describe("subscribe", function() { - describe "when there is no existing subscription for this redis client", -> - beforeEach (done) -> - @rclient.subscribe = sinon.stub().resolves() - @ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef" - 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); + }); - it "should subscribe to the redis channel", -> - @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", -> - beforeEach (done) -> - @rclient.subscribe = sinon.stub().resolves() - @ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef" - @ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef" - 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); + }); - it "should subscribe to the redis channel again", -> - @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", -> - beforeEach (done) -> - @rclient.subscribe = sinon.stub() + describe("when subscribe errors", function() { + beforeEach(function(done) { + this.rclient.subscribe = sinon.stub() .onFirstCall().rejects(new Error("some redis error")) - .onSecondCall().resolves() - p = @ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef" - p.then () -> - done(new Error('should not subscribe but fail')) - .catch (err) => - err.message.should.equal "some redis error" - @ChannelManager.getClientMapEntry(@rclient).has("applied-ops:1234567890abcdef").should.equal false - @ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef" - # subscribe is wrapped in Promise, delay other assertions - setTimeout done - return null + .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", -> - expect(@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", -> - @rclient.subscribe.callCount.should.equal 2 + it("should subscribe again", function() { + return this.rclient.subscribe.callCount.should.equal(2); + }); - it "should cleanup", -> - @ChannelManager.getClientMapEntry(@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", -> - beforeEach (done) -> - @rclient.subscribe = sinon.stub() + 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() - @first = @ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef" - # ignore error - @first.catch((()->)) - expect(@ChannelManager.getClientMapEntry(@rclient).get("applied-ops:1234567890abcdef")).to.equal @first + .onSecondCall().resolves(); + this.first = this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef"); + // ignore error + this.first.catch((function(){})); + expect(this.ChannelManager.getClientMapEntry(this.rclient).get("applied-ops:1234567890abcdef")).to.equal(this.first); - @rclient.unsubscribe = sinon.stub().resolves() - @ChannelManager.unsubscribe @rclient, "applied-ops", "1234567890abcdef" - @second = @ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef" - # should get replaced immediately - expect(@ChannelManager.getClientMapEntry(@rclient).get("applied-ops:1234567890abcdef")).to.equal @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 - setTimeout done + // let the first subscribe error -> unsubscribe -> subscribe + return setTimeout(done); + }); - it "should cleanup the second subscribePromise", -> - expect(@ChannelManager.getClientMapEntry(@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); + }); + }); - describe "when there is an existing subscription for another redis client but not this one", -> - beforeEach (done) -> - @other_rclient.subscribe = sinon.stub().resolves() - @ChannelManager.subscribe @other_rclient, "applied-ops", "1234567890abcdef" - @rclient.subscribe = sinon.stub().resolves() # discard the original stub - @ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef" - 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); + }); - it "should subscribe to the redis channel on this redis client", -> - @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", -> + describe("unsubscribe", function() { - describe "when there is no existing subscription for this redis client", -> - beforeEach (done) -> - @rclient.unsubscribe = sinon.stub().resolves() - @ChannelManager.unsubscribe @rclient, "applied-ops", "1234567890abcdef" - setTimeout done + 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); + }); - it "should unsubscribe from the redis channel", -> - @rclient.unsubscribe.called.should.equal true + return it("should unsubscribe from the redis channel", function() { + return this.rclient.unsubscribe.called.should.equal(true); + }); + }); - describe "when there is an existing subscription for this another redis client but not this one", -> - beforeEach (done) -> - @other_rclient.subscribe = sinon.stub().resolves() - @rclient.unsubscribe = sinon.stub().resolves() - @ChannelManager.subscribe @other_rclient, "applied-ops", "1234567890abcdef" - @ChannelManager.unsubscribe @rclient, "applied-ops", "1234567890abcdef" - 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); + }); - it "should still unsubscribe from the redis channel on this client", -> - @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", -> - beforeEach (done) -> - @rclient.subscribe = sinon.stub().resolves() - @ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef" - @rclient.unsubscribe = sinon.stub().rejects(new Error("some redis error")) - @ChannelManager.unsubscribe @rclient, "applied-ops", "1234567890abcdef" - setTimeout 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; + }); - it "should have cleaned up", -> - @ChannelManager.getClientMapEntry(@rclient).has("applied-ops:1234567890abcdef").should.equal false + it("should have cleaned up", function() { + return this.ChannelManager.getClientMapEntry(this.rclient).has("applied-ops:1234567890abcdef").should.equal(false); + }); - it "should not error out when subscribing again", (done) -> - p = @ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef" - p.then () -> - done() - .catch done - 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; + }); + }); - describe "when unsubscribe errors and another client subscribes at the same time", -> - beforeEach (done) -> - @rclient.subscribe = sinon.stub().resolves() - @ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef" - rejectSubscribe = undefined - @rclient.unsubscribe = () -> - return new Promise (resolve, reject) -> - rejectSubscribe = reject - @ChannelManager.unsubscribe @rclient, "applied-ops", "1234567890abcdef" + 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 = undefined; + this.rclient.unsubscribe = () => new Promise((resolve, reject) => rejectSubscribe = reject); + this.ChannelManager.unsubscribe(this.rclient, "applied-ops", "1234567890abcdef"); - setTimeout () => - # delay, actualUnsubscribe should not see the new subscribe request - @ChannelManager.subscribe(@rclient, "applied-ops", "1234567890abcdef") - .then () -> - setTimeout done - .catch done - setTimeout -> - # delay, rejectSubscribe is not defined immediately - rejectSubscribe(new Error("redis error")) - return null + 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; + }); - it "should have recorded the error", -> - expect(@metrics.inc.calledWithExactly("unsubscribe.failed.applied-ops")).to.equal(true) + it("should have recorded the error", function() { + return expect(this.metrics.inc.calledWithExactly("unsubscribe.failed.applied-ops")).to.equal(true); + }); - it "should have subscribed", -> - @rclient.subscribe.called.should.equal true + it("should have subscribed", function() { + return this.rclient.subscribe.called.should.equal(true); + }); - it "should have discarded the finished Promise", -> - @ChannelManager.getClientMapEntry(@rclient).has("applied-ops:1234567890abcdef").should.equal false + return it("should have discarded the finished Promise", function() { + return this.ChannelManager.getClientMapEntry(this.rclient).has("applied-ops:1234567890abcdef").should.equal(false); + }); + }); - describe "when there is an existing subscription for this redis client", -> - beforeEach (done) -> - @rclient.subscribe = sinon.stub().resolves() - @rclient.unsubscribe = sinon.stub().resolves() - @ChannelManager.subscribe @rclient, "applied-ops", "1234567890abcdef" - @ChannelManager.unsubscribe @rclient, "applied-ops", "1234567890abcdef" - setTimeout done + 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 unsubscribe from the redis channel", -> - @rclient.unsubscribe.calledWithExactly("applied-ops:1234567890abcdef").should.equal true + return it("should unsubscribe from the redis channel", function() { + return this.rclient.unsubscribe.calledWithExactly("applied-ops:1234567890abcdef").should.equal(true); + }); + }); + }); - describe "publish", -> + return describe("publish", function() { - describe "when the channel is 'all'", -> - beforeEach -> - @rclient.publish = sinon.stub() - @ChannelManager.publish @rclient, "applied-ops", "all", "random-message" + 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"); + }); - it "should publish on the base channel", -> - @rclient.publish.calledWithExactly("applied-ops", "random-message").should.equal true + return it("should publish on the base channel", function() { + return this.rclient.publish.calledWithExactly("applied-ops", "random-message").should.equal(true); + }); + }); - describe "when the channel has an specific id", -> + describe("when the channel has an specific id", function() { - describe "when the individual channel setting is false", -> - beforeEach -> - @rclient.publish = sinon.stub() - @settings.publishOnIndividualChannels = false - @ChannelManager.publish @rclient, "applied-ops", "1234567890abcdef", "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"); + }); - it "should publish on the per-id channel", -> - @rclient.publish.calledWithExactly("applied-ops", "random-message").should.equal true - @rclient.publish.calledOnce.should.equal true + 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 individual channel setting is true", -> - beforeEach -> - @rclient.publish = sinon.stub() - @settings.publishOnIndividualChannels = true - @ChannelManager.publish @rclient, "applied-ops", "1234567890abcdef", "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"); + }); - it "should publish on the per-id channel", -> - @rclient.publish.calledWithExactly("applied-ops:1234567890abcdef", "random-message").should.equal true - @rclient.publish.calledOnce.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 "metrics", -> - beforeEach -> - @rclient.publish = sinon.stub() - @ChannelManager.publish @rclient, "applied-ops", "all", "random-message" + return describe("metrics", function() { + beforeEach(function() { + this.rclient.publish = sinon.stub(); + return this.ChannelManager.publish(this.rclient, "applied-ops", "all", "random-message"); + }); - it "should track the payload size", -> - @metrics.summary.calledWithExactly( + return it("should track the payload size", function() { + return this.metrics.summary.calledWithExactly( "redis.publish.applied-ops", "random-message".length - ).should.equal true + ).should.equal(true); + }); + }); + }); +}); diff --git a/services/real-time/test/unit/coffee/ConnectedUsersManagerTests.js b/services/real-time/test/unit/coffee/ConnectedUsersManagerTests.js index 6fb3942b64..c1657e3669 100644 --- a/services/real-time/test/unit/coffee/ConnectedUsersManagerTests.js +++ b/services/real-time/test/unit/coffee/ConnectedUsersManagerTests.js @@ -1,164 +1,221 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../app/js/ConnectedUsersManager" -expect = require("chai").expect -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", -> +describe("ConnectedUsersManager", function() { - beforeEach -> + beforeEach(function() { - @settings = - redis: - realtime: - key_schema: - clientsInProject: ({project_id}) -> "clients_in_project:#{project_id}" - connectedUser: ({project_id, client_id})-> "connected_user:#{project_id}:#{client_id}" - @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 @rClient - tk.freeze(new Date()) + 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()); - @ConnectedUsersManager = SandboxedModule.require modulePath, requires: - "settings-sharelatex":@settings - "logger-sharelatex": log:-> - "redis-sharelatex": createClient:=> - return @rClient - @client_id = "32132132" - @project_id = "dskjh2u21321" - @user = { - _id: "user-id-123" - first_name: "Joe" - last_name: "Bloggs" - email: "joe@example.com" + this.ConnectedUsersManager = SandboxedModule.require(modulePath, { requires: { + "settings-sharelatex":this.settings, + "logger-sharelatex": { log() {} + }, + "redis-sharelatex": { createClient:() => { + return this.rClient; + } } - @cursorData = { row: 12, column: 9, doc_id: '53c3b8c85fee64000023dc6e' } + } + } + ); + 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' };}); - afterEach -> - tk.reset() + afterEach(() => tk.reset()); - describe "updateUserPosition", -> - beforeEach -> - @rClient.exec.callsArgWith(0) + describe("updateUserPosition", function() { + beforeEach(function() { + return this.rClient.exec.callsArgWith(0); + }); - it "should set a key with the date and give it a ttl", (done)-> - @ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=> - @rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "last_updated_at", Date.now()).should.equal true - 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 user_id", (done)-> - @ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=> - @rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "user_id", @user._id).should.equal true - 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 first_name", (done)-> - @ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=> - @rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "first_name", @user.first_name).should.equal true - 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 set a key with the last_name", (done)-> - @ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=> - @rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "last_name", @user.last_name).should.equal true - 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 email", (done)-> - @ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=> - @rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "email", @user.email).should.equal true - 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 push the client_id on to the project list", (done)-> - @ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=> - @rClient.sadd.calledWith("clients_in_project:#{@project_id}", @client_id).should.equal true - 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 add a ttl to the project set so it stays clean", (done)-> - @ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=> - @rClient.expire.calledWith("clients_in_project:#{@project_id}", 24 * 4 * 60 * 60).should.equal true - 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 add a ttl to the connected user so it stays clean", (done) -> - @ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, null, (err)=> - @rClient.expire.calledWith("connected_user:#{@project_id}:#{@client_id}", 60 * 15).should.equal true - 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 set the cursor position when provided", (done)-> - @ConnectedUsersManager.updateUserPosition @project_id, @client_id, @user, @cursorData, (err)=> - @rClient.hset.calledWith("connected_user:#{@project_id}:#{@client_id}", "cursorData", JSON.stringify(@cursorData)).should.equal true - 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(); + }); + }); + }); - describe "markUserAsDisconnected", -> - beforeEach -> - @rClient.exec.callsArgWith(0) + describe("markUserAsDisconnected", function() { + beforeEach(function() { + return this.rClient.exec.callsArgWith(0); + }); - it "should remove the user from the set", (done)-> - @ConnectedUsersManager.markUserAsDisconnected @project_id, @client_id, (err)=> - @rClient.srem.calledWith("clients_in_project:#{@project_id}", @client_id).should.equal true - 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(); + }); + }); - it "should delete the connected_user string", (done)-> - @ConnectedUsersManager.markUserAsDisconnected @project_id, @client_id, (err)=> - @rClient.del.calledWith("connected_user:#{@project_id}:#{@client_id}").should.equal true - 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 add a ttl to the connected user set so it stays clean", (done)-> - @ConnectedUsersManager.markUserAsDisconnected @project_id, @client_id, (err)=> - @rClient.expire.calledWith("clients_in_project:#{@project_id}", 24 * 4 * 60 * 60).should.equal true - 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(); + }); + }); + }); - describe "_getConnectedUser", -> + describe("_getConnectedUser", function() { - it "should return a connected user if there is a user object", (done)-> - cursorData = JSON.stringify(cursorData:{row:1}) - @rClient.hgetall.callsArgWith(1, null, {connected_at:new Date(), user_id: @user._id, last_updated_at: "#{Date.now()}", cursorData}) - @ConnectedUsersManager._getConnectedUser @project_id, @client_id, (err, result)=> - result.connected.should.equal true - result.client_id.should.equal @client_id - done() + 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", (done)-> - @rClient.hgetall.callsArgWith(1, null, null) - @ConnectedUsersManager._getConnectedUser @project_id, @client_id, (err, result)=> - result.connected.should.equal false - result.client_id.should.equal @client_id - 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(); + }); + }); - it "should return a not connected user if there is an empty object", (done)-> - @rClient.hgetall.callsArgWith(1, null, {}) - @ConnectedUsersManager._getConnectedUser @project_id, @client_id, (err, result)=> - result.connected.should.equal false - result.client_id.should.equal @client_id - 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 "getConnectedUsers", -> + return describe("getConnectedUsers", function() { - beforeEach -> - @users = ["1234", "5678", "9123", "8234"] - @rClient.smembers.callsArgWith(1, null, @users) - @ConnectedUsersManager._getConnectedUser = sinon.stub() - @ConnectedUsersManager._getConnectedUser.withArgs(@project_id, @users[0]).callsArgWith(2, null, {connected:true, client_age: 2, client_id:@users[0]}) - @ConnectedUsersManager._getConnectedUser.withArgs(@project_id, @users[1]).callsArgWith(2, null, {connected:false, client_age: 1, client_id:@users[1]}) - @ConnectedUsersManager._getConnectedUser.withArgs(@project_id, @users[2]).callsArgWith(2, null, {connected:true, client_age: 3, client_id:@users[2]}) - @ConnectedUsersManager._getConnectedUser.withArgs(@project_id, @users[3]).callsArgWith(2, null, {connected:true, client_age: 11, client_id:@users[3]}) # connected but old + 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 - it "should only return the users in the list which are still in redis and recently updated", (done)-> - @ConnectedUsersManager.getConnectedUsers @project_id, (err, users)=> - users.length.should.equal 2 - users[0].should.deep.equal {client_id:@users[0], client_age: 2, connected:true} - users[1].should.deep.equal {client_id:@users[2], client_age: 3, connected:true} - done() + 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/coffee/DocumentUpdaterControllerTests.js b/services/real-time/test/unit/coffee/DocumentUpdaterControllerTests.js index b2e52c7d56..9d15a77394 100644 --- a/services/real-time/test/unit/coffee/DocumentUpdaterControllerTests.js +++ b/services/real-time/test/unit/coffee/DocumentUpdaterControllerTests.js @@ -1,153 +1,203 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -modulePath = require('path').join __dirname, '../../../app/js/DocumentUpdaterController' -MockClient = require "./helpers/MockClient" +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * 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"); -describe "DocumentUpdaterController", -> - beforeEach -> - @project_id = "project-id-123" - @doc_id = "doc-id-123" - @callback = sinon.stub() - @io = { "mock": "socket.io" } - @rclient = [] - @RoomEvents = { on: sinon.stub() } - @EditorUpdatesController = SandboxedModule.require modulePath, requires: - "logger-sharelatex": @logger = { error: sinon.stub(), log: sinon.stub(), warn: sinon.stub() } - "settings-sharelatex": @settings = - redis: - documentupdater: - key_schema: - pendingUpdates: ({doc_id}) -> "PendingUpdates:#{doc_id}" +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" : @redis = - createClient: (name) => - @rclient.push(rclientStub = {name:name}) - return rclientStub - "./SafeJsonParse": @SafeJsonParse = - parse: (data, cb) => cb null, JSON.parse(data) - "./EventLogger": @EventLogger = {checkEventOrder: sinon.stub()} - "./HealthCheckManager": {check: sinon.stub()} - "metrics-sharelatex": @metrics = {inc: sinon.stub()} - "./RoomManager" : @RoomManager = { eventSource: sinon.stub().returns @RoomEvents} - "./ChannelManager": @ChannelManager = {} + } + }), + "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", -> - beforeEach -> - @rclient.length = 0 # clear any existing clients - @EditorUpdatesController.rclientList = [@redis.createClient("first"), @redis.createClient("second")] - @rclient[0].subscribe = sinon.stub() - @rclient[0].on = sinon.stub() - @rclient[1].subscribe = sinon.stub() - @rclient[1].on = sinon.stub() - @EditorUpdatesController.listenForUpdatesFromDocumentUpdater() + 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", -> - @rclient[0].subscribe.calledWith("applied-ops").should.equal true + it("should subscribe to the doc-updater stream", function() { + return this.rclient[0].subscribe.calledWith("applied-ops").should.equal(true); + }); - it "should register a callback to handle updates", -> - @rclient[0].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); + }); - it "should subscribe to any additional doc-updater stream", -> - @rclient[1].subscribe.calledWith("applied-ops").should.equal true - @rclient[1].on.calledWith("message").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 "_processMessageFromDocumentUpdater", -> - describe "with bad JSON", -> - beforeEach -> - @SafeJsonParse.parse = sinon.stub().callsArgWith 1, new Error("oops") - @EditorUpdatesController._processMessageFromDocumentUpdater @io, "applied-ops", "blah" + 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"); + }); - it "should log an error", -> - @logger.error.called.should.equal true + return it("should log an error", function() { + return this.logger.error.called.should.equal(true); + }); + }); - describe "with update", -> - beforeEach -> - @message = - doc_id: @doc_id + describe("with update", function() { + beforeEach(function() { + this.message = { + doc_id: this.doc_id, op: {t: "foo", p: 12} - @EditorUpdatesController._applyUpdateFromDocumentUpdater = sinon.stub() - @EditorUpdatesController._processMessageFromDocumentUpdater @io, "applied-ops", JSON.stringify(@message) + }; + this.EditorUpdatesController._applyUpdateFromDocumentUpdater = sinon.stub(); + return this.EditorUpdatesController._processMessageFromDocumentUpdater(this.io, "applied-ops", JSON.stringify(this.message)); + }); - it "should apply the update", -> - @EditorUpdatesController._applyUpdateFromDocumentUpdater - .calledWith(@io, @doc_id, @message.op) - .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 "with error", -> - beforeEach -> - @message = - doc_id: @doc_id + return describe("with error", function() { + beforeEach(function() { + this.message = { + doc_id: this.doc_id, error: "Something went wrong" - @EditorUpdatesController._processErrorFromDocumentUpdater = sinon.stub() - @EditorUpdatesController._processMessageFromDocumentUpdater @io, "applied-ops", JSON.stringify(@message) + }; + this.EditorUpdatesController._processErrorFromDocumentUpdater = sinon.stub(); + return this.EditorUpdatesController._processMessageFromDocumentUpdater(this.io, "applied-ops", JSON.stringify(this.message)); + }); - it "should process the error", -> - @EditorUpdatesController._processErrorFromDocumentUpdater - .calledWith(@io, @doc_id, @message.error) - .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); + }); + }); + }); - describe "_applyUpdateFromDocumentUpdater", -> - beforeEach -> - @sourceClient = new MockClient() - @otherClients = [new MockClient(), new MockClient()] - @update = - op: [ t: "foo", p: 12 ] - meta: source: @sourceClient.publicId - v: @version = 42 - doc: @doc_id - @io.sockets = - clients: sinon.stub().returns([@sourceClient, @otherClients..., @sourceClient]) # include a duplicate client + 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", -> - beforeEach -> - @EditorUpdatesController._applyUpdateFromDocumentUpdater @io, @doc_id, @update + describe("normally", function() { + beforeEach(function() { + return this.EditorUpdatesController._applyUpdateFromDocumentUpdater(this.io, this.doc_id, this.update); + }); - it "should send a version bump to the source client", -> - @sourceClient.emit - .calledWith("otUpdateApplied", v: @version, doc: @doc_id) - .should.equal true - @sourceClient.emit.calledOnce.should.equal true + 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); + }); - it "should get the clients connected to the document", -> - @io.sockets.clients - .calledWith(@doc_id) - .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); + }); - it "should send the full update to the other clients", -> - for client in @otherClients + return it("should send the full update to the other clients", function() { + return Array.from(this.otherClients).map((client) => client.emit - .calledWith("otUpdateApplied", @update) - .should.equal true + .calledWith("otUpdateApplied", this.update) + .should.equal(true)); + }); + }); - describe "with a duplicate op", -> - beforeEach -> - @update.dup = true - @EditorUpdatesController._applyUpdateFromDocumentUpdater @io, @doc_id, @update + 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", -> - @sourceClient.emit - .calledWith("otUpdateApplied", v: @version, doc: @doc_id) - .should.equal true + 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); + }); - it "should not send anything to the other clients (they've already had the op)", -> - for client in @otherClients + 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 + .should.equal(false)); + }); + }); + }); - describe "_processErrorFromDocumentUpdater", -> - beforeEach -> - @clients = [new MockClient(), new MockClient()] - @io.sockets = - clients: sinon.stub().returns(@clients) - @EditorUpdatesController._processErrorFromDocumentUpdater @io, @doc_id, "Something went wrong" + 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", -> - @logger.warn.called.should.equal true + it("should log a warning", function() { + return this.logger.warn.called.should.equal(true); + }); - it "should disconnect all clients in that document", -> - @io.sockets.clients.calledWith(@doc_id).should.equal true - for client in @clients - client.disconnect.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/coffee/DocumentUpdaterManagerTests.js b/services/real-time/test/unit/coffee/DocumentUpdaterManagerTests.js index aa4600d757..c5117a2fc0 100644 --- a/services/real-time/test/unit/coffee/DocumentUpdaterManagerTests.js +++ b/services/real-time/test/unit/coffee/DocumentUpdaterManagerTests.js @@ -1,193 +1,260 @@ -require('chai').should() -sinon = require("sinon") -SandboxedModule = require('sandboxed-module') -path = require "path" -modulePath = '../../../app/js/DocumentUpdaterManager' +/* + * decaffeinate suggestions: + * 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'; -describe 'DocumentUpdaterManager', -> - beforeEach -> - @project_id = "project-id-923" - @doc_id = "doc-id-394" - @lines = ["one", "two", "three"] - @version = 42 - @settings = - apis: documentupdater: url: "http://doc-updater.example.com" - redis: documentupdater: - key_schema: - pendingUpdates: ({doc_id}) -> "PendingUpdates:#{doc_id}" - maxUpdateSize: 7 * 1024 * 1024 - @rclient = {auth:->} - - @DocumentUpdaterManager = SandboxedModule.require modulePath, - requires: - 'settings-sharelatex':@settings - 'logger-sharelatex': @logger = {log: sinon.stub(), error: sinon.stub(), warn: sinon.stub()} - 'request': @request = {} - 'redis-sharelatex' : createClient: () => @rclient - 'metrics-sharelatex': @Metrics = - summary: sinon.stub() - Timer: class Timer - done: () -> - globals: - JSON: @JSON = Object.create(JSON) # avoid modifying JSON object directly - - describe "getDocument", -> - beforeEach -> - @callback = sinon.stub() - - describe "successfully", -> - beforeEach -> - @body = JSON.stringify - lines: @lines - version: @version - ops: @ops = ["mock-op-1", "mock-op-2"] - ranges: @ranges = {"mock": "ranges"} - @fromVersion = 2 - @request.get = sinon.stub().callsArgWith(1, null, {statusCode: 200}, @body) - @DocumentUpdaterManager.getDocument @project_id, @doc_id, @fromVersion, @callback - - it 'should get the document from the document updater', -> - url = "#{@settings.apis.documentupdater.url}/project/#{@project_id}/doc/#{@doc_id}?fromVersion=#{@fromVersion}" - @request.get.calledWith(url).should.equal true - - it "should call the callback with the lines, version, ranges and ops", -> - @callback.calledWith(null, @lines, @version, @ranges, @ops).should.equal true - - describe "when the document updater API returns an error", -> - beforeEach -> - @request.get = sinon.stub().callsArgWith(1, @error = new Error("something went wrong"), null, null) - @DocumentUpdaterManager.getDocument @project_id, @doc_id, @fromVersion, @callback - - it "should return an error to the callback", -> - @callback.calledWith(@error).should.equal true - - [404, 422].forEach (statusCode) -> - describe "when the document updater returns a #{statusCode} status code", -> - beforeEach -> - @request.get = sinon.stub().callsArgWith(1, null, { statusCode }, "") - @DocumentUpdaterManager.getDocument @project_id, @doc_id, @fromVersion, @callback - - it "should return the callback with an error", -> - @callback.called.should.equal(true) - err = @callback.getCall(0).args[0] - err.should.have.property('statusCode', statusCode) - err.should.have.property('message', "doc updater could not load requested ops") - @logger.error.called.should.equal(false) - @logger.warn.called.should.equal(true) - - describe "when the document updater returns a failure error code", -> - beforeEach -> - @request.get = sinon.stub().callsArgWith(1, null, { statusCode: 500 }, "") - @DocumentUpdaterManager.getDocument @project_id, @doc_id, @fromVersion, @callback - - it "should return the callback with an error", -> - @callback.called.should.equal(true) - err = @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") - @logger.error.called.should.equal(true) - - describe 'flushProjectToMongoAndDelete', -> - beforeEach -> - @callback = sinon.stub() - - describe "successfully", -> - beforeEach -> - @request.del = sinon.stub().callsArgWith(1, null, {statusCode: 204}, "") - @DocumentUpdaterManager.flushProjectToMongoAndDelete @project_id, @callback - - it 'should delete the project from the document updater', -> - url = "#{@settings.apis.documentupdater.url}/project/#{@project_id}?background=true" - @request.del.calledWith(url).should.equal true - - it "should call the callback with no error", -> - @callback.calledWith(null).should.equal true - - describe "when the document updater API returns an error", -> - beforeEach -> - @request.del = sinon.stub().callsArgWith(1, @error = new Error("something went wrong"), null, null) - @DocumentUpdaterManager.flushProjectToMongoAndDelete @project_id, @callback - - it "should return an error to the callback", -> - @callback.calledWith(@error).should.equal true - - describe "when the document updater returns a failure error code", -> - beforeEach -> - @request.del = sinon.stub().callsArgWith(1, null, { statusCode: 500 }, "") - @DocumentUpdaterManager.flushProjectToMongoAndDelete @project_id, @callback - - it "should return the callback with an error", -> - @callback.called.should.equal(true) - err = @callback.getCall(0).args[0] - err.should.have.property('statusCode', 500) - err.should.have.property('message', "document updater returned a failure status code: 500") - - describe 'queueChange', -> - beforeEach -> - @change = { - "doc":"1234567890", - "op":["d":"test", "p":345] - "v": 789 - } - @rclient.rpush = sinon.stub().yields() - @callback = sinon.stub() - - describe "successfully", -> - beforeEach -> - @DocumentUpdaterManager.queueChange(@project_id, @doc_id, @change, @callback) - - it "should push the change", -> - @rclient.rpush - .calledWith("PendingUpdates:#{@doc_id}", JSON.stringify(@change)) - .should.equal true - - it "should notify the doc updater of the change via the pending-updates-list queue", -> - @rclient.rpush - .calledWith("pending-updates-list", "#{@project_id}:#{@doc_id}") - .should.equal true - - describe "with error talking to redis during rpush", -> - beforeEach -> - @rclient.rpush = sinon.stub().yields(new Error("something went wrong")) - @DocumentUpdaterManager.queueChange(@project_id, @doc_id, @change, @callback) - - it "should return an error", -> - @callback.calledWithExactly(sinon.match(Error)).should.equal true - - describe "with null byte corruption", -> - beforeEach -> - @JSON.stringify = () -> return '["bad bytes! \u0000 <- here"]' - @DocumentUpdaterManager.queueChange(@project_id, @doc_id, @change, @callback) - - it "should return an error", -> - @callback.calledWithExactly(sinon.match(Error)).should.equal true - - it "should not push the change onto the pending-updates-list queue", -> - @rclient.rpush.called.should.equal false - - describe "when the update is too large", -> - beforeEach -> - @change = {op: {p: 12,t: "update is too large".repeat(1024 * 400)}} - @DocumentUpdaterManager.queueChange(@project_id, @doc_id, @change, @callback) - - it "should return an error", -> - @callback.calledWithExactly(sinon.match(Error)).should.equal true - - it "should add the size to the error", -> - @callback.args[0][0].updateSize.should.equal 7782422 - - it "should not push the change onto the pending-updates-list queue", -> - @rclient.rpush.called.should.equal false - - describe "with invalid keys", -> - beforeEach -> - @change = { - "op":["d":"test", "p":345] - "version": 789 # not a valid key +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}`; } } - @DocumentUpdaterManager.queueChange(@project_id, @doc_id, @change, @callback) + } + }, + maxUpdateSize: 7 * 1024 * 1024 + }; + this.rclient = {auth() {}}; - it "should remove the invalid keys from the change", -> - @rclient.rpush - .calledWith("PendingUpdates:#{@doc_id}", JSON.stringify({op:@change.op})) - .should.equal true + 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("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); + }); + + 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); + }); + + 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); + }); + + 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 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("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); + }); + + 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); + }); + + 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 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(); + }); + + 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); + }); + + 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); + }); + + 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); + }); + + 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); + }); + }); + + 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 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 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); + }); + }); + }); +}); diff --git a/services/real-time/test/unit/coffee/DrainManagerTests.js b/services/real-time/test/unit/coffee/DrainManagerTests.js index 88009f02cd..87bdaeb6d3 100644 --- a/services/real-time/test/unit/coffee/DrainManagerTests.js +++ b/services/real-time/test/unit/coffee/DrainManagerTests.js @@ -1,81 +1,113 @@ -should = require('chai').should() -sinon = require "sinon" -SandboxedModule = require('sandboxed-module') -path = require "path" -modulePath = path.join __dirname, "../../../app/js/DrainManager" +/* + * decaffeinate suggestions: + * 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"); -describe "DrainManager", -> - beforeEach -> - @DrainManager = SandboxedModule.require modulePath, requires: - "logger-sharelatex": @logger = log: sinon.stub() - @io = - sockets: +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", -> - beforeEach -> - @clients = [] - for i in [0..5399] - @clients[i] = { - id: i + describe("startDrainTimeWindow", function() { + beforeEach(function() { + this.clients = []; + for (let i = 0; i <= 5399; i++) { + this.clients[i] = { + id: i, emit: sinon.stub() - } - @io.sockets.clients.returns @clients - @DrainManager.startDrain = sinon.stub() + }; + } + this.io.sockets.clients.returns(this.clients); + return this.DrainManager.startDrain = sinon.stub(); + }); - it "should set a drain rate fast enough", (done)-> - @DrainManager.startDrainTimeWindow(@io, 9) - @DrainManager.startDrain.calledWith(@io, 10).should.equal true - 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(); + }); + }); - describe "reconnectNClients", -> - beforeEach -> - @clients = [] - for i in [0..9] - @clients[i] = { - id: i + return describe("reconnectNClients", function() { + beforeEach(function() { + this.clients = []; + for (let i = 0; i <= 9; i++) { + this.clients[i] = { + id: i, emit: sinon.stub() - } - @io.sockets.clients.returns @clients + }; + } + return this.io.sockets.clients.returns(this.clients); + }); - describe "after first pass", -> - beforeEach -> - @DrainManager.reconnectNClients(@io, 3) + return describe("after first pass", function() { + beforeEach(function() { + return this.DrainManager.reconnectNClients(this.io, 3); + }); - it "should reconnect the first 3 clients", -> - for i in [0..2] - @clients[i].emit.calledWith("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", -> - for i in [3..9] - @clients[i].emit.calledWith("reconnectGracefully").should.equal false + 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)); + }); - describe "after second pass", -> - beforeEach -> - @DrainManager.reconnectNClients(@io, 3) + return describe("after second pass", function() { + beforeEach(function() { + return this.DrainManager.reconnectNClients(this.io, 3); + }); - it "should reconnect the next 3 clients", -> - for i in [3..5] - @clients[i].emit.calledWith("reconnectGracefully").should.equal true + 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", -> - for i in [6..9] - @clients[i].emit.calledWith("reconnectGracefully").should.equal false + 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", -> - for i in [0..2] - @clients[i].emit.calledOnce.should.equal true + it("should not reconnect the first 3 clients again", function() { + return [0, 1, 2].map((i) => + this.clients[i].emit.calledOnce.should.equal(true)); + }); - describe "after final pass", -> - beforeEach -> - @DrainManager.reconnectNClients(@io, 100) + return describe("after final pass", function() { + beforeEach(function() { + return this.DrainManager.reconnectNClients(this.io, 100); + }); - it "should not reconnect the first 6 clients again", -> - for i in [0..5] - @clients[i].emit.calledOnce.should.equal true + 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)); + }); - it "should log out that it reached the end", -> - @logger.log + 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 + .should.equal(true); + }); + }); + }); + }); + }); +}); diff --git a/services/real-time/test/unit/coffee/EventLoggerTests.js b/services/real-time/test/unit/coffee/EventLoggerTests.js index 93350af848..ab74861069 100644 --- a/services/real-time/test/unit/coffee/EventLoggerTests.js +++ b/services/real-time/test/unit/coffee/EventLoggerTests.js @@ -1,76 +1,101 @@ -require('chai').should() -expect = require("chai").expect -SandboxedModule = require('sandboxed-module') -modulePath = '../../../app/js/EventLogger' -sinon = require("sinon") -tk = require "timekeeper" +/* + * decaffeinate suggestions: + * 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"); -describe 'EventLogger', -> - beforeEach -> - @start = Date.now() - tk.freeze(new Date(@start)) - @EventLogger = SandboxedModule.require modulePath, requires: - "logger-sharelatex": @logger = {error: sinon.stub(), warn: sinon.stub()} - "metrics-sharelatex": @metrics = {inc: sinon.stub()} - @channel = "applied-ops" - @id_1 = "random-hostname:abc-1" - @message_1 = "message-1" - @id_2 = "random-hostname:abc-2" - @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 -> - tk.reset() + afterEach(() => tk.reset()); - describe 'checkEventOrder', -> + return describe('checkEventOrder', function() { - describe 'when the events are in order', -> - beforeEach -> - @EventLogger.checkEventOrder(@channel, @id_1, @message_1) - @status = @EventLogger.checkEventOrder(@channel, @id_2, @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', -> - expect(@status).to.be.undefined + it('should accept events in order', function() { + return expect(this.status).to.be.undefined; + }); - it 'should increment the valid event metric', -> - @metrics.inc.calledWith("event.#{@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', -> - beforeEach -> - @EventLogger.checkEventOrder(@channel, @id_1, @message_1) - @status = @EventLogger.checkEventOrder(@channel, @id_1, @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', -> - expect(@status).to.equal "duplicate" + it('should return "duplicate" for the same event', function() { + return expect(this.status).to.equal("duplicate"); + }); - it 'should increment the duplicate event metric', -> - @metrics.inc.calledWith("event.#{@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', -> - beforeEach -> - @EventLogger.checkEventOrder(@channel, @id_1, @message_1) - @EventLogger.checkEventOrder(@channel, @id_2, @message_2) - @status = @EventLogger.checkEventOrder(@channel, @id_1, @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', -> - expect(@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"); + }); - it 'should increment the out-of-order event metric', -> - @metrics.inc.calledWith("event.#{@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; + }); + }); - describe 'after MAX_STALE_TIME_IN_MS', -> - it 'should flush old entries', -> - @EventLogger.MAX_EVENTS_BEFORE_CLEAN = 10 - @EventLogger.checkEventOrder(@channel, @id_1, @message_1) - for i in [1..8] - status = @EventLogger.checkEventOrder(@channel, @id_1, @message_1) - expect(status).to.equal "duplicate" - # the next event should flush the old entries aboce - @EventLogger.MAX_STALE_TIME_IN_MS=1000 - tk.freeze(new Date(@start + 5 * 1000)) - # because we flushed the entries this should not be a duplicate - @EventLogger.checkEventOrder(@channel, 'other-1', @message_2) - status = @EventLogger.checkEventOrder(@channel, @id_1, @message_1) - expect(status).to.be.undefined \ No newline at end of file + return describe('after MAX_STALE_TIME_IN_MS', () => 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 diff --git a/services/real-time/test/unit/coffee/RoomManagerTests.js b/services/real-time/test/unit/coffee/RoomManagerTests.js index c81663576d..63c25b3eae 100644 --- a/services/real-time/test/unit/coffee/RoomManagerTests.js +++ b/services/real-time/test/unit/coffee/RoomManagerTests.js @@ -1,288 +1,359 @@ -chai = require('chai') -expect = chai.expect -should = chai.should() -sinon = require("sinon") -modulePath = "../../../app/js/RoomManager.js" -SandboxedModule = require('sandboxed-module') +/* + * decaffeinate suggestions: + * 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'); -describe 'RoomManager', -> - beforeEach -> - @project_id = "project-id-123" - @doc_id = "doc-id-456" - @other_doc_id = "doc-id-789" - @client = {namespace: {name: ''}, id: "first-client"} - @RoomManager = SandboxedModule.require modulePath, requires: - "settings-sharelatex": @settings = {} - "logger-sharelatex": @logger = { log: sinon.stub(), warn: sinon.stub(), error: sinon.stub() } - "metrics-sharelatex": @metrics = { gauge: sinon.stub() } - @RoomManager._clientsInRoom = sinon.stub() - @RoomManager._clientAlreadyInRoom = sinon.stub() - @RoomEvents = @RoomManager.eventSource() - sinon.spy(@RoomEvents, 'emit') - sinon.spy(@RoomEvents, 'once') +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", -> - describe "when a subscribe errors", -> - afterEach () -> - process.removeListener("unhandledRejection", @onUnhandled) + describe("emitOnCompletion", () => describe("when a subscribe errors", function() { + afterEach(function() { + return process.removeListener("unhandledRejection", this.onUnhandled); + }); - beforeEach (done) -> - @onUnhandled = (error) => - @unhandledError = error - done(new Error("unhandledRejection: #{error.message}")) - process.on("unhandledRejection", @onUnhandled) + beforeEach(function(done) { + this.onUnhandled = error => { + this.unhandledError = error; + return done(new Error(`unhandledRejection: ${error.message}`)); + }; + process.on("unhandledRejection", this.onUnhandled); - reject = undefined - subscribePromise = new Promise((_, r) -> reject = r) - promises = [subscribePromise] - eventName = "project-subscribed-123" - @RoomEvents.once eventName, () -> - setTimeout(done, 100) - @RoomManager.emitOnCompletion(promises, eventName) - setTimeout(() -> reject(new Error("subscribe failed"))) + let reject = undefined; + 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"))); + }); - it "should keep going", () -> - expect(@unhandledError).to.not.exist + return it("should keep going", function() { + return expect(this.unhandledError).to.not.exist; + }); + })); - describe "joinProject", -> + describe("joinProject", function() { - describe "when the project room is empty", -> + describe("when the project room is empty", function() { - beforeEach (done) -> - @RoomManager._clientsInRoom - .withArgs(@client, @project_id) - .onFirstCall().returns(0) - @client.join = sinon.stub() - @callback = sinon.stub() - @RoomEvents.on 'project-active', (id) => - setTimeout () => - @RoomEvents.emit "project-subscribed-#{id}" - , 100 - @RoomManager.joinProject @client, @project_id, (err) => - @callback(err) - done() + 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", -> - @RoomEvents.emit.calledWithExactly('project-active', @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", -> - @RoomEvents.once.calledWith("project-subscribed-#{@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 join the room using the id", -> - @client.join.calledWithExactly(@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); + }); + }); - describe "when there are other clients in the project room", -> + return describe("when there are other clients in the project room", function() { - beforeEach -> - @RoomManager._clientsInRoom - .withArgs(@client, @project_id) + beforeEach(function() { + this.RoomManager._clientsInRoom + .withArgs(this.client, this.project_id) .onFirstCall().returns(123) - .onSecondCall().returns(124) - @client.join = sinon.stub() - @RoomManager.joinProject @client, @project_id + .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", -> - @client.join.called.should.equal true + it("should join the room using the id", function() { + return this.client.join.called.should.equal(true); + }); - it "should not emit any events", -> - @RoomEvents.emit.called.should.equal false + return it("should not emit any events", function() { + return this.RoomEvents.emit.called.should.equal(false); + }); + }); + }); - describe "joinDoc", -> + describe("joinDoc", function() { - describe "when the doc room is empty", -> + describe("when the doc room is empty", function() { - beforeEach (done) -> - @RoomManager._clientsInRoom - .withArgs(@client, @doc_id) - .onFirstCall().returns(0) - @client.join = sinon.stub() - @callback = sinon.stub() - @RoomEvents.on 'doc-active', (id) => - setTimeout () => - @RoomEvents.emit "doc-subscribed-#{id}" - , 100 - @RoomManager.joinDoc @client, @doc_id, (err) => - @callback(err) - done() + 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", -> - @RoomEvents.emit.calledWithExactly('doc-active', @doc_id).should.equal true + 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 listen for the 'doc-subscribed-id' event", -> - @RoomEvents.once.calledWith("doc-subscribed-#{@doc_id}").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); + }); - it "should join the room using the id", -> - @client.join.calledWithExactly(@doc_id).should.equal true + return it("should join the room using the id", function() { + return this.client.join.calledWithExactly(this.doc_id).should.equal(true); + }); + }); - describe "when there are other clients in the doc room", -> + return describe("when there are other clients in the doc room", function() { - beforeEach -> - @RoomManager._clientsInRoom - .withArgs(@client, @doc_id) + beforeEach(function() { + this.RoomManager._clientsInRoom + .withArgs(this.client, this.doc_id) .onFirstCall().returns(123) - .onSecondCall().returns(124) - @client.join = sinon.stub() - @RoomManager.joinDoc @client, @doc_id + .onSecondCall().returns(124); + this.client.join = sinon.stub(); + return this.RoomManager.joinDoc(this.client, this.doc_id); + }); - it "should join the room using the id", -> - @client.join.called.should.equal true + it("should join the room using the id", function() { + return this.client.join.called.should.equal(true); + }); - it "should not emit any events", -> - @RoomEvents.emit.called.should.equal false + return it("should not emit any events", function() { + return this.RoomEvents.emit.called.should.equal(false); + }); + }); + }); - describe "leaveDoc", -> + describe("leaveDoc", function() { - describe "when doc room will be empty after this client has left", -> + describe("when doc room will be empty after this client has left", function() { - beforeEach -> - @RoomManager._clientAlreadyInRoom - .withArgs(@client, @doc_id) - .returns(true) - @RoomManager._clientsInRoom - .withArgs(@client, @doc_id) - .onCall(0).returns(0) - @client.leave = sinon.stub() - @RoomManager.leaveDoc @client, @doc_id + 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); + }); - it "should leave the room using the id", -> - @client.leave.calledWithExactly(@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); + }); - it "should emit a 'doc-empty' event with the id", -> - @RoomEvents.emit.calledWithExactly('doc-empty', @doc_id).should.equal true + 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 there are other clients in the doc room", -> + describe("when there are other clients in the doc room", function() { - beforeEach -> - @RoomManager._clientAlreadyInRoom - .withArgs(@client, @doc_id) - .returns(true) - @RoomManager._clientsInRoom - .withArgs(@client, @doc_id) - .onCall(0).returns(123) - @client.leave = sinon.stub() - @RoomManager.leaveDoc @client, @doc_id + 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 leave the room using the id", -> - @client.leave.calledWithExactly(@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); + }); - it "should not emit any events", -> - @RoomEvents.emit.called.should.equal false + return it("should not emit any events", function() { + return this.RoomEvents.emit.called.should.equal(false); + }); + }); - describe "when the client is not in the doc room", -> + return describe("when the client is not in the doc room", function() { - beforeEach -> - @RoomManager._clientAlreadyInRoom - .withArgs(@client, @doc_id) - .returns(false) - @RoomManager._clientsInRoom - .withArgs(@client, @doc_id) - .onCall(0).returns(0) - @client.leave = sinon.stub() - @RoomManager.leaveDoc @client, @doc_id + 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 not leave the room", -> - @client.leave.called.should.equal false + it("should not leave the room", function() { + return this.client.leave.called.should.equal(false); + }); - it "should not emit any events", -> - @RoomEvents.emit.called.should.equal false + return it("should not emit any events", function() { + return this.RoomEvents.emit.called.should.equal(false); + }); + }); + }); - describe "leaveProjectAndDocs", -> + return describe("leaveProjectAndDocs", () => describe("when the client is connected to the project and multiple docs", function() { - describe "when the client is connected to the project and multiple docs", -> + 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(); + }); - beforeEach -> - @RoomManager._roomsClientIsIn = sinon.stub().returns [@project_id, @doc_id, @other_doc_id] - @client.join = sinon.stub() - @client.leave = sinon.stub() + describe("when this is the only client connected", function() { - describe "when this is the only client connected", -> + 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(); + }); + }); + }); + }); - beforeEach (done) -> - # first call is for the join, - # second for the leave - @RoomManager._clientsInRoom - .withArgs(@client, @doc_id) - .onCall(0).returns(0) - .onCall(1).returns(0) - @RoomManager._clientsInRoom - .withArgs(@client, @other_doc_id) - .onCall(0).returns(0) - .onCall(1).returns(0) - @RoomManager._clientsInRoom - .withArgs(@client, @project_id) - .onCall(0).returns(0) - .onCall(1).returns(0) - @RoomManager._clientAlreadyInRoom - .withArgs(@client, @doc_id) - .returns(true) - .withArgs(@client, @other_doc_id) - .returns(true) - .withArgs(@client, @project_id) - .returns(true) - @RoomEvents.on 'project-active', (id) => - setTimeout () => - @RoomEvents.emit "project-subscribed-#{id}" - , 100 - @RoomEvents.on 'doc-active', (id) => - setTimeout () => - @RoomEvents.emit "doc-subscribed-#{id}" - , 100 - # put the client in the rooms - @RoomManager.joinProject @client, @project_id, () => - @RoomManager.joinDoc @client, @doc_id, () => - @RoomManager.joinDoc @client, @other_doc_id, () => - # now leave the project - @RoomManager.leaveProjectAndDocs @client - 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 all the docs", -> - @client.leave.calledWithExactly(@doc_id).should.equal true - @client.leave.calledWithExactly(@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 leave the project", -> - @client.leave.calledWithExactly(@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); + }); - it "should emit a 'doc-empty' event with the id for each doc", -> - @RoomEvents.emit.calledWithExactly('doc-empty', @doc_id).should.equal true - @RoomEvents.emit.calledWithExactly('doc-empty', @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); + }); + }); - it "should emit a 'project-empty' event with the id for the project", -> - @RoomEvents.emit.calledWithExactly('project-empty', @project_id).should.equal true + return describe("when other clients are still connected", function() { - describe "when other clients are still connected", -> + 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); + }); - beforeEach -> - @RoomManager._clientsInRoom - .withArgs(@client, @doc_id) - .onFirstCall().returns(123) - .onSecondCall().returns(122) - @RoomManager._clientsInRoom - .withArgs(@client, @other_doc_id) - .onFirstCall().returns(123) - .onSecondCall().returns(122) - @RoomManager._clientsInRoom - .withArgs(@client, @project_id) - .onFirstCall().returns(123) - .onSecondCall().returns(122) - @RoomManager._clientAlreadyInRoom - .withArgs(@client, @doc_id) - .returns(true) - .withArgs(@client, @other_doc_id) - .returns(true) - .withArgs(@client, @project_id) - .returns(true) - @RoomManager.leaveProjectAndDocs @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 all the docs", -> - @client.leave.calledWithExactly(@doc_id).should.equal true - @client.leave.calledWithExactly(@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 leave the project", -> - @client.leave.calledWithExactly(@project_id).should.equal true - - it "should not emit any events", -> - @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); + }); + }); + })); +}); \ No newline at end of file diff --git a/services/real-time/test/unit/coffee/SafeJsonParseTest.js b/services/real-time/test/unit/coffee/SafeJsonParseTest.js index b652a2faae..f417513e47 100644 --- a/services/real-time/test/unit/coffee/SafeJsonParseTest.js +++ b/services/real-time/test/unit/coffee/SafeJsonParseTest.js @@ -1,34 +1,51 @@ -require('chai').should() -expect = require("chai").expect -SandboxedModule = require('sandboxed-module') -modulePath = '../../../app/js/SafeJsonParse' -sinon = require("sinon") +/* + * decaffeinate suggestions: + * 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"); -describe 'SafeJsonParse', -> - beforeEach -> - @SafeJsonParse = SandboxedModule.require modulePath, requires: - "settings-sharelatex": @Settings = { +describe('SafeJsonParse', function() { + beforeEach(function() { + return this.SafeJsonParse = SandboxedModule.require(modulePath, { requires: { + "settings-sharelatex": (this.Settings = { maxUpdateSize: 16 * 1024 - } - "logger-sharelatex": @logger = {error: sinon.stub()} + }), + "logger-sharelatex": (this.logger = {error: sinon.stub()}) + } + });}); - describe "parse", -> - it "should parse documents correctly", (done) -> - @SafeJsonParse.parse '{"foo": "bar"}', (error, parsed) -> - expect(parsed).to.deep.equal {foo: "bar"} - done() + return describe("parse", function() { + it("should parse documents correctly", function(done) { + return this.SafeJsonParse.parse('{"foo": "bar"}', function(error, parsed) { + expect(parsed).to.deep.equal({foo: "bar"}); + return done(); + }); + }); - it "should return an error on bad data", (done) -> - @SafeJsonParse.parse 'blah', (error, parsed) -> - expect(error).to.exist - done() + it("should return an error on bad data", function(done) { + return this.SafeJsonParse.parse('blah', function(error, parsed) { + expect(error).to.exist; + return done(); + }); + }); - it "should return an error on oversized data", (done) -> - # we have a 2k overhead on top of max size - big_blob = Array(16*1024).join("A") - data = "{\"foo\": \"#{big_blob}\"}" - @Settings.maxUpdateSize = 2 * 1024 - @SafeJsonParse.parse data, (error, parsed) => - @logger.error.called.should.equal true - expect(error).to.exist - done() \ No newline at end of file + 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 diff --git a/services/real-time/test/unit/coffee/SessionSocketsTests.js b/services/real-time/test/unit/coffee/SessionSocketsTests.js index 2f81699309..d85be502a7 100644 --- a/services/real-time/test/unit/coffee/SessionSocketsTests.js +++ b/services/real-time/test/unit/coffee/SessionSocketsTests.js @@ -1,126 +1,170 @@ -{EventEmitter} = require('events') -{expect} = require('chai') -SandboxedModule = require('sandboxed-module') -modulePath = '../../../app/js/SessionSockets' -sinon = require('sinon') +/* + * decaffeinate suggestions: + * 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'); -describe 'SessionSockets', -> - before -> - @SessionSocketsModule = SandboxedModule.require modulePath - @io = new EventEmitter() - @id1 = Math.random().toString() - @id2 = Math.random().toString() - redisResponses = - error: [new Error('Redis: something went wrong'), null] +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[@id1] = [null, {user: {_id: '123'}}] - redisResponses[@id2] = [null, {user: {_id: 'abc'}}] + }; + redisResponses[this.id1] = [null, {user: {_id: '123'}}]; + redisResponses[this.id2] = [null, {user: {_id: 'abc'}}]; - @sessionStore = - get: sinon.stub().callsFake (id, fn) -> - fn.apply(null, redisResponses[id]) - @cookieParser = (req, res, next) -> - req.signedCookies = req._signedCookies - next() - @SessionSockets = @SessionSocketsModule(@io, @sessionStore, @cookieParser, 'ol.sid') - @checkSocket = (socket, fn) => - @SessionSockets.once('connection', fn) - @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', -> - before -> - @socket = {handshake: {}} + describe('without cookies', function() { + before(function() { + return this.socket = {handshake: {}};}); - it 'should return a lookup error', (done) -> - @checkSocket @socket, (error) -> - expect(error).to.exist - expect(error.message).to.equal('could not look up session by key') - done() + it('should return a lookup error', function(done) { + return this.checkSocket(this.socket, function(error) { + expect(error).to.exist; + expect(error.message).to.equal('could not look up session by key'); + return done(); + }); + }); - it 'should not query redis', (done) -> - @checkSocket @socket, () => - expect(@sessionStore.get.called).to.equal(false) - 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', -> - before -> - @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', (done) -> - @checkSocket @socket, (error) -> - expect(error).to.exist - expect(error.message).to.equal('could not look up session by key') - done() + it('should return a lookup error', function(done) { + return this.checkSocket(this.socket, function(error) { + expect(error).to.exist; + expect(error.message).to.equal('could not look up session by key'); + return done(); + }); + }); - it 'should not query redis', (done) -> - @checkSocket @socket, () => - expect(@sessionStore.get.called).to.equal(false) - 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', -> - before -> - @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', (done) -> - @checkSocket @socket, () => - expect(@sessionStore.get.called).to.equal(true) - done() + it('should query redis', function(done) { + return this.checkSocket(this.socket, () => { + expect(this.sessionStore.get.called).to.equal(true); + return done(); + }); + }); - it 'should return a redis error', (done) -> - @checkSocket @socket, (error) -> - expect(error).to.exist - expect(error.message).to.equal('Redis: something went wrong') - done() + return it('should return a redis error', function(done) { + return this.checkSocket(this.socket, function(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', -> - before -> - @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', (done) -> - @checkSocket @socket, () => - expect(@sessionStore.get.called).to.equal(true) - done() + it('should query redis', function(done) { + return this.checkSocket(this.socket, () => { + expect(this.sessionStore.get.called).to.equal(true); + return done(); + }); + }); - it 'should return a lookup error', (done) -> - @checkSocket @socket, (error) -> - expect(error).to.exist - expect(error.message).to.equal('could not look up session by key') - done() + return it('should return a lookup error', function(done) { + return this.checkSocket(this.socket, function(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', -> - before -> - @socket = {handshake: {_signedCookies: {'ol.sid': @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', (done) -> - @checkSocket @socket, () => - expect(@sessionStore.get.called).to.equal(true) - 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', (done) -> - @checkSocket @socket, (error) -> - expect(error).to.not.exist - done() + it('should not return an error', function(done) { + return this.checkSocket(this.socket, function(error) { + expect(error).to.not.exist; + return done(); + }); + }); - it 'should return the session', (done) -> - @checkSocket @socket, (error, s, session) -> - expect(session).to.deep.equal({user: {_id: '123'}}) - done() + return it('should return the session', function(done) { + return this.checkSocket(this.socket, function(error, s, session) { + expect(session).to.deep.equal({user: {_id: '123'}}); + return done(); + }); + }); + }); - describe 'with a different valid cookie and matching session', -> - before -> - @socket = {handshake: {_signedCookies: {'ol.sid': @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', (done) -> - @checkSocket @socket, () => - expect(@sessionStore.get.called).to.equal(true) - 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', (done) -> - @checkSocket @socket, (error) -> - expect(error).to.not.exist - done() + it('should not return an error', function(done) { + return this.checkSocket(this.socket, function(error) { + expect(error).to.not.exist; + return done(); + }); + }); - it 'should return the other session', (done) -> - @checkSocket @socket, (error, s, session) -> - expect(session).to.deep.equal({user: {_id: 'abc'}}) - done() + return it('should return the other session', function(done) { + return this.checkSocket(this.socket, function(error, s, session) { + expect(session).to.deep.equal({user: {_id: 'abc'}}); + return done(); + }); + }); + }); +}); diff --git a/services/real-time/test/unit/coffee/WebApiManagerTests.js b/services/real-time/test/unit/coffee/WebApiManagerTests.js index e65ba93859..19d2bbe444 100644 --- a/services/real-time/test/unit/coffee/WebApiManagerTests.js +++ b/services/real-time/test/unit/coffee/WebApiManagerTests.js @@ -1,84 +1,111 @@ -chai = require('chai') -should = chai.should() -sinon = require("sinon") -modulePath = "../../../app/js/WebApiManager.js" -SandboxedModule = require('sandboxed-module') -{ CodedError } = require('../../../app/js/Errors') +/* + * decaffeinate suggestions: + * 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'); -describe 'WebApiManager', -> - beforeEach -> - @project_id = "project-id-123" - @user_id = "user-id-123" - @user = {_id: @user_id} - @callback = sinon.stub() - @WebApiManager = SandboxedModule.require modulePath, requires: - "request": @request = {} - "settings-sharelatex": @settings = - apis: - web: - url: "http://web.example.com" - user: "username" +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": @logger = { log: sinon.stub(), error: sinon.stub() } + } + } + }), + "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }) + } + });}); - describe "joinProject", -> - describe "successfully", -> - beforeEach -> - @response = { - project: { name: "Test project" } + return describe("joinProject", function() { + describe("successfully", function() { + beforeEach(function() { + this.response = { + project: { name: "Test project" }, privilegeLevel: "owner", isRestrictedUser: true - } - @request.post = sinon.stub().callsArgWith(1, null, {statusCode: 200}, @response) - @WebApiManager.joinProject @project_id, @user, @callback + }; + 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", -> - @request.post + it("should send a request to web to join the project", function() { + return this.request.post .calledWith({ - url: "#{@settings.apis.web.url}/project/#{@project_id}/join" - qs: - user_id: @user_id - auth: - user: @settings.apis.web.user - pass: @settings.apis.web.pass + 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 + }, + json: true, + jar: false, headers: {} }) - .should.equal true + .should.equal(true); + }); - it "should return the project, privilegeLevel, and restricted flag", -> - @callback - .calledWith(null, @response.project, @response.privilegeLevel, @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", -> - beforeEach -> - @request.post = sinon.stub().callsArgWith(1, null, {statusCode: 500}, null) - @WebApiManager.joinProject @project_id, @user_id, @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); + }); - it "should call the callback with an error", -> - @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 + .should.equal(true); + }); + }); - describe "with no data from web", -> - beforeEach -> - @request.post = sinon.stub().callsArgWith(1, null, {statusCode: 200}, null) - @WebApiManager.joinProject @project_id, @user_id, @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); + }); - it "should call the callback with an error", -> - @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 + .should.equal(true); + }); + }); - describe "when the project is over its rate limit", -> - beforeEach -> - @request.post = sinon.stub().callsArgWith(1, null, {statusCode: 429}, null) - @WebApiManager.joinProject @project_id, @user_id, @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); + }); - it "should call the callback with a TooManyRequests error code", -> - @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 + .should.equal(true); + }); + }); + }); +}); diff --git a/services/real-time/test/unit/coffee/WebsocketControllerTests.js b/services/real-time/test/unit/coffee/WebsocketControllerTests.js index c0047c49b7..92d64d7cd2 100644 --- a/services/real-time/test/unit/coffee/WebsocketControllerTests.js +++ b/services/real-time/test/unit/coffee/WebsocketControllerTests.js @@ -1,872 +1,1088 @@ -chai = require('chai') -should = chai.should() -sinon = require("sinon") -expect = chai.expect -modulePath = "../../../app/js/WebsocketController.js" -SandboxedModule = require('sandboxed-module') -tk = require "timekeeper" +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * 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', -> - beforeEach -> - tk.freeze(new Date()) - @project_id = "project-id-123" - @user = { - _id: @user_id = "user-id-123" - first_name: "James" - last_name: "Allen" - email: "james@example.com" - signUpDate: new Date("2014-01-01") +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 - } - @callback = sinon.stub() - @client = - disconnected: false - id: @client_id = "mock-client-id-123" - publicId: "other-id-#{Math.random()}" - ol_context: {} - join: sinon.stub() + }; + 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() - @WebsocketController = SandboxedModule.require modulePath, requires: - "./WebApiManager": @WebApiManager = {} - "./AuthorizationManager": @AuthorizationManager = {} - "./DocumentUpdaterManager": @DocumentUpdaterManager = {} - "./ConnectedUsersManager": @ConnectedUsersManager = {} - "./WebsocketLoadBalancer": @WebsocketLoadBalancer = {} - "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() } - "metrics-sharelatex": @metrics = - inc: 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": @RoomManager = {} + }), + "./RoomManager": (this.RoomManager = {}) + } + });}); - afterEach -> - tk.reset() + afterEach(() => tk.reset()); - describe "joinProject", -> - describe "when authorised", -> - beforeEach -> - @client.id = "mock-client-id" - @project = { - name: "Test Project" + describe("joinProject", function() { + describe("when authorised", function() { + beforeEach(function() { + this.client.id = "mock-client-id"; + this.project = { + name: "Test Project", owner: { - _id: @owner_id = "mock-owner-id-123" + _id: (this.owner_id = "mock-owner-id-123") } - } - @privilegeLevel = "owner" - @ConnectedUsersManager.updateUserPosition = sinon.stub().callsArg(4) - @isRestrictedUser = true - @WebApiManager.joinProject = sinon.stub().callsArgWith(2, null, @project, @privilegeLevel, @isRestrictedUser) - @RoomManager.joinProject = sinon.stub().callsArg(2) - @WebsocketController.joinProject @client, @user, @project_id, @callback + }; + 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", -> - @WebApiManager.joinProject - .calledWith(@project_id, @user) - .should.equal true + 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", -> - @RoomManager.joinProject.calledWith(@client, @project_id).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", -> - @client.ol_context["privilege_level"].should.equal @privilegeLevel - it "should set the user's id on the client", -> - @client.ol_context["user_id"].should.equal @user._id - it "should set the user's email on the client", -> - @client.ol_context["email"].should.equal @user.email - it "should set the user's first_name on the client", -> - @client.ol_context["first_name"].should.equal @user.first_name - it "should set the user's last_name on the client", -> - @client.ol_context["last_name"].should.equal @user.last_name - it "should set the user's sign up date on the client", -> - @client.ol_context["signup_date"].should.equal @user.signUpDate - it "should set the user's login_count on the client", -> - @client.ol_context["login_count"].should.equal @user.loginCount - it "should set the connected time on the client", -> - @client.ol_context["connected_time"].should.equal new Date() - it "should set the project_id on the client", -> - @client.ol_context["project_id"].should.equal @project_id - it "should set the project owner id on the client", -> - @client.ol_context["owner_id"].should.equal @owner_id - it "should set the is_restricted_user flag on the client", -> - @client.ol_context["is_restricted_user"].should.equal @isRestrictedUser - it "should call the callback with the project, privilegeLevel and protocolVersion", -> - @callback - .calledWith(null, @project, @privilegeLevel, @WebsocketController.PROTOCOL_VERSION) - .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", -> - @ConnectedUsersManager.updateUserPosition - .calledWith(@project_id, @client.publicId, @user, null) - .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); + }); - it "should increment the join-project metric", -> - @metrics.inc.calledWith("editor.join-project").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", -> - beforeEach -> - @WebApiManager.joinProject = sinon.stub().callsArgWith(2, null, null, null) - @WebsocketController.joinProject @client, @user, @project_id, @callback + 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", -> - @callback + it("should return an error", function() { + return this.callback .calledWith(sinon.match({message: "not authorized"})) - .should.equal true + .should.equal(true); + }); - it "should not log an error", -> - @logger.error.called.should.equal false + return it("should not log an error", function() { + return this.logger.error.called.should.equal(false); + }); + }); - describe "when the subscribe failed", -> - beforeEach -> - @client.id = "mock-client-id" - @project = { - name: "Test Project" + describe("when the subscribe failed", function() { + beforeEach(function() { + this.client.id = "mock-client-id"; + this.project = { + name: "Test Project", owner: { - _id: @owner_id = "mock-owner-id-123" + _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; } } - @privilegeLevel = "owner" - @ConnectedUsersManager.updateUserPosition = sinon.stub().callsArg(4) - @isRestrictedUser = true - @WebApiManager.joinProject = sinon.stub().callsArgWith(2, null, @project, @privilegeLevel, @isRestrictedUser) - @RoomManager.joinProject = sinon.stub().callsArgWith(2, new Error("subscribe failed")) - @WebsocketController.joinProject @client, @user, @project_id, @callback - - it "should return an error", -> - @callback - .calledWith(sinon.match({message: "subscribe failed"})) - .should.equal true - @callback.args[0][0].message.should.equal "subscribe failed" - - describe "when the client has disconnected", -> - beforeEach -> - @client.disconnected = true - @WebApiManager.joinProject = sinon.stub().callsArg(2) - @WebsocketController.joinProject @client, @user, @project_id, @callback - - it "should not call WebApiManager.joinProject", -> - expect(@WebApiManager.joinProject.called).to.equal(false) - - it "should call the callback with no details", -> - expect(@callback.args[0]).to.deep.equal [] - - it "should increment the editor.join-project.disconnected metric with a status", -> - expect(@metrics.inc.calledWith('editor.join-project.disconnected', 1, {status: 'immediately'})).to.equal(true) - - describe "when the client disconnects while WebApiManager.joinProject is running", -> - beforeEach -> - @WebApiManager.joinProject = (project, user, cb) => - @client.disconnected = true - cb(null, @project, @privilegeLevel, @isRestrictedUser) - - @WebsocketController.joinProject @client, @user, @project_id, @callback - - it "should call the callback with no details", -> - expect(@callback.args[0]).to.deep.equal [] - - it "should increment the editor.join-project.disconnected metric with a status", -> - expect(@metrics.inc.calledWith('editor.join-project.disconnected', 1, {status: 'after-web-api-call'})).to.equal(true) - - describe "leaveProject", -> - beforeEach -> - @DocumentUpdaterManager.flushProjectToMongoAndDelete = sinon.stub().callsArg(1) - @ConnectedUsersManager.markUserAsDisconnected = sinon.stub().callsArg(2) - @WebsocketLoadBalancer.emitToRoom = sinon.stub() - @RoomManager.leaveProjectAndDocs = sinon.stub() - @clientsInRoom = [] - @io = - sockets: - clients: (room_id) => - if room_id != @project_id - throw "expected room_id to be project_id" - return @clientsInRoom - @client.ol_context.project_id = @project_id - @client.ol_context.user_id = @user_id - @WebsocketController.FLUSH_IF_EMPTY_DELAY = 0 - tk.reset() # Allow setTimeout to work. - - describe "when the client did not joined a project yet", -> - beforeEach (done) -> - @client.ol_context = {} - @WebsocketController.leaveProject @io, @client, done - - it "should bail out when calling leaveProject", () -> - @WebsocketLoadBalancer.emitToRoom.called.should.equal false - @RoomManager.leaveProjectAndDocs.called.should.equal false - @ConnectedUsersManager.markUserAsDisconnected.called.should.equal false - - it "should not inc any metric", () -> - @metrics.inc.called.should.equal false - - describe "when the project is empty", -> - beforeEach (done) -> - @clientsInRoom = [] - @WebsocketController.leaveProject @io, @client, done - - it "should end clientTracking.clientDisconnected to the project room", -> - @WebsocketLoadBalancer.emitToRoom - .calledWith(@project_id, "clientTracking.clientDisconnected", @client.publicId) - .should.equal true - - it "should mark the user as disconnected", -> - @ConnectedUsersManager.markUserAsDisconnected - .calledWith(@project_id, @client.publicId) - .should.equal true - - it "should flush the project in the document updater", -> - @DocumentUpdaterManager.flushProjectToMongoAndDelete - .calledWith(@project_id) - .should.equal true - - it "should increment the leave-project metric", -> - @metrics.inc.calledWith("editor.leave-project").should.equal true - - it "should track the disconnection in RoomManager", -> - @RoomManager.leaveProjectAndDocs - .calledWith(@client) - .should.equal true - - describe "when the project is not empty", -> - beforeEach -> - @clientsInRoom = ["mock-remaining-client"] - @WebsocketController.leaveProject @io, @client - - it "should not flush the project in the document updater", -> - @DocumentUpdaterManager.flushProjectToMongoAndDelete - .called.should.equal false - - describe "when client has not authenticated", -> - beforeEach (done) -> - @client.ol_context.user_id = null - @client.ol_context.project_id = null - @WebsocketController.leaveProject @io, @client, done - - it "should not end clientTracking.clientDisconnected to the project room", -> - @WebsocketLoadBalancer.emitToRoom - .calledWith(@project_id, "clientTracking.clientDisconnected", @client.publicId) - .should.equal false - - it "should not mark the user as disconnected", -> - @ConnectedUsersManager.markUserAsDisconnected - .calledWith(@project_id, @client.publicId) - .should.equal false - - it "should not flush the project in the document updater", -> - @DocumentUpdaterManager.flushProjectToMongoAndDelete - .calledWith(@project_id) - .should.equal false - - it "should not increment the leave-project metric", -> - @metrics.inc.calledWith("editor.leave-project").should.equal false - - describe "when client has not joined a project", -> - beforeEach (done) -> - @client.ol_context.user_id = @user_id - @client.ol_context.project_id = null - @WebsocketController.leaveProject @io, @client, done - - it "should not end clientTracking.clientDisconnected to the project room", -> - @WebsocketLoadBalancer.emitToRoom - .calledWith(@project_id, "clientTracking.clientDisconnected", @client.publicId) - .should.equal false - - it "should not mark the user as disconnected", -> - @ConnectedUsersManager.markUserAsDisconnected - .calledWith(@project_id, @client.publicId) - .should.equal false - - it "should not flush the project in the document updater", -> - @DocumentUpdaterManager.flushProjectToMongoAndDelete - .calledWith(@project_id) - .should.equal false - - it "should not increment the leave-project metric", -> - @metrics.inc.calledWith("editor.leave-project").should.equal false - - describe "joinDoc", -> - beforeEach -> - @doc_id = "doc-id-123" - @doc_lines = ["doc", "lines"] - @version = 42 - @ops = ["mock", "ops"] - @ranges = { "mock": "ranges" } - @options = {} - - @client.ol_context.project_id = @project_id - @client.ol_context.is_restricted_user = false - @AuthorizationManager.addAccessToDoc = sinon.stub() - @AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, null) - @DocumentUpdaterManager.getDocument = sinon.stub().callsArgWith(3, null, @doc_lines, @version, @ranges, @ops) - @RoomManager.joinDoc = sinon.stub().callsArg(2) - - describe "works", -> - beforeEach -> - @WebsocketController.joinDoc @client, @doc_id, -1, @options, @callback - - it "should check that the client is authorized to view the project", -> - @AuthorizationManager.assertClientCanViewProject - .calledWith(@client) - .should.equal true - - it "should get the document from the DocumentUpdaterManager with fromVersion", -> - @DocumentUpdaterManager.getDocument - .calledWith(@project_id, @doc_id, -1) - .should.equal true - - it "should add permissions for the client to access the doc", -> - @AuthorizationManager.addAccessToDoc - .calledWith(@client, @doc_id) - .should.equal true - - it "should join the client to room for the doc_id", -> - @RoomManager.joinDoc - .calledWith(@client, @doc_id) - .should.equal true - - it "should call the callback with the lines, version, ranges and ops", -> - @callback - .calledWith(null, @doc_lines, @version, @ops, @ranges) - .should.equal true - - it "should increment the join-doc metric", -> - @metrics.inc.calledWith("editor.join-doc").should.equal true - - describe "with a fromVersion", -> - beforeEach -> - @fromVersion = 40 - @WebsocketController.joinDoc @client, @doc_id, @fromVersion, @options, @callback - - it "should get the document from the DocumentUpdaterManager with fromVersion", -> - @DocumentUpdaterManager.getDocument - .calledWith(@project_id, @doc_id, @fromVersion) - .should.equal true - - describe "with doclines that need escaping", -> - beforeEach -> - @doc_lines.push ["räksmörgås"] - @WebsocketController.joinDoc @client, @doc_id, -1, @options, @callback - - it "should call the callback with the escaped lines", -> - escaped_lines = @callback.args[0][1] - escaped_word = escaped_lines.pop() - escaped_word.should.equal 'räksmörgÃ¥s' - # Check that unescaping works - decodeURIComponent(escape(escaped_word)).should.equal "räksmörgås" - - describe "with comments that need encoding", -> - beforeEach -> - @ranges.comments = [{ op: { c: "räksmörgås" } }] - @WebsocketController.joinDoc @client, @doc_id, -1, { encodeRanges: true }, @callback - - it "should call the callback with the encoded comment", -> - encoded_comments = @callback.args[0][4] - encoded_comment = encoded_comments.comments.pop() - encoded_comment_text = encoded_comment.op.c - encoded_comment_text.should.equal 'räksmörgÃ¥s' - - describe "with changes that need encoding", -> - it "should call the callback with the encoded insert change", -> - @ranges.changes = [{ op: { i: "räksmörgås" } }] - @WebsocketController.joinDoc @client, @doc_id, -1, { encodeRanges: true }, @callback - - encoded_changes = @callback.args[0][4] - encoded_change = encoded_changes.changes.pop() - encoded_change_text = encoded_change.op.i - encoded_change_text.should.equal 'räksmörgÃ¥s' - - it "should call the callback with the encoded delete change", -> - @ranges.changes = [{ op: { d: "räksmörgås" } }] - @WebsocketController.joinDoc @client, @doc_id, -1, { encodeRanges: true }, @callback - - encoded_changes = @callback.args[0][4] - encoded_change = encoded_changes.changes.pop() - encoded_change_text = encoded_change.op.d - encoded_change_text.should.equal 'räksmörgÃ¥s' - - describe "when not authorized", -> - beforeEach -> - @AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, @err = new Error("not authorized")) - @WebsocketController.joinDoc @client, @doc_id, -1, @options, @callback - - it "should call the callback with an error", -> - @callback.calledWith(sinon.match({message: "not authorized"})).should.equal true - - it "should not call the DocumentUpdaterManager", -> - @DocumentUpdaterManager.getDocument.called.should.equal false - - describe "with a restricted client", -> - beforeEach -> - @ranges.comments = [{op: {a: 1}}, {op: {a: 2}}] - @client.ol_context.is_restricted_user = true - @WebsocketController.joinDoc @client, @doc_id, -1, @options, @callback - - it "should overwrite ranges.comments with an empty list", -> - ranges = @callback.args[0][4] - expect(ranges.comments).to.deep.equal [] - - describe "when the client has disconnected", -> - beforeEach -> - @client.disconnected = true - @WebsocketController.joinDoc @client, @doc_id, -1, @options, @callback - - it "should call the callback with no details", -> - expect(@callback.args[0]).to.deep.equal([]) - - it "should increment the editor.join-doc.disconnected metric with a status", -> - expect(@metrics.inc.calledWith('editor.join-doc.disconnected', 1, {status: 'immediately'})).to.equal(true) - - it "should not get the document", -> - expect(@DocumentUpdaterManager.getDocument.called).to.equal(false) - - describe "when the client disconnects while RoomManager.joinDoc is running", -> - beforeEach -> - @RoomManager.joinDoc = (client, doc_id, cb) => - @client.disconnected = true - cb() - - @WebsocketController.joinDoc @client, @doc_id, -1, @options, @callback - - it "should call the callback with no details", -> - expect(@callback.args[0]).to.deep.equal([]) - - it "should increment the editor.join-doc.disconnected metric with a status", -> - expect(@metrics.inc.calledWith('editor.join-doc.disconnected', 1, {status: 'after-joining-room'})).to.equal(true) - - it "should not get the document", -> - expect(@DocumentUpdaterManager.getDocument.called).to.equal(false) - - describe "when the client disconnects while DocumentUpdaterManager.getDocument is running", -> - beforeEach -> - @DocumentUpdaterManager.getDocument = (project_id, doc_id, fromVersion, callback) => - @client.disconnected = true - callback(null, @doc_lines, @version, @ranges, @ops) - - @WebsocketController.joinDoc @client, @doc_id, -1, @options, @callback - - it "should call the callback with no details", -> - expect(@callback.args[0]).to.deep.equal [] - - it "should increment the editor.join-doc.disconnected metric with a status", -> - expect(@metrics.inc.calledWith('editor.join-doc.disconnected', 1, {status: 'after-doc-updater-call'})).to.equal(true) - - describe "leaveDoc", -> - beforeEach -> - @doc_id = "doc-id-123" - @client.ol_context.project_id = @project_id - @RoomManager.leaveDoc = sinon.stub() - @WebsocketController.leaveDoc @client, @doc_id, @callback - - it "should remove the client from the doc_id room", -> - @RoomManager.leaveDoc - .calledWith(@client, @doc_id).should.equal true - - it "should call the callback", -> - @callback.called.should.equal true - - it "should increment the leave-doc metric", -> - @metrics.inc.calledWith("editor.leave-doc").should.equal true - - describe "getConnectedUsers", -> - beforeEach -> - @client.ol_context.project_id = @project_id - @users = ["mock", "users"] - @WebsocketLoadBalancer.emitToRoom = sinon.stub() - @ConnectedUsersManager.getConnectedUsers = sinon.stub().callsArgWith(1, null, @users) - - describe "when authorized", -> - beforeEach (done) -> - @AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, null) - @WebsocketController.getConnectedUsers @client, (args...) => - @callback(args...) - done() - - it "should check that the client is authorized to view the project", -> - @AuthorizationManager.assertClientCanViewProject - .calledWith(@client) - .should.equal true - - it "should broadcast a request to update the client list", -> - @WebsocketLoadBalancer.emitToRoom - .calledWith(@project_id, "clientTracking.refresh") - .should.equal true - - it "should get the connected users for the project", -> - @ConnectedUsersManager.getConnectedUsers - .calledWith(@project_id) - .should.equal true - - it "should return the users", -> - @callback.calledWith(null, @users).should.equal true - - it "should increment the get-connected-users metric", -> - @metrics.inc.calledWith("editor.get-connected-users").should.equal true - - describe "when not authorized", -> - beforeEach -> - @AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, @err = new Error("not authorized")) - @WebsocketController.getConnectedUsers @client, @callback - - it "should not get the connected users for the project", -> - @ConnectedUsersManager.getConnectedUsers + }; + 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 + .should.equal(false); + }); - it "should return an error", -> - @callback.calledWith(@err).should.equal true + return it("should return an error", function() { + return this.callback.calledWith(this.err).should.equal(true); + }); + }); - describe "when restricted user", -> - beforeEach -> - @client.ol_context.is_restricted_user = true - @AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, null) - @WebsocketController.getConnectedUsers @client, @callback + 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", -> - @callback.calledWith(null, []).should.equal true + it("should return an empty array of users", function() { + return this.callback.calledWith(null, []).should.equal(true); + }); - it "should not get the connected users for the project", -> - @ConnectedUsersManager.getConnectedUsers + return it("should not get the connected users for the project", function() { + return this.ConnectedUsersManager.getConnectedUsers .called - .should.equal false + .should.equal(false); + }); + }); - describe "when the client has disconnected", -> - beforeEach -> - @client.disconnected = true - @AuthorizationManager.assertClientCanViewProject = sinon.stub() - @WebsocketController.getConnectedUsers @client, @callback + 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", -> - expect(@callback.args[0]).to.deep.equal([]) + it("should call the callback with no details", function() { + return expect(this.callback.args[0]).to.deep.equal([]); + }); - it "should not check permissions", -> - expect(@AuthorizationManager.assertClientCanViewProject.called).to.equal(false) + return it("should not check permissions", function() { + return expect(this.AuthorizationManager.assertClientCanViewProject.called).to.equal(false); + }); + }); + }); - describe "updateClientPosition", -> - beforeEach -> - @WebsocketLoadBalancer.emitToRoom = sinon.stub() - @ConnectedUsersManager.updateUserPosition = sinon.stub().callsArgWith(4) - @AuthorizationManager.assertClientCanViewProjectAndDoc = sinon.stub().callsArgWith(2, null) - @update = { - doc_id: @doc_id = "doc-id-123" - row: @row = 42 - column: @column = 37 - } + 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", -> - beforeEach -> - @client.ol_context = { - project_id: @project_id - first_name: @first_name = "Douglas" - last_name: @last_name = "Adams" - email: @email = "joe@example.com" - user_id: @user_id = "user-id-123" - } - @WebsocketController.updateClientPosition @client, @update + 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); - @populatedCursorData = - doc_id: @doc_id, - id: @client.publicId - name: "#{@first_name} #{@last_name}" - row: @row - column: @column - email: @email - user_id: @user_id + 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", -> - @WebsocketLoadBalancer.emitToRoom.calledWith(@project_id, "clientTracking.clientUpdated", @populatedCursorData).should.equal true + 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", (done)-> - @ConnectedUsersManager.updateUserPosition.calledWith(@project_id, @client.publicId, { - _id: @user_id, - email: @email, - first_name: @first_name, - last_name: @last_name + 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: @row - column: @column - doc_id: @doc_id - }).should.equal true - done() + row: this.row, + column: this.column, + doc_id: this.doc_id + }).should.equal(true); + return done(); + }); - it "should increment the update-client-position metric at 0.1 frequency", -> - @metrics.inc.calledWith("editor.update-client-position", 0.1).should.equal true + 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", -> - beforeEach -> - @client.ol_context = { - project_id: @project_id - first_name: @first_name = "Douglas" - last_name: undefined - email: @email = "joe@example.com" - user_id: @user_id = "user-id-123" - } - @WebsocketController.updateClientPosition @client, @update + 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); - @populatedCursorData = - doc_id: @doc_id, - id: @client.publicId - name: "#{@first_name}" - row: @row - column: @column - email: @email - user_id: @user_id + 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", -> - @WebsocketLoadBalancer.emitToRoom.calledWith(@project_id, "clientTracking.clientUpdated", @populatedCursorData).should.equal true + 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", (done)-> - @ConnectedUsersManager.updateUserPosition.calledWith(@project_id, @client.publicId, { - _id: @user_id, - email: @email, - first_name: @first_name, + 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: @row - column: @column - doc_id: @doc_id - }).should.equal true - done() + row: this.row, + column: this.column, + doc_id: this.doc_id + }).should.equal(true); + return done(); + }); - it "should increment the update-client-position metric at 0.1 frequency", -> - @metrics.inc.calledWith("editor.update-client-position", 0.1).should.equal true + 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", -> - beforeEach -> - @client.ol_context = { - project_id: @project_id - first_name: undefined - last_name: @last_name = "Adams" - email: @email = "joe@example.com" - user_id: @user_id = "user-id-123" - } - @WebsocketController.updateClientPosition @client, @update - - @populatedCursorData = - doc_id: @doc_id, - id: @client.publicId - name: "#{@last_name}" - row: @row - column: @column - email: @email - user_id: @user_id - - it "should send the update to the project room with the user's name", -> - @WebsocketLoadBalancer.emitToRoom.calledWith(@project_id, "clientTracking.clientUpdated", @populatedCursorData).should.equal true - - it "should send the cursor data to the connected user manager", (done)-> - @ConnectedUsersManager.updateUserPosition.calledWith(@project_id, @client.publicId, { - _id: @user_id, - email: @email, + 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: @last_name + 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: @row - column: @column - doc_id: @doc_id - }).should.equal true - done() + row: this.row, + column: this.column, + doc_id: this.doc_id + }).should.equal(true); + return done(); + }); - it "should increment the update-client-position metric at 0.1 frequency", -> - @metrics.inc.calledWith("editor.update-client-position", 0.1).should.equal true - describe "with a logged in user who has no names set", -> - beforeEach -> - @client.ol_context = { - project_id: @project_id - first_name: undefined - last_name: undefined - email: @email = "joe@example.com" - user_id: @user_id = "user-id-123" - } - @WebsocketController.updateClientPosition @client, @update + 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); + }); - it "should send the update to the project name with no name", -> - @WebsocketLoadBalancer.emitToRoom - .calledWith(@project_id, "clientTracking.clientUpdated", { - doc_id: @doc_id, - id: @client.publicId, - user_id: @user_id, + 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: @row, - column: @column, - email: @email + row: this.row, + column: this.column, + email: this.email }) - .should.equal true + .should.equal(true); + }); + }); - describe "with an anonymous user", -> - beforeEach -> - @client.ol_context = { - project_id: @project_id - } - @WebsocketController.updateClientPosition @client, @update + 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", -> - @WebsocketLoadBalancer.emitToRoom - .calledWith(@project_id, "clientTracking.clientUpdated", { - doc_id: @doc_id, - id: @client.publicId - name: "" - row: @row - column: @column + 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 + .should.equal(true); + }); - it "should not send cursor data to the connected user manager", (done)-> - @ConnectedUsersManager.updateUserPosition.called.should.equal false - done() + return it("should not send cursor data to the connected user manager", function(done){ + this.ConnectedUsersManager.updateUserPosition.called.should.equal(false); + return done(); + }); + }); - describe "when the client has disconnected", -> - beforeEach -> - @client.disconnected = true - @AuthorizationManager.assertClientCanViewProjectAndDoc = sinon.stub() - @WebsocketController.updateClientPosition @client, @update, @callback + 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", -> - expect(@callback.args[0]).to.deep.equal([]) + it("should call the callback with no details", function() { + return expect(this.callback.args[0]).to.deep.equal([]); + }); - it "should not check permissions", -> - expect(@AuthorizationManager.assertClientCanViewProjectAndDoc.called).to.equal(false) + return it("should not check permissions", function() { + return expect(this.AuthorizationManager.assertClientCanViewProjectAndDoc.called).to.equal(false); + }); + }); + }); - describe "applyOtUpdate", -> - beforeEach -> - @update = {op: {p: 12, t: "foo"}} - @client.ol_context.user_id = @user_id - @client.ol_context.project_id = @project_id - @WebsocketController._assertClientCanApplyUpdate = sinon.stub().yields() - @DocumentUpdaterManager.queueChange = sinon.stub().callsArg(3) + 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", -> - beforeEach -> - @WebsocketController.applyOtUpdate @client, @doc_id, @update, @callback + 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", -> - @update.meta.source.should.equal @client.publicId + 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", -> - @update.meta.user_id.should.equal @user_id + 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", -> - @DocumentUpdaterManager.queueChange - .calledWith(@project_id, @doc_id, @update) - .should.equal true + 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", -> - @callback.called.should.equal true + it("should call the callback", function() { + return this.callback.called.should.equal(true); + }); - it "should increment the doc updates", -> - @metrics.inc.calledWith("editor.doc-update").should.equal true + return it("should increment the doc updates", function() { + return this.metrics.inc.calledWith("editor.doc-update").should.equal(true); + }); + }); - describe "unsuccessfully", -> - beforeEach -> - @client.disconnect = sinon.stub() - @DocumentUpdaterManager.queueChange = sinon.stub().callsArgWith(3, @error = new Error("Something went wrong")) - @WebsocketController.applyOtUpdate @client, @doc_id, @update, @callback + 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", -> - @client.disconnect.called.should.equal true + it("should disconnect the client", function() { + return this.client.disconnect.called.should.equal(true); + }); - it "should log an error", -> - @logger.error.called.should.equal true + it("should log an error", function() { + return this.logger.error.called.should.equal(true); + }); - it "should call the callback with the error", -> - @callback.calledWith(@error).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", -> - beforeEach -> - @client.disconnect = sinon.stub() - @WebsocketController._assertClientCanApplyUpdate = sinon.stub().yields(@error = new Error("not authorized")) - @WebsocketController.applyOtUpdate @client, @doc_id, @update, @callback + 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 + // 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", -> - @logger.warn.called.should.equal true + it("should log a warning", function() { + return this.logger.warn.called.should.equal(true); + }); - it "should call the callback with the error", -> - @callback.calledWith(@error).should.equal true + return it("should call the callback with the error", function() { + return this.callback.calledWith(this.error).should.equal(true); + }); + }); - describe "update_too_large", -> - beforeEach (done) -> - @client.disconnect = sinon.stub() - @client.emit = sinon.stub() - @client.ol_context.user_id = @user_id - @client.ol_context.project_id = @project_id - error = new Error("update is too large") - error.updateSize = 7372835 - @DocumentUpdaterManager.queueChange = sinon.stub().callsArgWith(3, error) - @WebsocketController.applyOtUpdate @client, @doc_id, @update, @callback - setTimeout -> - done() - , 1 + 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", -> - @callback.called.should.equal true - @callback.args[0].should.deep.equal [] + 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", -> - @logger.warn.called.should.equal true - @logger.warn.args[0].should.deep.equal [{ - @user_id, @project_id, @doc_id, updateSize: 7372835 - }, 'update is too large'] + 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", -> - beforeEach (done) -> - setTimeout done, 100 + describe("after 100ms", function() { + beforeEach(done => setTimeout(done, 100)); - it "should send an otUpdateError the client", -> - @client.emit.calledWith('otUpdateError').should.equal true + it("should send an otUpdateError the client", function() { + return this.client.emit.calledWith('otUpdateError').should.equal(true); + }); - it "should disconnect the client", -> - @client.disconnect.called.should.equal true + return it("should disconnect the client", function() { + return this.client.disconnect.called.should.equal(true); + }); + }); - describe "when the client disconnects during the next 100ms", -> - beforeEach (done) -> - @client.disconnected = true - setTimeout done, 100 + 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", -> - @client.emit.calledWith('otUpdateError').should.equal false + it("should not send an otUpdateError the client", function() { + return this.client.emit.calledWith('otUpdateError').should.equal(false); + }); - it "should not disconnect the client", -> - @client.disconnect.called.should.equal false + it("should not disconnect the client", function() { + return this.client.disconnect.called.should.equal(false); + }); - it "should increment the editor.doc-update.disconnected metric with a status", -> - expect(@metrics.inc.calledWith('editor.doc-update.disconnected', 1, {status:'at-otUpdateError'})).to.equal(true) + 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); + }); + }); + }); + }); - describe "_assertClientCanApplyUpdate", -> - beforeEach -> - @edit_update = { op: [{i: "foo", p: 42}, {c: "bar", p: 132}] } # comments may still be in an edit op - @comment_update = { op: [{c: "bar", p: 132}] } - @AuthorizationManager.assertClientCanEditProjectAndDoc = sinon.stub() - @AuthorizationManager.assertClientCanViewProjectAndDoc = sinon.stub() + 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", -> - it "should return successfully", (done) -> - @AuthorizationManager.assertClientCanEditProjectAndDoc.yields(null) - @WebsocketController._assertClientCanApplyUpdate @client, @doc_id, @edit_update, (error) -> - expect(error).to.be.null - done() + describe("with a read-write client", () => it("should return successfully", function(done) { + this.AuthorizationManager.assertClientCanEditProjectAndDoc.yields(null); + return this.WebsocketController._assertClientCanApplyUpdate(this.client, this.doc_id, this.edit_update, function(error) { + expect(error).to.be.null; + return done(); + }); + })); - describe "with a read-only client and an edit op", -> - it "should return an error", (done) -> - @AuthorizationManager.assertClientCanEditProjectAndDoc.yields(new Error("not authorized")) - @AuthorizationManager.assertClientCanViewProjectAndDoc.yields(null) - @WebsocketController._assertClientCanApplyUpdate @client, @doc_id, @edit_update, (error) -> - expect(error.message).to.equal "not authorized" - done() + describe("with a read-only client and an edit op", () => 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, function(error) { + expect(error.message).to.equal("not authorized"); + return done(); + }); + })); - describe "with a read-only client and a comment op", -> - it "should return successfully", (done) -> - @AuthorizationManager.assertClientCanEditProjectAndDoc.yields(new Error("not authorized")) - @AuthorizationManager.assertClientCanViewProjectAndDoc.yields(null) - @WebsocketController._assertClientCanApplyUpdate @client, @doc_id, @comment_update, (error) -> - expect(error).to.be.null - done() + describe("with a read-only client and a comment op", () => 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, function(error) { + expect(error).to.be.null; + return done(); + }); + })); - describe "with a totally unauthorized client", -> - it "should return an error", (done) -> - @AuthorizationManager.assertClientCanEditProjectAndDoc.yields(new Error("not authorized")) - @AuthorizationManager.assertClientCanViewProjectAndDoc.yields(new Error("not authorized")) - @WebsocketController._assertClientCanApplyUpdate @client, @doc_id, @comment_update, (error) -> - expect(error.message).to.equal "not authorized" - done() + return describe("with a totally unauthorized client", () => 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, function(error) { + expect(error.message).to.equal("not authorized"); + return done(); + }); + })); + }); +}); diff --git a/services/real-time/test/unit/coffee/WebsocketLoadBalancerTests.js b/services/real-time/test/unit/coffee/WebsocketLoadBalancerTests.js index b2441cd6d0..5b9ab079fb 100644 --- a/services/real-time/test/unit/coffee/WebsocketLoadBalancerTests.js +++ b/services/real-time/test/unit/coffee/WebsocketLoadBalancerTests.js @@ -1,161 +1,204 @@ -SandboxedModule = require('sandboxed-module') -sinon = require('sinon') -require('chai').should() -modulePath = require('path').join __dirname, '../../../app/js/WebsocketLoadBalancer' +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * 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'); -describe "WebsocketLoadBalancer", -> - beforeEach -> - @rclient = {} - @RoomEvents = {on: sinon.stub()} - @WebsocketLoadBalancer = SandboxedModule.require modulePath, requires: - "./RedisClientManager": +describe("WebsocketLoadBalancer", function() { + beforeEach(function() { + this.rclient = {}; + this.RoomEvents = {on: sinon.stub()}; + this.WebsocketLoadBalancer = SandboxedModule.require(modulePath, { requires: { + "./RedisClientManager": { createClientList: () => [] - "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() } - "./SafeJsonParse": @SafeJsonParse = - parse: (data, cb) => cb null, JSON.parse(data) - "./EventLogger": {checkEventOrder: sinon.stub()} - "./HealthCheckManager": {check: sinon.stub()} - "./RoomManager" : @RoomManager = {eventSource: sinon.stub().returns @RoomEvents} - "./ChannelManager": @ChannelManager = {publish: sinon.stub()} - "./ConnectedUsersManager": @ConnectedUsersManager = {refreshClient: sinon.stub()} - @io = {} - @WebsocketLoadBalancer.rclientPubList = [{publish: sinon.stub()}] - @WebsocketLoadBalancer.rclientSubList = [{ - subscribe: sinon.stub() + }, + "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() - }] + }]; - @room_id = "room-id" - @message = "otUpdateApplied" - @payload = ["argument one", 42] + this.room_id = "room-id"; + this.message = "otUpdateApplied"; + return this.payload = ["argument one", 42];}); - describe "emitToRoom", -> - beforeEach -> - @WebsocketLoadBalancer.emitToRoom(@room_id, @message, @payload...) + describe("emitToRoom", function() { + beforeEach(function() { + return this.WebsocketLoadBalancer.emitToRoom(this.room_id, this.message, ...Array.from(this.payload)); + }); - it "should publish the message to redis", -> - @ChannelManager.publish - .calledWith(@WebsocketLoadBalancer.rclientPubList[0], "editor-events", @room_id, JSON.stringify( - room_id: @room_id, - message: @message - payload: @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", -> - beforeEach -> - @WebsocketLoadBalancer.emitToRoom = sinon.stub() - @WebsocketLoadBalancer.emitToAll @message, @payload... + describe("emitToAll", function() { + beforeEach(function() { + this.WebsocketLoadBalancer.emitToRoom = sinon.stub(); + return this.WebsocketLoadBalancer.emitToAll(this.message, ...Array.from(this.payload)); + }); - it "should emit to the room 'all'", -> - @WebsocketLoadBalancer.emitToRoom - .calledWith("all", @message, @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", -> - beforeEach -> - @WebsocketLoadBalancer._processEditorEvent = sinon.stub() - @WebsocketLoadBalancer.listenForEditorEvents() + describe("listenForEditorEvents", function() { + beforeEach(function() { + this.WebsocketLoadBalancer._processEditorEvent = sinon.stub(); + return this.WebsocketLoadBalancer.listenForEditorEvents(); + }); - it "should subscribe to the editor-events channel", -> - @WebsocketLoadBalancer.rclientSubList[0].subscribe + it("should subscribe to the editor-events channel", function() { + return this.WebsocketLoadBalancer.rclientSubList[0].subscribe .calledWith("editor-events") - .should.equal true + .should.equal(true); + }); - it "should process the events with _processEditorEvent", -> - @WebsocketLoadBalancer.rclientSubList[0].on + return it("should process the events with _processEditorEvent", function() { + return this.WebsocketLoadBalancer.rclientSubList[0].on .calledWith("message", sinon.match.func) - .should.equal true + .should.equal(true); + }); + }); - describe "_processEditorEvent", -> - describe "with bad JSON", -> - beforeEach -> - @isRestrictedUser = false - @SafeJsonParse.parse = sinon.stub().callsArgWith 1, new Error("oops") - @WebsocketLoadBalancer._processEditorEvent(@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"); + }); - it "should log an error", -> - @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", -> - beforeEach -> - @io.sockets = + describe("with a designated room", function() { + beforeEach(function() { + this.io.sockets = { clients: sinon.stub().returns([ - {id: 'client-id-1', emit: @emit1 = sinon.stub(), ol_context: {}} - {id: 'client-id-2', emit: @emit2 = sinon.stub(), ol_context: {}} - {id: 'client-id-1', emit: @emit3 = sinon.stub(), ol_context: {}} # duplicate client + {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 ]) - data = JSON.stringify - room_id: @room_id - message: @message - payload: @payload - @WebsocketLoadBalancer._processEditorEvent(@io, "editor-events", data) + }; + const data = JSON.stringify({ + room_id: this.room_id, + message: this.message, + payload: this.payload + }); + return this.WebsocketLoadBalancer._processEditorEvent(this.io, "editor-events", data); + }); - it "should send the message to all (unique) clients in the room", -> - @io.sockets.clients - .calledWith(@room_id) - .should.equal true - @emit1.calledWith(@message, @payload...).should.equal true - @emit2.calledWith(@message, @payload...).should.equal true - @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", -> - beforeEach -> - @io.sockets = + 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: @emit1 = sinon.stub(), ol_context: {}} - {id: 'client-id-2', emit: @emit2 = sinon.stub(), ol_context: {}} - {id: 'client-id-1', emit: @emit3 = sinon.stub(), ol_context: {}} # duplicate client - {id: 'client-id-4', emit: @emit4 = sinon.stub(), ol_context: {is_restricted_user: true}} + {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}} ]) - data = JSON.stringify - room_id: @room_id - message: @message - payload: @payload - @WebsocketLoadBalancer._processEditorEvent(@io, "editor-events", data) + }; + const data = JSON.stringify({ + room_id: this.room_id, + message: this.message, + payload: this.payload + }); + return this.WebsocketLoadBalancer._processEditorEvent(this.io, "editor-events", data); + }); - it "should send the message to all (unique) clients in the room", -> - @io.sockets.clients - .calledWith(@room_id) - .should.equal true - @emit1.calledWith(@message, @payload...).should.equal true - @emit2.calledWith(@message, @payload...).should.equal true - @emit3.called.should.equal false # duplicate client should be ignored - @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", -> - beforeEach -> - @io.sockets = + 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: @emit1 = sinon.stub(), ol_context: {}} - {id: 'client-id-2', emit: @emit2 = sinon.stub(), ol_context: {}} - {id: 'client-id-1', emit: @emit3 = sinon.stub(), ol_context: {}} # duplicate client - {id: 'client-id-4', emit: @emit4 = sinon.stub(), ol_context: {is_restricted_user: true}} + {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}} ]) - data = JSON.stringify - room_id: @room_id - message: @restrictedMessage = 'new-comment' - payload: @payload - @WebsocketLoadBalancer._processEditorEvent(@io, "editor-events", data) + }; + 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); + }); - it "should send the message to all (unique) clients in the room, who are not restricted", -> - @io.sockets.clients - .calledWith(@room_id) - .should.equal true - @emit1.calledWith(@restrictedMessage, @payload...).should.equal true - @emit2.calledWith(@restrictedMessage, @payload...).should.equal true - @emit3.called.should.equal false # duplicate client should be ignored - @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 - describe "when emitting to all", -> - beforeEach -> - @io.sockets = - emit: @emit = sinon.stub() - data = JSON.stringify - room_id: "all" - message: @message - payload: @payload - @WebsocketLoadBalancer._processEditorEvent(@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); + }); - it "should send the message to all clients", -> - @emit.calledWith(@message, @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/coffee/helpers/MockClient.js b/services/real-time/test/unit/coffee/helpers/MockClient.js index 497928132a..bdf711ac8d 100644 --- a/services/real-time/test/unit/coffee/helpers/MockClient.js +++ b/services/real-time/test/unit/coffee/helpers/MockClient.js @@ -1,13 +1,16 @@ -sinon = require('sinon') +let MockClient; +const sinon = require('sinon'); -idCounter = 0 +let idCounter = 0; -module.exports = class MockClient - constructor: () -> - @ol_context = {} - @join = sinon.stub() - @emit = sinon.stub() - @disconnect = sinon.stub() - @id = idCounter++ - @publicId = idCounter++ - disconnect: () -> +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() {} +});