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

prettier: convert test/acceptance decaffeinated files to Prettier format

decaffeinate 6 лет назад
Родитель
Сommit
8a7fc78dc8

+ 433 - 310
services/real-time/test/acceptance/js/ApplyUpdateTests.js

@@ -12,313 +12,436 @@
  * DS201: Simplify complex destructure assignments
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-const async = require("async");
-const chai = require("chai");
-const {
-    expect
-} = chai;
-chai.should();
-
-const RealTimeClient = require("./helpers/RealTimeClient");
-const FixturesManager = require("./helpers/FixturesManager");
-
-const settings = require("settings-sharelatex");
-const redis = require("redis-sharelatex");
-const rclient = redis.createClient(settings.redis.documentupdater);
-
-const redisSettings = settings.redis;
-
-describe("applyOtUpdate", function() {
-	before(function() {
-		return this.update = {
-			op: [{i: "foo", p: 42}]
-		};});
-	describe("when authorized", function() {
-		before(function(done) {
-			return async.series([
-				cb => {
-					return FixturesManager.setUpProject({
-						privilegeLevel: "readAndWrite"
-					}, (e, {project_id, user_id}) => {
-						this.project_id = project_id;
-						this.user_id = user_id;
-						return cb(e);
-					});
-				},
-					
-				cb => {
-					return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => {
-						this.doc_id = doc_id;
-						return cb(e);
-					});
-				},
-
-				cb => {
-					this.client = RealTimeClient.connect();
-					return this.client.on("connectionAccepted", cb);
-				},
-						
-				cb => {
-					return this.client.emit("joinProject", {project_id: this.project_id}, cb);
-				},
-				
-				cb => {
-					return this.client.emit("joinDoc", this.doc_id, cb);
-				},
-					
-				cb => {
-					return this.client.emit("applyOtUpdate", this.doc_id, this.update, cb);
-				}
-			], done);
-		});
-		
-		it("should push the doc into the pending updates list", function(done) {
-			rclient.lrange("pending-updates-list", 0, -1, (error, ...rest) => {
-				const [doc_id] = Array.from(rest[0]);
-				doc_id.should.equal(`${this.project_id}:${this.doc_id}`);
-				return done();
-			});
-			return null;
-		});
-
-		it("should push the update into redis", function(done) {
-			rclient.lrange(redisSettings.documentupdater.key_schema.pendingUpdates({doc_id: this.doc_id}), 0, -1, (error, ...rest) => {
-				let [update] = Array.from(rest[0]);
-				update = JSON.parse(update);
-				update.op.should.deep.equal(this.update.op);
-				update.meta.should.deep.equal({
-					source: this.client.publicId,
-					user_id: this.user_id
-				});
-				return done();
-			});
-			return null;
-		});
-
-		return after(function(done) {
-			return async.series([
-				cb => rclient.del("pending-updates-list", cb),
-				cb => rclient.del("DocsWithPendingUpdates", `${this.project_id}:${this.doc_id}`, cb),
-				cb => rclient.del(redisSettings.documentupdater.key_schema.pendingUpdates(this.doc_id), cb)
-			], done);
-		});
-	});
-		
-	describe("when authorized with a huge edit update", function() {
-		before(function(done) {
-			this.update = {
-				op: {
-					p: 12,
-					t: "update is too large".repeat(1024 * 400) // >7MB
-				}
-			};
-			return async.series([
-				cb => {
-					return FixturesManager.setUpProject({
-						privilegeLevel: "readAndWrite"
-					}, (e, {project_id, user_id}) => {
-						this.project_id = project_id;
-						this.user_id = user_id;
-						return cb(e);
-					});
-				},
-
-				cb => {
-					return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => {
-						this.doc_id = doc_id;
-						return cb(e);
-					});
-				},
-
-				cb => {
-					this.client = RealTimeClient.connect();
-					this.client.on("connectionAccepted", cb);
-					return this.client.on("otUpdateError", otUpdateError => {
-						this.otUpdateError = otUpdateError;
-						
-				});
-				},
-
-				cb => {
-					return this.client.emit("joinProject", {project_id: this.project_id}, cb);
-				},
-
-				cb => {
-					return this.client.emit("joinDoc", this.doc_id, cb);
-				},
-
-				cb => {
-					return this.client.emit("applyOtUpdate", this.doc_id, this.update, error => {
-						this.error = error;
-						return cb();
-					});
-				}
-			], done);
-		});
-
-		it("should not return an error", function() {
-			return expect(this.error).to.not.exist;
-		});
-
-		it("should send an otUpdateError to the client", function(done) {
-			return setTimeout(() => {
-				expect(this.otUpdateError).to.exist;
-				return done();
-			}
-			, 300);
-		});
-
-		it("should disconnect the client", function(done) {
-			return setTimeout(() => {
-				this.client.socket.connected.should.equal(false);
-				return done();
-			}
-			, 300);
-		});
-
-		return it("should not put the update in redis", function(done) {
-			rclient.llen(redisSettings.documentupdater.key_schema.pendingUpdates({doc_id: this.doc_id}), (error, len) => {
-				len.should.equal(0);
-				return done();
-			});
-			return null;
-		});
-	});
-
-	describe("when authorized to read-only with an edit update", function() {
-		before(function(done) {
-			return async.series([
-				cb => {
-					return FixturesManager.setUpProject({
-						privilegeLevel: "readOnly"
-					}, (e, {project_id, user_id}) => {
-						this.project_id = project_id;
-						this.user_id = user_id;
-						return cb(e);
-					});
-				},
-					
-				cb => {
-					return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => {
-						this.doc_id = doc_id;
-						return cb(e);
-					});
-				},
-
-				cb => {
-					this.client = RealTimeClient.connect();
-					return this.client.on("connectionAccepted", cb);
-				},
-						
-				cb => {
-					return this.client.emit("joinProject", {project_id: this.project_id}, cb);
-				},
-				
-				cb => {
-					return this.client.emit("joinDoc", this.doc_id, cb);
-				},
-					
-				cb => {
-					return this.client.emit("applyOtUpdate", this.doc_id, this.update, error => {
-						this.error = error;
-						return cb();
-					});
-				}
-			], done);
-		});
-		
-		it("should return an error", function() {
-			return expect(this.error).to.exist;
-		});
-		
-		it("should disconnect the client", function(done) {
-			return setTimeout(() => {
-				this.client.socket.connected.should.equal(false);
-				return done();
-			}
-			, 300);
-		});
-			
-		return it("should not put the update in redis", function(done) {
-			rclient.llen(redisSettings.documentupdater.key_schema.pendingUpdates({doc_id: this.doc_id}), (error, len) => {
-				len.should.equal(0);
-				return done();
-			});
-			return null;
-		});
-	});
-				
-	return describe("when authorized to read-only with a comment update", function() {
-		before(function(done) {
-			this.comment_update = {
-				op: [{c: "foo", p: 42}]
-			};
-			return async.series([
-				cb => {
-					return FixturesManager.setUpProject({
-						privilegeLevel: "readOnly"
-					}, (e, {project_id, user_id}) => {
-						this.project_id = project_id;
-						this.user_id = user_id;
-						return cb(e);
-					});
-				},
-					
-				cb => {
-					return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => {
-						this.doc_id = doc_id;
-						return cb(e);
-					});
-				},
-
-				cb => {
-					this.client = RealTimeClient.connect();
-					return this.client.on("connectionAccepted", cb);
-				},
-						
-				cb => {
-					return this.client.emit("joinProject", {project_id: this.project_id}, cb);
-				},
-				
-				cb => {
-					return this.client.emit("joinDoc", this.doc_id, cb);
-				},
-					
-				cb => {
-					return this.client.emit("applyOtUpdate", this.doc_id, this.comment_update, cb);
-				}
-			], done);
-		});
-		
-		it("should push the doc into the pending updates list", function(done) {
-			rclient.lrange("pending-updates-list", 0, -1, (error, ...rest) => {
-				const [doc_id] = Array.from(rest[0]);
-				doc_id.should.equal(`${this.project_id}:${this.doc_id}`);
-				return done();
-			});
-			return null;
-		});
-
-		it("should push the update into redis", function(done) {
-			rclient.lrange(redisSettings.documentupdater.key_schema.pendingUpdates({doc_id: this.doc_id}), 0, -1, (error, ...rest) => {
-				let [update] = Array.from(rest[0]);
-				update = JSON.parse(update);
-				update.op.should.deep.equal(this.comment_update.op);
-				update.meta.should.deep.equal({
-					source: this.client.publicId,
-					user_id: this.user_id
-				});
-				return done();
-			});
-			return null;
-		});
-
-		return after(function(done) {
-			return async.series([
-				cb => rclient.del("pending-updates-list", cb),
-				cb => rclient.del("DocsWithPendingUpdates", `${this.project_id}:${this.doc_id}`, cb),
-				cb => rclient.del(redisSettings.documentupdater.key_schema.pendingUpdates({doc_id: this.doc_id}), cb)
-			], done);
-		});
-	});
-});
+const async = require('async')
+const chai = require('chai')
+const { expect } = chai
+chai.should()
+
+const RealTimeClient = require('./helpers/RealTimeClient')
+const FixturesManager = require('./helpers/FixturesManager')
+
+const settings = require('settings-sharelatex')
+const redis = require('redis-sharelatex')
+const rclient = redis.createClient(settings.redis.documentupdater)
+
+const redisSettings = settings.redis
+
+describe('applyOtUpdate', function () {
+  before(function () {
+    return (this.update = {
+      op: [{ i: 'foo', p: 42 }]
+    })
+  })
+  describe('when authorized', function () {
+    before(function (done) {
+      return async.series(
+        [
+          (cb) => {
+            return FixturesManager.setUpProject(
+              {
+                privilegeLevel: 'readAndWrite'
+              },
+              (e, { project_id, user_id }) => {
+                this.project_id = project_id
+                this.user_id = user_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            return FixturesManager.setUpDoc(
+              this.project_id,
+              { lines: this.lines, version: this.version, ops: this.ops },
+              (e, { doc_id }) => {
+                this.doc_id = doc_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            this.client = RealTimeClient.connect()
+            return this.client.on('connectionAccepted', cb)
+          },
+
+          (cb) => {
+            return this.client.emit(
+              'joinProject',
+              { project_id: this.project_id },
+              cb
+            )
+          },
+
+          (cb) => {
+            return this.client.emit('joinDoc', this.doc_id, cb)
+          },
+
+          (cb) => {
+            return this.client.emit(
+              'applyOtUpdate',
+              this.doc_id,
+              this.update,
+              cb
+            )
+          }
+        ],
+        done
+      )
+    })
+
+    it('should push the doc into the pending updates list', function (done) {
+      rclient.lrange('pending-updates-list', 0, -1, (error, ...rest) => {
+        const [doc_id] = Array.from(rest[0])
+        doc_id.should.equal(`${this.project_id}:${this.doc_id}`)
+        return done()
+      })
+      return null
+    })
+
+    it('should push the update into redis', function (done) {
+      rclient.lrange(
+        redisSettings.documentupdater.key_schema.pendingUpdates({
+          doc_id: this.doc_id
+        }),
+        0,
+        -1,
+        (error, ...rest) => {
+          let [update] = Array.from(rest[0])
+          update = JSON.parse(update)
+          update.op.should.deep.equal(this.update.op)
+          update.meta.should.deep.equal({
+            source: this.client.publicId,
+            user_id: this.user_id
+          })
+          return done()
+        }
+      )
+      return null
+    })
+
+    return after(function (done) {
+      return async.series(
+        [
+          (cb) => rclient.del('pending-updates-list', cb),
+          (cb) =>
+            rclient.del(
+              'DocsWithPendingUpdates',
+              `${this.project_id}:${this.doc_id}`,
+              cb
+            ),
+          (cb) =>
+            rclient.del(
+              redisSettings.documentupdater.key_schema.pendingUpdates(
+                this.doc_id
+              ),
+              cb
+            )
+        ],
+        done
+      )
+    })
+  })
+
+  describe('when authorized with a huge edit update', function () {
+    before(function (done) {
+      this.update = {
+        op: {
+          p: 12,
+          t: 'update is too large'.repeat(1024 * 400) // >7MB
+        }
+      }
+      return async.series(
+        [
+          (cb) => {
+            return FixturesManager.setUpProject(
+              {
+                privilegeLevel: 'readAndWrite'
+              },
+              (e, { project_id, user_id }) => {
+                this.project_id = project_id
+                this.user_id = user_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            return FixturesManager.setUpDoc(
+              this.project_id,
+              { lines: this.lines, version: this.version, ops: this.ops },
+              (e, { doc_id }) => {
+                this.doc_id = doc_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            this.client = RealTimeClient.connect()
+            this.client.on('connectionAccepted', cb)
+            return this.client.on('otUpdateError', (otUpdateError) => {
+              this.otUpdateError = otUpdateError
+            })
+          },
+
+          (cb) => {
+            return this.client.emit(
+              'joinProject',
+              { project_id: this.project_id },
+              cb
+            )
+          },
+
+          (cb) => {
+            return this.client.emit('joinDoc', this.doc_id, cb)
+          },
+
+          (cb) => {
+            return this.client.emit(
+              'applyOtUpdate',
+              this.doc_id,
+              this.update,
+              (error) => {
+                this.error = error
+                return cb()
+              }
+            )
+          }
+        ],
+        done
+      )
+    })
+
+    it('should not return an error', function () {
+      return expect(this.error).to.not.exist
+    })
+
+    it('should send an otUpdateError to the client', function (done) {
+      return setTimeout(() => {
+        expect(this.otUpdateError).to.exist
+        return done()
+      }, 300)
+    })
+
+    it('should disconnect the client', function (done) {
+      return setTimeout(() => {
+        this.client.socket.connected.should.equal(false)
+        return done()
+      }, 300)
+    })
+
+    return it('should not put the update in redis', function (done) {
+      rclient.llen(
+        redisSettings.documentupdater.key_schema.pendingUpdates({
+          doc_id: this.doc_id
+        }),
+        (error, len) => {
+          len.should.equal(0)
+          return done()
+        }
+      )
+      return null
+    })
+  })
+
+  describe('when authorized to read-only with an edit update', function () {
+    before(function (done) {
+      return async.series(
+        [
+          (cb) => {
+            return FixturesManager.setUpProject(
+              {
+                privilegeLevel: 'readOnly'
+              },
+              (e, { project_id, user_id }) => {
+                this.project_id = project_id
+                this.user_id = user_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            return FixturesManager.setUpDoc(
+              this.project_id,
+              { lines: this.lines, version: this.version, ops: this.ops },
+              (e, { doc_id }) => {
+                this.doc_id = doc_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            this.client = RealTimeClient.connect()
+            return this.client.on('connectionAccepted', cb)
+          },
+
+          (cb) => {
+            return this.client.emit(
+              'joinProject',
+              { project_id: this.project_id },
+              cb
+            )
+          },
+
+          (cb) => {
+            return this.client.emit('joinDoc', this.doc_id, cb)
+          },
+
+          (cb) => {
+            return this.client.emit(
+              'applyOtUpdate',
+              this.doc_id,
+              this.update,
+              (error) => {
+                this.error = error
+                return cb()
+              }
+            )
+          }
+        ],
+        done
+      )
+    })
+
+    it('should return an error', function () {
+      return expect(this.error).to.exist
+    })
+
+    it('should disconnect the client', function (done) {
+      return setTimeout(() => {
+        this.client.socket.connected.should.equal(false)
+        return done()
+      }, 300)
+    })
+
+    return it('should not put the update in redis', function (done) {
+      rclient.llen(
+        redisSettings.documentupdater.key_schema.pendingUpdates({
+          doc_id: this.doc_id
+        }),
+        (error, len) => {
+          len.should.equal(0)
+          return done()
+        }
+      )
+      return null
+    })
+  })
+
+  return describe('when authorized to read-only with a comment update', function () {
+    before(function (done) {
+      this.comment_update = {
+        op: [{ c: 'foo', p: 42 }]
+      }
+      return async.series(
+        [
+          (cb) => {
+            return FixturesManager.setUpProject(
+              {
+                privilegeLevel: 'readOnly'
+              },
+              (e, { project_id, user_id }) => {
+                this.project_id = project_id
+                this.user_id = user_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            return FixturesManager.setUpDoc(
+              this.project_id,
+              { lines: this.lines, version: this.version, ops: this.ops },
+              (e, { doc_id }) => {
+                this.doc_id = doc_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            this.client = RealTimeClient.connect()
+            return this.client.on('connectionAccepted', cb)
+          },
+
+          (cb) => {
+            return this.client.emit(
+              'joinProject',
+              { project_id: this.project_id },
+              cb
+            )
+          },
+
+          (cb) => {
+            return this.client.emit('joinDoc', this.doc_id, cb)
+          },
+
+          (cb) => {
+            return this.client.emit(
+              'applyOtUpdate',
+              this.doc_id,
+              this.comment_update,
+              cb
+            )
+          }
+        ],
+        done
+      )
+    })
+
+    it('should push the doc into the pending updates list', function (done) {
+      rclient.lrange('pending-updates-list', 0, -1, (error, ...rest) => {
+        const [doc_id] = Array.from(rest[0])
+        doc_id.should.equal(`${this.project_id}:${this.doc_id}`)
+        return done()
+      })
+      return null
+    })
+
+    it('should push the update into redis', function (done) {
+      rclient.lrange(
+        redisSettings.documentupdater.key_schema.pendingUpdates({
+          doc_id: this.doc_id
+        }),
+        0,
+        -1,
+        (error, ...rest) => {
+          let [update] = Array.from(rest[0])
+          update = JSON.parse(update)
+          update.op.should.deep.equal(this.comment_update.op)
+          update.meta.should.deep.equal({
+            source: this.client.publicId,
+            user_id: this.user_id
+          })
+          return done()
+        }
+      )
+      return null
+    })
+
+    return after(function (done) {
+      return async.series(
+        [
+          (cb) => rclient.del('pending-updates-list', cb),
+          (cb) =>
+            rclient.del(
+              'DocsWithPendingUpdates',
+              `${this.project_id}:${this.doc_id}`,
+              cb
+            ),
+          (cb) =>
+            rclient.del(
+              redisSettings.documentupdater.key_schema.pendingUpdates({
+                doc_id: this.doc_id
+              }),
+              cb
+            )
+        ],
+        done
+      )
+    })
+  })
+})

+ 241 - 184
services/real-time/test/acceptance/js/ClientTrackingTests.js

@@ -12,187 +12,244 @@
  * DS207: Consider shorter variations of null checks
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-const chai = require("chai");
-const {
-    expect
-} = chai;
-chai.should();
-
-const RealTimeClient = require("./helpers/RealTimeClient");
-const MockWebServer = require("./helpers/MockWebServer");
-const FixturesManager = require("./helpers/FixturesManager");
-
-const async = require("async");
-
-describe("clientTracking", function() {
-	describe("when a client updates its cursor location", function() {
-		before(function(done) {
-			return async.series([
-				cb => {
-					return FixturesManager.setUpProject({
-						privilegeLevel: "owner",
-						project: { name: "Test Project" }
-					}, (error, {user_id, project_id}) => { this.user_id = user_id; this.project_id = project_id; return cb(); });
-				},
-				
-				cb => {
-					return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => {
-						this.doc_id = doc_id;
-						return cb(e);
-					});
-				},
-				
-				cb => {
-					this.clientA = RealTimeClient.connect();
-					return this.clientA.on("connectionAccepted", cb);
-				},
-					
-				cb => {
-					this.clientB = RealTimeClient.connect();
-					return this.clientB.on("connectionAccepted", cb);
-				},
-					
-				cb => {
-					return this.clientA.emit("joinProject", {
-						project_id: this.project_id
-					}, cb);
-				},
-				
-				cb => {
-					return this.clientA.emit("joinDoc", this.doc_id, cb);
-				},
-					
-				cb => {
-					return this.clientB.emit("joinProject", {
-						project_id: this.project_id
-					}, cb);
-				},
-					
-				cb => {
-					this.updates = [];
-					this.clientB.on("clientTracking.clientUpdated", data => {
-						return this.updates.push(data);
-					});
-
-					return this.clientA.emit("clientTracking.updatePosition", {
-						row: (this.row = 42),
-						column: (this.column = 36),
-						doc_id: this.doc_id
-					}, (error) => {
-						if (error != null) { throw error; }
-						return setTimeout(cb, 300);
-					});
-				} // Give the message a chance to reach client B.
-			], done);
-		});
-			
-		it("should tell other clients about the update", function() {
-			return this.updates.should.deep.equal([
-				{
-					row: this.row,
-					column: this.column,
-					doc_id: this.doc_id,
-					id: this.clientA.publicId,
-					user_id: this.user_id,
-					name: "Joe Bloggs"
-				}
-			]);
-	});
-		
-		return it("should record the update in getConnectedUsers", function(done) {
-			return this.clientB.emit("clientTracking.getConnectedUsers", (error, users) => {
-				for (const user of Array.from(users)) {
-					if (user.client_id === this.clientA.publicId) {
-						expect(user.cursorData).to.deep.equal({
-							row: this.row,
-							column: this.column,
-							doc_id: this.doc_id 
-						});
-						return done();
-					}
-				}
-				throw new Error("user was never found");
-			});
-		});
-	});
-				
-	return describe("when an anonymous client updates its cursor location", function() {
-		before(function(done) {
-			return async.series([
-				cb => {
-					return FixturesManager.setUpProject({
-						privilegeLevel: "owner",
-						project: { name: "Test Project"	},
-						publicAccess: "readAndWrite"
-					}, (error, {user_id, project_id}) => { this.user_id = user_id; this.project_id = project_id; return cb(); });
-				},
-				
-				cb => {
-					return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => {
-						this.doc_id = doc_id;
-						return cb(e);
-					});
-				},
-				
-				cb => {
-					this.clientA = RealTimeClient.connect();
-					return this.clientA.on("connectionAccepted", cb);
-				},
-
-				cb => {
-					return this.clientA.emit("joinProject", {
-						project_id: this.project_id
-					}, cb);
-				},
-			
-				cb => {
-					return RealTimeClient.setSession({}, cb);
-				},
-					
-				cb => {
-					this.anonymous = RealTimeClient.connect();
-					return this.anonymous.on("connectionAccepted", cb);
-				},	
-					
-				cb => {
-					return this.anonymous.emit("joinProject", {
-						project_id: this.project_id
-					}, cb);
-				},
-				
-				cb => {
-					return this.anonymous.emit("joinDoc", this.doc_id, cb);
-				},
-					
-				cb => {
-					this.updates = [];
-					this.clientA.on("clientTracking.clientUpdated", data => {
-						return this.updates.push(data);
-					});
-
-					return this.anonymous.emit("clientTracking.updatePosition", {
-						row: (this.row = 42),
-						column: (this.column = 36),
-						doc_id: this.doc_id
-					}, (error) => {
-						if (error != null) { throw error; }
-						return setTimeout(cb, 300);
-					});
-				} // Give the message a chance to reach client B.
-			], done);
-		});
-			
-		return it("should tell other clients about the update", function() {
-			return this.updates.should.deep.equal([
-				{
-					row: this.row,
-					column: this.column,
-					doc_id: this.doc_id,
-					id: this.anonymous.publicId,
-					user_id: "anonymous-user",
-					name: ""
-				}
-			]);
-	});
-});
-});
+const chai = require('chai')
+const { expect } = chai
+chai.should()
+
+const RealTimeClient = require('./helpers/RealTimeClient')
+const MockWebServer = require('./helpers/MockWebServer')
+const FixturesManager = require('./helpers/FixturesManager')
+
+const async = require('async')
+
+describe('clientTracking', function () {
+  describe('when a client updates its cursor location', function () {
+    before(function (done) {
+      return async.series(
+        [
+          (cb) => {
+            return FixturesManager.setUpProject(
+              {
+                privilegeLevel: 'owner',
+                project: { name: 'Test Project' }
+              },
+              (error, { user_id, project_id }) => {
+                this.user_id = user_id
+                this.project_id = project_id
+                return cb()
+              }
+            )
+          },
+
+          (cb) => {
+            return FixturesManager.setUpDoc(
+              this.project_id,
+              { lines: this.lines, version: this.version, ops: this.ops },
+              (e, { doc_id }) => {
+                this.doc_id = doc_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            this.clientA = RealTimeClient.connect()
+            return this.clientA.on('connectionAccepted', cb)
+          },
+
+          (cb) => {
+            this.clientB = RealTimeClient.connect()
+            return this.clientB.on('connectionAccepted', cb)
+          },
+
+          (cb) => {
+            return this.clientA.emit(
+              'joinProject',
+              {
+                project_id: this.project_id
+              },
+              cb
+            )
+          },
+
+          (cb) => {
+            return this.clientA.emit('joinDoc', this.doc_id, cb)
+          },
+
+          (cb) => {
+            return this.clientB.emit(
+              'joinProject',
+              {
+                project_id: this.project_id
+              },
+              cb
+            )
+          },
+
+          (cb) => {
+            this.updates = []
+            this.clientB.on('clientTracking.clientUpdated', (data) => {
+              return this.updates.push(data)
+            })
+
+            return this.clientA.emit(
+              'clientTracking.updatePosition',
+              {
+                row: (this.row = 42),
+                column: (this.column = 36),
+                doc_id: this.doc_id
+              },
+              (error) => {
+                if (error != null) {
+                  throw error
+                }
+                return setTimeout(cb, 300)
+              }
+            )
+          } // Give the message a chance to reach client B.
+        ],
+        done
+      )
+    })
+
+    it('should tell other clients about the update', function () {
+      return this.updates.should.deep.equal([
+        {
+          row: this.row,
+          column: this.column,
+          doc_id: this.doc_id,
+          id: this.clientA.publicId,
+          user_id: this.user_id,
+          name: 'Joe Bloggs'
+        }
+      ])
+    })
+
+    return it('should record the update in getConnectedUsers', function (done) {
+      return this.clientB.emit(
+        'clientTracking.getConnectedUsers',
+        (error, users) => {
+          for (const user of Array.from(users)) {
+            if (user.client_id === this.clientA.publicId) {
+              expect(user.cursorData).to.deep.equal({
+                row: this.row,
+                column: this.column,
+                doc_id: this.doc_id
+              })
+              return done()
+            }
+          }
+          throw new Error('user was never found')
+        }
+      )
+    })
+  })
+
+  return describe('when an anonymous client updates its cursor location', function () {
+    before(function (done) {
+      return async.series(
+        [
+          (cb) => {
+            return FixturesManager.setUpProject(
+              {
+                privilegeLevel: 'owner',
+                project: { name: 'Test Project' },
+                publicAccess: 'readAndWrite'
+              },
+              (error, { user_id, project_id }) => {
+                this.user_id = user_id
+                this.project_id = project_id
+                return cb()
+              }
+            )
+          },
+
+          (cb) => {
+            return FixturesManager.setUpDoc(
+              this.project_id,
+              { lines: this.lines, version: this.version, ops: this.ops },
+              (e, { doc_id }) => {
+                this.doc_id = doc_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            this.clientA = RealTimeClient.connect()
+            return this.clientA.on('connectionAccepted', cb)
+          },
+
+          (cb) => {
+            return this.clientA.emit(
+              'joinProject',
+              {
+                project_id: this.project_id
+              },
+              cb
+            )
+          },
+
+          (cb) => {
+            return RealTimeClient.setSession({}, cb)
+          },
+
+          (cb) => {
+            this.anonymous = RealTimeClient.connect()
+            return this.anonymous.on('connectionAccepted', cb)
+          },
+
+          (cb) => {
+            return this.anonymous.emit(
+              'joinProject',
+              {
+                project_id: this.project_id
+              },
+              cb
+            )
+          },
+
+          (cb) => {
+            return this.anonymous.emit('joinDoc', this.doc_id, cb)
+          },
+
+          (cb) => {
+            this.updates = []
+            this.clientA.on('clientTracking.clientUpdated', (data) => {
+              return this.updates.push(data)
+            })
+
+            return this.anonymous.emit(
+              'clientTracking.updatePosition',
+              {
+                row: (this.row = 42),
+                column: (this.column = 36),
+                doc_id: this.doc_id
+              },
+              (error) => {
+                if (error != null) {
+                  throw error
+                }
+                return setTimeout(cb, 300)
+              }
+            )
+          } // Give the message a chance to reach client B.
+        ],
+        done
+      )
+    })
+
+    return it('should tell other clients about the update', function () {
+      return this.updates.should.deep.equal([
+        {
+          row: this.row,
+          column: this.column,
+          doc_id: this.doc_id,
+          id: this.anonymous.publicId,
+          user_id: 'anonymous-user',
+          name: ''
+        }
+      ])
+    })
+  })
+})

+ 125 - 95
services/real-time/test/acceptance/js/DrainManagerTests.js

@@ -8,98 +8,128 @@
  * DS102: Remove unnecessary code created because of implicit returns
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-const RealTimeClient = require("./helpers/RealTimeClient");
-const FixturesManager = require("./helpers/FixturesManager");
-
-const {
-    expect
-} = require("chai");
-
-const async = require("async");
-const request = require("request");
-
-const Settings = require("settings-sharelatex");
-
-const drain = function(rate, callback) {
-	request.post({
-		url: `http://localhost:3026/drain?rate=${rate}`,
-		auth: {
-			user: Settings.internal.realTime.user,
-			pass: Settings.internal.realTime.pass
-		}
-	}, (error, response, data) => callback(error, data));
-	return null;
-};
-
-describe("DrainManagerTests", function() {
-	before(function(done) {
-		FixturesManager.setUpProject({
-			privilegeLevel: "owner",
-			project: {
-				name: "Test Project"
-			}
-		}, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return done(); });
-		return null;
-	});
-
-	before(function(done) {
-		// cleanup to speedup reconnecting
-		this.timeout(10000);
-		return RealTimeClient.disconnectAllClients(done);
-	});
-
-	// trigger and check cleanup
-	it("should have disconnected all previous clients", function(done) { return RealTimeClient.getConnectedClients((error, data) => {
-        if (error) { return done(error); }
-        expect(data.length).to.equal(0);
-        return done();
-    }); });
-
-	return describe("with two clients in the project", function() {
-		beforeEach(function(done) {
-			return async.series([
-				cb => {
-					this.clientA = RealTimeClient.connect();
-					return this.clientA.on("connectionAccepted", cb);
-				},
-
-				cb => {
-					this.clientB = RealTimeClient.connect();
-					return this.clientB.on("connectionAccepted", cb);
-				},
-
-				cb => {
-					return this.clientA.emit("joinProject", {project_id: this.project_id}, cb);
-				},
-
-				cb => {
-					return this.clientB.emit("joinProject", {project_id: this.project_id}, cb);
-				}
-			], done);
-		});
-
-		return describe("starting to drain", function() {
-			beforeEach(function(done) {
-				return async.parallel([
-					cb => {
-						return this.clientA.on("reconnectGracefully", cb);
-					},
-					cb => {
-						return this.clientB.on("reconnectGracefully", cb);
-					},
-
-					cb => drain(2, cb)
-				], done);
-			});
-
-			afterEach(function(done) { return drain(0, done); }); // reset drain
-
-			it("should not timeout", function() { return expect(true).to.equal(true); });
-
-			return it("should not have disconnected", function() {
-				expect(this.clientA.socket.connected).to.equal(true);
-				return expect(this.clientB.socket.connected).to.equal(true);
-			});
-		});
-	});
-});
+const RealTimeClient = require('./helpers/RealTimeClient')
+const FixturesManager = require('./helpers/FixturesManager')
+
+const { expect } = require('chai')
+
+const async = require('async')
+const request = require('request')
+
+const Settings = require('settings-sharelatex')
+
+const drain = function (rate, callback) {
+  request.post(
+    {
+      url: `http://localhost:3026/drain?rate=${rate}`,
+      auth: {
+        user: Settings.internal.realTime.user,
+        pass: Settings.internal.realTime.pass
+      }
+    },
+    (error, response, data) => callback(error, data)
+  )
+  return null
+}
+
+describe('DrainManagerTests', function () {
+  before(function (done) {
+    FixturesManager.setUpProject(
+      {
+        privilegeLevel: 'owner',
+        project: {
+          name: 'Test Project'
+        }
+      },
+      (e, { project_id, user_id }) => {
+        this.project_id = project_id
+        this.user_id = user_id
+        return done()
+      }
+    )
+    return null
+  })
+
+  before(function (done) {
+    // cleanup to speedup reconnecting
+    this.timeout(10000)
+    return RealTimeClient.disconnectAllClients(done)
+  })
+
+  // trigger and check cleanup
+  it('should have disconnected all previous clients', function (done) {
+    return RealTimeClient.getConnectedClients((error, data) => {
+      if (error) {
+        return done(error)
+      }
+      expect(data.length).to.equal(0)
+      return done()
+    })
+  })
+
+  return describe('with two clients in the project', function () {
+    beforeEach(function (done) {
+      return async.series(
+        [
+          (cb) => {
+            this.clientA = RealTimeClient.connect()
+            return this.clientA.on('connectionAccepted', cb)
+          },
+
+          (cb) => {
+            this.clientB = RealTimeClient.connect()
+            return this.clientB.on('connectionAccepted', cb)
+          },
+
+          (cb) => {
+            return this.clientA.emit(
+              'joinProject',
+              { project_id: this.project_id },
+              cb
+            )
+          },
+
+          (cb) => {
+            return this.clientB.emit(
+              'joinProject',
+              { project_id: this.project_id },
+              cb
+            )
+          }
+        ],
+        done
+      )
+    })
+
+    return describe('starting to drain', function () {
+      beforeEach(function (done) {
+        return async.parallel(
+          [
+            (cb) => {
+              return this.clientA.on('reconnectGracefully', cb)
+            },
+            (cb) => {
+              return this.clientB.on('reconnectGracefully', cb)
+            },
+
+            (cb) => drain(2, cb)
+          ],
+          done
+        )
+      })
+
+      afterEach(function (done) {
+        return drain(0, done)
+      }) // reset drain
+
+      it('should not timeout', function () {
+        return expect(true).to.equal(true)
+      })
+
+      return it('should not have disconnected', function () {
+        expect(this.clientA.socket.connected).to.equal(true)
+        return expect(this.clientB.socket.connected).to.equal(true)
+      })
+    })
+  })
+})

+ 276 - 203
services/real-time/test/acceptance/js/EarlyDisconnect.js

@@ -10,206 +10,279 @@
  * DS102: Remove unnecessary code created because of implicit returns
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-const async = require("async");
-const {expect} = require("chai");
-
-const RealTimeClient = require("./helpers/RealTimeClient");
-const MockDocUpdaterServer = require("./helpers/MockDocUpdaterServer");
-const MockWebServer = require("./helpers/MockWebServer");
-const FixturesManager = require("./helpers/FixturesManager");
-
-const settings = require("settings-sharelatex");
-const redis = require("redis-sharelatex");
-const rclient = redis.createClient(settings.redis.pubsub);
-const rclientRT = redis.createClient(settings.redis.realtime);
-const KeysRT = settings.redis.realtime.key_schema;
-
-describe("EarlyDisconnect", function() {
-	before(function(done) { return MockDocUpdaterServer.run(done); });
-
-	describe("when the client disconnects before joinProject completes", function() {
-		before(function() {
-			// slow down web-api requests to force the race condition
-			let joinProject;
-			this.actualWebAPIjoinProject = (joinProject = MockWebServer.joinProject);
-			return MockWebServer.joinProject = (project_id, user_id, cb) => setTimeout(() => joinProject(project_id, user_id, cb)
-            , 300);
-		});
-
-		after(function() {
-			return MockWebServer.joinProject = this.actualWebAPIjoinProject;
-		});
-
-		beforeEach(function(done) {
-			return async.series([
-				cb => {
-					return FixturesManager.setUpProject({
-						privilegeLevel: "owner",
-						project: {
-							name: "Test Project"
-						}
-					}, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return cb(); });
-				},
-
-				cb => {
-					this.clientA = RealTimeClient.connect();
-					return this.clientA.on("connectionAccepted", cb);
-				},
-
-				cb => {
-					this.clientA.emit("joinProject", {project_id: this.project_id}, (() => {}));
-					// disconnect before joinProject completes
-					this.clientA.on("disconnect", () => cb());
-					return this.clientA.disconnect();
-				},
-
-				cb => {
-					// wait for joinDoc and subscribe
-					return setTimeout(cb, 500);
-				}
-			], done);
-		});
-
-		// we can force the race condition, there is no need to repeat too often
-		return Array.from(Array.from({length: 5}).map((_, i) => i+1)).map((attempt) =>
-			it(`should not subscribe to the pub/sub channel anymore (race ${attempt})`, function(done) {
-				rclient.pubsub('CHANNELS', (err, resp) => {
-					if (err) { return done(err); }
-					expect(resp).to.not.include(`editor-events:${this.project_id}`);
-					return done();
-				});
-				return null;
-			}));
-	});
-
-	describe("when the client disconnects before joinDoc completes", function() {
-		beforeEach(function(done) {
-			return async.series([
-				cb => {
-					return FixturesManager.setUpProject({
-						privilegeLevel: "owner",
-						project: {
-							name: "Test Project"
-						}
-					}, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return cb(); });
-				},
-
-				cb => {
-					this.clientA = RealTimeClient.connect();
-					return this.clientA.on("connectionAccepted", cb);
-				},
-
-				cb => {
-					return this.clientA.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => {
-						this.project = project;
-						this.privilegeLevel = privilegeLevel;
-						this.protocolVersion = protocolVersion;
-						return cb(error);
-					});
-				},
-
-				cb => {
-					return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => {
-						this.doc_id = doc_id;
-						return cb(e);
-					});
-				},
-
-				cb => {
-					this.clientA.emit("joinDoc", this.doc_id, (() => {}));
-					// disconnect before joinDoc completes
-					this.clientA.on("disconnect", () => cb());
-					return this.clientA.disconnect();
-				},
-
-				cb => {
-					// wait for subscribe and unsubscribe
-					return setTimeout(cb, 100);
-				}
-			], done);
-		});
-
-		// we can not force the race condition, so we have to try many times
-		return Array.from(Array.from({length: 20}).map((_, i) => i+1)).map((attempt) =>
-			it(`should not subscribe to the pub/sub channels anymore (race ${attempt})`, function(done) {
-				rclient.pubsub('CHANNELS', (err, resp) => {
-					if (err) { return done(err); }
-					expect(resp).to.not.include(`editor-events:${this.project_id}`);
-
-					return rclient.pubsub('CHANNELS', (err, resp) => {
-						if (err) { return done(err); }
-						expect(resp).to.not.include(`applied-ops:${this.doc_id}`);
-						return done();
-					});
-				});
-				return null;
-			}));
-	});
-
-	return describe("when the client disconnects before clientTracking.updatePosition starts", function() {
-		beforeEach(function(done) {
-			return async.series([
-				cb => {
-					return FixturesManager.setUpProject({
-						privilegeLevel: "owner",
-						project: {
-							name: "Test Project"
-						}
-					}, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return cb(); });
-				},
-
-				cb => {
-					this.clientA = RealTimeClient.connect();
-					return this.clientA.on("connectionAccepted", cb);
-				},
-
-				cb => {
-					return this.clientA.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => {
-						this.project = project;
-						this.privilegeLevel = privilegeLevel;
-						this.protocolVersion = protocolVersion;
-						return cb(error);
-					});
-				},
-
-				cb => {
-					return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => {
-						this.doc_id = doc_id;
-						return cb(e);
-					});
-				},
-
-				cb => {
-					return this.clientA.emit("joinDoc", this.doc_id, cb);
-				},
-
-				cb => {
-					this.clientA.emit("clientTracking.updatePosition", {
-							row: 42,
-							column: 36,
-							doc_id: this.doc_id
-						}, (() => {}));
-					// disconnect before updateClientPosition completes
-					this.clientA.on("disconnect", () => cb());
-					return this.clientA.disconnect();
-				},
-
-				cb => {
-					// wait for updateClientPosition
-					return setTimeout(cb, 100);
-				}
-			], done);
-		});
-
-		// we can not force the race condition, so we have to try many times
-		return Array.from(Array.from({length: 20}).map((_, i) => i+1)).map((attempt) =>
-			it(`should not show the client as connected (race ${attempt})`, function(done) {
-				rclientRT.smembers(KeysRT.clientsInProject({project_id: this.project_id}), (err, results) => {
-					if (err) { return done(err); }
-					expect(results).to.deep.equal([]);
-					return done();
-				});
-				return null;
-			}));
-	});
-});
+const async = require('async')
+const { expect } = require('chai')
+
+const RealTimeClient = require('./helpers/RealTimeClient')
+const MockDocUpdaterServer = require('./helpers/MockDocUpdaterServer')
+const MockWebServer = require('./helpers/MockWebServer')
+const FixturesManager = require('./helpers/FixturesManager')
+
+const settings = require('settings-sharelatex')
+const redis = require('redis-sharelatex')
+const rclient = redis.createClient(settings.redis.pubsub)
+const rclientRT = redis.createClient(settings.redis.realtime)
+const KeysRT = settings.redis.realtime.key_schema
+
+describe('EarlyDisconnect', function () {
+  before(function (done) {
+    return MockDocUpdaterServer.run(done)
+  })
+
+  describe('when the client disconnects before joinProject completes', function () {
+    before(function () {
+      // slow down web-api requests to force the race condition
+      let joinProject
+      this.actualWebAPIjoinProject = joinProject = MockWebServer.joinProject
+      return (MockWebServer.joinProject = (project_id, user_id, cb) =>
+        setTimeout(() => joinProject(project_id, user_id, cb), 300))
+    })
+
+    after(function () {
+      return (MockWebServer.joinProject = this.actualWebAPIjoinProject)
+    })
+
+    beforeEach(function (done) {
+      return async.series(
+        [
+          (cb) => {
+            return FixturesManager.setUpProject(
+              {
+                privilegeLevel: 'owner',
+                project: {
+                  name: 'Test Project'
+                }
+              },
+              (e, { project_id, user_id }) => {
+                this.project_id = project_id
+                this.user_id = user_id
+                return cb()
+              }
+            )
+          },
+
+          (cb) => {
+            this.clientA = RealTimeClient.connect()
+            return this.clientA.on('connectionAccepted', cb)
+          },
+
+          (cb) => {
+            this.clientA.emit(
+              'joinProject',
+              { project_id: this.project_id },
+              () => {}
+            )
+            // disconnect before joinProject completes
+            this.clientA.on('disconnect', () => cb())
+            return this.clientA.disconnect()
+          },
+
+          (cb) => {
+            // wait for joinDoc and subscribe
+            return setTimeout(cb, 500)
+          }
+        ],
+        done
+      )
+    })
+
+    // we can force the race condition, there is no need to repeat too often
+    return Array.from(Array.from({ length: 5 }).map((_, i) => i + 1)).map(
+      (attempt) =>
+        it(`should not subscribe to the pub/sub channel anymore (race ${attempt})`, function (done) {
+          rclient.pubsub('CHANNELS', (err, resp) => {
+            if (err) {
+              return done(err)
+            }
+            expect(resp).to.not.include(`editor-events:${this.project_id}`)
+            return done()
+          })
+          return null
+        })
+    )
+  })
+
+  describe('when the client disconnects before joinDoc completes', function () {
+    beforeEach(function (done) {
+      return async.series(
+        [
+          (cb) => {
+            return FixturesManager.setUpProject(
+              {
+                privilegeLevel: 'owner',
+                project: {
+                  name: 'Test Project'
+                }
+              },
+              (e, { project_id, user_id }) => {
+                this.project_id = project_id
+                this.user_id = user_id
+                return cb()
+              }
+            )
+          },
+
+          (cb) => {
+            this.clientA = RealTimeClient.connect()
+            return this.clientA.on('connectionAccepted', cb)
+          },
+
+          (cb) => {
+            return this.clientA.emit(
+              'joinProject',
+              { project_id: this.project_id },
+              (error, project, privilegeLevel, protocolVersion) => {
+                this.project = project
+                this.privilegeLevel = privilegeLevel
+                this.protocolVersion = protocolVersion
+                return cb(error)
+              }
+            )
+          },
+
+          (cb) => {
+            return FixturesManager.setUpDoc(
+              this.project_id,
+              { lines: this.lines, version: this.version, ops: this.ops },
+              (e, { doc_id }) => {
+                this.doc_id = doc_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            this.clientA.emit('joinDoc', this.doc_id, () => {})
+            // disconnect before joinDoc completes
+            this.clientA.on('disconnect', () => cb())
+            return this.clientA.disconnect()
+          },
+
+          (cb) => {
+            // wait for subscribe and unsubscribe
+            return setTimeout(cb, 100)
+          }
+        ],
+        done
+      )
+    })
+
+    // we can not force the race condition, so we have to try many times
+    return Array.from(Array.from({ length: 20 }).map((_, i) => i + 1)).map(
+      (attempt) =>
+        it(`should not subscribe to the pub/sub channels anymore (race ${attempt})`, function (done) {
+          rclient.pubsub('CHANNELS', (err, resp) => {
+            if (err) {
+              return done(err)
+            }
+            expect(resp).to.not.include(`editor-events:${this.project_id}`)
+
+            return rclient.pubsub('CHANNELS', (err, resp) => {
+              if (err) {
+                return done(err)
+              }
+              expect(resp).to.not.include(`applied-ops:${this.doc_id}`)
+              return done()
+            })
+          })
+          return null
+        })
+    )
+  })
+
+  return describe('when the client disconnects before clientTracking.updatePosition starts', function () {
+    beforeEach(function (done) {
+      return async.series(
+        [
+          (cb) => {
+            return FixturesManager.setUpProject(
+              {
+                privilegeLevel: 'owner',
+                project: {
+                  name: 'Test Project'
+                }
+              },
+              (e, { project_id, user_id }) => {
+                this.project_id = project_id
+                this.user_id = user_id
+                return cb()
+              }
+            )
+          },
+
+          (cb) => {
+            this.clientA = RealTimeClient.connect()
+            return this.clientA.on('connectionAccepted', cb)
+          },
+
+          (cb) => {
+            return this.clientA.emit(
+              'joinProject',
+              { project_id: this.project_id },
+              (error, project, privilegeLevel, protocolVersion) => {
+                this.project = project
+                this.privilegeLevel = privilegeLevel
+                this.protocolVersion = protocolVersion
+                return cb(error)
+              }
+            )
+          },
+
+          (cb) => {
+            return FixturesManager.setUpDoc(
+              this.project_id,
+              { lines: this.lines, version: this.version, ops: this.ops },
+              (e, { doc_id }) => {
+                this.doc_id = doc_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            return this.clientA.emit('joinDoc', this.doc_id, cb)
+          },
+
+          (cb) => {
+            this.clientA.emit(
+              'clientTracking.updatePosition',
+              {
+                row: 42,
+                column: 36,
+                doc_id: this.doc_id
+              },
+              () => {}
+            )
+            // disconnect before updateClientPosition completes
+            this.clientA.on('disconnect', () => cb())
+            return this.clientA.disconnect()
+          },
+
+          (cb) => {
+            // wait for updateClientPosition
+            return setTimeout(cb, 100)
+          }
+        ],
+        done
+      )
+    })
+
+    // we can not force the race condition, so we have to try many times
+    return Array.from(Array.from({ length: 20 }).map((_, i) => i + 1)).map(
+      (attempt) =>
+        it(`should not show the client as connected (race ${attempt})`, function (done) {
+          rclientRT.smembers(
+            KeysRT.clientsInProject({ project_id: this.project_id }),
+            (err, results) => {
+              if (err) {
+                return done(err)
+              }
+              expect(results).to.deep.equal([])
+              return done()
+            }
+          )
+          return null
+        })
+    )
+  })
+})

+ 98 - 77
services/real-time/test/acceptance/js/HttpControllerTests.js

@@ -8,89 +8,110 @@
  * DS102: Remove unnecessary code created because of implicit returns
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-const async = require('async');
-const {
-    expect
-} = require('chai');
+const async = require('async')
+const { expect } = require('chai')
 const request = require('request').defaults({
-	baseUrl: 'http://localhost:3026'
-});
+  baseUrl: 'http://localhost:3026'
+})
 
-const RealTimeClient = require("./helpers/RealTimeClient");
-const FixturesManager = require("./helpers/FixturesManager");
+const RealTimeClient = require('./helpers/RealTimeClient')
+const FixturesManager = require('./helpers/FixturesManager')
 
-describe('HttpControllerTests', function() {
-	describe('without a user', function() { return it('should return 404 for the client view', function(done) {
-        const client_id = 'not-existing';
-        return request.get({
-            url: `/clients/${client_id}`,
-            json: true
-        }, (error, response, data) => {
-            if (error) { return done(error); }
-            expect(response.statusCode).to.equal(404);
-            return done();
-        });
-    }); });
+describe('HttpControllerTests', function () {
+  describe('without a user', function () {
+    return it('should return 404 for the client view', function (done) {
+      const client_id = 'not-existing'
+      return request.get(
+        {
+          url: `/clients/${client_id}`,
+          json: true
+        },
+        (error, response, data) => {
+          if (error) {
+            return done(error)
+          }
+          expect(response.statusCode).to.equal(404)
+          return done()
+        }
+      )
+    })
+  })
 
-	return describe('with a user and after joining a project', function() {
-		before(function(done) {
-			return async.series([
-				cb => {
-					return FixturesManager.setUpProject({
-						privilegeLevel: "owner"
-					}, (error, {project_id, user_id}) => {
-						this.project_id = project_id;
-						this.user_id = user_id;
-						return cb(error);
-					});
-				},
+  return describe('with a user and after joining a project', function () {
+    before(function (done) {
+      return async.series(
+        [
+          (cb) => {
+            return FixturesManager.setUpProject(
+              {
+                privilegeLevel: 'owner'
+              },
+              (error, { project_id, user_id }) => {
+                this.project_id = project_id
+                this.user_id = user_id
+                return cb(error)
+              }
+            )
+          },
 
-				cb => {
-					return FixturesManager.setUpDoc(this.project_id, {}, (error, {doc_id}) => {
-						this.doc_id = doc_id;
-						return cb(error);
-					});
-				},
+          (cb) => {
+            return FixturesManager.setUpDoc(
+              this.project_id,
+              {},
+              (error, { doc_id }) => {
+                this.doc_id = doc_id
+                return cb(error)
+              }
+            )
+          },
 
-				cb => {
-					this.client = RealTimeClient.connect();
-					return this.client.on("connectionAccepted", cb);
-				},
+          (cb) => {
+            this.client = RealTimeClient.connect()
+            return this.client.on('connectionAccepted', cb)
+          },
 
-				cb => {
-					return this.client.emit("joinProject", {project_id: this.project_id}, cb);
-				},
+          (cb) => {
+            return this.client.emit(
+              'joinProject',
+              { project_id: this.project_id },
+              cb
+            )
+          },
 
-				cb => {
-					return this.client.emit("joinDoc", this.doc_id, cb);
-				}
-			], done);
-		});
+          (cb) => {
+            return this.client.emit('joinDoc', this.doc_id, cb)
+          }
+        ],
+        done
+      )
+    })
 
-		return it('should send a client view', function(done) {
-			return request.get({
-				url: `/clients/${this.client.socket.sessionid}`,
-				json: true
-			}, (error, response, data) => {
-				if (error) { return done(error); }
-				expect(response.statusCode).to.equal(200);
-				expect(data.connected_time).to.exist;
-				delete data.connected_time;
-				// .email is not set in the session
-				delete data.email;
-				expect(data).to.deep.equal({
-					client_id: this.client.socket.sessionid,
-					first_name: 'Joe',
-					last_name: 'Bloggs',
-					project_id: this.project_id,
-					user_id: this.user_id,
-					rooms: [
-						this.project_id,
-						this.doc_id,
-					]
-				});
-				return done();
-			});
-		});
-	});
-});
+    return it('should send a client view', function (done) {
+      return request.get(
+        {
+          url: `/clients/${this.client.socket.sessionid}`,
+          json: true
+        },
+        (error, response, data) => {
+          if (error) {
+            return done(error)
+          }
+          expect(response.statusCode).to.equal(200)
+          expect(data.connected_time).to.exist
+          delete data.connected_time
+          // .email is not set in the session
+          delete data.email
+          expect(data).to.deep.equal({
+            client_id: this.client.socket.sessionid,
+            first_name: 'Joe',
+            last_name: 'Bloggs',
+            project_id: this.project_id,
+            user_id: this.user_id,
+            rooms: [this.project_id, this.doc_id]
+          })
+          return done()
+        }
+      )
+    })
+  })
+})

+ 552 - 345
services/real-time/test/acceptance/js/JoinDocTests.js

@@ -11,348 +11,555 @@
  * 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;
-chai.should();
-
-const RealTimeClient = require("./helpers/RealTimeClient");
-const MockDocUpdaterServer = require("./helpers/MockDocUpdaterServer");
-const FixturesManager = require("./helpers/FixturesManager");
-
-const async = require("async");
-
-describe("joinDoc", function() {
-	before(function() {
-		this.lines = ["test", "doc", "lines"];
-		this.version = 42;
-		this.ops = ["mock", "doc", "ops"];
-		return this.ranges = {"mock": "ranges"};});
-			
-	describe("when authorised readAndWrite", function() {
-		before(function(done) {
-			return async.series([
-				cb => {
-					return FixturesManager.setUpProject({
-						privilegeLevel: "readAndWrite"
-					}, (e, {project_id, user_id}) => {
-						this.project_id = project_id;
-						this.user_id = user_id;
-						return cb(e);
-					});
-				},
-					
-				cb => {
-					return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops, ranges: this.ranges}, (e, {doc_id}) => {
-						this.doc_id = doc_id;
-						return cb(e);
-					});
-				},
-
-				cb => {
-					this.client = RealTimeClient.connect();
-					return this.client.on("connectionAccepted", cb);
-				},
-						
-				cb => {
-					return this.client.emit("joinProject", {project_id: this.project_id}, cb);
-				},
-				
-				cb => {
-					return this.client.emit("joinDoc", this.doc_id, (error, ...rest) => { [...this.returnedArgs] = Array.from(rest); return cb(error); });
-				}
-			], done);
-		});
-
-		it("should get the doc from the doc updater", function() {
-			return MockDocUpdaterServer.getDocument
-				.calledWith(this.project_id, this.doc_id, -1)
-				.should.equal(true);
-		});
-		
-		it("should return the doc lines, version, ranges and ops", function() {
-			return this.returnedArgs.should.deep.equal([this.lines, this.version, this.ops, this.ranges]);
-	});
-			
-		return it("should have joined the doc room", function(done) {
-			return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => {
-				expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true);
-				return done();
-			});
-		});
-	});
-		
-	describe("when authorised readOnly", function() {
-		before(function(done) {
-			return async.series([
-				cb => {
-					return FixturesManager.setUpProject({
-						privilegeLevel: "readOnly"
-					}, (e, {project_id, user_id}) => {
-						this.project_id = project_id;
-						this.user_id = user_id;
-						return cb(e);
-					});
-				},
-					
-				cb => {
-					return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops, ranges: this.ranges}, (e, {doc_id}) => {
-						this.doc_id = doc_id;
-						return cb(e);
-					});
-				},
-
-				cb => {
-					this.client = RealTimeClient.connect();
-					return this.client.on("connectionAccepted", cb);
-				},
-						
-				cb => {
-					return this.client.emit("joinProject", {project_id: this.project_id}, cb);
-				},
-				
-				cb => {
-					return this.client.emit("joinDoc", this.doc_id, (error, ...rest) => { [...this.returnedArgs] = Array.from(rest); return cb(error); });
-				}
-			], done);
-		});
-
-		it("should get the doc from the doc updater", function() {
-			return MockDocUpdaterServer.getDocument
-				.calledWith(this.project_id, this.doc_id, -1)
-				.should.equal(true);
-		});
-		
-		it("should return the doc lines, version, ranges and ops", function() {
-			return this.returnedArgs.should.deep.equal([this.lines, this.version, this.ops, this.ranges]);
-	});
-			
-		return it("should have joined the doc room", function(done) {
-			return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => {
-				expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true);
-				return done();
-			});
-		});
-	});
-		
-	describe("when authorised as owner", function() {
-		before(function(done) {
-			return async.series([
-				cb => {
-					return FixturesManager.setUpProject({
-						privilegeLevel: "owner"
-					}, (e, {project_id, user_id}) => {
-						this.project_id = project_id;
-						this.user_id = user_id;
-						return cb(e);
-					});
-				},
-					
-				cb => {
-					return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops, ranges: this.ranges}, (e, {doc_id}) => {
-						this.doc_id = doc_id;
-						return cb(e);
-					});
-				},
-
-				cb => {
-					this.client = RealTimeClient.connect();
-					return this.client.on("connectionAccepted", cb);
-				},
-						
-				cb => {
-					return this.client.emit("joinProject", {project_id: this.project_id}, cb);
-				},
-				
-				cb => {
-					return this.client.emit("joinDoc", this.doc_id, (error, ...rest) => { [...this.returnedArgs] = Array.from(rest); return cb(error); });
-				}
-			], done);
-		});
-
-		it("should get the doc from the doc updater", function() {
-			return MockDocUpdaterServer.getDocument
-				.calledWith(this.project_id, this.doc_id, -1)
-				.should.equal(true);
-		});
-		
-		it("should return the doc lines, version, ranges and ops", function() {
-			return this.returnedArgs.should.deep.equal([this.lines, this.version, this.ops, this.ranges]);
-	});
-			
-		return it("should have joined the doc room", function(done) {
-			return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => {
-				expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true);
-				return done();
-			});
-		});
-	});
-
-	// It is impossible to write an acceptance test to test joining an unauthorized
-	// project, since joinProject already catches that. If you can join a project,
-	// then you can join a doc in that project.
-			
-	describe("with a fromVersion", function() {
-		before(function(done) {
-			this.fromVersion = 36;
-			return async.series([
-				cb => {
-					return FixturesManager.setUpProject({
-						privilegeLevel: "readAndWrite"
-					}, (e, {project_id, user_id}) => {
-						this.project_id = project_id;
-						this.user_id = user_id;
-						return cb(e);
-					});
-				},
-					
-				cb => {
-					return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops, ranges: this.ranges}, (e, {doc_id}) => {
-						this.doc_id = doc_id;
-						return cb(e);
-					});
-				},
-
-				cb => {
-					this.client = RealTimeClient.connect();
-					return this.client.on("connectionAccepted", cb);
-				},
-						
-				cb => {
-					return this.client.emit("joinProject", {project_id: this.project_id}, cb);
-				},
-				
-				cb => {
-					return this.client.emit("joinDoc", this.doc_id, this.fromVersion, (error, ...rest) => { [...this.returnedArgs] = Array.from(rest); return cb(error); });
-				}
-			], done);
-		});
-
-		it("should get the doc from the doc updater with the fromVersion", function() {
-			return MockDocUpdaterServer.getDocument
-				.calledWith(this.project_id, this.doc_id, this.fromVersion)
-				.should.equal(true);
-		});
-		
-		it("should return the doc lines, version, ranges and ops", function() {
-			return this.returnedArgs.should.deep.equal([this.lines, this.version, this.ops, this.ranges]);
-	});
-			
-		return it("should have joined the doc room", function(done) {
-			return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => {
-				expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true);
-				return done();
-			});
-		});
-	});
-
-	describe("with options", function() {
-		before(function(done) {
-			this.options = { encodeRanges: true };
-			return async.series([
-				cb => {
-					return FixturesManager.setUpProject({
-						privilegeLevel: "readAndWrite"
-					}, (e, {project_id, user_id}) => {
-						this.project_id = project_id;
-						this.user_id = user_id;
-						return cb(e);
-					});
-				},
-
-				cb => {
-					return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops, ranges: this.ranges}, (e, {doc_id}) => {
-						this.doc_id = doc_id;
-						return cb(e);
-					});
-				},
-
-				cb => {
-					this.client = RealTimeClient.connect();
-					return this.client.on("connectionAccepted", cb);
-				},
-
-				cb => {
-					return this.client.emit("joinProject", {project_id: this.project_id}, cb);
-				},
-
-				cb => {
-					return this.client.emit("joinDoc", this.doc_id, this.options, (error, ...rest) => { [...this.returnedArgs] = Array.from(rest); return cb(error); });
-				}
-			], done);
-		});
-
-		it("should get the doc from the doc updater with the default fromVersion", function() {
-			return MockDocUpdaterServer.getDocument
-				.calledWith(this.project_id, this.doc_id, -1)
-				.should.equal(true);
-		});
-
-		it("should return the doc lines, version, ranges and ops", function() {
-			return this.returnedArgs.should.deep.equal([this.lines, this.version, this.ops, this.ranges]);
-	});
-
-		return it("should have joined the doc room", function(done) {
-			return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => {
-				expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true);
-				return done();
-			});
-		});
-	});
-
-	return describe("with fromVersion and options", function() {
-		before(function(done) {
-			this.fromVersion = 36;
-			this.options = { encodeRanges: true };
-			return async.series([
-				cb => {
-					return FixturesManager.setUpProject({
-						privilegeLevel: "readAndWrite"
-					}, (e, {project_id, user_id}) => {
-						this.project_id = project_id;
-						this.user_id = user_id;
-						return cb(e);
-					});
-				},
-
-				cb => {
-					return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops, ranges: this.ranges}, (e, {doc_id}) => {
-						this.doc_id = doc_id;
-						return cb(e);
-					});
-				},
-
-				cb => {
-					this.client = RealTimeClient.connect();
-					return this.client.on("connectionAccepted", cb);
-				},
-
-				cb => {
-					return this.client.emit("joinProject", {project_id: this.project_id}, cb);
-				},
-
-				cb => {
-					return this.client.emit("joinDoc", this.doc_id, this.fromVersion, this.options, (error, ...rest) => { [...this.returnedArgs] = Array.from(rest); return cb(error); });
-				}
-			], done);
-		});
-
-		it("should get the doc from the doc updater with the fromVersion", function() {
-			return MockDocUpdaterServer.getDocument
-				.calledWith(this.project_id, this.doc_id, this.fromVersion)
-				.should.equal(true);
-		});
-
-		it("should return the doc lines, version, ranges and ops", function() {
-			return this.returnedArgs.should.deep.equal([this.lines, this.version, this.ops, this.ranges]);
-	});
-
-		return it("should have joined the doc room", function(done) {
-			return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => {
-				expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true);
-				return done();
-			});
-		});
-	});
-});
+const chai = require('chai')
+const { expect } = chai
+chai.should()
+
+const RealTimeClient = require('./helpers/RealTimeClient')
+const MockDocUpdaterServer = require('./helpers/MockDocUpdaterServer')
+const FixturesManager = require('./helpers/FixturesManager')
+
+const async = require('async')
+
+describe('joinDoc', function () {
+  before(function () {
+    this.lines = ['test', 'doc', 'lines']
+    this.version = 42
+    this.ops = ['mock', 'doc', 'ops']
+    return (this.ranges = { mock: 'ranges' })
+  })
+
+  describe('when authorised readAndWrite', function () {
+    before(function (done) {
+      return async.series(
+        [
+          (cb) => {
+            return FixturesManager.setUpProject(
+              {
+                privilegeLevel: 'readAndWrite'
+              },
+              (e, { project_id, user_id }) => {
+                this.project_id = project_id
+                this.user_id = user_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            return FixturesManager.setUpDoc(
+              this.project_id,
+              {
+                lines: this.lines,
+                version: this.version,
+                ops: this.ops,
+                ranges: this.ranges
+              },
+              (e, { doc_id }) => {
+                this.doc_id = doc_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            this.client = RealTimeClient.connect()
+            return this.client.on('connectionAccepted', cb)
+          },
+
+          (cb) => {
+            return this.client.emit(
+              'joinProject',
+              { project_id: this.project_id },
+              cb
+            )
+          },
+
+          (cb) => {
+            return this.client.emit(
+              'joinDoc',
+              this.doc_id,
+              (error, ...rest) => {
+                ;[...this.returnedArgs] = Array.from(rest)
+                return cb(error)
+              }
+            )
+          }
+        ],
+        done
+      )
+    })
+
+    it('should get the doc from the doc updater', function () {
+      return MockDocUpdaterServer.getDocument
+        .calledWith(this.project_id, this.doc_id, -1)
+        .should.equal(true)
+    })
+
+    it('should return the doc lines, version, ranges and ops', function () {
+      return this.returnedArgs.should.deep.equal([
+        this.lines,
+        this.version,
+        this.ops,
+        this.ranges
+      ])
+    })
+
+    return it('should have joined the doc room', function (done) {
+      return RealTimeClient.getConnectedClient(
+        this.client.socket.sessionid,
+        (error, client) => {
+          expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true)
+          return done()
+        }
+      )
+    })
+  })
+
+  describe('when authorised readOnly', function () {
+    before(function (done) {
+      return async.series(
+        [
+          (cb) => {
+            return FixturesManager.setUpProject(
+              {
+                privilegeLevel: 'readOnly'
+              },
+              (e, { project_id, user_id }) => {
+                this.project_id = project_id
+                this.user_id = user_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            return FixturesManager.setUpDoc(
+              this.project_id,
+              {
+                lines: this.lines,
+                version: this.version,
+                ops: this.ops,
+                ranges: this.ranges
+              },
+              (e, { doc_id }) => {
+                this.doc_id = doc_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            this.client = RealTimeClient.connect()
+            return this.client.on('connectionAccepted', cb)
+          },
+
+          (cb) => {
+            return this.client.emit(
+              'joinProject',
+              { project_id: this.project_id },
+              cb
+            )
+          },
+
+          (cb) => {
+            return this.client.emit(
+              'joinDoc',
+              this.doc_id,
+              (error, ...rest) => {
+                ;[...this.returnedArgs] = Array.from(rest)
+                return cb(error)
+              }
+            )
+          }
+        ],
+        done
+      )
+    })
+
+    it('should get the doc from the doc updater', function () {
+      return MockDocUpdaterServer.getDocument
+        .calledWith(this.project_id, this.doc_id, -1)
+        .should.equal(true)
+    })
+
+    it('should return the doc lines, version, ranges and ops', function () {
+      return this.returnedArgs.should.deep.equal([
+        this.lines,
+        this.version,
+        this.ops,
+        this.ranges
+      ])
+    })
+
+    return it('should have joined the doc room', function (done) {
+      return RealTimeClient.getConnectedClient(
+        this.client.socket.sessionid,
+        (error, client) => {
+          expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true)
+          return done()
+        }
+      )
+    })
+  })
+
+  describe('when authorised as owner', function () {
+    before(function (done) {
+      return async.series(
+        [
+          (cb) => {
+            return FixturesManager.setUpProject(
+              {
+                privilegeLevel: 'owner'
+              },
+              (e, { project_id, user_id }) => {
+                this.project_id = project_id
+                this.user_id = user_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            return FixturesManager.setUpDoc(
+              this.project_id,
+              {
+                lines: this.lines,
+                version: this.version,
+                ops: this.ops,
+                ranges: this.ranges
+              },
+              (e, { doc_id }) => {
+                this.doc_id = doc_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            this.client = RealTimeClient.connect()
+            return this.client.on('connectionAccepted', cb)
+          },
+
+          (cb) => {
+            return this.client.emit(
+              'joinProject',
+              { project_id: this.project_id },
+              cb
+            )
+          },
+
+          (cb) => {
+            return this.client.emit(
+              'joinDoc',
+              this.doc_id,
+              (error, ...rest) => {
+                ;[...this.returnedArgs] = Array.from(rest)
+                return cb(error)
+              }
+            )
+          }
+        ],
+        done
+      )
+    })
+
+    it('should get the doc from the doc updater', function () {
+      return MockDocUpdaterServer.getDocument
+        .calledWith(this.project_id, this.doc_id, -1)
+        .should.equal(true)
+    })
+
+    it('should return the doc lines, version, ranges and ops', function () {
+      return this.returnedArgs.should.deep.equal([
+        this.lines,
+        this.version,
+        this.ops,
+        this.ranges
+      ])
+    })
+
+    return it('should have joined the doc room', function (done) {
+      return RealTimeClient.getConnectedClient(
+        this.client.socket.sessionid,
+        (error, client) => {
+          expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true)
+          return done()
+        }
+      )
+    })
+  })
+
+  // It is impossible to write an acceptance test to test joining an unauthorized
+  // project, since joinProject already catches that. If you can join a project,
+  // then you can join a doc in that project.
+
+  describe('with a fromVersion', function () {
+    before(function (done) {
+      this.fromVersion = 36
+      return async.series(
+        [
+          (cb) => {
+            return FixturesManager.setUpProject(
+              {
+                privilegeLevel: 'readAndWrite'
+              },
+              (e, { project_id, user_id }) => {
+                this.project_id = project_id
+                this.user_id = user_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            return FixturesManager.setUpDoc(
+              this.project_id,
+              {
+                lines: this.lines,
+                version: this.version,
+                ops: this.ops,
+                ranges: this.ranges
+              },
+              (e, { doc_id }) => {
+                this.doc_id = doc_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            this.client = RealTimeClient.connect()
+            return this.client.on('connectionAccepted', cb)
+          },
+
+          (cb) => {
+            return this.client.emit(
+              'joinProject',
+              { project_id: this.project_id },
+              cb
+            )
+          },
+
+          (cb) => {
+            return this.client.emit(
+              'joinDoc',
+              this.doc_id,
+              this.fromVersion,
+              (error, ...rest) => {
+                ;[...this.returnedArgs] = Array.from(rest)
+                return cb(error)
+              }
+            )
+          }
+        ],
+        done
+      )
+    })
+
+    it('should get the doc from the doc updater with the fromVersion', function () {
+      return MockDocUpdaterServer.getDocument
+        .calledWith(this.project_id, this.doc_id, this.fromVersion)
+        .should.equal(true)
+    })
+
+    it('should return the doc lines, version, ranges and ops', function () {
+      return this.returnedArgs.should.deep.equal([
+        this.lines,
+        this.version,
+        this.ops,
+        this.ranges
+      ])
+    })
+
+    return it('should have joined the doc room', function (done) {
+      return RealTimeClient.getConnectedClient(
+        this.client.socket.sessionid,
+        (error, client) => {
+          expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true)
+          return done()
+        }
+      )
+    })
+  })
+
+  describe('with options', function () {
+    before(function (done) {
+      this.options = { encodeRanges: true }
+      return async.series(
+        [
+          (cb) => {
+            return FixturesManager.setUpProject(
+              {
+                privilegeLevel: 'readAndWrite'
+              },
+              (e, { project_id, user_id }) => {
+                this.project_id = project_id
+                this.user_id = user_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            return FixturesManager.setUpDoc(
+              this.project_id,
+              {
+                lines: this.lines,
+                version: this.version,
+                ops: this.ops,
+                ranges: this.ranges
+              },
+              (e, { doc_id }) => {
+                this.doc_id = doc_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            this.client = RealTimeClient.connect()
+            return this.client.on('connectionAccepted', cb)
+          },
+
+          (cb) => {
+            return this.client.emit(
+              'joinProject',
+              { project_id: this.project_id },
+              cb
+            )
+          },
+
+          (cb) => {
+            return this.client.emit(
+              'joinDoc',
+              this.doc_id,
+              this.options,
+              (error, ...rest) => {
+                ;[...this.returnedArgs] = Array.from(rest)
+                return cb(error)
+              }
+            )
+          }
+        ],
+        done
+      )
+    })
+
+    it('should get the doc from the doc updater with the default fromVersion', function () {
+      return MockDocUpdaterServer.getDocument
+        .calledWith(this.project_id, this.doc_id, -1)
+        .should.equal(true)
+    })
+
+    it('should return the doc lines, version, ranges and ops', function () {
+      return this.returnedArgs.should.deep.equal([
+        this.lines,
+        this.version,
+        this.ops,
+        this.ranges
+      ])
+    })
+
+    return it('should have joined the doc room', function (done) {
+      return RealTimeClient.getConnectedClient(
+        this.client.socket.sessionid,
+        (error, client) => {
+          expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true)
+          return done()
+        }
+      )
+    })
+  })
+
+  return describe('with fromVersion and options', function () {
+    before(function (done) {
+      this.fromVersion = 36
+      this.options = { encodeRanges: true }
+      return async.series(
+        [
+          (cb) => {
+            return FixturesManager.setUpProject(
+              {
+                privilegeLevel: 'readAndWrite'
+              },
+              (e, { project_id, user_id }) => {
+                this.project_id = project_id
+                this.user_id = user_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            return FixturesManager.setUpDoc(
+              this.project_id,
+              {
+                lines: this.lines,
+                version: this.version,
+                ops: this.ops,
+                ranges: this.ranges
+              },
+              (e, { doc_id }) => {
+                this.doc_id = doc_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            this.client = RealTimeClient.connect()
+            return this.client.on('connectionAccepted', cb)
+          },
+
+          (cb) => {
+            return this.client.emit(
+              'joinProject',
+              { project_id: this.project_id },
+              cb
+            )
+          },
+
+          (cb) => {
+            return this.client.emit(
+              'joinDoc',
+              this.doc_id,
+              this.fromVersion,
+              this.options,
+              (error, ...rest) => {
+                ;[...this.returnedArgs] = Array.from(rest)
+                return cb(error)
+              }
+            )
+          }
+        ],
+        done
+      )
+    })
+
+    it('should get the doc from the doc updater with the fromVersion', function () {
+      return MockDocUpdaterServer.getDocument
+        .calledWith(this.project_id, this.doc_id, this.fromVersion)
+        .should.equal(true)
+    })
+
+    it('should return the doc lines, version, ranges and ops', function () {
+      return this.returnedArgs.should.deep.equal([
+        this.lines,
+        this.version,
+        this.ops,
+        this.ranges
+      ])
+    })
+
+    return it('should have joined the doc room', function (done) {
+      return RealTimeClient.getConnectedClient(
+        this.client.socket.sessionid,
+        (error, client) => {
+          expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(true)
+          return done()
+        }
+      )
+    })
+  })
+})

+ 195 - 155
services/real-time/test/acceptance/js/JoinProjectTests.js

@@ -10,159 +10,199 @@
  * 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;
-chai.should();
-
-const RealTimeClient = require("./helpers/RealTimeClient");
-const MockWebServer = require("./helpers/MockWebServer");
-const FixturesManager = require("./helpers/FixturesManager");
-
-const async = require("async");
-
-describe("joinProject", function() {
-	describe("when authorized", function() {
-		before(function(done) {
-			return async.series([
-				cb => {
-					return FixturesManager.setUpProject({
-						privilegeLevel: "owner",
-						project: {
-							name: "Test Project"
-						}
-					}, (e, {project_id, user_id}) => {
-						this.project_id = project_id;
-						this.user_id = user_id;
-						return cb(e);
-					});
-				},
-
-				cb => {
-					this.client = RealTimeClient.connect();
-					return this.client.on("connectionAccepted", cb);
-				},
-						
-				cb => {
-					return this.client.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => {
-						this.project = project;
-						this.privilegeLevel = privilegeLevel;
-						this.protocolVersion = protocolVersion;
-						return cb(error);
-					});
-				}
-			], done);
-		});
-					
-		it("should get the project from web", function() {
-			return MockWebServer.joinProject
-				.calledWith(this.project_id, this.user_id)
-				.should.equal(true);
-		});
-					
-		it("should return the project", function() {
-			return this.project.should.deep.equal({
-				name: "Test Project"
-			});
-	});
-		
-		it("should return the privilege level", function() {
-			return this.privilegeLevel.should.equal("owner");
-		});
-		
-		it("should return the protocolVersion", function() {
-			return this.protocolVersion.should.equal(2);
-		});
-			
-		it("should have joined the project room", function(done) {
-			return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => {
-				expect(Array.from(client.rooms).includes(this.project_id)).to.equal(true);
-				return done();
-			});
-		});
-				
-		return it("should have marked the user as connected", function(done) {
-			return this.client.emit("clientTracking.getConnectedUsers", (error, users) => {
-				let connected = false;
-				for (const user of Array.from(users)) {
-					if ((user.client_id === this.client.publicId) && (user.user_id === this.user_id)) {
-						connected = true;
-						break;
-					}
-				}
-				expect(connected).to.equal(true);
-				return done();
-			});
-		});
-	});
-			
-	describe("when not authorized", function() {
-		before(function(done) {
-			return async.series([
-				cb => {
-					return FixturesManager.setUpProject({
-						privilegeLevel: null,
-						project: {
-							name: "Test Project"
-						}
-					}, (e, {project_id, user_id}) => {
-						this.project_id = project_id;
-						this.user_id = user_id;
-						return cb(e);
-					});
-				},
-
-				cb => {
-					this.client = RealTimeClient.connect();
-					return this.client.on("connectionAccepted", cb);
-				},
-						
-				cb => {
-					return this.client.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => {
-						this.error = error;
-						this.project = project;
-						this.privilegeLevel = privilegeLevel;
-						this.protocolVersion = protocolVersion;
-						return cb();
-					});
-				}
-			], done);
-		});
-		
-		it("should return an error", function() {
-			return this.error.message.should.equal("not authorized");
-		});
-			
-		return it("should not have joined the project room", function(done) {
-			return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => {
-				expect(Array.from(client.rooms).includes(this.project_id)).to.equal(false);
-				return done();
-			});
-		});
-	});
-
-	return describe("when over rate limit", function() {
-		before(function(done) {
-			return async.series([
-				cb => {
-					this.client = RealTimeClient.connect();
-					return this.client.on("connectionAccepted", cb);
-				},
-
-				cb => {
-					return this.client.emit("joinProject", {project_id: 'rate-limited'}, error => {
-						this.error = error;
-						return cb();
-					});
-				}
-			], done);
-		});
-
-		return it("should return a TooManyRequests error code", function() {
-			this.error.message.should.equal("rate-limit hit when joining project");
-			return this.error.code.should.equal("TooManyRequests");
-		});
-	});
-});
+const chai = require('chai')
+const { expect } = chai
+chai.should()
 
+const RealTimeClient = require('./helpers/RealTimeClient')
+const MockWebServer = require('./helpers/MockWebServer')
+const FixturesManager = require('./helpers/FixturesManager')
+
+const async = require('async')
+
+describe('joinProject', function () {
+  describe('when authorized', function () {
+    before(function (done) {
+      return async.series(
+        [
+          (cb) => {
+            return FixturesManager.setUpProject(
+              {
+                privilegeLevel: 'owner',
+                project: {
+                  name: 'Test Project'
+                }
+              },
+              (e, { project_id, user_id }) => {
+                this.project_id = project_id
+                this.user_id = user_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            this.client = RealTimeClient.connect()
+            return this.client.on('connectionAccepted', cb)
+          },
+
+          (cb) => {
+            return this.client.emit(
+              'joinProject',
+              { project_id: this.project_id },
+              (error, project, privilegeLevel, protocolVersion) => {
+                this.project = project
+                this.privilegeLevel = privilegeLevel
+                this.protocolVersion = protocolVersion
+                return cb(error)
+              }
+            )
+          }
+        ],
+        done
+      )
+    })
+
+    it('should get the project from web', function () {
+      return MockWebServer.joinProject
+        .calledWith(this.project_id, this.user_id)
+        .should.equal(true)
+    })
+
+    it('should return the project', function () {
+      return this.project.should.deep.equal({
+        name: 'Test Project'
+      })
+    })
+
+    it('should return the privilege level', function () {
+      return this.privilegeLevel.should.equal('owner')
+    })
+
+    it('should return the protocolVersion', function () {
+      return this.protocolVersion.should.equal(2)
+    })
+
+    it('should have joined the project room', function (done) {
+      return RealTimeClient.getConnectedClient(
+        this.client.socket.sessionid,
+        (error, client) => {
+          expect(Array.from(client.rooms).includes(this.project_id)).to.equal(
+            true
+          )
+          return done()
+        }
+      )
+    })
+
+    return it('should have marked the user as connected', function (done) {
+      return this.client.emit(
+        'clientTracking.getConnectedUsers',
+        (error, users) => {
+          let connected = false
+          for (const user of Array.from(users)) {
+            if (
+              user.client_id === this.client.publicId &&
+              user.user_id === this.user_id
+            ) {
+              connected = true
+              break
+            }
+          }
+          expect(connected).to.equal(true)
+          return done()
+        }
+      )
+    })
+  })
+
+  describe('when not authorized', function () {
+    before(function (done) {
+      return async.series(
+        [
+          (cb) => {
+            return FixturesManager.setUpProject(
+              {
+                privilegeLevel: null,
+                project: {
+                  name: 'Test Project'
+                }
+              },
+              (e, { project_id, user_id }) => {
+                this.project_id = project_id
+                this.user_id = user_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            this.client = RealTimeClient.connect()
+            return this.client.on('connectionAccepted', cb)
+          },
+
+          (cb) => {
+            return this.client.emit(
+              'joinProject',
+              { project_id: this.project_id },
+              (error, project, privilegeLevel, protocolVersion) => {
+                this.error = error
+                this.project = project
+                this.privilegeLevel = privilegeLevel
+                this.protocolVersion = protocolVersion
+                return cb()
+              }
+            )
+          }
+        ],
+        done
+      )
+    })
+
+    it('should return an error', function () {
+      return this.error.message.should.equal('not authorized')
+    })
+
+    return it('should not have joined the project room', function (done) {
+      return RealTimeClient.getConnectedClient(
+        this.client.socket.sessionid,
+        (error, client) => {
+          expect(Array.from(client.rooms).includes(this.project_id)).to.equal(
+            false
+          )
+          return done()
+        }
+      )
+    })
+  })
+
+  return describe('when over rate limit', function () {
+    before(function (done) {
+      return async.series(
+        [
+          (cb) => {
+            this.client = RealTimeClient.connect()
+            return this.client.on('connectionAccepted', cb)
+          },
+
+          (cb) => {
+            return this.client.emit(
+              'joinProject',
+              { project_id: 'rate-limited' },
+              (error) => {
+                this.error = error
+                return cb()
+              }
+            )
+          }
+        ],
+        done
+      )
+    })
+
+    return it('should return a TooManyRequests error code', function () {
+      this.error.message.should.equal('rate-limit hit when joining project')
+      return this.error.code.should.equal('TooManyRequests')
+    })
+  })
+})

+ 161 - 114
services/real-time/test/acceptance/js/LeaveDocTests.js

@@ -13,117 +13,164 @@
  * DS207: Consider shorter variations of null checks
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-const chai = require("chai");
-const {
-    expect
-} = chai;
-chai.should();
-const sinon = require("sinon");
-
-const RealTimeClient = require("./helpers/RealTimeClient");
-const MockDocUpdaterServer = require("./helpers/MockDocUpdaterServer");
-const FixturesManager = require("./helpers/FixturesManager");
-const logger = require("logger-sharelatex");
-
-const async = require("async");
-
-describe("leaveDoc", function() {
-	before(function() {
-		this.lines = ["test", "doc", "lines"];
-		this.version = 42;
-		this.ops = ["mock", "doc", "ops"];
-		sinon.spy(logger, "error");
-		sinon.spy(logger, "warn");
-		sinon.spy(logger, "log");
-		return this.other_doc_id = FixturesManager.getRandomId();
-	});
-	
-	after(function() {
-		logger.error.restore(); // remove the spy
-		logger.warn.restore();
-		return logger.log.restore();
-	});
-
-	return describe("when joined to a doc", function() {
-		beforeEach(function(done) {
-			return async.series([
-				cb => {
-					return FixturesManager.setUpProject({
-						privilegeLevel: "readAndWrite"
-					}, (e, {project_id, user_id}) => {
-						this.project_id = project_id;
-						this.user_id = user_id;
-						return cb(e);
-					});
-				},
-					
-				cb => {
-					return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => {
-						this.doc_id = doc_id;
-						return cb(e);
-					});
-				},
-						
-				cb => {
-					this.client = RealTimeClient.connect();
-					return this.client.on("connectionAccepted", cb);
-				},
-						
-				cb => {
-					return this.client.emit("joinProject", {project_id: this.project_id}, cb);
-				},
-				
-				cb => {
-					return this.client.emit("joinDoc", this.doc_id, (error, ...rest) => { [...this.returnedArgs] = Array.from(rest); return cb(error); });
-				}
-			], done);
-		});
-							
-		describe("then leaving the doc", function() {
-			beforeEach(function(done) {
-				return this.client.emit("leaveDoc", this.doc_id, (error) => {
-					if (error != null) { throw error; }
-					return done();
-				});
-			});
-			
-			return it("should have left the doc room", function(done) {
-				return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => {
-					expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(false);
-					return done();
-				});
-			});
-		});
-
-		describe("when sending a leaveDoc request before the previous joinDoc request has completed", function() {
-			beforeEach(function(done) {
-				this.client.emit("leaveDoc", this.doc_id, () => {});
-				this.client.emit("joinDoc", this.doc_id, () => {});
-				return this.client.emit("leaveDoc", this.doc_id, (error) => {
-					if (error != null) { throw error; }
-					return done();
-				});
-			});
-
-			it("should not trigger an error", function() { return sinon.assert.neverCalledWith(logger.error, sinon.match.any, "not subscribed - shouldn't happen"); });
-
-			return it("should have left the doc room", function(done) {
-				return RealTimeClient.getConnectedClient(this.client.socket.sessionid, (error, client) => {
-					expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(false);
-					return done();
-				});
-			});
-		});
-
-		return describe("when sending a leaveDoc for a room the client has not joined ", function() {
-			beforeEach(function(done) {
-				return this.client.emit("leaveDoc", this.other_doc_id, (error) => {
-					if (error != null) { throw error; }
-					return done();
-				});
-			});
-
-			return it("should trigger a low level message only", function() { return sinon.assert.calledWith(logger.log, sinon.match.any, "ignoring request from client to leave room it is not in"); });
-		});
-	});
-});
+const chai = require('chai')
+const { expect } = chai
+chai.should()
+const sinon = require('sinon')
+
+const RealTimeClient = require('./helpers/RealTimeClient')
+const MockDocUpdaterServer = require('./helpers/MockDocUpdaterServer')
+const FixturesManager = require('./helpers/FixturesManager')
+const logger = require('logger-sharelatex')
+
+const async = require('async')
+
+describe('leaveDoc', function () {
+  before(function () {
+    this.lines = ['test', 'doc', 'lines']
+    this.version = 42
+    this.ops = ['mock', 'doc', 'ops']
+    sinon.spy(logger, 'error')
+    sinon.spy(logger, 'warn')
+    sinon.spy(logger, 'log')
+    return (this.other_doc_id = FixturesManager.getRandomId())
+  })
+
+  after(function () {
+    logger.error.restore() // remove the spy
+    logger.warn.restore()
+    return logger.log.restore()
+  })
+
+  return describe('when joined to a doc', function () {
+    beforeEach(function (done) {
+      return async.series(
+        [
+          (cb) => {
+            return FixturesManager.setUpProject(
+              {
+                privilegeLevel: 'readAndWrite'
+              },
+              (e, { project_id, user_id }) => {
+                this.project_id = project_id
+                this.user_id = user_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            return FixturesManager.setUpDoc(
+              this.project_id,
+              { lines: this.lines, version: this.version, ops: this.ops },
+              (e, { doc_id }) => {
+                this.doc_id = doc_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            this.client = RealTimeClient.connect()
+            return this.client.on('connectionAccepted', cb)
+          },
+
+          (cb) => {
+            return this.client.emit(
+              'joinProject',
+              { project_id: this.project_id },
+              cb
+            )
+          },
+
+          (cb) => {
+            return this.client.emit(
+              'joinDoc',
+              this.doc_id,
+              (error, ...rest) => {
+                ;[...this.returnedArgs] = Array.from(rest)
+                return cb(error)
+              }
+            )
+          }
+        ],
+        done
+      )
+    })
+
+    describe('then leaving the doc', function () {
+      beforeEach(function (done) {
+        return this.client.emit('leaveDoc', this.doc_id, (error) => {
+          if (error != null) {
+            throw error
+          }
+          return done()
+        })
+      })
+
+      return it('should have left the doc room', function (done) {
+        return RealTimeClient.getConnectedClient(
+          this.client.socket.sessionid,
+          (error, client) => {
+            expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(
+              false
+            )
+            return done()
+          }
+        )
+      })
+    })
+
+    describe('when sending a leaveDoc request before the previous joinDoc request has completed', function () {
+      beforeEach(function (done) {
+        this.client.emit('leaveDoc', this.doc_id, () => {})
+        this.client.emit('joinDoc', this.doc_id, () => {})
+        return this.client.emit('leaveDoc', this.doc_id, (error) => {
+          if (error != null) {
+            throw error
+          }
+          return done()
+        })
+      })
+
+      it('should not trigger an error', function () {
+        return sinon.assert.neverCalledWith(
+          logger.error,
+          sinon.match.any,
+          "not subscribed - shouldn't happen"
+        )
+      })
+
+      return it('should have left the doc room', function (done) {
+        return RealTimeClient.getConnectedClient(
+          this.client.socket.sessionid,
+          (error, client) => {
+            expect(Array.from(client.rooms).includes(this.doc_id)).to.equal(
+              false
+            )
+            return done()
+          }
+        )
+      })
+    })
+
+    return describe('when sending a leaveDoc for a room the client has not joined ', function () {
+      beforeEach(function (done) {
+        return this.client.emit('leaveDoc', this.other_doc_id, (error) => {
+          if (error != null) {
+            throw error
+          }
+          return done()
+        })
+      })
+
+      return it('should trigger a low level message only', function () {
+        return sinon.assert.calledWith(
+          logger.log,
+          sinon.match.any,
+          'ignoring request from client to leave room it is not in'
+        )
+      })
+    })
+  })
+})

+ 257 - 200
services/real-time/test/acceptance/js/LeaveProjectTests.js

@@ -11,203 +11,260 @@
  * DS102: Remove unnecessary code created because of implicit returns
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-const RealTimeClient = require("./helpers/RealTimeClient");
-const MockDocUpdaterServer = require("./helpers/MockDocUpdaterServer");
-const FixturesManager = require("./helpers/FixturesManager");
-
-const async = require("async");
-
-const settings = require("settings-sharelatex");
-const redis = require("redis-sharelatex");
-const rclient = redis.createClient(settings.redis.pubsub);
-
-describe("leaveProject", function() {
-	before(function(done) { return MockDocUpdaterServer.run(done); });
-
-	describe("with other clients in the project", function() {
-		before(function(done) {
-			return async.series([
-				cb => {
-					return FixturesManager.setUpProject({
-						privilegeLevel: "owner",
-						project: {
-							name: "Test Project"
-						}
-					}, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return cb(); });
-				},
-
-				cb => {
-					this.clientA = RealTimeClient.connect();
-					return this.clientA.on("connectionAccepted", cb);
-				},
-
-				cb => {
-					this.clientB = RealTimeClient.connect();
-					this.clientB.on("connectionAccepted", cb);
-
-					this.clientBDisconnectMessages = [];
-					return this.clientB.on("clientTracking.clientDisconnected", data => {
-						return this.clientBDisconnectMessages.push(data);
-					});
-				},
-
-				cb => {
-					return this.clientA.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => {
-						this.project = project;
-						this.privilegeLevel = privilegeLevel;
-						this.protocolVersion = protocolVersion;
-						return cb(error);
-					});
-				},
-
-				cb => {
-					return this.clientB.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => {
-						this.project = project;
-						this.privilegeLevel = privilegeLevel;
-						this.protocolVersion = protocolVersion;
-						return cb(error);
-					});
-				},
-
-				cb => {
-					return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => {
-						this.doc_id = doc_id;
-						return cb(e);
-					});
-				},
-
-				cb => {
-					return this.clientA.emit("joinDoc", this.doc_id, cb);
-				},
-				cb => {
-					return this.clientB.emit("joinDoc", this.doc_id, cb);
-				},
-
-				cb => {
-					// leaveProject is called when the client disconnects
-					this.clientA.on("disconnect", () => cb());
-					return this.clientA.disconnect();
-				},
-
-				cb => {
-					// The API waits a little while before flushing changes
-					return setTimeout(done, 1000);
-				}
-
-			], done);
-		});
-
-		it("should emit a disconnect message to the room", function() {
-			return this.clientBDisconnectMessages.should.deep.equal([this.clientA.publicId]);
-	});
-
-		it("should no longer list the client in connected users", function(done) {
-			return this.clientB.emit("clientTracking.getConnectedUsers", (error, users) => {
-				for (const user of Array.from(users)) {
-					if (user.client_id === this.clientA.publicId) {
-						throw "Expected clientA to not be listed in connected users";
-					}
-				}
-				return done();
-			});
-		});
-
-		it("should not flush the project to the document updater", function() {
-			return MockDocUpdaterServer.deleteProject
-				.calledWith(this.project_id)
-				.should.equal(false);
-		});
-
-		it("should remain subscribed to the editor-events channels", function(done) {
-			rclient.pubsub('CHANNELS', (err, resp) => {
-				if (err) { return done(err); }
-				resp.should.include(`editor-events:${this.project_id}`);
-				return done();
-			});
-			return null;
-		});
-
-		return it("should remain subscribed to the applied-ops channels", function(done) {
-			rclient.pubsub('CHANNELS', (err, resp) => {
-				if (err) { return done(err); }
-				resp.should.include(`applied-ops:${this.doc_id}`);
-				return done();
-			});
-			return null;
-		});
-	});
-
-	return describe("with no other clients in the project", function() {
-		before(function(done) {
-			return async.series([
-				cb => {
-					return FixturesManager.setUpProject({
-						privilegeLevel: "owner",
-						project: {
-							name: "Test Project"
-						}
-					}, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return cb(); });
-				},
-
-				cb => {
-					this.clientA = RealTimeClient.connect();
-					return this.clientA.on("connect", cb);
-				},
-
-				cb => {
-					return this.clientA.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => {
-						this.project = project;
-						this.privilegeLevel = privilegeLevel;
-						this.protocolVersion = protocolVersion;
-						return cb(error);
-					});
-				},
-
-				cb => {
-					return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => {
-						this.doc_id = doc_id;
-						return cb(e);
-					});
-				},
-				cb => {
-					return this.clientA.emit("joinDoc", this.doc_id, cb);
-				},
-
-				cb => {
-					// leaveProject is called when the client disconnects
-					this.clientA.on("disconnect", () => cb());
-					return this.clientA.disconnect();
-				},
-
-				cb => {
-					// The API waits a little while before flushing changes
-					return setTimeout(done, 1000);
-				}
-			], done);
-		});
-
-		it("should flush the project to the document updater", function() {
-			return MockDocUpdaterServer.deleteProject
-				.calledWith(this.project_id)
-				.should.equal(true);
-		});
-
-		it("should not subscribe to the editor-events channels anymore", function(done) {
-			rclient.pubsub('CHANNELS', (err, resp) => {
-				if (err) { return done(err); }
-				resp.should.not.include(`editor-events:${this.project_id}`);
-				return done();
-			});
-			return null;
-		});
-
-		return it("should not subscribe to the applied-ops channels anymore", function(done) {
-			rclient.pubsub('CHANNELS', (err, resp) => {
-				if (err) { return done(err); }
-				resp.should.not.include(`applied-ops:${this.doc_id}`);
-				return done();
-			});
-			return null;
-		});
-	});
-});
+const RealTimeClient = require('./helpers/RealTimeClient')
+const MockDocUpdaterServer = require('./helpers/MockDocUpdaterServer')
+const FixturesManager = require('./helpers/FixturesManager')
+
+const async = require('async')
+
+const settings = require('settings-sharelatex')
+const redis = require('redis-sharelatex')
+const rclient = redis.createClient(settings.redis.pubsub)
+
+describe('leaveProject', function () {
+  before(function (done) {
+    return MockDocUpdaterServer.run(done)
+  })
+
+  describe('with other clients in the project', function () {
+    before(function (done) {
+      return async.series(
+        [
+          (cb) => {
+            return FixturesManager.setUpProject(
+              {
+                privilegeLevel: 'owner',
+                project: {
+                  name: 'Test Project'
+                }
+              },
+              (e, { project_id, user_id }) => {
+                this.project_id = project_id
+                this.user_id = user_id
+                return cb()
+              }
+            )
+          },
+
+          (cb) => {
+            this.clientA = RealTimeClient.connect()
+            return this.clientA.on('connectionAccepted', cb)
+          },
+
+          (cb) => {
+            this.clientB = RealTimeClient.connect()
+            this.clientB.on('connectionAccepted', cb)
+
+            this.clientBDisconnectMessages = []
+            return this.clientB.on(
+              'clientTracking.clientDisconnected',
+              (data) => {
+                return this.clientBDisconnectMessages.push(data)
+              }
+            )
+          },
+
+          (cb) => {
+            return this.clientA.emit(
+              'joinProject',
+              { project_id: this.project_id },
+              (error, project, privilegeLevel, protocolVersion) => {
+                this.project = project
+                this.privilegeLevel = privilegeLevel
+                this.protocolVersion = protocolVersion
+                return cb(error)
+              }
+            )
+          },
+
+          (cb) => {
+            return this.clientB.emit(
+              'joinProject',
+              { project_id: this.project_id },
+              (error, project, privilegeLevel, protocolVersion) => {
+                this.project = project
+                this.privilegeLevel = privilegeLevel
+                this.protocolVersion = protocolVersion
+                return cb(error)
+              }
+            )
+          },
+
+          (cb) => {
+            return FixturesManager.setUpDoc(
+              this.project_id,
+              { lines: this.lines, version: this.version, ops: this.ops },
+              (e, { doc_id }) => {
+                this.doc_id = doc_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            return this.clientA.emit('joinDoc', this.doc_id, cb)
+          },
+          (cb) => {
+            return this.clientB.emit('joinDoc', this.doc_id, cb)
+          },
+
+          (cb) => {
+            // leaveProject is called when the client disconnects
+            this.clientA.on('disconnect', () => cb())
+            return this.clientA.disconnect()
+          },
+
+          (cb) => {
+            // The API waits a little while before flushing changes
+            return setTimeout(done, 1000)
+          }
+        ],
+        done
+      )
+    })
+
+    it('should emit a disconnect message to the room', function () {
+      return this.clientBDisconnectMessages.should.deep.equal([
+        this.clientA.publicId
+      ])
+    })
+
+    it('should no longer list the client in connected users', function (done) {
+      return this.clientB.emit(
+        'clientTracking.getConnectedUsers',
+        (error, users) => {
+          for (const user of Array.from(users)) {
+            if (user.client_id === this.clientA.publicId) {
+              throw 'Expected clientA to not be listed in connected users'
+            }
+          }
+          return done()
+        }
+      )
+    })
+
+    it('should not flush the project to the document updater', function () {
+      return MockDocUpdaterServer.deleteProject
+        .calledWith(this.project_id)
+        .should.equal(false)
+    })
+
+    it('should remain subscribed to the editor-events channels', function (done) {
+      rclient.pubsub('CHANNELS', (err, resp) => {
+        if (err) {
+          return done(err)
+        }
+        resp.should.include(`editor-events:${this.project_id}`)
+        return done()
+      })
+      return null
+    })
+
+    return it('should remain subscribed to the applied-ops channels', function (done) {
+      rclient.pubsub('CHANNELS', (err, resp) => {
+        if (err) {
+          return done(err)
+        }
+        resp.should.include(`applied-ops:${this.doc_id}`)
+        return done()
+      })
+      return null
+    })
+  })
+
+  return describe('with no other clients in the project', function () {
+    before(function (done) {
+      return async.series(
+        [
+          (cb) => {
+            return FixturesManager.setUpProject(
+              {
+                privilegeLevel: 'owner',
+                project: {
+                  name: 'Test Project'
+                }
+              },
+              (e, { project_id, user_id }) => {
+                this.project_id = project_id
+                this.user_id = user_id
+                return cb()
+              }
+            )
+          },
+
+          (cb) => {
+            this.clientA = RealTimeClient.connect()
+            return this.clientA.on('connect', cb)
+          },
+
+          (cb) => {
+            return this.clientA.emit(
+              'joinProject',
+              { project_id: this.project_id },
+              (error, project, privilegeLevel, protocolVersion) => {
+                this.project = project
+                this.privilegeLevel = privilegeLevel
+                this.protocolVersion = protocolVersion
+                return cb(error)
+              }
+            )
+          },
+
+          (cb) => {
+            return FixturesManager.setUpDoc(
+              this.project_id,
+              { lines: this.lines, version: this.version, ops: this.ops },
+              (e, { doc_id }) => {
+                this.doc_id = doc_id
+                return cb(e)
+              }
+            )
+          },
+          (cb) => {
+            return this.clientA.emit('joinDoc', this.doc_id, cb)
+          },
+
+          (cb) => {
+            // leaveProject is called when the client disconnects
+            this.clientA.on('disconnect', () => cb())
+            return this.clientA.disconnect()
+          },
+
+          (cb) => {
+            // The API waits a little while before flushing changes
+            return setTimeout(done, 1000)
+          }
+        ],
+        done
+      )
+    })
+
+    it('should flush the project to the document updater', function () {
+      return MockDocUpdaterServer.deleteProject
+        .calledWith(this.project_id)
+        .should.equal(true)
+    })
+
+    it('should not subscribe to the editor-events channels anymore', function (done) {
+      rclient.pubsub('CHANNELS', (err, resp) => {
+        if (err) {
+          return done(err)
+        }
+        resp.should.not.include(`editor-events:${this.project_id}`)
+        return done()
+      })
+      return null
+    })
+
+    return it('should not subscribe to the applied-ops channels anymore', function (done) {
+      rclient.pubsub('CHANNELS', (err, resp) => {
+        if (err) {
+          return done(err)
+        }
+        resp.should.not.include(`applied-ops:${this.doc_id}`)
+        return done()
+      })
+      return null
+    })
+  })
+})

+ 362 - 272
services/real-time/test/acceptance/js/PubSubRace.js

@@ -9,275 +9,365 @@
  * DS102: Remove unnecessary code created because of implicit returns
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-const RealTimeClient = require("./helpers/RealTimeClient");
-const MockDocUpdaterServer = require("./helpers/MockDocUpdaterServer");
-const FixturesManager = require("./helpers/FixturesManager");
-
-const async = require("async");
-
-const settings = require("settings-sharelatex");
-const redis = require("redis-sharelatex");
-const rclient = redis.createClient(settings.redis.pubsub);
-
-describe("PubSubRace", function() {
-	before(function(done) { return MockDocUpdaterServer.run(done); });
-
-	describe("when the client leaves a doc before joinDoc completes", function() {
-		before(function(done) {
-			return async.series([
-				cb => {
-					return FixturesManager.setUpProject({
-						privilegeLevel: "owner",
-						project: {
-							name: "Test Project"
-						}
-					}, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return cb(); });
-				},
-
-				cb => {
-					this.clientA = RealTimeClient.connect();
-					return this.clientA.on("connect", cb);
-				},
-
-				cb => {
-					return this.clientA.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => {
-						this.project = project;
-						this.privilegeLevel = privilegeLevel;
-						this.protocolVersion = protocolVersion;
-						return cb(error);
-					});
-				},
-
-				cb => {
-					return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => {
-						this.doc_id = doc_id;
-						return cb(e);
-					});
-				},
-
-				cb => {
-					this.clientA.emit("joinDoc", this.doc_id, () => {});
-					// leave before joinDoc completes
-					return this.clientA.emit("leaveDoc", this.doc_id, cb);
-				},
-
-				cb => {
-					// wait for subscribe and unsubscribe
-					return setTimeout(cb, 100);
-				}
-			], done);
-		});
-
-		return it("should not subscribe to the applied-ops channels anymore", function(done) {
-			rclient.pubsub('CHANNELS', (err, resp) => {
-				if (err) { return done(err); }
-				resp.should.not.include(`applied-ops:${this.doc_id}`);
-				return done();
-			});
-			return null;
-		});
-	});
-
-	describe("when the client emits joinDoc and leaveDoc requests frequently and leaves eventually", function() {
-		before(function(done) {
-			return async.series([
-				cb => {
-					return FixturesManager.setUpProject({
-						privilegeLevel: "owner",
-						project: {
-							name: "Test Project"
-						}
-					}, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return cb(); });
-				},
-
-				cb => {
-					this.clientA = RealTimeClient.connect();
-					return this.clientA.on("connect", cb);
-				},
-
-				cb => {
-					return this.clientA.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => {
-						this.project = project;
-						this.privilegeLevel = privilegeLevel;
-						this.protocolVersion = protocolVersion;
-						return cb(error);
-					});
-				},
-
-				cb => {
-					return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => {
-						this.doc_id = doc_id;
-						return cb(e);
-					});
-				},
-
-				cb => {
-					this.clientA.emit("joinDoc", this.doc_id, () => {});
-					this.clientA.emit("leaveDoc", this.doc_id, () => {});
-					this.clientA.emit("joinDoc", this.doc_id, () => {});
-					this.clientA.emit("leaveDoc", this.doc_id, () => {});
-					this.clientA.emit("joinDoc", this.doc_id, () => {});
-					this.clientA.emit("leaveDoc", this.doc_id, () => {});
-					this.clientA.emit("joinDoc", this.doc_id, () => {});
-					this.clientA.emit("leaveDoc", this.doc_id, () => {});
-					this.clientA.emit("joinDoc", this.doc_id, () => {});
-					return this.clientA.emit("leaveDoc", this.doc_id, cb);
-				},
-
-				cb => {
-					// wait for subscribe and unsubscribe
-					return setTimeout(cb, 100);
-				}
-			], done);
-		});
-
-		return it("should not subscribe to the applied-ops channels anymore", function(done) {
-			rclient.pubsub('CHANNELS', (err, resp) => {
-				if (err) { return done(err); }
-				resp.should.not.include(`applied-ops:${this.doc_id}`);
-				return done();
-			});
-			return null;
-		});
-	});
-
-	describe("when the client emits joinDoc and leaveDoc requests frequently and remains in the doc", function() {
-		before(function(done) {
-			return async.series([
-				cb => {
-					return FixturesManager.setUpProject({
-						privilegeLevel: "owner",
-						project: {
-							name: "Test Project"
-						}
-					}, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return cb(); });
-				},
-
-				cb => {
-					this.clientA = RealTimeClient.connect();
-					return this.clientA.on("connect", cb);
-				},
-
-				cb => {
-					return this.clientA.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => {
-						this.project = project;
-						this.privilegeLevel = privilegeLevel;
-						this.protocolVersion = protocolVersion;
-						return cb(error);
-					});
-				},
-
-				cb => {
-					return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => {
-						this.doc_id = doc_id;
-						return cb(e);
-					});
-				},
-
-				cb => {
-					this.clientA.emit("joinDoc", this.doc_id, () => {});
-					this.clientA.emit("leaveDoc", this.doc_id, () => {});
-					this.clientA.emit("joinDoc", this.doc_id, () => {});
-					this.clientA.emit("leaveDoc", this.doc_id, () => {});
-					this.clientA.emit("joinDoc", this.doc_id, () => {});
-					this.clientA.emit("leaveDoc", this.doc_id, () => {});
-					this.clientA.emit("joinDoc", this.doc_id, () => {});
-					this.clientA.emit("leaveDoc", this.doc_id, () => {});
-					return this.clientA.emit("joinDoc", this.doc_id, cb);
-				},
-
-				cb => {
-					// wait for subscribe and unsubscribe
-					return setTimeout(cb, 100);
-				}
-			], done);
-		});
-
-		return it("should subscribe to the applied-ops channels", function(done) {
-			rclient.pubsub('CHANNELS', (err, resp) => {
-				if (err) { return done(err); }
-				resp.should.include(`applied-ops:${this.doc_id}`);
-				return done();
-			});
-			return null;
-		});
-	});
-
-	return describe("when the client disconnects before joinDoc completes", function() {
-		before(function(done) {
-			return async.series([
-				cb => {
-					return FixturesManager.setUpProject({
-						privilegeLevel: "owner",
-						project: {
-							name: "Test Project"
-						}
-					}, (e, {project_id, user_id}) => { this.project_id = project_id; this.user_id = user_id; return cb(); });
-				},
-
-				cb => {
-					this.clientA = RealTimeClient.connect();
-					return this.clientA.on("connect", cb);
-				},
-
-				cb => {
-					return this.clientA.emit("joinProject", {project_id: this.project_id}, (error, project, privilegeLevel, protocolVersion) => {
-						this.project = project;
-						this.privilegeLevel = privilegeLevel;
-						this.protocolVersion = protocolVersion;
-						return cb(error);
-					});
-				},
-
-				cb => {
-					return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => {
-						this.doc_id = doc_id;
-						return cb(e);
-					});
-				},
-
-				cb => {
-					let joinDocCompleted = false;
-					this.clientA.emit("joinDoc", this.doc_id, () => joinDocCompleted = true);
-					// leave before joinDoc completes
-					return setTimeout(() => {
-						if (joinDocCompleted) {
-							return cb(new Error('joinDocCompleted -- lower timeout'));
-						}
-						this.clientA.on("disconnect", () => cb());
-						return this.clientA.disconnect();
-					}
-					// socket.io processes joinDoc and disconnect with different delays:
-					//  - joinDoc goes through two process.nextTick
-					//  - disconnect goes through one process.nextTick
-					// We have to inject the disconnect event into a different event loop
-					//  cycle.
-					, 3);
-				},
-
-				cb => {
-					// wait for subscribe and unsubscribe
-					return setTimeout(cb, 100);
-				}
-			], done);
-		});
-
-		it("should not subscribe to the editor-events channels anymore", function(done) {
-			rclient.pubsub('CHANNELS', (err, resp) => {
-				if (err) { return done(err); }
-				resp.should.not.include(`editor-events:${this.project_id}`);
-				return done();
-			});
-			return null;
-		});
-
-		return it("should not subscribe to the applied-ops channels anymore", function(done) {
-			rclient.pubsub('CHANNELS', (err, resp) => {
-				if (err) { return done(err); }
-				resp.should.not.include(`applied-ops:${this.doc_id}`);
-				return done();
-			});
-			return null;
-		});
-	});
-});
+const RealTimeClient = require('./helpers/RealTimeClient')
+const MockDocUpdaterServer = require('./helpers/MockDocUpdaterServer')
+const FixturesManager = require('./helpers/FixturesManager')
+
+const async = require('async')
+
+const settings = require('settings-sharelatex')
+const redis = require('redis-sharelatex')
+const rclient = redis.createClient(settings.redis.pubsub)
+
+describe('PubSubRace', function () {
+  before(function (done) {
+    return MockDocUpdaterServer.run(done)
+  })
+
+  describe('when the client leaves a doc before joinDoc completes', function () {
+    before(function (done) {
+      return async.series(
+        [
+          (cb) => {
+            return FixturesManager.setUpProject(
+              {
+                privilegeLevel: 'owner',
+                project: {
+                  name: 'Test Project'
+                }
+              },
+              (e, { project_id, user_id }) => {
+                this.project_id = project_id
+                this.user_id = user_id
+                return cb()
+              }
+            )
+          },
+
+          (cb) => {
+            this.clientA = RealTimeClient.connect()
+            return this.clientA.on('connect', cb)
+          },
+
+          (cb) => {
+            return this.clientA.emit(
+              'joinProject',
+              { project_id: this.project_id },
+              (error, project, privilegeLevel, protocolVersion) => {
+                this.project = project
+                this.privilegeLevel = privilegeLevel
+                this.protocolVersion = protocolVersion
+                return cb(error)
+              }
+            )
+          },
+
+          (cb) => {
+            return FixturesManager.setUpDoc(
+              this.project_id,
+              { lines: this.lines, version: this.version, ops: this.ops },
+              (e, { doc_id }) => {
+                this.doc_id = doc_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            this.clientA.emit('joinDoc', this.doc_id, () => {})
+            // leave before joinDoc completes
+            return this.clientA.emit('leaveDoc', this.doc_id, cb)
+          },
+
+          (cb) => {
+            // wait for subscribe and unsubscribe
+            return setTimeout(cb, 100)
+          }
+        ],
+        done
+      )
+    })
+
+    return it('should not subscribe to the applied-ops channels anymore', function (done) {
+      rclient.pubsub('CHANNELS', (err, resp) => {
+        if (err) {
+          return done(err)
+        }
+        resp.should.not.include(`applied-ops:${this.doc_id}`)
+        return done()
+      })
+      return null
+    })
+  })
+
+  describe('when the client emits joinDoc and leaveDoc requests frequently and leaves eventually', function () {
+    before(function (done) {
+      return async.series(
+        [
+          (cb) => {
+            return FixturesManager.setUpProject(
+              {
+                privilegeLevel: 'owner',
+                project: {
+                  name: 'Test Project'
+                }
+              },
+              (e, { project_id, user_id }) => {
+                this.project_id = project_id
+                this.user_id = user_id
+                return cb()
+              }
+            )
+          },
+
+          (cb) => {
+            this.clientA = RealTimeClient.connect()
+            return this.clientA.on('connect', cb)
+          },
+
+          (cb) => {
+            return this.clientA.emit(
+              'joinProject',
+              { project_id: this.project_id },
+              (error, project, privilegeLevel, protocolVersion) => {
+                this.project = project
+                this.privilegeLevel = privilegeLevel
+                this.protocolVersion = protocolVersion
+                return cb(error)
+              }
+            )
+          },
+
+          (cb) => {
+            return FixturesManager.setUpDoc(
+              this.project_id,
+              { lines: this.lines, version: this.version, ops: this.ops },
+              (e, { doc_id }) => {
+                this.doc_id = doc_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            this.clientA.emit('joinDoc', this.doc_id, () => {})
+            this.clientA.emit('leaveDoc', this.doc_id, () => {})
+            this.clientA.emit('joinDoc', this.doc_id, () => {})
+            this.clientA.emit('leaveDoc', this.doc_id, () => {})
+            this.clientA.emit('joinDoc', this.doc_id, () => {})
+            this.clientA.emit('leaveDoc', this.doc_id, () => {})
+            this.clientA.emit('joinDoc', this.doc_id, () => {})
+            this.clientA.emit('leaveDoc', this.doc_id, () => {})
+            this.clientA.emit('joinDoc', this.doc_id, () => {})
+            return this.clientA.emit('leaveDoc', this.doc_id, cb)
+          },
+
+          (cb) => {
+            // wait for subscribe and unsubscribe
+            return setTimeout(cb, 100)
+          }
+        ],
+        done
+      )
+    })
+
+    return it('should not subscribe to the applied-ops channels anymore', function (done) {
+      rclient.pubsub('CHANNELS', (err, resp) => {
+        if (err) {
+          return done(err)
+        }
+        resp.should.not.include(`applied-ops:${this.doc_id}`)
+        return done()
+      })
+      return null
+    })
+  })
+
+  describe('when the client emits joinDoc and leaveDoc requests frequently and remains in the doc', function () {
+    before(function (done) {
+      return async.series(
+        [
+          (cb) => {
+            return FixturesManager.setUpProject(
+              {
+                privilegeLevel: 'owner',
+                project: {
+                  name: 'Test Project'
+                }
+              },
+              (e, { project_id, user_id }) => {
+                this.project_id = project_id
+                this.user_id = user_id
+                return cb()
+              }
+            )
+          },
+
+          (cb) => {
+            this.clientA = RealTimeClient.connect()
+            return this.clientA.on('connect', cb)
+          },
+
+          (cb) => {
+            return this.clientA.emit(
+              'joinProject',
+              { project_id: this.project_id },
+              (error, project, privilegeLevel, protocolVersion) => {
+                this.project = project
+                this.privilegeLevel = privilegeLevel
+                this.protocolVersion = protocolVersion
+                return cb(error)
+              }
+            )
+          },
+
+          (cb) => {
+            return FixturesManager.setUpDoc(
+              this.project_id,
+              { lines: this.lines, version: this.version, ops: this.ops },
+              (e, { doc_id }) => {
+                this.doc_id = doc_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            this.clientA.emit('joinDoc', this.doc_id, () => {})
+            this.clientA.emit('leaveDoc', this.doc_id, () => {})
+            this.clientA.emit('joinDoc', this.doc_id, () => {})
+            this.clientA.emit('leaveDoc', this.doc_id, () => {})
+            this.clientA.emit('joinDoc', this.doc_id, () => {})
+            this.clientA.emit('leaveDoc', this.doc_id, () => {})
+            this.clientA.emit('joinDoc', this.doc_id, () => {})
+            this.clientA.emit('leaveDoc', this.doc_id, () => {})
+            return this.clientA.emit('joinDoc', this.doc_id, cb)
+          },
+
+          (cb) => {
+            // wait for subscribe and unsubscribe
+            return setTimeout(cb, 100)
+          }
+        ],
+        done
+      )
+    })
+
+    return it('should subscribe to the applied-ops channels', function (done) {
+      rclient.pubsub('CHANNELS', (err, resp) => {
+        if (err) {
+          return done(err)
+        }
+        resp.should.include(`applied-ops:${this.doc_id}`)
+        return done()
+      })
+      return null
+    })
+  })
+
+  return describe('when the client disconnects before joinDoc completes', function () {
+    before(function (done) {
+      return async.series(
+        [
+          (cb) => {
+            return FixturesManager.setUpProject(
+              {
+                privilegeLevel: 'owner',
+                project: {
+                  name: 'Test Project'
+                }
+              },
+              (e, { project_id, user_id }) => {
+                this.project_id = project_id
+                this.user_id = user_id
+                return cb()
+              }
+            )
+          },
+
+          (cb) => {
+            this.clientA = RealTimeClient.connect()
+            return this.clientA.on('connect', cb)
+          },
+
+          (cb) => {
+            return this.clientA.emit(
+              'joinProject',
+              { project_id: this.project_id },
+              (error, project, privilegeLevel, protocolVersion) => {
+                this.project = project
+                this.privilegeLevel = privilegeLevel
+                this.protocolVersion = protocolVersion
+                return cb(error)
+              }
+            )
+          },
+
+          (cb) => {
+            return FixturesManager.setUpDoc(
+              this.project_id,
+              { lines: this.lines, version: this.version, ops: this.ops },
+              (e, { doc_id }) => {
+                this.doc_id = doc_id
+                return cb(e)
+              }
+            )
+          },
+
+          (cb) => {
+            let joinDocCompleted = false
+            this.clientA.emit(
+              'joinDoc',
+              this.doc_id,
+              () => (joinDocCompleted = true)
+            )
+            // leave before joinDoc completes
+            return setTimeout(
+              () => {
+                if (joinDocCompleted) {
+                  return cb(new Error('joinDocCompleted -- lower timeout'))
+                }
+                this.clientA.on('disconnect', () => cb())
+                return this.clientA.disconnect()
+              },
+              // socket.io processes joinDoc and disconnect with different delays:
+              //  - joinDoc goes through two process.nextTick
+              //  - disconnect goes through one process.nextTick
+              // We have to inject the disconnect event into a different event loop
+              //  cycle.
+              3
+            )
+          },
+
+          (cb) => {
+            // wait for subscribe and unsubscribe
+            return setTimeout(cb, 100)
+          }
+        ],
+        done
+      )
+    })
+
+    it('should not subscribe to the editor-events channels anymore', function (done) {
+      rclient.pubsub('CHANNELS', (err, resp) => {
+        if (err) {
+          return done(err)
+        }
+        resp.should.not.include(`editor-events:${this.project_id}`)
+        return done()
+      })
+      return null
+    })
+
+    return it('should not subscribe to the applied-ops channels anymore', function (done) {
+      rclient.pubsub('CHANNELS', (err, resp) => {
+        if (err) {
+          return done(err)
+        }
+        resp.should.not.include(`applied-ops:${this.doc_id}`)
+        return done()
+      })
+      return null
+    })
+  })
+})

+ 336 - 268
services/real-time/test/acceptance/js/ReceiveUpdateTests.js

@@ -11,271 +11,339 @@
  * DS207: Consider shorter variations of null checks
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-const chai = require("chai");
-const {
-    expect
-} = chai;
-chai.should();
-
-const RealTimeClient = require("./helpers/RealTimeClient");
-const MockWebServer = require("./helpers/MockWebServer");
-const FixturesManager = require("./helpers/FixturesManager");
-
-const async = require("async");
-
-const settings = require("settings-sharelatex");
-const redis = require("redis-sharelatex");
-const rclient = redis.createClient(settings.redis.pubsub);
-
-describe("receiveUpdate", function() {
-	beforeEach(function(done) {
-		this.lines = ["test", "doc", "lines"];
-		this.version = 42;
-		this.ops = ["mock", "doc", "ops"];
-		
-		return async.series([
-			cb => {
-				return FixturesManager.setUpProject({
-					privilegeLevel: "owner",
-					project: { name: "Test Project"	}
-				}, (error, {user_id, project_id}) => { this.user_id = user_id; this.project_id = project_id; return cb(); });
-			},
-			
-			cb => {
-				return FixturesManager.setUpDoc(this.project_id, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id}) => {
-					this.doc_id = doc_id;
-					return cb(e);
-				});
-			},
-			
-			cb => {
-				this.clientA = RealTimeClient.connect();
-				return this.clientA.on("connectionAccepted", cb);
-			},
-				
-			cb => {
-				this.clientB = RealTimeClient.connect();
-				return this.clientB.on("connectionAccepted", cb);
-			},
-				
-			cb => {
-				return this.clientA.emit("joinProject", {
-					project_id: this.project_id
-				}, cb);
-			},
-			
-			cb => {
-				return this.clientA.emit("joinDoc", this.doc_id, cb);
-			},
-				
-			cb => {
-				return this.clientB.emit("joinProject", {
-					project_id: this.project_id
-				}, cb);
-			},
-			
-			cb => {
-				return this.clientB.emit("joinDoc", this.doc_id, cb);
-			},
-
-			cb => {
-				return FixturesManager.setUpProject({
-					privilegeLevel: "owner",
-					project: {name: "Test Project"}
-				}, (error, {user_id: user_id_second, project_id: project_id_second}) => { this.user_id_second = user_id_second; this.project_id_second = project_id_second; return cb(); });
-			},
-
-			cb => {
-				return FixturesManager.setUpDoc(this.project_id_second, {lines: this.lines, version: this.version, ops: this.ops}, (e, {doc_id: doc_id_second}) => {
-					this.doc_id_second = doc_id_second;
-					return cb(e);
-				});
-			},
-
-			cb => {
-				this.clientC = RealTimeClient.connect();
-				return this.clientC.on("connectionAccepted", cb);
-			},
-
-			cb => {
-				return this.clientC.emit("joinProject", {
-					project_id: this.project_id_second
-				}, cb);
-			},
-			cb => {
-				return this.clientC.emit("joinDoc", this.doc_id_second, cb);
-			},
-
-			cb => {
-				this.clientAUpdates = [];
-				this.clientA.on("otUpdateApplied", update => this.clientAUpdates.push(update));
-				this.clientBUpdates = [];
-				this.clientB.on("otUpdateApplied", update => this.clientBUpdates.push(update));
-				this.clientCUpdates = [];
-				this.clientC.on("otUpdateApplied", update => this.clientCUpdates.push(update));
-
-				this.clientAErrors = [];
-				this.clientA.on("otUpdateError", error => this.clientAErrors.push(error));
-				this.clientBErrors = [];
-				this.clientB.on("otUpdateError", error => this.clientBErrors.push(error));
-				this.clientCErrors = [];
-				this.clientC.on("otUpdateError", error => this.clientCErrors.push(error));
-				return cb();
-			}
-		], done);
-	});
-
-	afterEach(function() {
-		if (this.clientA != null) {
-			this.clientA.disconnect();
-		}
-		if (this.clientB != null) {
-			this.clientB.disconnect();
-		}
-		return (this.clientC != null ? this.clientC.disconnect() : undefined);
-	});
-
-	describe("with an update from clientA", function() {
-		beforeEach(function(done) {
-			this.update = {
-				doc_id: this.doc_id,
-				op: {
-					meta: {
-						source: this.clientA.publicId
-					},
-					v: this.version,
-					doc: this.doc_id,
-					op: [{i: "foo", p: 50}]
-				}				
-			};
-			rclient.publish("applied-ops", JSON.stringify(this.update));
-			return setTimeout(done, 200);
-		}); // Give clients time to get message
-			
-		it("should send the full op to clientB", function() {
-			return this.clientBUpdates.should.deep.equal([this.update.op]);
-	});
-			
-		it("should send an ack to clientA", function() {
-			return this.clientAUpdates.should.deep.equal([{
-				v: this.version, doc: this.doc_id
-			}]);
-	});
-
-		return it("should send nothing to clientC", function() {
-			return this.clientCUpdates.should.deep.equal([]);
-	});
-});
-
-	describe("with an update from clientC", function() {
-		beforeEach(function(done) {
-			this.update = {
-				doc_id: this.doc_id_second,
-				op: {
-					meta: {
-						source: this.clientC.publicId
-					},
-					v: this.version,
-					doc: this.doc_id_second,
-					op: [{i: "update from clientC", p: 50}]
-				}
-			};
-			rclient.publish("applied-ops", JSON.stringify(this.update));
-			return setTimeout(done, 200);
-		}); // Give clients time to get message
-
-		it("should send nothing to clientA", function() {
-			return this.clientAUpdates.should.deep.equal([]);
-	});
-
-		it("should send nothing to clientB", function() {
-			return this.clientBUpdates.should.deep.equal([]);
-	});
-
-		return it("should send an ack to clientC", function() {
-			return this.clientCUpdates.should.deep.equal([{
-				v: this.version, doc: this.doc_id_second
-			}]);
-	});
-});
-
-	describe("with an update from a remote client for project 1", function() {
-		beforeEach(function(done) {
-			this.update = {
-				doc_id: this.doc_id,
-				op: {
-					meta: {
-						source: 'this-is-a-remote-client-id'
-					},
-					v: this.version,
-					doc: this.doc_id,
-					op: [{i: "foo", p: 50}]
-				}
-			};
-			rclient.publish("applied-ops", JSON.stringify(this.update));
-			return setTimeout(done, 200);
-		}); // Give clients time to get message
-
-		it("should send the full op to clientA", function() {
-			return this.clientAUpdates.should.deep.equal([this.update.op]);
-	});
-			
-		it("should send the full op to clientB", function() {
-			return this.clientBUpdates.should.deep.equal([this.update.op]);
-	});
-
-		return it("should send nothing to clientC", function() {
-			return this.clientCUpdates.should.deep.equal([]);
-	});
-});
-
-	describe("with an error for the first project", function() {
-		beforeEach(function(done) {
-			rclient.publish("applied-ops", JSON.stringify({doc_id: this.doc_id, error: (this.error = "something went wrong")}));
-			return setTimeout(done, 200);
-		}); // Give clients time to get message
-
-		it("should send the error to the clients in the first project", function() {
-			this.clientAErrors.should.deep.equal([this.error]);
-			return this.clientBErrors.should.deep.equal([this.error]);
-	});
-
-		it("should not send any errors to the client in the second project", function() {
-			return this.clientCErrors.should.deep.equal([]);
-	});
-
-		it("should disconnect the clients of the first project", function() {
-			this.clientA.socket.connected.should.equal(false);
-			return this.clientB.socket.connected.should.equal(false);
-		});
-
-		return it("should not disconnect the client in the second project", function() {
-			return this.clientC.socket.connected.should.equal(true);
-		});
-	});
-
-	return describe("with an error for the second project", function() {
-		beforeEach(function(done) {
-			rclient.publish("applied-ops", JSON.stringify({doc_id: this.doc_id_second, error: (this.error = "something went wrong")}));
-			return setTimeout(done, 200);
-		}); // Give clients time to get message
-
-		it("should not send any errors to the clients in the first project", function() {
-			this.clientAErrors.should.deep.equal([]);
-			return this.clientBErrors.should.deep.equal([]);
-	});
-
-		it("should send the error to the client in the second project", function() {
-			return this.clientCErrors.should.deep.equal([this.error]);
-	});
-
-		it("should not disconnect the clients of the first project", function() {
-			this.clientA.socket.connected.should.equal(true);
-			return this.clientB.socket.connected.should.equal(true);
-		});
-
-		return it("should disconnect the client in the second project", function() {
-			return this.clientC.socket.connected.should.equal(false);
-		});
-	});
-});
+const chai = require('chai')
+const { expect } = chai
+chai.should()
+
+const RealTimeClient = require('./helpers/RealTimeClient')
+const MockWebServer = require('./helpers/MockWebServer')
+const FixturesManager = require('./helpers/FixturesManager')
+
+const async = require('async')
+
+const settings = require('settings-sharelatex')
+const redis = require('redis-sharelatex')
+const rclient = redis.createClient(settings.redis.pubsub)
+
+describe('receiveUpdate', function () {
+  beforeEach(function (done) {
+    this.lines = ['test', 'doc', 'lines']
+    this.version = 42
+    this.ops = ['mock', 'doc', 'ops']
+
+    return async.series(
+      [
+        (cb) => {
+          return FixturesManager.setUpProject(
+            {
+              privilegeLevel: 'owner',
+              project: { name: 'Test Project' }
+            },
+            (error, { user_id, project_id }) => {
+              this.user_id = user_id
+              this.project_id = project_id
+              return cb()
+            }
+          )
+        },
+
+        (cb) => {
+          return FixturesManager.setUpDoc(
+            this.project_id,
+            { lines: this.lines, version: this.version, ops: this.ops },
+            (e, { doc_id }) => {
+              this.doc_id = doc_id
+              return cb(e)
+            }
+          )
+        },
+
+        (cb) => {
+          this.clientA = RealTimeClient.connect()
+          return this.clientA.on('connectionAccepted', cb)
+        },
+
+        (cb) => {
+          this.clientB = RealTimeClient.connect()
+          return this.clientB.on('connectionAccepted', cb)
+        },
+
+        (cb) => {
+          return this.clientA.emit(
+            'joinProject',
+            {
+              project_id: this.project_id
+            },
+            cb
+          )
+        },
+
+        (cb) => {
+          return this.clientA.emit('joinDoc', this.doc_id, cb)
+        },
+
+        (cb) => {
+          return this.clientB.emit(
+            'joinProject',
+            {
+              project_id: this.project_id
+            },
+            cb
+          )
+        },
+
+        (cb) => {
+          return this.clientB.emit('joinDoc', this.doc_id, cb)
+        },
+
+        (cb) => {
+          return FixturesManager.setUpProject(
+            {
+              privilegeLevel: 'owner',
+              project: { name: 'Test Project' }
+            },
+            (
+              error,
+              { user_id: user_id_second, project_id: project_id_second }
+            ) => {
+              this.user_id_second = user_id_second
+              this.project_id_second = project_id_second
+              return cb()
+            }
+          )
+        },
+
+        (cb) => {
+          return FixturesManager.setUpDoc(
+            this.project_id_second,
+            { lines: this.lines, version: this.version, ops: this.ops },
+            (e, { doc_id: doc_id_second }) => {
+              this.doc_id_second = doc_id_second
+              return cb(e)
+            }
+          )
+        },
+
+        (cb) => {
+          this.clientC = RealTimeClient.connect()
+          return this.clientC.on('connectionAccepted', cb)
+        },
+
+        (cb) => {
+          return this.clientC.emit(
+            'joinProject',
+            {
+              project_id: this.project_id_second
+            },
+            cb
+          )
+        },
+        (cb) => {
+          return this.clientC.emit('joinDoc', this.doc_id_second, cb)
+        },
+
+        (cb) => {
+          this.clientAUpdates = []
+          this.clientA.on('otUpdateApplied', (update) =>
+            this.clientAUpdates.push(update)
+          )
+          this.clientBUpdates = []
+          this.clientB.on('otUpdateApplied', (update) =>
+            this.clientBUpdates.push(update)
+          )
+          this.clientCUpdates = []
+          this.clientC.on('otUpdateApplied', (update) =>
+            this.clientCUpdates.push(update)
+          )
+
+          this.clientAErrors = []
+          this.clientA.on('otUpdateError', (error) =>
+            this.clientAErrors.push(error)
+          )
+          this.clientBErrors = []
+          this.clientB.on('otUpdateError', (error) =>
+            this.clientBErrors.push(error)
+          )
+          this.clientCErrors = []
+          this.clientC.on('otUpdateError', (error) =>
+            this.clientCErrors.push(error)
+          )
+          return cb()
+        }
+      ],
+      done
+    )
+  })
+
+  afterEach(function () {
+    if (this.clientA != null) {
+      this.clientA.disconnect()
+    }
+    if (this.clientB != null) {
+      this.clientB.disconnect()
+    }
+    return this.clientC != null ? this.clientC.disconnect() : undefined
+  })
+
+  describe('with an update from clientA', function () {
+    beforeEach(function (done) {
+      this.update = {
+        doc_id: this.doc_id,
+        op: {
+          meta: {
+            source: this.clientA.publicId
+          },
+          v: this.version,
+          doc: this.doc_id,
+          op: [{ i: 'foo', p: 50 }]
+        }
+      }
+      rclient.publish('applied-ops', JSON.stringify(this.update))
+      return setTimeout(done, 200)
+    }) // Give clients time to get message
+
+    it('should send the full op to clientB', function () {
+      return this.clientBUpdates.should.deep.equal([this.update.op])
+    })
+
+    it('should send an ack to clientA', function () {
+      return this.clientAUpdates.should.deep.equal([
+        {
+          v: this.version,
+          doc: this.doc_id
+        }
+      ])
+    })
+
+    return it('should send nothing to clientC', function () {
+      return this.clientCUpdates.should.deep.equal([])
+    })
+  })
+
+  describe('with an update from clientC', function () {
+    beforeEach(function (done) {
+      this.update = {
+        doc_id: this.doc_id_second,
+        op: {
+          meta: {
+            source: this.clientC.publicId
+          },
+          v: this.version,
+          doc: this.doc_id_second,
+          op: [{ i: 'update from clientC', p: 50 }]
+        }
+      }
+      rclient.publish('applied-ops', JSON.stringify(this.update))
+      return setTimeout(done, 200)
+    }) // Give clients time to get message
+
+    it('should send nothing to clientA', function () {
+      return this.clientAUpdates.should.deep.equal([])
+    })
+
+    it('should send nothing to clientB', function () {
+      return this.clientBUpdates.should.deep.equal([])
+    })
+
+    return it('should send an ack to clientC', function () {
+      return this.clientCUpdates.should.deep.equal([
+        {
+          v: this.version,
+          doc: this.doc_id_second
+        }
+      ])
+    })
+  })
+
+  describe('with an update from a remote client for project 1', function () {
+    beforeEach(function (done) {
+      this.update = {
+        doc_id: this.doc_id,
+        op: {
+          meta: {
+            source: 'this-is-a-remote-client-id'
+          },
+          v: this.version,
+          doc: this.doc_id,
+          op: [{ i: 'foo', p: 50 }]
+        }
+      }
+      rclient.publish('applied-ops', JSON.stringify(this.update))
+      return setTimeout(done, 200)
+    }) // Give clients time to get message
+
+    it('should send the full op to clientA', function () {
+      return this.clientAUpdates.should.deep.equal([this.update.op])
+    })
+
+    it('should send the full op to clientB', function () {
+      return this.clientBUpdates.should.deep.equal([this.update.op])
+    })
+
+    return it('should send nothing to clientC', function () {
+      return this.clientCUpdates.should.deep.equal([])
+    })
+  })
+
+  describe('with an error for the first project', function () {
+    beforeEach(function (done) {
+      rclient.publish(
+        'applied-ops',
+        JSON.stringify({
+          doc_id: this.doc_id,
+          error: (this.error = 'something went wrong')
+        })
+      )
+      return setTimeout(done, 200)
+    }) // Give clients time to get message
+
+    it('should send the error to the clients in the first project', function () {
+      this.clientAErrors.should.deep.equal([this.error])
+      return this.clientBErrors.should.deep.equal([this.error])
+    })
+
+    it('should not send any errors to the client in the second project', function () {
+      return this.clientCErrors.should.deep.equal([])
+    })
+
+    it('should disconnect the clients of the first project', function () {
+      this.clientA.socket.connected.should.equal(false)
+      return this.clientB.socket.connected.should.equal(false)
+    })
+
+    return it('should not disconnect the client in the second project', function () {
+      return this.clientC.socket.connected.should.equal(true)
+    })
+  })
+
+  return describe('with an error for the second project', function () {
+    beforeEach(function (done) {
+      rclient.publish(
+        'applied-ops',
+        JSON.stringify({
+          doc_id: this.doc_id_second,
+          error: (this.error = 'something went wrong')
+        })
+      )
+      return setTimeout(done, 200)
+    }) // Give clients time to get message
+
+    it('should not send any errors to the clients in the first project', function () {
+      this.clientAErrors.should.deep.equal([])
+      return this.clientBErrors.should.deep.equal([])
+    })
+
+    it('should send the error to the client in the second project', function () {
+      return this.clientCErrors.should.deep.equal([this.error])
+    })
+
+    it('should not disconnect the clients of the first project', function () {
+      this.clientA.socket.connected.should.equal(true)
+      return this.clientB.socket.connected.should.equal(true)
+    })
+
+    return it('should disconnect the client in the second project', function () {
+      return this.clientC.socket.connected.should.equal(false)
+    })
+  })
+})

+ 96 - 81
services/real-time/test/acceptance/js/RouterTests.js

@@ -8,99 +8,114 @@
  * DS102: Remove unnecessary code created because of implicit returns
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-const async = require("async");
-const {expect} = require("chai");
+const async = require('async')
+const { expect } = require('chai')
 
-const RealTimeClient = require("./helpers/RealTimeClient");
-const FixturesManager = require("./helpers/FixturesManager");
+const RealTimeClient = require('./helpers/RealTimeClient')
+const FixturesManager = require('./helpers/FixturesManager')
 
+describe('Router', function () {
+  return describe('joinProject', function () {
+    describe('when there is no callback provided', function () {
+      after(function () {
+        return process.removeListener('unhandledRejection', this.onUnhandled)
+      })
 
-describe("Router", function() { return describe("joinProject", function() {
-    describe("when there is no callback provided", function() {
-        after(function() {
-            return process.removeListener('unhandledRejection', this.onUnhandled);
-        });
-
-        before(function(done) {
-            this.onUnhandled = error => done(error);
-            process.on('unhandledRejection', this.onUnhandled);
-            return async.series([
-                cb => {
-                    return FixturesManager.setUpProject({
-                        privilegeLevel: "owner",
-                        project: {
-                            name: "Test Project"
-                        }
-                    }, (e, {project_id, user_id}) => {
-                        this.project_id = project_id;
-                        this.user_id = user_id;
-                        return cb(e);
-                    });
+      before(function (done) {
+        this.onUnhandled = (error) => done(error)
+        process.on('unhandledRejection', this.onUnhandled)
+        return async.series(
+          [
+            (cb) => {
+              return FixturesManager.setUpProject(
+                {
+                  privilegeLevel: 'owner',
+                  project: {
+                    name: 'Test Project'
+                  }
                 },
+                (e, { project_id, user_id }) => {
+                  this.project_id = project_id
+                  this.user_id = user_id
+                  return cb(e)
+                }
+              )
+            },
 
-                cb => {
-                    this.client = RealTimeClient.connect();
-                    return this.client.on("connectionAccepted", cb);
-                },
+            (cb) => {
+              this.client = RealTimeClient.connect()
+              return this.client.on('connectionAccepted', cb)
+            },
 
-                cb => {
-                    this.client = RealTimeClient.connect();
-                    return this.client.on("connectionAccepted", cb);
-                },
+            (cb) => {
+              this.client = RealTimeClient.connect()
+              return this.client.on('connectionAccepted', cb)
+            },
 
-                cb => {
-                    this.client.emit("joinProject", {project_id: this.project_id});
-                    return setTimeout(cb, 100);
-                }
-            ], done);
-        });
+            (cb) => {
+              this.client.emit('joinProject', { project_id: this.project_id })
+              return setTimeout(cb, 100)
+            }
+          ],
+          done
+        )
+      })
 
-        return it("should keep on going", function() { return expect('still running').to.exist; });
-    });
+      return it('should keep on going', function () {
+        return expect('still running').to.exist
+      })
+    })
 
-    return describe("when there are too many arguments", function() {
-        after(function() {
-            return process.removeListener('unhandledRejection', this.onUnhandled);
-        });
+    return describe('when there are too many arguments', function () {
+      after(function () {
+        return process.removeListener('unhandledRejection', this.onUnhandled)
+      })
 
-        before(function(done) {
-            this.onUnhandled = error => done(error);
-            process.on('unhandledRejection', this.onUnhandled);
-            return async.series([
-                cb => {
-                    return FixturesManager.setUpProject({
-                        privilegeLevel: "owner",
-                        project: {
-                            name: "Test Project"
-                        }
-                    }, (e, {project_id, user_id}) => {
-                        this.project_id = project_id;
-                        this.user_id = user_id;
-                        return cb(e);
-                    });
+      before(function (done) {
+        this.onUnhandled = (error) => done(error)
+        process.on('unhandledRejection', this.onUnhandled)
+        return async.series(
+          [
+            (cb) => {
+              return FixturesManager.setUpProject(
+                {
+                  privilegeLevel: 'owner',
+                  project: {
+                    name: 'Test Project'
+                  }
                 },
+                (e, { project_id, user_id }) => {
+                  this.project_id = project_id
+                  this.user_id = user_id
+                  return cb(e)
+                }
+              )
+            },
 
-                cb => {
-                    this.client = RealTimeClient.connect();
-                    return this.client.on("connectionAccepted", cb);
-                },
+            (cb) => {
+              this.client = RealTimeClient.connect()
+              return this.client.on('connectionAccepted', cb)
+            },
 
-                cb => {
-                    this.client = RealTimeClient.connect();
-                    return this.client.on("connectionAccepted", cb);
-                },
+            (cb) => {
+              this.client = RealTimeClient.connect()
+              return this.client.on('connectionAccepted', cb)
+            },
 
-                cb => {
-                    return this.client.emit("joinProject", 1, 2, 3, 4, 5, error => {
-                        this.error = error;
-                        return cb();
-                    });
-                }
-            ], done);
-        });
+            (cb) => {
+              return this.client.emit('joinProject', 1, 2, 3, 4, 5, (error) => {
+                this.error = error
+                return cb()
+              })
+            }
+          ],
+          done
+        )
+      })
 
-        return it("should return an error message", function() {
-            return expect(this.error.message).to.equal('unexpected arguments');
-        });
-    });
-}); });
+      return it('should return an error message', function () {
+        return expect(this.error.message).to.equal('unexpected arguments')
+      })
+    })
+  })
+})

+ 82 - 74
services/real-time/test/acceptance/js/SessionSocketsTests.js

@@ -8,88 +8,96 @@
  * DS102: Remove unnecessary code created because of implicit returns
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-const RealTimeClient = require("./helpers/RealTimeClient");
-const Settings = require("settings-sharelatex");
-const {expect} = require('chai');
+const RealTimeClient = require('./helpers/RealTimeClient')
+const Settings = require('settings-sharelatex')
+const { expect } = require('chai')
 
-describe('SessionSockets', function() {
-	before(function() {
-		return this.checkSocket = function(fn) {
-			const client = RealTimeClient.connect();
-			client.on('connectionAccepted', fn);
-			client.on('connectionRejected', fn);
-			return null;
-		};
-	});
+describe('SessionSockets', function () {
+  before(function () {
+    return (this.checkSocket = function (fn) {
+      const client = RealTimeClient.connect()
+      client.on('connectionAccepted', fn)
+      client.on('connectionRejected', fn)
+      return null
+    })
+  })
 
-	describe('without cookies', function() {
-		before(function() { return RealTimeClient.cookie = null; });
+  describe('without cookies', function () {
+    before(function () {
+      return (RealTimeClient.cookie = null)
+    })
 
-		return it('should return a lookup error', function(done) {
-			return this.checkSocket((error) => {
-				expect(error).to.exist;
-				expect(error.message).to.equal('invalid session');
-				return done();
-			});
-		});
-	});
+    return it('should return a lookup error', function (done) {
+      return this.checkSocket((error) => {
+        expect(error).to.exist
+        expect(error.message).to.equal('invalid session')
+        return done()
+      })
+    })
+  })
 
-	describe('with a different cookie', function() {
-		before(function() { return RealTimeClient.cookie = "some.key=someValue"; });
+  describe('with a different cookie', function () {
+    before(function () {
+      return (RealTimeClient.cookie = 'some.key=someValue')
+    })
 
-		return it('should return a lookup error', function(done) {
-			return this.checkSocket((error) => {
-				expect(error).to.exist;
-				expect(error.message).to.equal('invalid session');
-				return done();
-			});
-		});
-	});
+    return it('should return a lookup error', function (done) {
+      return this.checkSocket((error) => {
+        expect(error).to.exist
+        expect(error.message).to.equal('invalid session')
+        return done()
+      })
+    })
+  })
 
-	describe('with an invalid cookie', function() {
-		before(function(done) {
-			RealTimeClient.setSession({}, (error) => {
-				if (error) { return done(error); }
-				RealTimeClient.cookie = `${Settings.cookieName}=${
-					RealTimeClient.cookie.slice(17, 49)
-				}`;
-				return done();
-			});
-			return null;
-		});
+  describe('with an invalid cookie', function () {
+    before(function (done) {
+      RealTimeClient.setSession({}, (error) => {
+        if (error) {
+          return done(error)
+        }
+        RealTimeClient.cookie = `${
+          Settings.cookieName
+        }=${RealTimeClient.cookie.slice(17, 49)}`
+        return done()
+      })
+      return null
+    })
 
-		return it('should return a lookup error', function(done) {
-			return this.checkSocket((error) => {
-				expect(error).to.exist;
-				expect(error.message).to.equal('invalid session');
-				return done();
-			});
-		});
-	});
+    return it('should return a lookup error', function (done) {
+      return this.checkSocket((error) => {
+        expect(error).to.exist
+        expect(error.message).to.equal('invalid session')
+        return done()
+      })
+    })
+  })
 
-	describe('with a valid cookie and no matching session', function() {
-		before(function() { return RealTimeClient.cookie = `${Settings.cookieName}=unknownId`; });
+  describe('with a valid cookie and no matching session', function () {
+    before(function () {
+      return (RealTimeClient.cookie = `${Settings.cookieName}=unknownId`)
+    })
 
-		return it('should return a lookup error', function(done) {
-			return this.checkSocket((error) => {
-				expect(error).to.exist;
-				expect(error.message).to.equal('invalid session');
-				return done();
-			});
-		});
-	});
+    return it('should return a lookup error', function (done) {
+      return this.checkSocket((error) => {
+        expect(error).to.exist
+        expect(error.message).to.equal('invalid session')
+        return done()
+      })
+    })
+  })
 
-	return describe('with a valid cookie and a matching session', function() {
-		before(function(done) {
-			RealTimeClient.setSession({}, done);
-			return null;
-		});
+  return describe('with a valid cookie and a matching session', function () {
+    before(function (done) {
+      RealTimeClient.setSession({}, done)
+      return null
+    })
 
-		return it('should not return an error', function(done) {
-			return this.checkSocket((error) => {
-				expect(error).to.not.exist;
-				return done();
-			});
-		});
-	});
-});
+    return it('should not return an error', function (done) {
+      return this.checkSocket((error) => {
+        expect(error).to.not.exist
+        return done()
+      })
+    })
+  })
+})

+ 44 - 40
services/real-time/test/acceptance/js/SessionTests.js

@@ -11,47 +11,51 @@
  * DS207: Consider shorter variations of null checks
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-const chai = require("chai");
-const {
-    expect
-} = chai;
+const chai = require('chai')
+const { expect } = chai
 
-const RealTimeClient = require("./helpers/RealTimeClient");
+const RealTimeClient = require('./helpers/RealTimeClient')
 
-describe("Session", function() { return describe("with an established session", function() {
-    before(function(done) {
-        this.user_id = "mock-user-id";
-        RealTimeClient.setSession({
-            user: { _id: this.user_id }
-        }, error => {
-            if (error != null) { throw error; }
-            this.client = RealTimeClient.connect();
-            return done();
-        });
-        return null;
-    });
+describe('Session', function () {
+  return describe('with an established session', function () {
+    before(function (done) {
+      this.user_id = 'mock-user-id'
+      RealTimeClient.setSession(
+        {
+          user: { _id: this.user_id }
+        },
+        (error) => {
+          if (error != null) {
+            throw error
+          }
+          this.client = RealTimeClient.connect()
+          return done()
+        }
+      )
+      return null
+    })
+
+    it('should not get disconnected', function (done) {
+      let disconnected = false
+      this.client.on('disconnect', () => (disconnected = true))
+      return setTimeout(() => {
+        expect(disconnected).to.equal(false)
+        return done()
+      }, 500)
+    })
 
-    it("should not get disconnected", function(done) {
-        let disconnected = false;
-        this.client.on("disconnect", () => disconnected = true);
-        return setTimeout(() => {
-            expect(disconnected).to.equal(false);
-            return done();
+    return it('should appear in the list of connected clients', function (done) {
+      return RealTimeClient.getConnectedClients((error, clients) => {
+        let included = false
+        for (const client of Array.from(clients)) {
+          if (client.client_id === this.client.socket.sessionid) {
+            included = true
+            break
+          }
         }
-        , 500);
-    });
-        
-    return it("should appear in the list of connected clients", function(done) {
-        return RealTimeClient.getConnectedClients((error, clients) => {
-            let included = false;
-            for (const client of Array.from(clients)) {
-                if (client.client_id === this.client.socket.sessionid) {
-                    included = true;
-                    break;
-                }
-            }
-            expect(included).to.equal(true);
-            return done();
-        });
-    });
-}); });
+        expect(included).to.equal(true)
+        return done()
+      })
+    })
+  })
+})

+ 106 - 60
services/real-time/test/acceptance/js/helpers/FixturesManager.js

@@ -10,64 +10,110 @@
  * DS207: Consider shorter variations of null checks
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-let FixturesManager;
-const RealTimeClient = require("./RealTimeClient");
-const MockWebServer = require("./MockWebServer");
-const MockDocUpdaterServer = require("./MockDocUpdaterServer");
+let FixturesManager
+const RealTimeClient = require('./RealTimeClient')
+const MockWebServer = require('./MockWebServer')
+const MockDocUpdaterServer = require('./MockDocUpdaterServer')
 
-module.exports = (FixturesManager = {
-	setUpProject(options, callback) {
-		if (options == null) { options = {}; }
-		if (callback == null) { callback = function(error, data) {}; }
-		if (!options.user_id) { options.user_id = FixturesManager.getRandomId(); }
-		if (!options.project_id) { options.project_id = FixturesManager.getRandomId(); }
-		if (!options.project) { options.project = { name: "Test Project" }; }
-		const {project_id, user_id, privilegeLevel, project, publicAccess} = options;
-		
-		const privileges = {};
-		privileges[user_id] = privilegeLevel;
-		if (publicAccess) {
-			privileges["anonymous-user"] = publicAccess;
-		}
-		
-		MockWebServer.createMockProject(project_id, privileges, project);
-		return MockWebServer.run(error => {
-			if (error != null) { throw error; }
-			return RealTimeClient.setSession({
-				user: {
-					_id: user_id,
-					first_name: "Joe",
-					last_name: "Bloggs"
-				}
-			}, error => {
-				if (error != null) { throw error; }
-				return callback(null, {project_id, user_id, privilegeLevel, project});
-		});
-	});
-	},
-				
-	setUpDoc(project_id, options, callback) {
-		if (options == null) { options = {}; }
-		if (callback == null) { callback = function(error, data) {}; }
-		if (!options.doc_id) { options.doc_id = FixturesManager.getRandomId(); }
-		if (!options.lines) { options.lines = ["doc", "lines"]; }
-		if (!options.version) { options.version = 42; }
-		if (!options.ops) { options.ops = ["mock", "ops"]; }
-		const {doc_id, lines, version, ops, ranges} = options;
-				
-		MockDocUpdaterServer.createMockDoc(project_id, doc_id, {lines, version, ops, ranges});
-		return MockDocUpdaterServer.run(error => {
-			if (error != null) { throw error; }
-			return callback(null, {project_id, doc_id, lines, version, ops});
-	});
-	},
-		
-	getRandomId() {
-		return require("crypto")
-			.createHash("sha1")
-			.update(Math.random().toString())
-			.digest("hex")
-			.slice(0,24);
-	}
-});
-		
+module.exports = FixturesManager = {
+  setUpProject(options, callback) {
+    if (options == null) {
+      options = {}
+    }
+    if (callback == null) {
+      callback = function (error, data) {}
+    }
+    if (!options.user_id) {
+      options.user_id = FixturesManager.getRandomId()
+    }
+    if (!options.project_id) {
+      options.project_id = FixturesManager.getRandomId()
+    }
+    if (!options.project) {
+      options.project = { name: 'Test Project' }
+    }
+    const {
+      project_id,
+      user_id,
+      privilegeLevel,
+      project,
+      publicAccess
+    } = options
+
+    const privileges = {}
+    privileges[user_id] = privilegeLevel
+    if (publicAccess) {
+      privileges['anonymous-user'] = publicAccess
+    }
+
+    MockWebServer.createMockProject(project_id, privileges, project)
+    return MockWebServer.run((error) => {
+      if (error != null) {
+        throw error
+      }
+      return RealTimeClient.setSession(
+        {
+          user: {
+            _id: user_id,
+            first_name: 'Joe',
+            last_name: 'Bloggs'
+          }
+        },
+        (error) => {
+          if (error != null) {
+            throw error
+          }
+          return callback(null, {
+            project_id,
+            user_id,
+            privilegeLevel,
+            project
+          })
+        }
+      )
+    })
+  },
+
+  setUpDoc(project_id, options, callback) {
+    if (options == null) {
+      options = {}
+    }
+    if (callback == null) {
+      callback = function (error, data) {}
+    }
+    if (!options.doc_id) {
+      options.doc_id = FixturesManager.getRandomId()
+    }
+    if (!options.lines) {
+      options.lines = ['doc', 'lines']
+    }
+    if (!options.version) {
+      options.version = 42
+    }
+    if (!options.ops) {
+      options.ops = ['mock', 'ops']
+    }
+    const { doc_id, lines, version, ops, ranges } = options
+
+    MockDocUpdaterServer.createMockDoc(project_id, doc_id, {
+      lines,
+      version,
+      ops,
+      ranges
+    })
+    return MockDocUpdaterServer.run((error) => {
+      if (error != null) {
+        throw error
+      }
+      return callback(null, { project_id, doc_id, lines, version, ops })
+    })
+  },
+
+  getRandomId() {
+    return require('crypto')
+      .createHash('sha1')
+      .update(Math.random().toString())
+      .digest('hex')
+      .slice(0, 24)
+  }
+}

+ 75 - 57
services/real-time/test/acceptance/js/helpers/MockDocUpdaterServer.js

@@ -11,62 +11,80 @@
  * DS207: Consider shorter variations of null checks
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-let MockDocUpdaterServer;
-const sinon = require("sinon");
-const express = require("express");
+let MockDocUpdaterServer
+const sinon = require('sinon')
+const express = require('express')
 
-module.exports = (MockDocUpdaterServer = {
-	docs: {},
-	
-	createMockDoc(project_id, doc_id, data) {
-		return MockDocUpdaterServer.docs[`${project_id}:${doc_id}`] = data;
-	},
-		
-	getDocument(project_id, doc_id, fromVersion, callback) {
-		if (callback == null) { callback = function(error, data) {}; }
-		return callback(
-			null, MockDocUpdaterServer.docs[`${project_id}:${doc_id}`]
-		);
-	},
-		
-	deleteProject: sinon.stub().callsArg(1),
-		
-	getDocumentRequest(req, res, next) {
-		const {project_id, doc_id} = req.params;
-		let {fromVersion} = req.query;
-		fromVersion = parseInt(fromVersion, 10);
-		return MockDocUpdaterServer.getDocument(project_id, doc_id, fromVersion, (error, data) => {
-			if (error != null) { return next(error); }
-			return res.json(data);
-		});
-	},
-			
-	deleteProjectRequest(req, res, next) {
-		const {project_id} = req.params;
-		return MockDocUpdaterServer.deleteProject(project_id, (error) => {
-			if (error != null) { return next(error); }
-			return res.sendStatus(204);
-		});
-	},
-	
-	running: false,
-	run(callback) {
-		if (callback == null) { callback = function(error) {}; }
-		if (MockDocUpdaterServer.running) {
-			return callback();
-		}
-		const app = express();
-		app.get("/project/:project_id/doc/:doc_id", MockDocUpdaterServer.getDocumentRequest);
-		app.delete("/project/:project_id", MockDocUpdaterServer.deleteProjectRequest);
-		return app.listen(3003, (error) => {
-			MockDocUpdaterServer.running = true;
-			return callback(error);
-	}).on("error", (error) => {
-			console.error("error starting MockDocUpdaterServer:", error.message);
-			return process.exit(1);
-		});
-	}
-});
+module.exports = MockDocUpdaterServer = {
+  docs: {},
 
-			
-sinon.spy(MockDocUpdaterServer, "getDocument");
+  createMockDoc(project_id, doc_id, data) {
+    return (MockDocUpdaterServer.docs[`${project_id}:${doc_id}`] = data)
+  },
+
+  getDocument(project_id, doc_id, fromVersion, callback) {
+    if (callback == null) {
+      callback = function (error, data) {}
+    }
+    return callback(null, MockDocUpdaterServer.docs[`${project_id}:${doc_id}`])
+  },
+
+  deleteProject: sinon.stub().callsArg(1),
+
+  getDocumentRequest(req, res, next) {
+    const { project_id, doc_id } = req.params
+    let { fromVersion } = req.query
+    fromVersion = parseInt(fromVersion, 10)
+    return MockDocUpdaterServer.getDocument(
+      project_id,
+      doc_id,
+      fromVersion,
+      (error, data) => {
+        if (error != null) {
+          return next(error)
+        }
+        return res.json(data)
+      }
+    )
+  },
+
+  deleteProjectRequest(req, res, next) {
+    const { project_id } = req.params
+    return MockDocUpdaterServer.deleteProject(project_id, (error) => {
+      if (error != null) {
+        return next(error)
+      }
+      return res.sendStatus(204)
+    })
+  },
+
+  running: false,
+  run(callback) {
+    if (callback == null) {
+      callback = function (error) {}
+    }
+    if (MockDocUpdaterServer.running) {
+      return callback()
+    }
+    const app = express()
+    app.get(
+      '/project/:project_id/doc/:doc_id',
+      MockDocUpdaterServer.getDocumentRequest
+    )
+    app.delete(
+      '/project/:project_id',
+      MockDocUpdaterServer.deleteProjectRequest
+    )
+    return app
+      .listen(3003, (error) => {
+        MockDocUpdaterServer.running = true
+        return callback(error)
+      })
+      .on('error', (error) => {
+        console.error('error starting MockDocUpdaterServer:', error.message)
+        return process.exit(1)
+      })
+  }
+}
+
+sinon.spy(MockDocUpdaterServer, 'getDocument')

+ 67 - 56
services/real-time/test/acceptance/js/helpers/MockWebServer.js

@@ -11,61 +11,72 @@
  * DS207: Consider shorter variations of null checks
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-let MockWebServer;
-const sinon = require("sinon");
-const express = require("express");
+let MockWebServer
+const sinon = require('sinon')
+const express = require('express')
 
-module.exports = (MockWebServer = {
-	projects: {},
-	privileges: {},
-	
-	createMockProject(project_id, privileges, project) {
-		MockWebServer.privileges[project_id] = privileges;
-		return MockWebServer.projects[project_id] = project;
-	},
-		
-	joinProject(project_id, user_id, callback) {
-		if (callback == null) { callback = function(error, project, privilegeLevel) {}; }
-		return callback(
-			null,
-			MockWebServer.projects[project_id], 
-			MockWebServer.privileges[project_id][user_id]
-		);
-	},
-		
-	joinProjectRequest(req, res, next) {
-		const {project_id} = req.params;
-		const {user_id} = req.query;
-		if (project_id === 'rate-limited') {
-			return res.status(429).send();
-		} else {
-			return MockWebServer.joinProject(project_id, user_id, (error, project, privilegeLevel) => {
-				if (error != null) { return next(error); }
-				return res.json({
-					project,
-					privilegeLevel
-				});
-		});
-		}
-	},
-	
-	running: false,
-	run(callback) {
-		if (callback == null) { callback = function(error) {}; }
-		if (MockWebServer.running) {
-			return callback();
-		}
-		const app = express();
-		app.post("/project/:project_id/join", MockWebServer.joinProjectRequest);
-		return app.listen(3000, (error) => {
-			MockWebServer.running = true;
-			return callback(error);
-	}).on("error", (error) => {
-			console.error("error starting MockWebServer:", error.message);
-			return process.exit(1);
-		});
-	}
-});
+module.exports = MockWebServer = {
+  projects: {},
+  privileges: {},
 
-			
-sinon.spy(MockWebServer, "joinProject");
+  createMockProject(project_id, privileges, project) {
+    MockWebServer.privileges[project_id] = privileges
+    return (MockWebServer.projects[project_id] = project)
+  },
+
+  joinProject(project_id, user_id, callback) {
+    if (callback == null) {
+      callback = function (error, project, privilegeLevel) {}
+    }
+    return callback(
+      null,
+      MockWebServer.projects[project_id],
+      MockWebServer.privileges[project_id][user_id]
+    )
+  },
+
+  joinProjectRequest(req, res, next) {
+    const { project_id } = req.params
+    const { user_id } = req.query
+    if (project_id === 'rate-limited') {
+      return res.status(429).send()
+    } else {
+      return MockWebServer.joinProject(
+        project_id,
+        user_id,
+        (error, project, privilegeLevel) => {
+          if (error != null) {
+            return next(error)
+          }
+          return res.json({
+            project,
+            privilegeLevel
+          })
+        }
+      )
+    }
+  },
+
+  running: false,
+  run(callback) {
+    if (callback == null) {
+      callback = function (error) {}
+    }
+    if (MockWebServer.running) {
+      return callback()
+    }
+    const app = express()
+    app.post('/project/:project_id/join', MockWebServer.joinProjectRequest)
+    return app
+      .listen(3000, (error) => {
+        MockWebServer.running = true
+        return callback(error)
+      })
+      .on('error', (error) => {
+        console.error('error starting MockWebServer:', error.message)
+        return process.exit(1)
+      })
+  }
+}
+
+sinon.spy(MockWebServer, 'joinProject')

+ 110 - 80
services/real-time/test/acceptance/js/helpers/RealTimeClient.js

@@ -11,91 +11,121 @@
  * DS207: Consider shorter variations of null checks
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-let Client;
-const {
-    XMLHttpRequest
-} = require("../../libs/XMLHttpRequest");
-const io = require("socket.io-client");
-const async = require("async");
+let Client
+const { XMLHttpRequest } = require('../../libs/XMLHttpRequest')
+const io = require('socket.io-client')
+const async = require('async')
 
-const request = require("request");
-const Settings = require("settings-sharelatex");
-const redis = require("redis-sharelatex");
-const rclient = redis.createClient(Settings.redis.websessions);
+const request = require('request')
+const Settings = require('settings-sharelatex')
+const redis = require('redis-sharelatex')
+const rclient = redis.createClient(Settings.redis.websessions)
 
-const uid = require('uid-safe').sync;
-const signature = require("cookie-signature");
+const uid = require('uid-safe').sync
+const signature = require('cookie-signature')
 
-io.util.request = function() {
-	const xhr = new XMLHttpRequest();
-	const _open = xhr.open;
-	xhr.open = function() {
-		_open.apply(xhr, arguments);
-		if (Client.cookie != null) {
-			return xhr.setRequestHeader("Cookie", Client.cookie);
-		}
-	};
-	return xhr;
-};
+io.util.request = function () {
+  const xhr = new XMLHttpRequest()
+  const _open = xhr.open
+  xhr.open = function () {
+    _open.apply(xhr, arguments)
+    if (Client.cookie != null) {
+      return xhr.setRequestHeader('Cookie', Client.cookie)
+    }
+  }
+  return xhr
+}
 
-module.exports = (Client = {
-	cookie: null,
+module.exports = Client = {
+  cookie: null,
 
-	setSession(session, callback) {
-		if (callback == null) { callback = function(error) {}; }
-		const sessionId = uid(24);
-		session.cookie = {};
-		return rclient.set("sess:" + sessionId, JSON.stringify(session), (error) => {
-			if (error != null) { return callback(error); }
-			const secret = Settings.security.sessionSecret;
-			const cookieKey = 's:' + signature.sign(sessionId, secret);
-			Client.cookie = `${Settings.cookieName}=${cookieKey}`;
-			return callback();
-		});
-	},
-			
-	unsetSession(callback) {
-		if (callback == null) { callback = function(error) {}; }
-		Client.cookie = null;
-		return callback();
-	},
-			
-	connect(cookie) {
-		const client = io.connect("http://localhost:3026", {'force new connection': true});
-		client.on('connectionAccepted', (_, publicId) => client.publicId = publicId);
-		return client;
-	},
-		
-	getConnectedClients(callback) {
-		if (callback == null) { callback = function(error, clients) {}; }
-		return request.get({
-			url: "http://localhost:3026/clients",
-			json: true
-		}, (error, response, data) => callback(error, data));
-	},
-		
-	getConnectedClient(client_id, callback) {
-		if (callback == null) { callback = function(error, clients) {}; }
-		return request.get({
-			url: `http://localhost:3026/clients/${client_id}`,
-			json: true
-		}, (error, response, data) => callback(error, data));
-	},
+  setSession(session, callback) {
+    if (callback == null) {
+      callback = function (error) {}
+    }
+    const sessionId = uid(24)
+    session.cookie = {}
+    return rclient.set(
+      'sess:' + sessionId,
+      JSON.stringify(session),
+      (error) => {
+        if (error != null) {
+          return callback(error)
+        }
+        const secret = Settings.security.sessionSecret
+        const cookieKey = 's:' + signature.sign(sessionId, secret)
+        Client.cookie = `${Settings.cookieName}=${cookieKey}`
+        return callback()
+      }
+    )
+  },
 
+  unsetSession(callback) {
+    if (callback == null) {
+      callback = function (error) {}
+    }
+    Client.cookie = null
+    return callback()
+  },
 
-	disconnectClient(client_id, callback) {
-		request.post({
-			url: `http://localhost:3026/client/${client_id}/disconnect`,
-			auth: {
-				user: Settings.internal.realTime.user,
-				pass: Settings.internal.realTime.pass
-			}
-		}, (error, response, data) => callback(error, data));
-		return null;
-	},
+  connect(cookie) {
+    const client = io.connect('http://localhost:3026', {
+      'force new connection': true
+    })
+    client.on(
+      'connectionAccepted',
+      (_, publicId) => (client.publicId = publicId)
+    )
+    return client
+  },
 
-	disconnectAllClients(callback) {
-		return Client.getConnectedClients((error, clients) => async.each(clients, (clientView, cb) => Client.disconnectClient(clientView.client_id, cb)
-        , callback));
-	}
-});
+  getConnectedClients(callback) {
+    if (callback == null) {
+      callback = function (error, clients) {}
+    }
+    return request.get(
+      {
+        url: 'http://localhost:3026/clients',
+        json: true
+      },
+      (error, response, data) => callback(error, data)
+    )
+  },
+
+  getConnectedClient(client_id, callback) {
+    if (callback == null) {
+      callback = function (error, clients) {}
+    }
+    return request.get(
+      {
+        url: `http://localhost:3026/clients/${client_id}`,
+        json: true
+      },
+      (error, response, data) => callback(error, data)
+    )
+  },
+
+  disconnectClient(client_id, callback) {
+    request.post(
+      {
+        url: `http://localhost:3026/client/${client_id}/disconnect`,
+        auth: {
+          user: Settings.internal.realTime.user,
+          pass: Settings.internal.realTime.pass
+        }
+      },
+      (error, response, data) => callback(error, data)
+    )
+    return null
+  },
+
+  disconnectAllClients(callback) {
+    return Client.getConnectedClients((error, clients) =>
+      async.each(
+        clients,
+        (clientView, cb) => Client.disconnectClient(clientView.client_id, cb),
+        callback
+      )
+    )
+  }
+}

+ 45 - 32
services/real-time/test/acceptance/js/helpers/RealtimeServer.js

@@ -12,40 +12,53 @@
  * DS207: Consider shorter variations of null checks
  * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  */
-const app = require('../../../../app');
-const logger = require("logger-sharelatex");
-const Settings = require("settings-sharelatex");
+const app = require('../../../../app')
+const logger = require('logger-sharelatex')
+const Settings = require('settings-sharelatex')
 
 module.exports = {
-	running: false,
-	initing: false,
-	callbacks: [],
-	ensureRunning(callback) {
-		if (callback == null) { callback = function(error) {}; }
-		if (this.running) {
-			return callback();
-		} else if (this.initing) {
-			return this.callbacks.push(callback);
-		} else {
-			this.initing = true;
-			this.callbacks.push(callback);
-			return app.listen(__guard__(Settings.internal != null ? Settings.internal.realtime : undefined, x => x.port), "localhost", error => { 
-				if (error != null) { throw error; }
-				this.running = true;
-				logger.log("clsi running in dev mode");
+  running: false,
+  initing: false,
+  callbacks: [],
+  ensureRunning(callback) {
+    if (callback == null) {
+      callback = function (error) {}
+    }
+    if (this.running) {
+      return callback()
+    } else if (this.initing) {
+      return this.callbacks.push(callback)
+    } else {
+      this.initing = true
+      this.callbacks.push(callback)
+      return app.listen(
+        __guard__(
+          Settings.internal != null ? Settings.internal.realtime : undefined,
+          (x) => x.port
+        ),
+        'localhost',
+        (error) => {
+          if (error != null) {
+            throw error
+          }
+          this.running = true
+          logger.log('clsi running in dev mode')
 
-				return (() => {
-					const result = [];
-					for (callback of Array.from(this.callbacks)) {
-						result.push(callback());
-					}
-					return result;
-				})();
-			});
-		}
-	}
-};
+          return (() => {
+            const result = []
+            for (callback of Array.from(this.callbacks)) {
+              result.push(callback())
+            }
+            return result
+          })()
+        }
+      )
+    }
+  }
+}
 
 function __guard__(value, transform) {
-  return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
-}
+  return typeof value !== 'undefined' && value !== null
+    ? transform(value)
+    : undefined
+}