Просмотр исходного кода

decaffeinate: Convert AuthorizationManagerTests.coffee and 13 other files to JS

decaffeinate 6 лет назад
Родитель
Сommit
2ca620e7a0

+ 214 - 164
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'
-
-describe 'AuthorizationManager', ->
-	beforeEach ->
-		@client =
-			ol_context: {}
-
-		@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()
-
-		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 owner privilegeLevel", (done) ->
-			@client.ol_context.privilege_level = "owner"
-			@AuthorizationManager.assertClientCanViewProject @client, (error) ->
-				expect(error).to.be.null
-				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()
-
-	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()
-
-		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 owner privilegeLevel", (done) ->
-			@client.ol_context.privilege_level = "owner"
-			@AuthorizationManager.assertClientCanEditProject @client, (error) ->
-				expect(error).to.be.null
-				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()
-
-	# check doc access for project
-
-	describe "assertClientCanViewProjectAndDoc", ->
-		beforeEach () ->
-			@doc_id = "12345"
-			@callback = sinon.stub()
-			@client.ol_context = {}
-
-		describe "when not authorised at the project level", ->
-			beforeEach () ->
-				@client.ol_context.privilege_level = "unknown"
-
-			it "should not allow access", () ->
-				@AuthorizationManager.assertClientCanViewProjectAndDoc @client, @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
-
-				it "should not allow access", () ->
-					@AuthorizationManager.assertClientCanViewProjectAndDoc @client, @doc_id, (err) ->
-						err.message.should.equal "not authorized"
-
-		describe "when authorised at the project level", ->
-			beforeEach () ->
-				@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 authorised at the document level", ->
-				beforeEach (done) ->
-					@AuthorizationManager.addAccessToDoc @client, @doc_id, done
-
-				it "should allow access", () ->
-					@AuthorizationManager.assertClientCanViewProjectAndDoc @client, @doc_id, @callback
-					@callback
+/*
+ * 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', function() {
+	beforeEach(function() {
+		this.client =
+			{ol_context: {}};
+
+		return this.AuthorizationManager = SandboxedModule.require(modulePath, {requires: {}});});
+
+	describe("assertClientCanViewProject", function() {
+		it("should allow the readOnly privilegeLevel", function(done) {
+			this.client.ol_context.privilege_level = "readOnly";
+			return this.AuthorizationManager.assertClientCanViewProject(this.client, function(error) {
+				expect(error).to.be.null;
+				return done();
+			});
+		});
+
+		it("should allow the readAndWrite privilegeLevel", function(done) {
+			this.client.ol_context.privilege_level = "readAndWrite";
+			return this.AuthorizationManager.assertClientCanViewProject(this.client, function(error) {
+				expect(error).to.be.null;
+				return done();
+			});
+		});
+
+		it("should allow the owner privilegeLevel", function(done) {
+			this.client.ol_context.privilege_level = "owner";
+			return this.AuthorizationManager.assertClientCanViewProject(this.client, function(error) {
+				expect(error).to.be.null;
+				return done();
+			});
+		});
+
+		return it("should return an error with any other privilegeLevel", function(done) {
+			this.client.ol_context.privilege_level = "unknown";
+			return this.AuthorizationManager.assertClientCanViewProject(this.client, function(error) {
+				error.message.should.equal("not authorized");
+				return done();
+			});
+		});
+	});
+
+	describe("assertClientCanEditProject", function() {
+		it("should not allow the readOnly privilegeLevel", function(done) {
+			this.client.ol_context.privilege_level = "readOnly";
+			return this.AuthorizationManager.assertClientCanEditProject(this.client, function(error) {
+				error.message.should.equal("not authorized");
+				return done();
+			});
+		});
+
+		it("should allow the readAndWrite privilegeLevel", function(done) {
+			this.client.ol_context.privilege_level = "readAndWrite";
+			return this.AuthorizationManager.assertClientCanEditProject(this.client, function(error) {
+				expect(error).to.be.null;
+				return done();
+			});
+		});
+
+		it("should allow the owner privilegeLevel", function(done) {
+			this.client.ol_context.privilege_level = "owner";
+			return this.AuthorizationManager.assertClientCanEditProject(this.client, function(error) {
+				expect(error).to.be.null;
+				return done();
+			});
+		});
+
+		return it("should return an error with any other privilegeLevel", function(done) {
+			this.client.ol_context.privilege_level = "unknown";
+			return this.AuthorizationManager.assertClientCanEditProject(this.client, function(error) {
+				error.message.should.equal("not authorized");
+				return done();
+			});
+		});
+	});
+
+	// check doc access for project
+
+	describe("assertClientCanViewProjectAndDoc", function() {
+		beforeEach(function() {
+			this.doc_id = "12345";
+			this.callback = sinon.stub();
+			return this.client.ol_context = {};});
+
+		describe("when not authorised at the project level", function() {
+			beforeEach(function() {
+				return this.client.ol_context.privilege_level = "unknown";
+			});
+
+			it("should not allow access", function() {
+				return this.AuthorizationManager.assertClientCanViewProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized"));
+			});
+
+			return describe("even when authorised at the doc level", function() {
+				beforeEach(function(done) {
+					return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, done);
+				});
+
+				return it("should not allow access", function() {
+					return this.AuthorizationManager.assertClientCanViewProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized"));
+				});
+			});
+		});
+
+		return describe("when authorised at the project level", function() {
+			beforeEach(function() {
+				return this.client.ol_context.privilege_level = "readOnly";
+			});
+
+			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", function() {
+				beforeEach(function(done) {
+					return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, done);
+				});
+
+				return it("should allow access", function() {
+					this.AuthorizationManager.assertClientCanViewProjectAndDoc(this.client, this.doc_id, this.callback);
+					return this.callback
 						.calledWith(null)
-						.should.equal true
-
-			describe "when document authorisation is added and then removed", ->
-				beforeEach (done) ->
-					@AuthorizationManager.addAccessToDoc @client, @doc_id, () =>
-						@AuthorizationManager.removeAccessToDoc @client, @doc_id, done
-
-				it "should deny access", () ->
-					@AuthorizationManager.assertClientCanViewProjectAndDoc @client, @doc_id, (err) ->
-						err.message.should.equal "not authorized"
-
-	describe "assertClientCanEditProjectAndDoc", ->
-		beforeEach () ->
-			@doc_id = "12345"
-			@callback = sinon.stub()
-			@client.ol_context = {}
-
-		describe "when not authorised at the project level", ->
-			beforeEach () ->
-				@client.ol_context.privilege_level = "readOnly"
-
-			it "should not allow access", () ->
-				@AuthorizationManager.assertClientCanEditProjectAndDoc @client, @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
-
-				it "should not allow access", () ->
-					@AuthorizationManager.assertClientCanEditProjectAndDoc @client, @doc_id, (err) ->
-						err.message.should.equal "not authorized"
-
-		describe "when authorised at the project level", ->
-			beforeEach () ->
-				@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 authorised at the document level", ->
-				beforeEach (done) ->
-					@AuthorizationManager.addAccessToDoc @client, @doc_id, done
-
-				it "should allow access", () ->
-					@AuthorizationManager.assertClientCanEditProjectAndDoc @client, @doc_id, @callback
-					@callback
+						.should.equal(true);
+				});
+			});
+
+			return describe("when document authorisation is added and then removed", function() {
+				beforeEach(function(done) {
+					return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, () => {
+						return this.AuthorizationManager.removeAccessToDoc(this.client, this.doc_id, done);
+					});
+				});
+
+				return it("should deny access", function() {
+					return this.AuthorizationManager.assertClientCanViewProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized"));
+				});
+			});
+		});
+	});
+
+	return describe("assertClientCanEditProjectAndDoc", function() {
+		beforeEach(function() {
+			this.doc_id = "12345";
+			this.callback = sinon.stub();
+			return this.client.ol_context = {};});
+
+		describe("when not authorised at the project level", function() {
+			beforeEach(function() {
+				return this.client.ol_context.privilege_level = "readOnly";
+			});
+
+			it("should not allow access", function() {
+				return this.AuthorizationManager.assertClientCanEditProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized"));
+			});
+
+			return describe("even when authorised at the doc level", function() {
+				beforeEach(function(done) {
+					return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, done);
+				});
+
+				return it("should not allow access", function() {
+					return this.AuthorizationManager.assertClientCanEditProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized"));
+				});
+			});
+		});
+
+		return 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", function() {
+                return this.AuthorizationManager.assertClientCanEditProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized"));
+            }));
+
+			describe("and authorised at the document level", function() {
+				beforeEach(function(done) {
+					return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, done);
+				});
+
+				return it("should allow access", function() {
+					this.AuthorizationManager.assertClientCanEditProjectAndDoc(this.client, this.doc_id, this.callback);
+					return this.callback
 						.calledWith(null)
-						.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
-
-				it "should deny access", () ->
-					@AuthorizationManager.assertClientCanEditProjectAndDoc @client, @doc_id, (err) ->
-						err.message.should.equal "not authorized"
+						.should.equal(true);
+				});
+			});
+
+			return describe("when document authorisation is added and then removed", function() {
+				beforeEach(function(done) {
+					return this.AuthorizationManager.addAccessToDoc(this.client, this.doc_id, () => {
+						return this.AuthorizationManager.removeAccessToDoc(this.client, this.doc_id, done);
+					});
+				});
+
+				return it("should deny access", function() {
+					return this.AuthorizationManager.assertClientCanEditProjectAndDoc(this.client, this.doc_id, err => err.message.should.equal("not authorized"));
+				});
+			});
+		});
+	});
+});

+ 270 - 216
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')
-
-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 "subscribe", ->
-
-		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
-
-			it "should subscribe to the redis channel", ->
-				@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
-
-			it "should subscribe to the redis channel again", ->
-				@rclient.subscribe.callCount.should.equal 2
-
-		describe "when subscribe errors", ->
-			beforeEach (done) ->
-				@rclient.subscribe = sinon.stub()
+/*
+ * 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', function() {
+	beforeEach(function() {
+		this.rclient = {};
+		this.other_rclient = {};
+		return this.ChannelManager = SandboxedModule.require(modulePath, { requires: {
+			"settings-sharelatex": (this.settings = {}),
+			"metrics-sharelatex": (this.metrics = {inc: sinon.stub(), summary: sinon.stub()}),
+			"logger-sharelatex": (this.logger = { log: sinon.stub(), warn: sinon.stub(), error: sinon.stub() })
+		}
+	});});
+
+	describe("subscribe", function() {
+
+		describe("when there is no existing subscription for this redis client", function() {
+			beforeEach(function(done) {
+				this.rclient.subscribe = sinon.stub().resolves();
+				this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef");
+				return setTimeout(done);
+			});
+
+			return it("should subscribe to the redis channel", function() {
+				return this.rclient.subscribe.calledWithExactly("applied-ops:1234567890abcdef").should.equal(true);
+			});
+		});
+
+		describe("when there is an existing subscription for this redis client", function() {
+			beforeEach(function(done) {
+				this.rclient.subscribe = sinon.stub().resolves();
+				this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef");
+				this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef");
+				return setTimeout(done);
+			});
+
+			return it("should subscribe to the redis channel again", function() {
+				return this.rclient.subscribe.callCount.should.equal(2);
+			});
+		});
+
+		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
-
-			it "should have recorded the error", ->
-				expect(@metrics.inc.calledWithExactly("subscribe.failed.applied-ops")).to.equal(true)
-
-			it "should subscribe again", ->
-				@rclient.subscribe.callCount.should.equal 2
-
-			it "should cleanup", ->
-				@ChannelManager.getClientMapEntry(@rclient).has("applied-ops:1234567890abcdef").should.equal false
-
-		describe "when subscribe errors and the clientChannelMap entry was replaced", ->
-			beforeEach (done) ->
-				@rclient.subscribe = sinon.stub()
+					.onSecondCall().resolves();
+				const p = this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef");
+				p.then(() => done(new Error('should not subscribe but fail'))).catch(err => {
+					err.message.should.equal("some redis error");
+					this.ChannelManager.getClientMapEntry(this.rclient).has("applied-ops:1234567890abcdef").should.equal(false);
+					this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef");
+					// subscribe is wrapped in Promise, delay other assertions
+					return setTimeout(done);
+				});
+				return null;
+			});
+
+			it("should have recorded the error", function() {
+				return expect(this.metrics.inc.calledWithExactly("subscribe.failed.applied-ops")).to.equal(true);
+			});
+
+			it("should subscribe again", function() {
+				return this.rclient.subscribe.callCount.should.equal(2);
+			});
+
+			return it("should cleanup", function() {
+				return this.ChannelManager.getClientMapEntry(this.rclient).has("applied-ops:1234567890abcdef").should.equal(false);
+			});
+		});
+
+		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
-
-				@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
-
-				# let the first subscribe error -> unsubscribe -> subscribe
-				setTimeout done
-
-			it "should cleanup the second subscribePromise", ->
-				expect(@ChannelManager.getClientMapEntry(@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
-
-			it "should subscribe to the redis channel on this redis client", ->
-				@rclient.subscribe.calledWithExactly("applied-ops:1234567890abcdef").should.equal true
-
-	describe "unsubscribe", ->
-
-		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
-
-			it "should unsubscribe from the redis channel", ->
-				@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
-
-			it "should still unsubscribe from the redis channel on this client", ->
-				@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
-
-			it "should have cleaned up", ->
-				@ChannelManager.getClientMapEntry(@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
-
-		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"
-
-				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
-
-			it "should have recorded the error", ->
-				expect(@metrics.inc.calledWithExactly("unsubscribe.failed.applied-ops")).to.equal(true)
-
-			it "should have subscribed", ->
-				@rclient.subscribe.called.should.equal true
-
-			it "should have discarded the finished Promise", ->
-				@ChannelManager.getClientMapEntry(@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
-
-			it "should unsubscribe from the redis channel", ->
-				@rclient.unsubscribe.calledWithExactly("applied-ops:1234567890abcdef").should.equal true
-
-	describe "publish", ->
-
-		describe "when the channel is 'all'", ->
-			beforeEach ->
-				@rclient.publish = sinon.stub()
-				@ChannelManager.publish @rclient, "applied-ops", "all", "random-message"
-
-			it "should publish on the base channel", ->
-				@rclient.publish.calledWithExactly("applied-ops", "random-message").should.equal true
-
-		describe "when the channel has an specific id", ->
-
-			describe "when the individual channel setting is false", ->
-				beforeEach ->
-					@rclient.publish = sinon.stub()
-					@settings.publishOnIndividualChannels = false
-					@ChannelManager.publish @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
-
-			describe "when the individual channel setting is true", ->
-				beforeEach ->
-					@rclient.publish = sinon.stub()
-					@settings.publishOnIndividualChannels = true
-					@ChannelManager.publish @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
-
-		describe "metrics", ->
-			beforeEach ->
-				@rclient.publish = sinon.stub()
-				@ChannelManager.publish @rclient, "applied-ops", "all", "random-message"
-
-			it "should track the payload size", ->
-				@metrics.summary.calledWithExactly(
+					.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);
+
+				this.rclient.unsubscribe = sinon.stub().resolves();
+				this.ChannelManager.unsubscribe(this.rclient, "applied-ops", "1234567890abcdef");
+				this.second = this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef");
+				// should get replaced immediately
+				expect(this.ChannelManager.getClientMapEntry(this.rclient).get("applied-ops:1234567890abcdef")).to.equal(this.second);
+
+				// let the first subscribe error -> unsubscribe -> subscribe
+				return setTimeout(done);
+			});
+
+			return it("should cleanup the second subscribePromise", function() {
+				return expect(this.ChannelManager.getClientMapEntry(this.rclient).has("applied-ops:1234567890abcdef")).to.equal(false);
+			});
+		});
+
+		return describe("when there is an existing subscription for another redis client but not this one", function() {
+			beforeEach(function(done) {
+				this.other_rclient.subscribe = sinon.stub().resolves();
+				this.ChannelManager.subscribe(this.other_rclient, "applied-ops", "1234567890abcdef");
+				this.rclient.subscribe = sinon.stub().resolves();  // discard the original stub
+				this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef");
+				return setTimeout(done);
+			});
+
+			return it("should subscribe to the redis channel on this redis client", function() {
+				return this.rclient.subscribe.calledWithExactly("applied-ops:1234567890abcdef").should.equal(true);
+			});
+		});
+	});
+
+	describe("unsubscribe", function() {
+
+		describe("when there is no existing subscription for this redis client", function() {
+			beforeEach(function(done) {
+				this.rclient.unsubscribe = sinon.stub().resolves();
+				this.ChannelManager.unsubscribe(this.rclient, "applied-ops", "1234567890abcdef");
+				return setTimeout(done);
+			});
+
+			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", function() {
+			beforeEach(function(done) {
+				this.other_rclient.subscribe = sinon.stub().resolves();
+				this.rclient.unsubscribe = sinon.stub().resolves();
+				this.ChannelManager.subscribe(this.other_rclient, "applied-ops", "1234567890abcdef");
+				this.ChannelManager.unsubscribe(this.rclient, "applied-ops", "1234567890abcdef");
+				return setTimeout(done);
+			});
+
+			return it("should still unsubscribe from the redis channel on this client", function() {
+				return this.rclient.unsubscribe.called.should.equal(true);
+			});
+		});
+
+		describe("when unsubscribe errors and completes", function() {
+			beforeEach(function(done) {
+				this.rclient.subscribe = sinon.stub().resolves();
+				this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef");
+				this.rclient.unsubscribe = sinon.stub().rejects(new Error("some redis error"));
+				this.ChannelManager.unsubscribe(this.rclient, "applied-ops", "1234567890abcdef");
+				setTimeout(done);
+				return null;
+			});
+
+			it("should have cleaned up", function() {
+				return this.ChannelManager.getClientMapEntry(this.rclient).has("applied-ops:1234567890abcdef").should.equal(false);
+			});
+
+			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", 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
+					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", function() {
+				return expect(this.metrics.inc.calledWithExactly("unsubscribe.failed.applied-ops")).to.equal(true);
+			});
+
+			it("should have subscribed", function() {
+				return this.rclient.subscribe.called.should.equal(true);
+			});
+
+			return it("should have discarded the finished Promise", function() {
+				return this.ChannelManager.getClientMapEntry(this.rclient).has("applied-ops:1234567890abcdef").should.equal(false);
+			});
+		});
+
+		return describe("when there is an existing subscription for this redis client", function() {
+			beforeEach(function(done) {
+				this.rclient.subscribe = sinon.stub().resolves();
+				this.rclient.unsubscribe = sinon.stub().resolves();
+				this.ChannelManager.subscribe(this.rclient, "applied-ops", "1234567890abcdef");
+				this.ChannelManager.unsubscribe(this.rclient, "applied-ops", "1234567890abcdef");
+				return setTimeout(done);
+			});
+
+			return it("should unsubscribe from the redis channel", function() {
+				return this.rclient.unsubscribe.calledWithExactly("applied-ops:1234567890abcdef").should.equal(true);
+			});
+		});
+	});
+
+	return describe("publish", function() {
+
+		describe("when the channel is 'all'", function() {
+			beforeEach(function() {
+				this.rclient.publish = sinon.stub();
+				return this.ChannelManager.publish(this.rclient, "applied-ops", "all", "random-message");
+			});
+
+			return 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", function() {
+
+			describe("when the individual channel setting is false", function() {
+				beforeEach(function() {
+					this.rclient.publish = sinon.stub();
+					this.settings.publishOnIndividualChannels = false;
+					return this.ChannelManager.publish(this.rclient, "applied-ops", "1234567890abcdef", "random-message");
+				});
+
+				return it("should publish on the per-id channel", function() {
+					this.rclient.publish.calledWithExactly("applied-ops", "random-message").should.equal(true);
+					return this.rclient.publish.calledOnce.should.equal(true);
+				});
+			});
+
+			return describe("when the individual channel setting is true", function() {
+				beforeEach(function() {
+					this.rclient.publish = sinon.stub();
+					this.settings.publishOnIndividualChannels = true;
+					return this.ChannelManager.publish(this.rclient, "applied-ops", "1234567890abcdef", "random-message");
+				});
+
+				return it("should publish on the per-id channel", function() {
+					this.rclient.publish.calledWithExactly("applied-ops:1234567890abcdef", "random-message").should.equal(true);
+					return this.rclient.publish.calledOnce.should.equal(true);
+				});
+			});
+		});
+
+		return describe("metrics", function() {
+			beforeEach(function() {
+				this.rclient.publish = sinon.stub();
+				return this.ChannelManager.publish(this.rclient, "applied-ops", "all", "random-message");
+			});
+
+			return it("should track the payload size", function() {
+				return this.metrics.summary.calledWithExactly(
 					"redis.publish.applied-ops",
 					"random-message".length
-				).should.equal true
+				).should.equal(true);
+			});
+		});
+	});
+});

+ 219 - 162
services/real-time/test/unit/coffee/ConnectedUsersManagerTests.js

@@ -1,164 +1,221 @@
-
-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")
-
-
-describe "ConnectedUsersManager", ->
-
-	beforeEach ->
-
-		@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())
-
-		@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"
+/*
+ * 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 SandboxedModule = require('sandboxed-module');
+const assert = require('assert');
+const path = require('path');
+const sinon = require('sinon');
+const modulePath = path.join(__dirname, "../../../app/js/ConnectedUsersManager");
+const {
+    expect
+} = require("chai");
+const tk = require("timekeeper");
+
+
+describe("ConnectedUsersManager", function() {
+
+	beforeEach(function() {
+
+		this.settings = {
+			redis: {
+				realtime: {
+					key_schema: {
+						clientsInProject({project_id}) { return `clients_in_project:${project_id}`; },
+						connectedUser({project_id, client_id}){ return `connected_user:${project_id}:${client_id}`; }
+					}
+				}
+			}
+		};
+		this.rClient = {
+			auth() {},
+			setex:sinon.stub(),
+			sadd:sinon.stub(),
+			get: sinon.stub(),
+			srem:sinon.stub(),
+			del:sinon.stub(),
+			smembers:sinon.stub(),
+			expire:sinon.stub(),
+			hset:sinon.stub(),
+			hgetall:sinon.stub(),
+			exec:sinon.stub(),
+			multi: () => { return this.rClient; }
+		};
+		tk.freeze(new Date());
+
+		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' }
-
-	afterEach -> 
-		tk.reset()
-
-	describe "updateUserPosition", ->
-		beforeEach ->
-			@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 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 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 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 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 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 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 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 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()
-
-	describe "markUserAsDisconnected", ->
-		beforeEach ->
-			@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 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 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()
-
-	describe "_getConnectedUser", ->
-
-		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 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 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()
-
-	describe "getConnectedUsers", ->
-
-		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
-
-		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()
+		}
+	}
+		);
+		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());
+
+	describe("updateUserPosition", function() {
+		beforeEach(function() {
+			return this.rClient.exec.callsArgWith(0);
+		});
+
+		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", 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", 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", 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", 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", 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", 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", 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();
+			});
+		});
+
+		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", function() {
+		beforeEach(function() {
+			return this.rClient.exec.callsArgWith(0);
+		});
+
+		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", 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();
+			});
+		});
+
+		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", function() {
+
+		it("should return a connected user if there is a user object", function(done){
+			const cursorData = JSON.stringify({cursorData:{row:1}});
+			this.rClient.hgetall.callsArgWith(1, null, {connected_at:new Date(), user_id: this.user._id, last_updated_at: `${Date.now()}`, cursorData});
+			return this.ConnectedUsersManager._getConnectedUser(this.project_id, this.client_id, (err, result)=> {
+				result.connected.should.equal(true);
+				result.client_id.should.equal(this.client_id);
+				return done();
+			});
+		});
+
+		it("should return a not connected user if there is no object", function(done){
+			this.rClient.hgetall.callsArgWith(1, null, null);
+			return this.ConnectedUsersManager._getConnectedUser(this.project_id, this.client_id, (err, result)=> {
+				result.connected.should.equal(false);
+				result.client_id.should.equal(this.client_id);
+				return done();
+			});
+		});
+
+		return it("should return a not connected user if there is an empty object", function(done){
+			this.rClient.hgetall.callsArgWith(1, null, {});
+			return this.ConnectedUsersManager._getConnectedUser(this.project_id, this.client_id, (err, result)=> {
+				result.connected.should.equal(false);
+				result.client_id.should.equal(this.client_id);
+				return done();
+			});
+		});
+	});
+
+	return describe("getConnectedUsers", function() {
+
+		beforeEach(function() {
+			this.users = ["1234", "5678", "9123", "8234"];
+			this.rClient.smembers.callsArgWith(1, null, this.users);
+			this.ConnectedUsersManager._getConnectedUser = sinon.stub();
+			this.ConnectedUsersManager._getConnectedUser.withArgs(this.project_id, this.users[0]).callsArgWith(2, null, {connected:true, client_age: 2, client_id:this.users[0]});
+			this.ConnectedUsersManager._getConnectedUser.withArgs(this.project_id, this.users[1]).callsArgWith(2, null, {connected:false, client_age: 1, client_id:this.users[1]});
+			this.ConnectedUsersManager._getConnectedUser.withArgs(this.project_id, this.users[2]).callsArgWith(2, null, {connected:true, client_age: 3, client_id:this.users[2]});
+			return this.ConnectedUsersManager._getConnectedUser.withArgs(this.project_id, this.users[3]).callsArgWith(2, null, {connected:true, client_age: 11, client_id:this.users[3]});
+		}); // connected but old
+
+		return it("should only return the users in the list which are still in redis and recently updated", function(done){
+			return this.ConnectedUsersManager.getConnectedUsers(this.project_id, (err, users)=> {
+				users.length.should.equal(2);
+				users[0].should.deep.equal({client_id:this.users[0], client_age: 2, connected:true});
+				users[1].should.deep.equal({client_id:this.users[2], client_age: 3, connected:true});
+				return done();
+			});
+		});
+	});
+});
 

+ 191 - 141
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"
-
-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}"
+/*
+ * 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", 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 = {}
-
-	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()
+				}
+			}),
+			"redis-sharelatex" : (this.redis = {
+				createClient: name => {
+					let rclientStub;
+					this.rclient.push(rclientStub = {name});
+					return rclientStub;
+				}
+			}),
+			"./SafeJsonParse": (this.SafeJsonParse =
+				{parse: (data, cb) => cb(null, JSON.parse(data))}),
+			"./EventLogger": (this.EventLogger = {checkEventOrder: sinon.stub()}),
+			"./HealthCheckManager": {check: sinon.stub()},
+			"metrics-sharelatex": (this.metrics = {inc: sinon.stub()}),
+			"./RoomManager" : (this.RoomManager = { eventSource: sinon.stub().returns(this.RoomEvents)}),
+			"./ChannelManager": (this.ChannelManager = {})
+		}
+	});});
+
+	describe("listenForUpdatesFromDocumentUpdater", function() {
+		beforeEach(function() {
+			this.rclient.length = 0;  // clear any existing clients
+			this.EditorUpdatesController.rclientList = [this.redis.createClient("first"), this.redis.createClient("second")];
+			this.rclient[0].subscribe = sinon.stub();
+			this.rclient[0].on = sinon.stub();
+			this.rclient[1].subscribe = sinon.stub();
+			this.rclient[1].on = sinon.stub();
+			return this.EditorUpdatesController.listenForUpdatesFromDocumentUpdater();
+		});
 		
-		it "should subscribe to the doc-updater stream", ->
-			@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 subscribe to any additional doc-updater stream", ->
-			@rclient[1].subscribe.calledWith("applied-ops").should.equal true
-			@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"
+		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", function() {
+			return this.rclient[0].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", 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
-
-		describe "with update", ->
-			beforeEach ->
-				@message =
-					doc_id: @doc_id
+			return it("should log an error", function() {
+				return this.logger.error.called.should.equal(true);
+			});
+		});
+
+		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)
-
-			it "should apply the update", ->
-				@EditorUpdatesController._applyUpdateFromDocumentUpdater
-					.calledWith(@io, @doc_id, @message.op)
-					.should.equal true
-
-		describe "with error", ->
-			beforeEach ->
-				@message =
-					doc_id: @doc_id
+				};
+				this.EditorUpdatesController._applyUpdateFromDocumentUpdater = sinon.stub();
+				return this.EditorUpdatesController._processMessageFromDocumentUpdater(this.io, "applied-ops", JSON.stringify(this.message));
+			});
+
+			return it("should apply the update", function() {
+				return this.EditorUpdatesController._applyUpdateFromDocumentUpdater
+					.calledWith(this.io, this.doc_id, this.message.op)
+					.should.equal(true);
+			});
+		});
+
+		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)
-
-			it "should process the error", ->
-				@EditorUpdatesController._processErrorFromDocumentUpdater
-					.calledWith(@io, @doc_id, @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
+				};
+				this.EditorUpdatesController._processErrorFromDocumentUpdater = sinon.stub();
+				return this.EditorUpdatesController._processMessageFromDocumentUpdater(this.io, "applied-ops", JSON.stringify(this.message));
+			});
+
+			return it("should process the error", function() {
+				return this.EditorUpdatesController._processErrorFromDocumentUpdater
+					.calledWith(this.io, this.doc_id, this.message.error)
+					.should.equal(true);
+			});
+		});
+	});
+
+	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
-
-			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 get the clients connected to the document", ->
-				@io.sockets.clients
-					.calledWith(@doc_id)
-					.should.equal true
-
-			it "should send the full update to the other clients", ->
-				for client in @otherClients
+		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", 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", function() {
+				return this.io.sockets.clients
+					.calledWith(this.doc_id)
+					.should.equal(true);
+			});
+
+			return it("should send the full update to the other clients", function() {
+				return Array.from(this.otherClients).map((client) =>
 					client.emit
-						.calledWith("otUpdateApplied", @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 not send anything to the other clients (they've already had the op)", ->
-				for client in @otherClients
+			it("should send a version bump to the source client as usual", function() {
+				return this.sourceClient.emit
+					.calledWith("otUpdateApplied", {v: this.version, doc: this.doc_id})
+					.should.equal(true);
+			});
+
+			return it("should not send anything to the other clients (they've already had the op)", function() {
+				return Array.from(this.otherClients).map((client) =>
 					client.emit
 						.calledWith("otUpdateApplied")
-						.should.equal false
-
-	describe "_processErrorFromDocumentUpdater", ->
-		beforeEach ->
-			@clients = [new MockClient(), new MockClient()]
-			@io.sockets =
-				clients: sinon.stub().returns(@clients)
-			@EditorUpdatesController._processErrorFromDocumentUpdater @io, @doc_id, "Something went wrong"
-
-		it "should log a warning", ->
-			@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
+						.should.equal(false));
+			});
+		});
+	});
+
+	return describe("_processErrorFromDocumentUpdater", function() {
+		beforeEach(function() {
+			this.clients = [new MockClient(), new MockClient()];
+			this.io.sockets =
+				{clients: sinon.stub().returns(this.clients)};
+			return this.EditorUpdatesController._processErrorFromDocumentUpdater(this.io, this.doc_id, "Something went wrong");
+		});
+
+		it("should log a warning", function() {
+			return this.logger.warn.called.should.equal(true);
+		});
+
+		return it("should disconnect all clients in that document", function() {
+			this.io.sockets.clients.calledWith(this.doc_id).should.equal(true);
+			return Array.from(this.clients).map((client) =>
+				client.disconnect.called.should.equal(true));
+		});
+	});
+});
 

+ 257 - 190
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'
-
-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}"
+/*
+ * 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', function() {
+	beforeEach(function() {
+		let Timer;
+		this.project_id = "project-id-923";
+		this.doc_id = "doc-id-394";
+		this.lines = ["one", "two", "three"];
+		this.version = 42;
+		this.settings = {
+			apis: { documentupdater: {url: "http://doc-updater.example.com"}
+		},
+			redis: { documentupdater: {
+				key_schema: {
+					pendingUpdates({doc_id}) { return `PendingUpdates:${doc_id}`; }
+				}
+			}
+		},
 			maxUpdateSize: 7 * 1024 * 1024
-		@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 = {
+		};
+		this.rclient = {auth() {}};
+
+		return this.DocumentUpdaterManager = SandboxedModule.require(modulePath, {
+			requires: {
+				'settings-sharelatex':this.settings,
+				'logger-sharelatex': (this.logger = {log: sinon.stub(), error: sinon.stub(), warn: sinon.stub()}),
+				'request': (this.request = {}),
+				'redis-sharelatex' : { createClient: () => this.rclient
+			},
+				'metrics-sharelatex': (this.Metrics = {
+					summary: sinon.stub(),
+					Timer: (Timer = class Timer {
+						done() {}
+					})
+				})
+			},
+			globals: {
+				JSON: (this.JSON = Object.create(JSON))
+			}
+		}
+		);
+	}); // avoid modifying JSON object directly
+
+	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]
+				"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
-				}
-				@DocumentUpdaterManager.queueChange(@project_id, @doc_id, @change, @callback)
-
-			it "should remove the invalid keys from the change", ->
-				@rclient.rpush
-					.calledWith("PendingUpdates:#{@doc_id}", JSON.stringify({op:@change.op}))
-					.should.equal true
+			};
+			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);
+			});
+		});
+	});
+});

+ 94 - 62
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);
+					});
+				});
+			});
+		});
+	});
+});

+ 88 - 63
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
+		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;
+        }));
+	});
+});

+ 355 - 284
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')
-
-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')
+/*
+ * 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', 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)
-
-			beforeEach (done) ->
-				@onUnhandled = (error) =>
-					@unhandledError = error
-					done(new Error("unhandledRejection: #{error.message}"))
-				process.on("unhandledRejection", @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")))
-
-			it "should keep going", () ->
-				expect(@unhandledError).to.not.exist
-
-	describe "joinProject", ->
+	describe("emitOnCompletion", () => describe("when a subscribe errors", function() {
+        afterEach(function() {
+            return process.removeListener("unhandledRejection", this.onUnhandled);
+        });
+
+        beforeEach(function(done) {
+            this.onUnhandled = error => {
+                this.unhandledError = error;
+                return done(new Error(`unhandledRejection: ${error.message}`));
+            };
+            process.on("unhandledRejection", this.onUnhandled);
+
+            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")));
+        });
+
+        return it("should keep going", function() {
+            return expect(this.unhandledError).to.not.exist;
+        });
+    }));
+
+	describe("joinProject", function() {
 	
-		describe "when the project room is empty", ->
-
-			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()
-
-			it "should emit a 'project-active' event with the id", ->
-				@RoomEvents.emit.calledWithExactly('project-active', @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 join the room using the id", ->
-				@client.join.calledWithExactly(@project_id).should.equal true
-
-		describe "when there are other clients in the project room", ->
-
-			beforeEach ->
-				@RoomManager._clientsInRoom
-					.withArgs(@client, @project_id)
+		describe("when the project room is empty", function() {
+
+			beforeEach(function(done) {
+				this.RoomManager._clientsInRoom
+					.withArgs(this.client, this.project_id)
+					.onFirstCall().returns(0);
+				this.client.join = sinon.stub();
+				this.callback = sinon.stub();
+				this.RoomEvents.on('project-active', id => {
+					return setTimeout(() => {
+						return this.RoomEvents.emit(`project-subscribed-${id}`);
+					}
+					, 100);
+				});
+				return this.RoomManager.joinProject(this.client, this.project_id, err => {
+					this.callback(err);
+					return done();
+				});
+			});
+
+			it("should emit a 'project-active' event with the id", function() {
+				return this.RoomEvents.emit.calledWithExactly('project-active', this.project_id).should.equal(true);
+			});
+
+			it("should listen for the 'project-subscribed-id' event", function() {
+				return this.RoomEvents.once.calledWith(`project-subscribed-${this.project_id}`).should.equal(true);
+			});
+
+			return it("should join the room using the id", function() {
+				return this.client.join.calledWithExactly(this.project_id).should.equal(true);
+			});
+		});
+
+		return describe("when there are other clients in the project room", function() {
+
+			beforeEach(function() {
+				this.RoomManager._clientsInRoom
+					.withArgs(this.client, this.project_id)
 					.onFirstCall().returns(123)
-					.onSecondCall().returns(124)
-				@client.join = sinon.stub()
-				@RoomManager.joinProject @client, @project_id
-
-			it "should join the room using the id", ->
-				@client.join.called.should.equal true
-
-			it "should not emit any events", ->
-				@RoomEvents.emit.called.should.equal false
-
-
-	describe "joinDoc", ->
-
-		describe "when the doc room is empty", ->
-
-			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()
-
-			it "should emit a 'doc-active' event with the id", ->
-				@RoomEvents.emit.calledWithExactly('doc-active', @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 join the room using the id", ->
-				@client.join.calledWithExactly(@doc_id).should.equal true
-
-		describe "when there are other clients in the doc room", ->
-
-			beforeEach ->
-				@RoomManager._clientsInRoom
-					.withArgs(@client, @doc_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", function() {
+				return this.client.join.called.should.equal(true);
+			});
+
+			return it("should not emit any events", function() {
+				return this.RoomEvents.emit.called.should.equal(false);
+			});
+		});
+	});
+
+
+	describe("joinDoc", function() {
+
+		describe("when the doc room is empty", function() {
+
+			beforeEach(function(done) {
+				this.RoomManager._clientsInRoom
+					.withArgs(this.client, this.doc_id)
+					.onFirstCall().returns(0);
+				this.client.join = sinon.stub();
+				this.callback = sinon.stub();
+				this.RoomEvents.on('doc-active', id => {
+					return setTimeout(() => {
+						return this.RoomEvents.emit(`doc-subscribed-${id}`);
+					}
+					, 100);
+				});
+				return this.RoomManager.joinDoc(this.client, this.doc_id, err => {
+					this.callback(err);
+					return done();
+				});
+			});
+
+			it("should emit a 'doc-active' event with the id", function() {
+				return this.RoomEvents.emit.calledWithExactly('doc-active', this.doc_id).should.equal(true);
+			});
+
+			it("should listen for the 'doc-subscribed-id' event", function() {
+				return this.RoomEvents.once.calledWith(`doc-subscribed-${this.doc_id}`).should.equal(true);
+			});
+
+			return it("should join the room using the id", function() {
+				return this.client.join.calledWithExactly(this.doc_id).should.equal(true);
+			});
+		});
+
+		return describe("when there are other clients in the doc room", function() {
+
+			beforeEach(function() {
+				this.RoomManager._clientsInRoom
+					.withArgs(this.client, this.doc_id)
 					.onFirstCall().returns(123)
-					.onSecondCall().returns(124)
-				@client.join = sinon.stub()
-				@RoomManager.joinDoc @client, @doc_id
-
-			it "should join the room using the id", ->
-				@client.join.called.should.equal true
-
-			it "should not emit any events", ->
-				@RoomEvents.emit.called.should.equal false
-
-
-	describe "leaveDoc", ->
-
-		describe "when doc room will be empty after this client has left", ->
-
-			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
-
-			it "should leave the room using the id", ->
-				@client.leave.calledWithExactly(@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
-
-
-		describe "when there are other clients in the doc room", ->
-
-			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
-
-			it "should leave the room using the id", ->
-				@client.leave.calledWithExactly(@doc_id).should.equal true
-
-			it "should not emit any events", ->
-				@RoomEvents.emit.called.should.equal false
-
-		describe "when the client is not in the doc room", ->
-
-			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
-
-			it "should not leave the room", ->
-				@client.leave.called.should.equal false
-
-			it "should not emit any events", ->
-				@RoomEvents.emit.called.should.equal false
-
-
-	describe "leaveProjectAndDocs", ->
-
-		describe "when the client is connected to the project and multiple docs", ->
-
-			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", ->
-
-				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", ->
-					@client.leave.calledWithExactly(@doc_id).should.equal true
-					@client.leave.calledWithExactly(@other_doc_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", ->
-					@RoomEvents.emit.calledWithExactly('doc-empty', @doc_id).should.equal true
-					@RoomEvents.emit.calledWithExactly('doc-empty', @other_doc_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
-
-			describe "when other clients are still connected", ->
-
-				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", ->
-					@client.leave.calledWithExactly(@doc_id).should.equal true
-					@client.leave.calledWithExactly(@other_doc_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
+					.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", function() {
+				return this.client.join.called.should.equal(true);
+			});
+
+			return it("should not emit any events", function() {
+				return this.RoomEvents.emit.called.should.equal(false);
+			});
+		});
+	});
+
+
+	describe("leaveDoc", function() {
+
+		describe("when doc room will be empty after this client has left", function() {
+
+			beforeEach(function() {
+				this.RoomManager._clientAlreadyInRoom
+					.withArgs(this.client, this.doc_id)
+					.returns(true);
+				this.RoomManager._clientsInRoom
+					.withArgs(this.client, this.doc_id)
+					.onCall(0).returns(0);
+				this.client.leave = sinon.stub();
+				return this.RoomManager.leaveDoc(this.client, this.doc_id);
+			});
+
+			it("should leave the room using the id", function() {
+				return this.client.leave.calledWithExactly(this.doc_id).should.equal(true);
+			});
+
+			return it("should emit a '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", function() {
+
+			beforeEach(function() {
+				this.RoomManager._clientAlreadyInRoom
+					.withArgs(this.client, this.doc_id)
+					.returns(true);
+				this.RoomManager._clientsInRoom
+					.withArgs(this.client, this.doc_id)
+					.onCall(0).returns(123);
+				this.client.leave = sinon.stub();
+				return this.RoomManager.leaveDoc(this.client, this.doc_id);
+			});
+
+			it("should leave the room using the id", function() {
+				return this.client.leave.calledWithExactly(this.doc_id).should.equal(true);
+			});
+
+			return it("should not emit any events", function() {
+				return this.RoomEvents.emit.called.should.equal(false);
+			});
+		});
+
+		return describe("when the client is not in the doc room", function() {
+
+			beforeEach(function() {
+				this.RoomManager._clientAlreadyInRoom
+					.withArgs(this.client, this.doc_id)
+					.returns(false);
+				this.RoomManager._clientsInRoom
+					.withArgs(this.client, this.doc_id)
+					.onCall(0).returns(0);
+				this.client.leave = sinon.stub();
+				return this.RoomManager.leaveDoc(this.client, this.doc_id);
+			});
+
+			it("should not leave the room", function() {
+				return this.client.leave.called.should.equal(false);
+			});
+
+			return it("should not emit any events", function() {
+				return this.RoomEvents.emit.called.should.equal(false);
+			});
+		});
+	});
+
+
+	return describe("leaveProjectAndDocs", () => describe("when the client is connected to the project and multiple docs", function() {
+
+        beforeEach(function() {
+            this.RoomManager._roomsClientIsIn = sinon.stub().returns([this.project_id, this.doc_id, this.other_doc_id]);
+            this.client.join = sinon.stub();
+            return this.client.leave = sinon.stub();
+        });
+
+        describe("when this is the only client connected", function() {
+
+            beforeEach(function(done) {
+                // first call is for the join,
+                // second for the leave
+                this.RoomManager._clientsInRoom
+                    .withArgs(this.client, this.doc_id)
+                    .onCall(0).returns(0)
+                    .onCall(1).returns(0);
+                this.RoomManager._clientsInRoom
+                    .withArgs(this.client, this.other_doc_id)
+                    .onCall(0).returns(0)
+                    .onCall(1).returns(0);
+                this.RoomManager._clientsInRoom
+                    .withArgs(this.client, this.project_id)
+                    .onCall(0).returns(0)
+                    .onCall(1).returns(0);
+                this.RoomManager._clientAlreadyInRoom
+                    .withArgs(this.client, this.doc_id)
+                    .returns(true)
+                    .withArgs(this.client, this.other_doc_id)
+                    .returns(true)
+                    .withArgs(this.client, this.project_id)
+                    .returns(true);
+                this.RoomEvents.on('project-active', id => {
+                    return setTimeout(() => {
+                        return this.RoomEvents.emit(`project-subscribed-${id}`);
+                    }
+                    , 100);
+                });
+                this.RoomEvents.on('doc-active', id => {
+                    return setTimeout(() => {
+                        return this.RoomEvents.emit(`doc-subscribed-${id}`);
+                    }
+                    , 100);
+                });
+                // put the client in the rooms
+                return this.RoomManager.joinProject(this.client, this.project_id, () => {
+                    return this.RoomManager.joinDoc(this.client, this.doc_id, () => {
+                        return this.RoomManager.joinDoc(this.client, this.other_doc_id, () => {
+                            // now leave the project
+                            this.RoomManager.leaveProjectAndDocs(this.client);
+                            return done();
+                        });
+                    });
+                });
+            });
+
+            it("should leave all the docs", function() {
+                this.client.leave.calledWithExactly(this.doc_id).should.equal(true);
+                return this.client.leave.calledWithExactly(this.other_doc_id).should.equal(true);
+            });
+
+            it("should leave the project", function() {
+                return this.client.leave.calledWithExactly(this.project_id).should.equal(true);
+            });
+
+            it("should emit a 'doc-empty' event with the id for each doc", function() {
+                this.RoomEvents.emit.calledWithExactly('doc-empty', this.doc_id).should.equal(true);
+                return this.RoomEvents.emit.calledWithExactly('doc-empty', this.other_doc_id).should.equal(true);
+            });
+
+            return it("should emit a 'project-empty' event with the id for the project", function() {
+                return this.RoomEvents.emit.calledWithExactly('project-empty', this.project_id).should.equal(true);
+            });
+        });
+
+        return describe("when other clients are still connected", function() {
+
+            beforeEach(function() {
+                this.RoomManager._clientsInRoom
+                    .withArgs(this.client, this.doc_id)
+                    .onFirstCall().returns(123)
+                    .onSecondCall().returns(122);
+                this.RoomManager._clientsInRoom
+                    .withArgs(this.client, this.other_doc_id)
+                    .onFirstCall().returns(123)
+                    .onSecondCall().returns(122);
+                this.RoomManager._clientsInRoom
+                    .withArgs(this.client, this.project_id)
+                    .onFirstCall().returns(123)
+                    .onSecondCall().returns(122);
+                this.RoomManager._clientAlreadyInRoom
+                    .withArgs(this.client, this.doc_id)
+                    .returns(true)
+                    .withArgs(this.client, this.other_doc_id)
+                    .returns(true)
+                    .withArgs(this.client, this.project_id)
+                    .returns(true);
+                return this.RoomManager.leaveProjectAndDocs(this.client);
+            });
+
+            it("should leave all the docs", function() {
+                this.client.leave.calledWithExactly(this.doc_id).should.equal(true);
+                return this.client.leave.calledWithExactly(this.other_doc_id).should.equal(true);
+            });
+
+            it("should leave the project", function() {
+                return this.client.leave.calledWithExactly(this.project_id).should.equal(true);
+            });
+
+            return it("should not emit any events", function() {
+                return this.RoomEvents.emit.called.should.equal(false);
+            });
+        });
+    }));
+});

+ 46 - 29
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()
+		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();
+			});
+		});
+	});
+});

+ 169 - 125
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')
-
-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]
+/*
+ * 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', 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'}}]
-
-		@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)
-
-	describe 'without cookies', ->
-		before ->
-			@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 not query redis', (done) ->
-			@checkSocket @socket, () =>
-				expect(@sessionStore.get.called).to.equal(false)
-				done()
-
-	describe 'with a different cookie', ->
-		before ->
-			@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 not query redis', (done) ->
-			@checkSocket @socket, () =>
-				expect(@sessionStore.get.called).to.equal(false)
-				done()
-
-	describe 'with a valid cookie and a failing session lookup', ->
-		before ->
-			@socket = {handshake: {_signedCookies: {'ol.sid': 'error'}}}
-
-		it 'should query redis', (done) ->
-			@checkSocket @socket, () =>
-				expect(@sessionStore.get.called).to.equal(true)
-				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()
-
-	describe 'with a valid cookie and no matching session', ->
-		before ->
-			@socket = {handshake: {_signedCookies: {'ol.sid': 'unknownId'}}}
-
-		it 'should query redis', (done) ->
-			@checkSocket @socket, () =>
-				expect(@sessionStore.get.called).to.equal(true)
-				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()
-
-	describe 'with a valid cookie and a matching session', ->
-		before ->
-			@socket = {handshake: {_signedCookies: {'ol.sid': @id1}}}
-
-		it 'should query redis', (done) ->
-			@checkSocket @socket, () =>
-				expect(@sessionStore.get.called).to.equal(true)
-				done()
-
-		it 'should not return an error', (done) ->
-			@checkSocket @socket, (error) ->
-				expect(error).to.not.exist
-				done()
-
-		it 'should return the session', (done) ->
-			@checkSocket @socket, (error, s, session) ->
-				expect(session).to.deep.equal({user: {_id: '123'}})
-				done()
-
-	describe 'with a different valid cookie and matching session', ->
-		before ->
-			@socket = {handshake: {_signedCookies: {'ol.sid': @id2}}}
-
-		it 'should query redis', (done) ->
-			@checkSocket @socket, () =>
-				expect(@sessionStore.get.called).to.equal(true)
-				done()
-
-		it 'should not return an error', (done) ->
-			@checkSocket @socket, (error) ->
-				expect(error).to.not.exist
-				done()
-
-		it 'should return the other session', (done) ->
-			@checkSocket @socket, (error, s, session) ->
-				expect(session).to.deep.equal({user: {_id: 'abc'}})
-				done()
+		};
+		redisResponses[this.id1] = [null, {user: {_id: '123'}}];
+		redisResponses[this.id2] = [null, {user: {_id: 'abc'}}];
+
+		this.sessionStore = {
+			get: sinon.stub().callsFake((id, fn) => fn.apply(null, redisResponses[id]))
+		};
+		this.cookieParser = function(req, res, next) {
+			req.signedCookies = req._signedCookies;
+			return next();
+		};
+		this.SessionSockets = this.SessionSocketsModule(this.io, this.sessionStore, this.cookieParser, 'ol.sid');
+		return this.checkSocket = (socket, fn) => {
+			this.SessionSockets.once('connection', fn);
+			return this.io.emit('connection', socket);
+		};
+	});
+
+	describe('without cookies', function() {
+		before(function() {
+			return this.socket = {handshake: {}};});
+
+		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();
+			});
+		});
+
+		return it('should not query redis', function(done) {
+			return this.checkSocket(this.socket, () => {
+				expect(this.sessionStore.get.called).to.equal(false);
+				return done();
+			});
+		});
+	});
+
+	describe('with a different cookie', function() {
+		before(function() {
+			return this.socket = {handshake: {_signedCookies: {other: 1}}};});
+
+		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();
+			});
+		});
+
+		return it('should not query redis', function(done) {
+			return this.checkSocket(this.socket, () => {
+				expect(this.sessionStore.get.called).to.equal(false);
+				return done();
+			});
+		});
+	});
+
+	describe('with a valid cookie and a failing session lookup', function() {
+		before(function() {
+			return this.socket = {handshake: {_signedCookies: {'ol.sid': 'error'}}};});
+
+		it('should query redis', function(done) {
+			return this.checkSocket(this.socket, () => {
+				expect(this.sessionStore.get.called).to.equal(true);
+				return done();
+			});
+		});
+
+		return it('should return a redis error', function(done) {
+			return this.checkSocket(this.socket, 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', function() {
+		before(function() {
+			return this.socket = {handshake: {_signedCookies: {'ol.sid': 'unknownId'}}};});
+
+		it('should query redis', function(done) {
+			return this.checkSocket(this.socket, () => {
+				expect(this.sessionStore.get.called).to.equal(true);
+				return done();
+			});
+		});
+
+		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', function() {
+		before(function() {
+			return this.socket = {handshake: {_signedCookies: {'ol.sid': this.id1}}};});
+
+		it('should query redis', function(done) {
+			return this.checkSocket(this.socket, () => {
+				expect(this.sessionStore.get.called).to.equal(true);
+				return done();
+			});
+		});
+
+		it('should not return an error', function(done) {
+			return this.checkSocket(this.socket, function(error) {
+				expect(error).to.not.exist;
+				return 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();
+			});
+		});
+	});
+
+	return describe('with a different valid cookie and matching session', function() {
+		before(function() {
+			return this.socket = {handshake: {_signedCookies: {'ol.sid': this.id2}}};});
+
+		it('should query redis', function(done) {
+			return this.checkSocket(this.socket, () => {
+				expect(this.sessionStore.get.called).to.equal(true);
+				return done();
+			});
+		});
+
+		it('should not return an error', function(done) {
+			return this.checkSocket(this.socket, function(error) {
+				expect(error).to.not.exist;
+				return 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();
+			});
+		});
+	});
+});

+ 91 - 64
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);
+			});
+		});
+	});
+});

+ 1067 - 851
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"
-
-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")
+/*
+ * 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', 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
-
-			it "should load the project from web", ->
-				@WebApiManager.joinProject
-					.calledWith(@project_id, @user)
-					.should.equal true
-
-			it "should join the project room", ->
-				@RoomManager.joinProject.calledWith(@client, @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 mark the user as connected in ConnectedUsersManager", ->
-				@ConnectedUsersManager.updateUserPosition
-					.calledWith(@project_id, @client.publicId, @user, null)
-					.should.equal true
-
-			it "should increment the join-project metric", ->
-				@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
-
-			it "should return an error", ->
-				@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", function() {
+				return this.WebApiManager.joinProject
+					.calledWith(this.project_id, this.user)
+					.should.equal(true);
+			});
+
+			it("should join the project room", function() {
+				return this.RoomManager.joinProject.calledWith(this.client, this.project_id).should.equal(true);
+			});
+
+			it("should set the privilege level on the client", function() {
+				return this.client.ol_context["privilege_level"].should.equal(this.privilegeLevel);
+			});
+			it("should set the user's id on the client", function() {
+				return this.client.ol_context["user_id"].should.equal(this.user._id);
+			});
+			it("should set the user's email on the client", function() {
+				return this.client.ol_context["email"].should.equal(this.user.email);
+			});
+			it("should set the user's first_name on the client", function() {
+				return this.client.ol_context["first_name"].should.equal(this.user.first_name);
+			});
+			it("should set the user's last_name on the client", function() {
+				return this.client.ol_context["last_name"].should.equal(this.user.last_name);
+			});
+			it("should set the user's sign up date on the client", function() {
+				return this.client.ol_context["signup_date"].should.equal(this.user.signUpDate);
+			});
+			it("should set the user's login_count on the client", function() {
+				return this.client.ol_context["login_count"].should.equal(this.user.loginCount);
+			});
+			it("should set the connected time on the client", function() {
+				return this.client.ol_context["connected_time"].should.equal(new Date());
+			});
+			it("should set the project_id on the client", function() {
+				return this.client.ol_context["project_id"].should.equal(this.project_id);
+			});
+			it("should set the project owner id on the client", function() {
+				return this.client.ol_context["owner_id"].should.equal(this.owner_id);
+			});
+			it("should set the is_restricted_user flag on the client", function() {
+				return this.client.ol_context["is_restricted_user"].should.equal(this.isRestrictedUser);
+			});
+			it("should call the callback with the project, privilegeLevel and protocolVersion", function() {
+				return this.callback
+					.calledWith(null, this.project, this.privilegeLevel, this.WebsocketController.PROTOCOL_VERSION)
+					.should.equal(true);
+			});
+
+			it("should mark the user as connected in ConnectedUsersManager", function() {
+				return this.ConnectedUsersManager.updateUserPosition
+					.calledWith(this.project_id, this.client.publicId, this.user, null)
+					.should.equal(true);
+			});
+
+			return it("should increment the join-project metric", function() {
+				return this.metrics.inc.calledWith("editor.join-project").should.equal(true);
+			});
+		});
+
+		describe("when not authorized", function() {
+			beforeEach(function() {
+				this.WebApiManager.joinProject = sinon.stub().callsArgWith(2, null, null, null);
+				return this.WebsocketController.joinProject(this.client, this.user, this.project_id, this.callback);
+			});
+
+			it("should return an error", function() {
+				return this.callback
 					.calledWith(sinon.match({message: "not authorized"}))
-					.should.equal true
-
-			it "should not log an error", ->
-				@logger.error.called.should.equal false
-
-		describe "when the subscribe failed", ->
-			beforeEach ->
-				@client.id = "mock-client-id"
-				@project = {
-					name: "Test Project"
+					.should.equal(true);
+			});
+
+			return it("should not log an error", function() {
+				return this.logger.error.called.should.equal(false);
+			});
+		});
+
+		describe("when the subscribe failed", function() {
+			beforeEach(function() {
+				this.client.id = "mock-client-id";
+				this.project = {
+					name: "Test Project",
 					owner: {
-						_id: @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().callsArgWith(2, new Error("subscribe failed"))
-				@WebsocketController.joinProject @client, @user, @project_id, @callback
-
-			it "should return an error", ->
-				@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().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
-				@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
+					.should.equal(true);
+				return this.callback.args[0][0].message.should.equal("subscribe failed");
+			});
+		});
+
+		describe("when the client has disconnected", function() {
+			beforeEach(function() {
+				this.client.disconnected = true;
+				this.WebApiManager.joinProject = sinon.stub().callsArg(2);
+				return this.WebsocketController.joinProject(this.client, this.user, this.project_id, this.callback);
+			});
+
+			it("should not call WebApiManager.joinProject", function() {
+				return expect(this.WebApiManager.joinProject.called).to.equal(false);
+			});
+
+			it("should call the callback with no details", function() {
+				return expect(this.callback.args[0]).to.deep.equal([]);
+		});
+
+			return it("should increment the editor.join-project.disconnected metric with a status", function() {
+				return expect(this.metrics.inc.calledWith('editor.join-project.disconnected', 1, {status: 'immediately'})).to.equal(true);
+			});
+		});
+
+		return describe("when the client disconnects while WebApiManager.joinProject is running", function() {
+			beforeEach(function() {
+				this.WebApiManager.joinProject = (project, user, cb) => {
+					this.client.disconnected = true;
+					return cb(null, this.project, this.privilegeLevel, this.isRestrictedUser);
+				};
+
+				return this.WebsocketController.joinProject(this.client, this.user, this.project_id, this.callback);
+			});
+
+			it("should call the callback with no details", function() {
+				return expect(this.callback.args[0]).to.deep.equal([]);
+		});
+
+			return it("should increment the editor.join-project.disconnected metric with a status", function() {
+				return expect(this.metrics.inc.calledWith('editor.join-project.disconnected', 1, {status: 'after-web-api-call'})).to.equal(true);
+			});
+		});
+	});
+
+	describe("leaveProject", function() {
+		beforeEach(function() {
+			this.DocumentUpdaterManager.flushProjectToMongoAndDelete = sinon.stub().callsArg(1);
+			this.ConnectedUsersManager.markUserAsDisconnected = sinon.stub().callsArg(2);
+			this.WebsocketLoadBalancer.emitToRoom = sinon.stub();
+			this.RoomManager.leaveProjectAndDocs = sinon.stub();
+			this.clientsInRoom = [];
+			this.io = {
+				sockets: {
+					clients: room_id => {
+						if (room_id !== this.project_id) {
+							throw "expected room_id to be project_id";
+						}
+						return this.clientsInRoom;
+					}
+				}
+			};
+			this.client.ol_context.project_id = this.project_id;
+			this.client.ol_context.user_id = this.user_id;
+			this.WebsocketController.FLUSH_IF_EMPTY_DELAY = 0;
+			return tk.reset();
+		}); // Allow setTimeout to work.
+
+		describe("when the client did not joined a project yet", function() {
+			beforeEach(function(done) {
+				this.client.ol_context = {};
+				return this.WebsocketController.leaveProject(this.io, this.client, done);
+			});
+
+			it("should bail out when calling leaveProject", function() {
+				this.WebsocketLoadBalancer.emitToRoom.called.should.equal(false);
+				this.RoomManager.leaveProjectAndDocs.called.should.equal(false);
+				return this.ConnectedUsersManager.markUserAsDisconnected.called.should.equal(false);
+			});
+
+			return it("should not inc any metric", function() {
+				return this.metrics.inc.called.should.equal(false);
+			});
+		});
+
+		describe("when the project is empty", function() {
+			beforeEach(function(done) {
+				this.clientsInRoom = [];
+				return this.WebsocketController.leaveProject(this.io, this.client, done);
+			});
+
+			it("should end clientTracking.clientDisconnected to the project room", function() {
+				return this.WebsocketLoadBalancer.emitToRoom
+					.calledWith(this.project_id, "clientTracking.clientDisconnected", this.client.publicId)
+					.should.equal(true);
+			});
+
+			it("should mark the user as disconnected", function() {
+				return this.ConnectedUsersManager.markUserAsDisconnected
+					.calledWith(this.project_id, this.client.publicId)
+					.should.equal(true);
+			});
+
+			it("should flush the project in the document updater", function() {
+				return this.DocumentUpdaterManager.flushProjectToMongoAndDelete
+					.calledWith(this.project_id)
+					.should.equal(true);
+			});
+
+			it("should increment the leave-project metric", function() {
+				return this.metrics.inc.calledWith("editor.leave-project").should.equal(true);
+			});
+
+			return it("should track the disconnection in RoomManager", function() {
+				return this.RoomManager.leaveProjectAndDocs
+					.calledWith(this.client)
+					.should.equal(true);
+			});
+		});
+
+		describe("when the project is not empty", function() {
+			beforeEach(function() {
+				this.clientsInRoom = ["mock-remaining-client"];
+				return this.WebsocketController.leaveProject(this.io, this.client);
+			});
+
+			return it("should not flush the project in the document updater", function() {
+				return this.DocumentUpdaterManager.flushProjectToMongoAndDelete
+					.called.should.equal(false);
+			});
+		});
+
+		describe("when client has not authenticated", function() {
+			beforeEach(function(done) {
+				this.client.ol_context.user_id = null;
+				this.client.ol_context.project_id = null;
+				return this.WebsocketController.leaveProject(this.io, this.client, done);
+			});
+
+			it("should not end clientTracking.clientDisconnected to the project room", function() {
+				return this.WebsocketLoadBalancer.emitToRoom
+					.calledWith(this.project_id, "clientTracking.clientDisconnected", this.client.publicId)
+					.should.equal(false);
+			});
+
+			it("should not mark the user as disconnected", function() {
+				return this.ConnectedUsersManager.markUserAsDisconnected
+					.calledWith(this.project_id, this.client.publicId)
+					.should.equal(false);
+			});
+
+			it("should not flush the project in the document updater", function() {
+				return this.DocumentUpdaterManager.flushProjectToMongoAndDelete
+					.calledWith(this.project_id)
+					.should.equal(false);
+			});
+
+			return it("should not increment the leave-project metric", function() {
+				return this.metrics.inc.calledWith("editor.leave-project").should.equal(false);
+			});
+		});
+
+		return describe("when client has not joined a project", function() {
+			beforeEach(function(done) {
+				this.client.ol_context.user_id = this.user_id;
+				this.client.ol_context.project_id = null;
+				return this.WebsocketController.leaveProject(this.io, this.client, done);
+			});
+
+			it("should not end clientTracking.clientDisconnected to the project room", function() {
+				return this.WebsocketLoadBalancer.emitToRoom
+					.calledWith(this.project_id, "clientTracking.clientDisconnected", this.client.publicId)
+					.should.equal(false);
+			});
+
+			it("should not mark the user as disconnected", function() {
+				return this.ConnectedUsersManager.markUserAsDisconnected
+					.calledWith(this.project_id, this.client.publicId)
+					.should.equal(false);
+			});
+
+			it("should not flush the project in the document updater", function() {
+				return this.DocumentUpdaterManager.flushProjectToMongoAndDelete
+					.calledWith(this.project_id)
+					.should.equal(false);
+			});
+
+			return it("should not increment the leave-project metric", function() {
+				return this.metrics.inc.calledWith("editor.leave-project").should.equal(false);
+			});
+		});
+	});
+
+	describe("joinDoc", function() {
+		beforeEach(function() {
+			this.doc_id = "doc-id-123";
+			this.doc_lines = ["doc", "lines"];
+			this.version = 42;
+			this.ops = ["mock", "ops"];
+			this.ranges = { "mock": "ranges" };
+			this.options = {};
+
+			this.client.ol_context.project_id = this.project_id;
+			this.client.ol_context.is_restricted_user = false;
+			this.AuthorizationManager.addAccessToDoc = sinon.stub();
+			this.AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, null);
+			this.DocumentUpdaterManager.getDocument = sinon.stub().callsArgWith(3, null, this.doc_lines, this.version, this.ranges, this.ops);
+			return this.RoomManager.joinDoc = sinon.stub().callsArg(2);
+		});
+
+		describe("works", function() {
+			beforeEach(function() {
+				return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, this.options, this.callback);
+			});
+
+			it("should check that the client is authorized to view the project", function() {
+				return this.AuthorizationManager.assertClientCanViewProject
+					.calledWith(this.client)
+					.should.equal(true);
+			});
+
+			it("should get the document from the DocumentUpdaterManager with fromVersion", function() {
+				return this.DocumentUpdaterManager.getDocument
+					.calledWith(this.project_id, this.doc_id, -1)
+					.should.equal(true);
+			});
+
+			it("should add permissions for the client to access the doc", function() {
+				return this.AuthorizationManager.addAccessToDoc
+					.calledWith(this.client, this.doc_id)
+					.should.equal(true);
+			});
+
+			it("should join the client to room for the doc_id", function() {
+				return this.RoomManager.joinDoc
+					.calledWith(this.client, this.doc_id)
+					.should.equal(true);
+			});
+
+			it("should call the callback with the lines, version, ranges and ops", function() {
+				return this.callback
+					.calledWith(null, this.doc_lines, this.version, this.ops, this.ranges)
+					.should.equal(true);
+			});
+
+			return it("should increment the join-doc metric", function() {
+				return this.metrics.inc.calledWith("editor.join-doc").should.equal(true);
+			});
+		});
+
+		describe("with a fromVersion", function() {
+			beforeEach(function() {
+				this.fromVersion = 40;
+				return this.WebsocketController.joinDoc(this.client, this.doc_id, this.fromVersion, this.options, this.callback);
+			});
+
+			return it("should get the document from the DocumentUpdaterManager with fromVersion", function() {
+				return this.DocumentUpdaterManager.getDocument
+					.calledWith(this.project_id, this.doc_id, this.fromVersion)
+					.should.equal(true);
+			});
+		});
+
+		describe("with doclines that need escaping", function() {
+			beforeEach(function() {
+				this.doc_lines.push(["räksmörgås"]);
+				return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, this.options, this.callback);
+			});
+
+			return it("should call the callback with the escaped lines", function() {
+				const escaped_lines = this.callback.args[0][1];
+				const escaped_word = escaped_lines.pop();
+				escaped_word.should.equal('räksmörgås');
+				// Check that unescaping works
+				return decodeURIComponent(escape(escaped_word)).should.equal("räksmörgås");
+			});
+		});
+
+		describe("with comments that need encoding", function() {
+			beforeEach(function() {
+				this.ranges.comments = [{ op: { c: "räksmörgås" } }];
+				return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, { encodeRanges: true }, this.callback);
+			});
+
+			return it("should call the callback with the encoded comment", function() {
+				const encoded_comments = this.callback.args[0][4];
+				const encoded_comment = encoded_comments.comments.pop();
+				const encoded_comment_text = encoded_comment.op.c;
+				return encoded_comment_text.should.equal('räksmörgås');
+			});
+		});
+
+		describe("with changes that need encoding", function() {
+			it("should call the callback with the encoded insert change", function() {
+				this.ranges.changes = [{ op: { i: "räksmörgås" } }];
+				this.WebsocketController.joinDoc(this.client, this.doc_id, -1, { encodeRanges: true }, this.callback);
+
+				const encoded_changes = this.callback.args[0][4];
+				const encoded_change = encoded_changes.changes.pop();
+				const encoded_change_text = encoded_change.op.i;
+				return encoded_change_text.should.equal('räksmörgås');
+			});
+
+			return it("should call the callback with the encoded delete change", function() {
+				this.ranges.changes = [{ op: { d: "räksmörgås" } }];
+				this.WebsocketController.joinDoc(this.client, this.doc_id, -1, { encodeRanges: true }, this.callback);
+
+				const encoded_changes = this.callback.args[0][4];
+				const encoded_change = encoded_changes.changes.pop();
+				const encoded_change_text = encoded_change.op.d;
+				return encoded_change_text.should.equal('räksmörgås');
+			});
+		});
+
+		describe("when not authorized", function() {
+			beforeEach(function() {
+				this.AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, (this.err = new Error("not authorized")));
+				return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, this.options, this.callback);
+			});
+
+			it("should call the callback with an error", function() {
+				return this.callback.calledWith(sinon.match({message: "not authorized"})).should.equal(true);
+			});
+
+			return it("should not call the DocumentUpdaterManager", function() {
+				return this.DocumentUpdaterManager.getDocument.called.should.equal(false);
+			});
+		});
+
+		describe("with a restricted client", function() {
+			beforeEach(function() {
+				this.ranges.comments = [{op: {a: 1}}, {op: {a: 2}}];
+				this.client.ol_context.is_restricted_user = true;
+				return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, this.options, this.callback);
+			});
+
+			return it("should overwrite ranges.comments with an empty list", function() {
+				const ranges = this.callback.args[0][4];
+				return expect(ranges.comments).to.deep.equal([]);
+		});
+	});
+
+		describe("when the client has disconnected", function() {
+			beforeEach(function() {
+				this.client.disconnected = true;
+				return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, this.options, this.callback);
+			});
+
+			it("should call the callback with no details", function() {
+				return expect(this.callback.args[0]).to.deep.equal([]);
+			});
+
+			it("should increment the editor.join-doc.disconnected metric with a status", function() {
+				return expect(this.metrics.inc.calledWith('editor.join-doc.disconnected', 1, {status: 'immediately'})).to.equal(true);
+			});
+
+			return it("should not get the document", function() {
+				return expect(this.DocumentUpdaterManager.getDocument.called).to.equal(false);
+			});
+		});
+
+		describe("when the client disconnects while RoomManager.joinDoc is running", function() {
+			beforeEach(function() {
+				this.RoomManager.joinDoc = (client, doc_id, cb) => {
+					this.client.disconnected = true;
+					return cb();
+				};
+
+				return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, this.options, this.callback);
+			});
+
+			it("should call the callback with no details", function() {
+				return expect(this.callback.args[0]).to.deep.equal([]);
+			});
+
+			it("should increment the editor.join-doc.disconnected metric with a status", function() {
+				return expect(this.metrics.inc.calledWith('editor.join-doc.disconnected', 1, {status: 'after-joining-room'})).to.equal(true);
+			});
+
+			return it("should not get the document", function() {
+				return expect(this.DocumentUpdaterManager.getDocument.called).to.equal(false);
+			});
+		});
+
+		return describe("when the client disconnects while DocumentUpdaterManager.getDocument is running", function() {
+			beforeEach(function() {
+				this.DocumentUpdaterManager.getDocument = (project_id, doc_id, fromVersion, callback) => {
+					this.client.disconnected = true;
+					return callback(null, this.doc_lines, this.version, this.ranges, this.ops);
+				};
+
+				return this.WebsocketController.joinDoc(this.client, this.doc_id, -1, this.options, this.callback);
+			});
+
+			it("should call the callback with no details", function() {
+				return expect(this.callback.args[0]).to.deep.equal([]);
+		});
+
+			return it("should increment the editor.join-doc.disconnected metric with a status", function() {
+				return expect(this.metrics.inc.calledWith('editor.join-doc.disconnected', 1, {status: 'after-doc-updater-call'})).to.equal(true);
+			});
+		});
+	});
+
+	describe("leaveDoc", function() {
+		beforeEach(function() {
+			this.doc_id = "doc-id-123";
+			this.client.ol_context.project_id = this.project_id;
+			this.RoomManager.leaveDoc = sinon.stub();
+			return this.WebsocketController.leaveDoc(this.client, this.doc_id, this.callback);
+		});
+
+		it("should remove the client from the doc_id room", function() {
+			return this.RoomManager.leaveDoc
+				.calledWith(this.client, this.doc_id).should.equal(true);
+		});
+
+		it("should call the callback", function() {
+			return this.callback.called.should.equal(true);
+		});
+
+		return it("should increment the leave-doc metric", function() {
+			return this.metrics.inc.calledWith("editor.leave-doc").should.equal(true);
+		});
+	});
+
+	describe("getConnectedUsers", function() {
+		beforeEach(function() {
+			this.client.ol_context.project_id = this.project_id;
+			this.users = ["mock", "users"];
+			this.WebsocketLoadBalancer.emitToRoom = sinon.stub();
+			return this.ConnectedUsersManager.getConnectedUsers = sinon.stub().callsArgWith(1, null, this.users);
+		});
+
+		describe("when authorized", function() {
+			beforeEach(function(done) {
+				this.AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, null);
+				return this.WebsocketController.getConnectedUsers(this.client, (...args) => {
+					this.callback(...Array.from(args || []));
+					return done();
+				});
+			});
+
+			it("should check that the client is authorized to view the project", function() {
+				return this.AuthorizationManager.assertClientCanViewProject
+					.calledWith(this.client)
+					.should.equal(true);
+			});
+
+			it("should broadcast a request to update the client list", function() {
+				return this.WebsocketLoadBalancer.emitToRoom
+					.calledWith(this.project_id, "clientTracking.refresh")
+					.should.equal(true);
+			});
+
+			it("should get the connected users for the project", function() {
+				return this.ConnectedUsersManager.getConnectedUsers
+					.calledWith(this.project_id)
+					.should.equal(true);
+			});
+
+			it("should return the users", function() {
+				return this.callback.calledWith(null, this.users).should.equal(true);
+			});
+
+			return it("should increment the get-connected-users metric", function() {
+				return this.metrics.inc.calledWith("editor.get-connected-users").should.equal(true);
+			});
+		});
+
+		describe("when not authorized", function() {
+			beforeEach(function() {
+				this.AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, (this.err = new Error("not authorized")));
+				return this.WebsocketController.getConnectedUsers(this.client, this.callback);
+			});
+
+			it("should not get the connected users for the project", function() {
+				return this.ConnectedUsersManager.getConnectedUsers
 					.called
-					.should.equal false
-
-			it "should return an error", ->
-				@callback.calledWith(@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
-
-			it "should return an empty array of users", ->
-				@callback.calledWith(null, []).should.equal true
-
-			it "should not get the connected users for the project", ->
-				@ConnectedUsersManager.getConnectedUsers
+					.should.equal(false);
+			});
+
+			return it("should return an error", function() {
+				return this.callback.calledWith(this.err).should.equal(true);
+			});
+		});
+
+		describe("when restricted user", function() {
+			beforeEach(function() {
+				this.client.ol_context.is_restricted_user = true;
+				this.AuthorizationManager.assertClientCanViewProject = sinon.stub().callsArgWith(1, null);
+				return this.WebsocketController.getConnectedUsers(this.client, this.callback);
+			});
+
+			it("should return an empty array of users", function() {
+				return this.callback.calledWith(null, []).should.equal(true);
+			});
+
+			return it("should not get the connected users for the project", function() {
+				return this.ConnectedUsersManager.getConnectedUsers
 					.called
-					.should.equal false
-
-		describe "when the client has disconnected", ->
-			beforeEach ->
-				@client.disconnected = true
-				@AuthorizationManager.assertClientCanViewProject = sinon.stub()
-				@WebsocketController.getConnectedUsers @client, @callback
-
-			it "should call the callback with no details", ->
-				expect(@callback.args[0]).to.deep.equal([])
-
-			it "should not check permissions", ->
-				expect(@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 "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
-
-				@populatedCursorData =
-					doc_id: @doc_id,
-					id: @client.publicId
-					name: "#{@first_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,
-					first_name: @first_name,
-					last_name: @last_name
+					.should.equal(false);
+			});
+		});
+
+		return describe("when the client has disconnected", function() {
+			beforeEach(function() {
+				this.client.disconnected = true;
+				this.AuthorizationManager.assertClientCanViewProject = sinon.stub();
+				return this.WebsocketController.getConnectedUsers(this.client, this.callback);
+			});
+
+			it("should call the callback with no details", function() {
+				return expect(this.callback.args[0]).to.deep.equal([]);
+			});
+
+			return it("should not check permissions", function() {
+				return expect(this.AuthorizationManager.assertClientCanViewProject.called).to.equal(false);
+			});
+		});
+	});
+
+	describe("updateClientPosition", function() {
+		beforeEach(function() {
+			this.WebsocketLoadBalancer.emitToRoom = sinon.stub();
+			this.ConnectedUsersManager.updateUserPosition = sinon.stub().callsArgWith(4);
+			this.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon.stub().callsArgWith(2, null);
+			return this.update = {
+				doc_id: (this.doc_id = "doc-id-123"),
+				row: (this.row = 42),
+				column: (this.column = 37)
+			};});
+
+		describe("with a logged in user", function() {
+			beforeEach(function() {
+				this.client.ol_context = {
+					project_id: this.project_id,
+					first_name: (this.first_name = "Douglas"),
+					last_name: (this.last_name = "Adams"),
+					email: (this.email = "joe@example.com"),
+					user_id: (this.user_id = "user-id-123")
+				};
+				this.WebsocketController.updateClientPosition(this.client, this.update);
+
+				return this.populatedCursorData = {
+					doc_id: this.doc_id,
+					id: this.client.publicId,
+					name: `${this.first_name} ${this.last_name}`,
+					row: this.row,
+					column: this.column,
+					email: this.email,
+					user_id: this.user_id
+				};
+			});
+
+			it("should send the update to the project room with the user's name", function() {
+				return this.WebsocketLoadBalancer.emitToRoom.calledWith(this.project_id, "clientTracking.clientUpdated", this.populatedCursorData).should.equal(true);
+			});
+
+			it("should send the  cursor data to the connected user manager", function(done){
+				this.ConnectedUsersManager.updateUserPosition.calledWith(this.project_id, this.client.publicId, {
+					_id: this.user_id,
+					email: this.email,
+					first_name: this.first_name,
+					last_name: this.last_name
 				}, {
-					row: @row
-					column: @column
-					doc_id: @doc_id
-				}).should.equal true
-				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 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
-
-				@populatedCursorData =
-					doc_id: @doc_id,
-					id: @client.publicId
-					name: "#{@first_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,
-					first_name: @first_name,
+					row: this.row,
+					column: this.column,
+					doc_id: this.doc_id
+				}).should.equal(true);
+				return done();
+			});
+
+			return it("should increment the update-client-position metric at 0.1 frequency", function() {
+				return this.metrics.inc.calledWith("editor.update-client-position", 0.1).should.equal(true);
+			});
+		});
+
+		describe("with a logged in user who has no last_name set", function() {
+			beforeEach(function() {
+				this.client.ol_context = {
+					project_id: this.project_id,
+					first_name: (this.first_name = "Douglas"),
+					last_name: undefined,
+					email: (this.email = "joe@example.com"),
+					user_id: (this.user_id = "user-id-123")
+				};
+				this.WebsocketController.updateClientPosition(this.client, this.update);
+
+				return this.populatedCursorData = {
+					doc_id: this.doc_id,
+					id: this.client.publicId,
+					name: `${this.first_name}`,
+					row: this.row,
+					column: this.column,
+					email: this.email,
+					user_id: this.user_id
+				};
+			});
+
+			it("should send the update to the project room with the user's name", function() {
+				return this.WebsocketLoadBalancer.emitToRoom.calledWith(this.project_id, "clientTracking.clientUpdated", this.populatedCursorData).should.equal(true);
+			});
+
+			it("should send the  cursor data to the connected user manager", function(done){
+				this.ConnectedUsersManager.updateUserPosition.calledWith(this.project_id, this.client.publicId, {
+					_id: this.user_id,
+					email: this.email,
+					first_name: this.first_name,
 					last_name: undefined
 				}, {
-					row: @row
-					column: @column
-					doc_id: @doc_id
-				}).should.equal true
-				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 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,
+					row: this.row,
+					column: this.column,
+					doc_id: this.doc_id
+				}).should.equal(true);
+				return done();
+			});
+
+			return it("should increment the update-client-position metric at 0.1 frequency", function() {
+				return this.metrics.inc.calledWith("editor.update-client-position", 0.1).should.equal(true);
+			});
+		});
+
+		describe("with a logged in user who has no first_name set", function() {
+			beforeEach(function() {
+				this.client.ol_context = {
+					project_id: this.project_id,
+					first_name: undefined,
+					last_name: (this.last_name = "Adams"),
+					email: (this.email = "joe@example.com"),
+					user_id: (this.user_id = "user-id-123")
+				};
+				this.WebsocketController.updateClientPosition(this.client, this.update);
+
+				return this.populatedCursorData = {
+					doc_id: this.doc_id,
+					id: this.client.publicId,
+					name: `${this.last_name}`,
+					row: this.row,
+					column: this.column,
+					email: this.email,
+					user_id: this.user_id
+				};
+			});
+
+			it("should send the update to the project room with the user's name", function() {
+				return this.WebsocketLoadBalancer.emitToRoom.calledWith(this.project_id, "clientTracking.clientUpdated", this.populatedCursorData).should.equal(true);
+			});
+
+			it("should send the  cursor data to the connected user manager", function(done){
+				this.ConnectedUsersManager.updateUserPosition.calledWith(this.project_id, this.client.publicId, {
+					_id: this.user_id,
+					email: this.email,
 					first_name: undefined,
-					last_name: @last_name
+					last_name: this.last_name
 				}, {
-					row: @row
-					column: @column
-					doc_id: @doc_id
-				}).should.equal true
-				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
-
-			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,
+					row: this.row,
+					column: this.column,
+					doc_id: this.doc_id
+				}).should.equal(true);
+				return done();
+			});
+
+			return it("should increment the update-client-position metric at 0.1 frequency", function() {
+				return this.metrics.inc.calledWith("editor.update-client-position", 0.1).should.equal(true);
+			});
+		});
+		describe("with a logged in user who has no names set", function() {
+			beforeEach(function() {
+				this.client.ol_context = {
+					project_id: this.project_id,
+					first_name: undefined,
+					last_name: undefined,
+					email: (this.email = "joe@example.com"),
+					user_id: (this.user_id = "user-id-123")
+				};
+				return this.WebsocketController.updateClientPosition(this.client, this.update);
+			});
+
+			return it("should send the update to the project name with no name", function() {
+				return this.WebsocketLoadBalancer.emitToRoom
+					.calledWith(this.project_id, "clientTracking.clientUpdated", {
+						doc_id: this.doc_id,
+						id: this.client.publicId,
+						user_id: this.user_id,
 						name: "",
-						row: @row,
-						column: @column,
-						email: @email
+						row: this.row,
+						column: this.column,
+						email: this.email
 					})
-					.should.equal true
-
-
-		describe "with an anonymous user", ->
-			beforeEach ->
-				@client.ol_context = {
-					project_id: @project_id
-				}
-				@WebsocketController.updateClientPosition @client, @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
+					.should.equal(true);
+			});
+		});
+
+
+		describe("with an anonymous user", function() {
+			beforeEach(function() {
+				this.client.ol_context = {
+					project_id: this.project_id
+				};
+				return this.WebsocketController.updateClientPosition(this.client, this.update);
+			});
+
+			it("should send the update to the project room with no name", function() {
+				return this.WebsocketLoadBalancer.emitToRoom
+					.calledWith(this.project_id, "clientTracking.clientUpdated", {
+						doc_id: this.doc_id,
+						id: this.client.publicId,
+						name: "",
+						row: this.row,
+						column: this.column
 					})
-					.should.equal true
-
-			it "should not send cursor data to the connected user manager", (done)->
-				@ConnectedUsersManager.updateUserPosition.called.should.equal false
-				done()
-
-		describe "when the client has disconnected", ->
-			beforeEach ->
-				@client.disconnected = true
-				@AuthorizationManager.assertClientCanViewProjectAndDoc = sinon.stub()
-				@WebsocketController.updateClientPosition @client, @update, @callback
-
-			it "should call the callback with no details", ->
-				expect(@callback.args[0]).to.deep.equal([])
-
-			it "should not check permissions", ->
-				expect(@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 "succesfully", ->
-			beforeEach ->
-				@WebsocketController.applyOtUpdate @client, @doc_id, @update, @callback
-
-			it "should set the source of the update to the client id", ->
-				@update.meta.source.should.equal @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 queue the update", ->
-				@DocumentUpdaterManager.queueChange
-					.calledWith(@project_id, @doc_id, @update)
-					.should.equal true
-
-			it "should call the callback", ->
-				@callback.called.should.equal true
-
-			it "should increment the doc updates", ->
-				@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
-
-			it "should disconnect the client", ->
-				@client.disconnect.called.should.equal true
-
-			it "should log an error", ->
-				@logger.error.called.should.equal true
-
-			it "should call the callback with the error", ->
-				@callback.calledWith(@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
-
-			# 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 call the callback with the error", ->
-				@callback.calledWith(@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
-
-			it "should call the callback with no error", ->
-				@callback.called.should.equal true
-				@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']
-
-			describe "after 100ms", ->
-				beforeEach (done) ->
-					setTimeout done, 100
-
-				it "should send an otUpdateError the client", ->
-					@client.emit.calledWith('otUpdateError').should.equal true
-
-				it "should disconnect the client", ->
-					@client.disconnect.called.should.equal true
-
-			describe "when the client disconnects during the next 100ms", ->
-				beforeEach (done) ->
-					@client.disconnected = true
-					setTimeout done, 100
-
-				it "should not send an otUpdateError the client", ->
-					@client.emit.calledWith('otUpdateError').should.equal false
-
-				it "should not disconnect the client", ->
-					@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)
-
-	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()
-
-		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-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 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 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()
+					.should.equal(true);
+			});
+
+			return it("should not send cursor data to the connected user manager", function(done){
+				this.ConnectedUsersManager.updateUserPosition.called.should.equal(false);
+				return done();
+			});
+		});
+
+		return describe("when the client has disconnected", function() {
+			beforeEach(function() {
+				this.client.disconnected = true;
+				this.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon.stub();
+				return this.WebsocketController.updateClientPosition(this.client, this.update, this.callback);
+			});
+
+			it("should call the callback with no details", function() {
+				return expect(this.callback.args[0]).to.deep.equal([]);
+			});
+
+			return it("should not check permissions", function() {
+				return expect(this.AuthorizationManager.assertClientCanViewProjectAndDoc.called).to.equal(false);
+			});
+		});
+	});
+
+	describe("applyOtUpdate", function() {
+		beforeEach(function() {
+			this.update = {op: {p: 12, t: "foo"}};
+			this.client.ol_context.user_id = this.user_id;
+			this.client.ol_context.project_id = this.project_id;
+			this.WebsocketController._assertClientCanApplyUpdate = sinon.stub().yields();
+			return this.DocumentUpdaterManager.queueChange = sinon.stub().callsArg(3);
+		});
+
+		describe("succesfully", function() {
+			beforeEach(function() {
+				return this.WebsocketController.applyOtUpdate(this.client, this.doc_id, this.update, this.callback);
+			});
+
+			it("should set the source of the update to the client id", function() {
+				return this.update.meta.source.should.equal(this.client.publicId);
+			});
+
+			it("should set the user_id of the update to the user id", function() {
+				return this.update.meta.user_id.should.equal(this.user_id);
+			});
+
+			it("should queue the update", function() {
+				return this.DocumentUpdaterManager.queueChange
+					.calledWith(this.project_id, this.doc_id, this.update)
+					.should.equal(true);
+			});
+
+			it("should call the callback", function() {
+				return this.callback.called.should.equal(true);
+			});
+
+			return it("should increment the doc updates", function() {
+				return this.metrics.inc.calledWith("editor.doc-update").should.equal(true);
+			});
+		});
+
+		describe("unsuccessfully", function() {
+			beforeEach(function() {
+				this.client.disconnect = sinon.stub();
+				this.DocumentUpdaterManager.queueChange = sinon.stub().callsArgWith(3, (this.error = new Error("Something went wrong")));
+				return this.WebsocketController.applyOtUpdate(this.client, this.doc_id, this.update, this.callback);
+			});
+
+			it("should disconnect the client", function() {
+				return this.client.disconnect.called.should.equal(true);
+			});
+
+			it("should log an error", function() {
+				return this.logger.error.called.should.equal(true);
+			});
+
+			return it("should call the callback with the error", function() {
+				return this.callback.calledWith(this.error).should.equal(true);
+			});
+		});
+
+		describe("when not authorized", function() {
+			beforeEach(function() {
+				this.client.disconnect = sinon.stub();
+				this.WebsocketController._assertClientCanApplyUpdate = sinon.stub().yields(this.error = new Error("not authorized"));
+				return this.WebsocketController.applyOtUpdate(this.client, this.doc_id, this.update, this.callback);
+			});
+
+			// This happens in a setTimeout to allow the client a chance to receive the error first.
+			// I'm not sure how to unit test, but it is acceptance tested.
+			// it "should disconnect the client", ->
+			// 	@client.disconnect.called.should.equal true
+
+			it("should log a warning", function() {
+				return this.logger.warn.called.should.equal(true);
+			});
+
+			return it("should call the callback with the error", function() {
+				return this.callback.calledWith(this.error).should.equal(true);
+			});
+		});
+
+		return describe("update_too_large", function() {
+			beforeEach(function(done) {
+				this.client.disconnect = sinon.stub();
+				this.client.emit = sinon.stub();
+				this.client.ol_context.user_id = this.user_id;
+				this.client.ol_context.project_id = this.project_id;
+				const error = new Error("update is too large");
+				error.updateSize = 7372835;
+				this.DocumentUpdaterManager.queueChange = sinon.stub().callsArgWith(3, error);
+				this.WebsocketController.applyOtUpdate(this.client, this.doc_id, this.update, this.callback);
+				return setTimeout(() => done()
+				, 1);
+			});
+
+			it("should call the callback with no error", function() {
+				this.callback.called.should.equal(true);
+				return this.callback.args[0].should.deep.equal([]);
+		});
+
+			it("should log a warning with the size and context", function() {
+				this.logger.warn.called.should.equal(true);
+				return this.logger.warn.args[0].should.deep.equal([{
+					user_id: this.user_id, project_id: this.project_id, doc_id: this.doc_id, updateSize: 7372835
+				}, 'update is too large']);
+		});
+
+			describe("after 100ms", function() {
+				beforeEach(done => setTimeout(done, 100));
+
+				it("should send an otUpdateError the client", function() {
+					return this.client.emit.calledWith('otUpdateError').should.equal(true);
+				});
+
+				return it("should disconnect the client", function() {
+					return this.client.disconnect.called.should.equal(true);
+				});
+			});
+
+			return describe("when the client disconnects during the next 100ms", function() {
+				beforeEach(function(done) {
+					this.client.disconnected = true;
+					return setTimeout(done, 100);
+				});
+
+				it("should not send an otUpdateError the client", function() {
+					return this.client.emit.calledWith('otUpdateError').should.equal(false);
+				});
+
+				it("should not disconnect the client", function() {
+					return this.client.disconnect.called.should.equal(false);
+				});
+
+				return it("should increment the editor.doc-update.disconnected metric with a status", function() {
+					return expect(this.metrics.inc.calledWith('editor.doc-update.disconnected', 1, {status:'at-otUpdateError'})).to.equal(true);
+				});
+			});
+		});
+	});
+
+	return describe("_assertClientCanApplyUpdate", function() {
+		beforeEach(function() {
+			this.edit_update = { op: [{i: "foo", p: 42}, {c: "bar", p: 132}] }; // comments may still be in an edit op
+			this.comment_update = { op: [{c: "bar", p: 132}] };
+			this.AuthorizationManager.assertClientCanEditProjectAndDoc = sinon.stub();
+			return this.AuthorizationManager.assertClientCanViewProjectAndDoc = sinon.stub();
+		});
+
+		describe("with a read-write client", () => 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", 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", 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();
+            });
+        }));
+
+		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();
+            });
+        }));
+	});
+});

+ 193 - 150
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'
-
-describe "WebsocketLoadBalancer", ->
-	beforeEach ->
-		@rclient = {}
-		@RoomEvents = {on: sinon.stub()}
-		@WebsocketLoadBalancer = SandboxedModule.require modulePath, requires:
-			"./RedisClientManager":
+/*
+ * 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", 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]
-
-	describe "emitToRoom", ->
-		beforeEach ->
-			@WebsocketLoadBalancer.emitToRoom(@room_id, @message, @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
-
-	describe "emitToAll", ->
-		beforeEach ->
-			@WebsocketLoadBalancer.emitToRoom = sinon.stub()
-			@WebsocketLoadBalancer.emitToAll @message, @payload...
-
-		it "should emit to the room 'all'", ->
-			@WebsocketLoadBalancer.emitToRoom
-				.calledWith("all", @message, @payload...)
-				.should.equal true
-
-	describe "listenForEditorEvents", ->
-		beforeEach ->
-			@WebsocketLoadBalancer._processEditorEvent = sinon.stub()
-			@WebsocketLoadBalancer.listenForEditorEvents()
-
-		it "should subscribe to the editor-events channel", ->
-			@WebsocketLoadBalancer.rclientSubList[0].subscribe
+		}];
+
+		this.room_id = "room-id";
+		this.message = "otUpdateApplied";
+		return this.payload = ["argument one", 42];});
+
+	describe("emitToRoom", function() {
+		beforeEach(function() {
+			return this.WebsocketLoadBalancer.emitToRoom(this.room_id, this.message, ...Array.from(this.payload));
+		});
+
+		return it("should publish the message to redis", function() {
+			return this.ChannelManager.publish
+				.calledWith(this.WebsocketLoadBalancer.rclientPubList[0], "editor-events", this.room_id, JSON.stringify({
+					room_id: this.room_id,
+					message: this.message,
+					payload: this.payload
+				}))
+				.should.equal(true);
+		});
+	});
+
+	describe("emitToAll", function() {
+		beforeEach(function() {
+			this.WebsocketLoadBalancer.emitToRoom = sinon.stub();
+			return this.WebsocketLoadBalancer.emitToAll(this.message, ...Array.from(this.payload));
+		});
+
+		return it("should emit to the room 'all'", function() {
+			return this.WebsocketLoadBalancer.emitToRoom
+				.calledWith("all", this.message, ...Array.from(this.payload))
+				.should.equal(true);
+		});
+	});
+
+	describe("listenForEditorEvents", function() {
+		beforeEach(function() {
+			this.WebsocketLoadBalancer._processEditorEvent = sinon.stub();
+			return this.WebsocketLoadBalancer.listenForEditorEvents();
+		});
+
+		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
-
-	describe "_processEditorEvent", ->
-		describe "with bad JSON", ->
-			beforeEach ->
-				@isRestrictedUser = false
-				@SafeJsonParse.parse = sinon.stub().callsArgWith 1, new Error("oops")
-				@WebsocketLoadBalancer._processEditorEvent(@io, "editor-events", "blah")
-
-			it "should log an error", ->
-				@logger.error.called.should.equal true
-
-		describe "with a designated room", ->
-			beforeEach ->
-				@io.sockets =
+				.should.equal(true);
+		});
+	});
+
+	return describe("_processEditorEvent", function() {
+		describe("with bad JSON", function() {
+			beforeEach(function() {
+				this.isRestrictedUser = false;
+				this.SafeJsonParse.parse = sinon.stub().callsArgWith(1, new Error("oops"));
+				return this.WebsocketLoadBalancer._processEditorEvent(this.io, "editor-events", "blah");
+			});
+
+			return it("should log an error", function() {
+				return this.logger.error.called.should.equal(true);
+			});
+		});
+
+		describe("with a designated room", function() {
+			beforeEach(function() {
+				this.io.sockets = {
 					clients: sinon.stub().returns([
-						{id: 'client-id-1', emit: @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)
-
-			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
-
-		describe "with a designated room, and restricted clients, not restricted message", ->
-			beforeEach ->
-				@io.sockets =
+				};
+				const data = JSON.stringify({
+					room_id: this.room_id,
+					message: this.message,
+					payload: this.payload
+				});
+				return this.WebsocketLoadBalancer._processEditorEvent(this.io, "editor-events", data);
+			});
+
+			return it("should send the message to all (unique) clients in the room", function() {
+				this.io.sockets.clients
+					.calledWith(this.room_id)
+					.should.equal(true);
+				this.emit1.calledWith(this.message, ...Array.from(this.payload)).should.equal(true);
+				this.emit2.calledWith(this.message, ...Array.from(this.payload)).should.equal(true);
+				return this.emit3.called.should.equal(false);
+			});
+		}); // duplicate client should be ignored
+
+		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)
-
-			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
-
-		describe "with a designated room, and restricted clients, restricted message", ->
-			beforeEach ->
-				@io.sockets =
+				};
+				const data = JSON.stringify({
+					room_id: this.room_id,
+					message: this.message,
+					payload: this.payload
+				});
+				return this.WebsocketLoadBalancer._processEditorEvent(this.io, "editor-events", data);
+			});
+
+			return it("should send the message to all (unique) clients in the room", function() {
+				this.io.sockets.clients
+					.calledWith(this.room_id)
+					.should.equal(true);
+				this.emit1.calledWith(this.message, ...Array.from(this.payload)).should.equal(true);
+				this.emit2.calledWith(this.message, ...Array.from(this.payload)).should.equal(true);
+				this.emit3.called.should.equal(false); // duplicate client should be ignored
+				return this.emit4.called.should.equal(true);
+			});
+		});  // restricted client, but should be called
+
+		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)
-
-			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
-
-		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)
-
-			it "should send the message to all clients", ->
-				@emit.calledWith(@message, @payload...).should.equal true
+				};
+				const data = JSON.stringify({
+					room_id: this.room_id,
+					message: (this.restrictedMessage = 'new-comment'),
+					payload: this.payload
+				});
+				return this.WebsocketLoadBalancer._processEditorEvent(this.io, "editor-events", data);
+			});
+
+			return it("should send the message to all (unique) clients in the room, who are not restricted", function() {
+				this.io.sockets.clients
+					.calledWith(this.room_id)
+					.should.equal(true);
+				this.emit1.calledWith(this.restrictedMessage, ...Array.from(this.payload)).should.equal(true);
+				this.emit2.calledWith(this.restrictedMessage, ...Array.from(this.payload)).should.equal(true);
+				this.emit3.called.should.equal(false); // duplicate client should be ignored
+				return this.emit4.called.should.equal(false);
+			});
+		}); // restricted client, should not be called
+
+		return describe("when emitting to all", function() {
+			beforeEach(function() {
+				this.io.sockets =
+					{emit: (this.emit = sinon.stub())};
+				const data = JSON.stringify({
+					room_id: "all",
+					message: this.message,
+					payload: this.payload
+				});
+				return this.WebsocketLoadBalancer._processEditorEvent(this.io, "editor-events", data);
+			});
+
+			return it("should send the message to all clients", function() {
+				return this.emit.calledWith(this.message, ...Array.from(this.payload)).should.equal(true);
+			});
+		});
+	});
+});

+ 14 - 11
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() {}
+});